#!/usr/bin/env python3 import sys sys.path.insert(0, "/app") import quickfix as fix import quickfix44 as fix44 from flask import Flask, render_template, request, jsonify import json import threading, time, os from collections import deque from threading import Lock from shared.config import Config app = Flask(__name__) fix_initiator = None fix_app = None _messages = deque(maxlen=500) _msgs_lock = Lock() # --- Global OrderID counter --- LOCK = threading.Lock() def next_order_id(): os.makedirs(os.path.dirname(Config.ORDER_ID_FILE), exist_ok=True) with LOCK: # thread safety inside container if not os.path.exists(Config.ORDER_ID_FILE): with open(Config.ORDER_ID_FILE, "w") as f: f.write("0") with open(Config.ORDER_ID_FILE, "r+") as f: current = int(f.read().strip() or 0) new_id = current + 1 f.seek(0) f.write(str(new_id)) f.truncate() return new_id def log(msg: str): with _msgs_lock: _messages.append(msg) class FixUIClientApp(fix.Application): def __init__(self): super().__init__() self.sessionID = None self.connected = False def onCreate(self, sessionID): self.sessionID = sessionID def onLogon(self, sessionID): self.connected = True self.sessionID = sessionID log(f"✅ Logon: {sessionID}") def onLogout(self, sessionID): self.connected = False log(f"❌ Logout: {sessionID}") def toAdmin(self, message, sessionID): pass def fromAdmin(self, message, sessionID): pass def toApp(self, message, sessionID): log(f"âžĄī¸ Sent App: {message.toString()}") def fromApp(self, message, sessionID): msg_type = fix.MsgType() message.getHeader().getField(msg_type) if msg_type.getValue() == fix.MsgType_ExecutionReport: execType, ordStatus, clOrdID = fix.ExecType(), fix.OrdStatus(), fix.ClOrdID() try: message.getField(execType) except: pass try: message.getField(ordStatus) except: pass try: message.getField(clOrdID) except: pass status_map = { fix.ExecType_NEW: "New", fix.ExecType_PARTIAL_FILL: "Partial Fill", fix.ExecType_FILL: "Fill", fix.ExecType_CANCELED: "Canceled", fix.ExecType_REPLACED: "Replaced", fix.ExecType_REJECTED: "Rejected", } status = status_map.get(execType.getValue(), f"ExecType={execType.getValue()}") log(f"đŸ“Ĩ ExecReport: ClOrdID={clOrdID.getValue() if clOrdID else '?'} Status={status}") else: log(f"📩 App: {message.toString()}") def send_order(self, side, symbol, qty, price): if not self.sessionID: return "âš ī¸ No FIX session active" order = fix44.NewOrderSingle() cl_ord_id = next_order_id() order.setField(fix.ClOrdID(str(cl_ord_id))) order.setField(fix.HandlInst('1')) order.setField(fix.Symbol(symbol)) order.setField(fix.Side(side)) order.setField(fix.TransactTime()) order.setField(fix.OrdType(fix.OrdType_LIMIT)) order.setField(fix.OrderQty(qty)) order.setField(fix.Price(price)) fix.Session.sendToTarget(order, self.sessionID) log(f"📤 Sent Order (ID={cl_ord_id}): {order.toString()}") return "Order sent!" # --- FIX lifecycle --- def start_fix(): global fix_initiator, fix_app if fix_initiator is not None: log("â„šī¸ FIX already starting/started") return settings = fix.SessionSettings(CONFIG_FILE) fix_app = FixUIClientApp() store = fix.FileStoreFactory(settings) logfile = fix.FileLogFactory(settings) fix_initiator = fix.SocketInitiator(fix_app, store, settings, logfile) fix_initiator.start() log(f"🔌 FIX initiator started with {CONFIG_FILE}") def stop_fix(): global fix_initiator if fix_initiator: fix_initiator.stop() log("đŸĒĢ FIX initiator stopped") fix_initiator = None def load_securities(): securities = {} try: with open(Config.SECURITIES_FILE) as f: for line in f: line = line.strip() if line and not line.startswith('#'): parts = line.split() if len(parts) >= 3: securities[parts[0]] = float(parts[2]) elif len(parts) >= 1: securities[parts[0]] = 10.0 except Exception: pass return securities or {"ALPHA": 5.65, "PEIR": 8.35, "EXAE": 6.90, "QUEST": 13.35, "NBG": 8.00} # --- Flask routes --- @app.route("/") def index(): with _msgs_lock: msgs = list(reversed(_messages)) # newest first connected = bool(fix_app and fix_app.connected) return render_template("index.html", messages=msgs, connected=connected, securities=load_securities()) @app.route("/status") def status(): return jsonify({"connected": bool(fix_app and fix_app.connected)}) @app.route("/connect") def connect(): threading.Thread(target=start_fix, daemon=True).start() return jsonify({"status": "ok", "message": "Connecting..."}) @app.route("/disconnect") def disconnect(): stop_fix() return jsonify({"status": "ok", "message": "Disconnected"}) @app.route("/order", methods=["POST"]) def order(): if not fix_app or not fix_app.connected: log("âš ī¸ Tried to send while disconnected") return jsonify({"status": "error", "message": "Not connected"}), 400 data = request.get_json(force=True) or {} side = data.get("side", "buy") side_tag = "1" if side.lower() == "buy" else "2" symbol = data.get("symbol", "FOO") qty = float(data.get("qty", 100)) price = float(data.get("price", 10)) fix_app.send_order(side_tag, symbol, qty, price) return jsonify({"status": "ok", "message": "Order sent"}) @app.route("/messages") def messages_route(): with _msgs_lock: msgs = list(reversed(_messages)) return jsonify(msgs) # --- Configurable --- CONFIG_FILE = os.getenv("FIX_CONFIG", "client.cfg") PORT = int(os.getenv("UI_PORT", "5002")) if __name__ == "__main__": app.run(host="0.0.0.0", port=PORT, debug=False, use_reloader=False)