initial commit
This commit is contained in:
@@ -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))
|
||||
Reference in New Issue
Block a user