172 lines
5.9 KiB
Python
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))
|