initial commit

This commit is contained in:
coddard
2026-05-18 00:31:08 +03:00
commit 0096c8819b
26 changed files with 2851 additions and 0 deletions
+171
View File
@@ -0,0 +1,171 @@
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))