Initial working version, positions show.
This commit is contained in:
75
backend/app.py
Normal file
75
backend/app.py
Normal file
@@ -0,0 +1,75 @@
|
||||
"""Flask backend for IB dashboard.
|
||||
|
||||
Provides simple REST endpoints that the vanilla-JS frontend polls:
|
||||
- GET /api/quotes -> latest market quotes
|
||||
- GET /api/positions -> latest positions
|
||||
- POST /api/subscribe { symbols: ["AAPL","MSFT"] } -> start subscribing
|
||||
|
||||
This app expects an IB Gateway or TWS running locally on port 7496.
|
||||
"""
|
||||
from flask import Flask, jsonify, request, send_from_directory
|
||||
import os
|
||||
import threading
|
||||
|
||||
from ib_client import IBClient
|
||||
import time
|
||||
|
||||
|
||||
# Use an absolute path for the static folder so Flask can reliably serve files
|
||||
STATIC_FOLDER = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "frontend"))
|
||||
app = Flask(__name__, static_folder=STATIC_FOLDER, static_url_path="/static")
|
||||
|
||||
# instantiate the IB client with a process-unique client id to avoid
|
||||
# collisions when Flask's reloader spawns multiple processes.
|
||||
ib = IBClient(client_id=os.getpid())
|
||||
# Start connection only in the main process (Werkzeug sets WERKZEUG_RUN_MAIN)
|
||||
if os.environ.get("WERKZEUG_RUN_MAIN") == "true" or not os.environ.get("WERKZEUG_RUN_MAIN"):
|
||||
# attempt to start the connection; failures are logged by the client
|
||||
try:
|
||||
ib.connect_and_start()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
@app.route("/")
|
||||
def index():
|
||||
return send_from_directory(app.static_folder, "index.html")
|
||||
|
||||
|
||||
@app.route("/api/quotes")
|
||||
def api_quotes():
|
||||
return jsonify(ib.get_quotes())
|
||||
|
||||
|
||||
@app.route("/api/positions")
|
||||
def api_positions():
|
||||
# request a fresh positions snapshot from IB and wait briefly for callbacks
|
||||
try:
|
||||
ib.request_positions()
|
||||
except Exception:
|
||||
pass
|
||||
# wait up to 2 seconds for positions to arrive
|
||||
deadline = time.time() + 2.0
|
||||
while time.time() < deadline:
|
||||
positions = ib.get_positions()
|
||||
if positions:
|
||||
return jsonify(positions)
|
||||
time.sleep(0.1)
|
||||
# return whatever we have (possibly empty)
|
||||
return jsonify(ib.get_positions())
|
||||
|
||||
|
||||
@app.route("/api/subscribe", methods=["POST"])
|
||||
def api_subscribe():
|
||||
data = request.get_json(force=True)
|
||||
symbols = data.get("symbols", []) if isinstance(data, dict) else []
|
||||
if not isinstance(symbols, list):
|
||||
return jsonify({"error": "symbols must be a list"}), 400
|
||||
for s in symbols:
|
||||
ib.subscribe_market_data(s)
|
||||
return jsonify({"subscribed": symbols})
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# simple dev server; disable reloader to avoid duplicate IB client instances
|
||||
app.run(debug=True, port=8000, use_reloader=False)
|
||||
Reference in New Issue
Block a user