Files
2026-05-18 00:31:08 +03:00

172 lines
5.9 KiB
Python

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))