import asyncio import logging import os from datetime import datetime, timezone from fastapi import APIRouter, HTTPException, Request from fastapi.responses import HTMLResponse from fastapi.templating import Jinja2Templates from ib_async import LimitOrder, MarketOrder, Stock import dependencies from risk_manager import RiskManager router = APIRouter() templates = Jinja2Templates(directory="templates") logger = logging.getLogger(__name__) WEBHOOK_SECRET = os.getenv("WEBHOOK_SECRET", "your_super_secret_string_123") risk = RiskManager() def log_trade(symbol, action, quantity, price, cursor, conn, lock): timestamp = int(datetime.now(timezone.utc).timestamp()) with lock: cursor.execute( "INSERT INTO trades (timestamp, symbol, action, quantity, price) VALUES (?, ?, ?, ?, ?)", (timestamp, symbol, action, quantity, price), ) conn.commit() logger.info(f"Logged trade: {action} {quantity} {symbol} @ ${price:.2f}") @router.get("/tradelog", response_class=HTMLResponse) async def tradelog(request: Request): cursor = dependencies.get_db_cursor() cursor.execute( "SELECT timestamp, symbol, action, quantity, price FROM trades ORDER BY timestamp DESC" ) rows = cursor.fetchall() trades = [ { "timestamp": datetime.fromtimestamp(ts).strftime("%Y-%m-%d %H:%M:%S"), "symbol": symbol, "action": action, "quantity": quantity, "price": price, } for ts, symbol, action, quantity, price in rows ] return templates.TemplateResponse(request, "tradelog.html", {"trades": trades}) @router.get("/risk/status") async def risk_status(): try: ib = dependencies.get_ib() cursor = dependencies.get_db_cursor() positions = ib.positions() open_count = len([p for p in positions if p.position != 0]) return { "limits": { "max_daily_loss": risk.max_daily_loss, "max_positions": risk.max_positions, "max_order_value": risk.max_order_value, }, "current": { "open_positions": open_count, "daily_loss_check_passed": risk.check_daily_loss(cursor), }, } except Exception as exc: return {"error": str(exc)} @router.post("/webhook") async def webhook(request: Request): try: data = await request.json() if data.get("secret") != WEBHOOK_SECRET: raise HTTPException(status_code=403, detail="Invalid secret") symbol = data["symbol"].upper() strategy = data["strategy"] action = strategy["order_action"].upper() quantity = strategy["order_contracts"] if action not in ["BUY", "SELL"]: raise HTTPException(status_code=400, detail=f"Invalid action: {action}") order_type = data.get("order_type", "MARKET").upper() limit_price = data.get("limit_price") take_profit = data.get("take_profit") stop_loss = data.get("stop_loss") # Validate order parameters before any IB calls if order_type == "LIMIT" and limit_price is None: raise HTTPException(status_code=400, detail="limit_price required for LIMIT orders") if order_type == "BRACKET" and ( limit_price is None or take_profit is None or stop_loss is None ): raise HTTPException( status_code=400, detail="limit_price, take_profit, and stop_loss required for BRACKET orders", ) ib = dependencies.get_ib() cursor = dependencies.get_db_cursor() conn = dependencies.get_db_conn() lock = dependencies.get_db_lock() est_price = float(limit_price) if limit_price else 1.0 risk_result = await risk.run_all_checks(ib, cursor, quantity, est_price) if not risk_result["passed"]: raise HTTPException( status_code=429, detail=f"Risk check failed: {', '.join(risk_result['failed_checks'])}", ) # Qualify contract before placing order (C3) raw_contract = Stock(symbol, "SMART", "USD") qualified = await ib.qualifyContractsAsync(raw_contract) if not qualified: raise HTTPException( status_code=400, detail=f"Could not qualify contract for {symbol}" ) contract = qualified[0] if order_type == "LIMIT": order = LimitOrder(action, quantity, float(limit_price)) trades_list = [ib.placeOrder(contract, order)] elif order_type == "BRACKET": bracket = ib.bracketOrder( action, quantity, float(limit_price), float(take_profit), float(stop_loss) ) trades_list = [ib.placeOrder(contract, leg) for leg in bracket] else: order = MarketOrder(action, quantity) trades_list = [ib.placeOrder(contract, order)] primary_trade = trades_list[0] for _ in range(60): await asyncio.sleep(0.5) if primary_trade.isDone(): break if not primary_trade.isDone() or not primary_trade.fills: for t in trades_list: try: ib.cancelOrder(t.order) except Exception: pass raise HTTPException(status_code=504, detail="Order did not fill in time") fill_price = primary_trade.fills[0].execution.price log_trade(symbol, action, quantity, fill_price, cursor, conn, lock) return { "status": "success", "symbol": symbol, "action": action, "quantity": quantity, "order_type": order_type, "fill_price": fill_price, } except HTTPException: raise except Exception as exc: logger.error(f"Webhook error: {exc}") raise HTTPException(status_code=500, detail=str(exc))