Files
IBKR-Full-Stack-Trading-Das…/routers/trades.py
T

219 lines
7.7 KiB
Python

import asyncio
import logging
import os
from datetime import datetime, timezone
from fastapi import APIRouter, HTTPException, Request
from fastapi.responses import HTMLResponse, JSONResponse
from fastapi.templating import Jinja2Templates
from ib_async import LimitOrder, MarketOrder, Stock
from config_utils import read_env_or_file
import dependencies
from risk_manager import RiskManager
router = APIRouter()
templates = Jinja2Templates(directory="templates")
logger = logging.getLogger(__name__)
WEBHOOK_SECRET = (
read_env_or_file("WEBHOOK_SECRET", "your_super_secret_string_123")
or "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}")
def _latest_trade_message(trade) -> str:
for entry in reversed(getattr(trade, "log", []) or []):
message = getattr(entry, "message", "")
if message:
return message
return ""
def _normalized_trade_status(trade) -> str:
return (getattr(getattr(trade, "orderStatus", None), "status", "") or "").strip()
@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)
status = _normalized_trade_status(primary_trade)
if status in {"ValidationError", "Cancelled", "ApiCancelled", "Inactive"}:
break
if primary_trade.isDone():
break
status = _normalized_trade_status(primary_trade)
if status == "ValidationError":
for t in trades_list:
try:
ib.cancelOrder(t.order)
except Exception:
pass
detail = _latest_trade_message(primary_trade) or "Order was rejected by IBKR"
raise HTTPException(status_code=409, detail=detail)
if status in {"Cancelled", "ApiCancelled", "Inactive"} and not primary_trade.fills:
detail = _latest_trade_message(primary_trade) or f"Order became {status}"
raise HTTPException(status_code=409, detail=detail)
if status in {"PendingSubmit", "PreSubmitted", "Submitted"} and not primary_trade.fills:
return JSONResponse(
status_code=202,
content={
"status": "accepted",
"symbol": symbol,
"action": action,
"quantity": quantity,
"order_type": order_type,
"order_status": status,
"message": _latest_trade_message(primary_trade),
},
)
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))