Add READ_ONLY_API environment variable and enhance trading error handling

This commit is contained in:
coddard
2026-05-19 02:41:00 +03:00
parent 32537ef6e2
commit 22ac0da394
5 changed files with 114 additions and 2 deletions
+62
View File
@@ -24,13 +24,66 @@ class ChartState:
def __init__(self):
self.latest_tick = None
self.current_ticker = None
self.current_symbol = None
self.current_minute = None
self.current_ohlc = None
self.history_poll_task = None
self.last_realtime_update = 0.0
chart_state = ChartState()
async def _stop_history_poll_task() -> None:
task = chart_state.history_poll_task
chart_state.history_poll_task = None
if task is None or task.done():
return
task.cancel()
try:
await task
except asyncio.CancelledError:
pass
async def _history_fallback_loop(symbol: str, contract) -> None:
ib = dependencies.get_ib()
try:
while chart_state.current_symbol == symbol:
# Only fall back when real-time bars have not updated recently.
if chart_state.last_realtime_update and (
time.monotonic() - chart_state.last_realtime_update
) < 20:
await asyncio.sleep(15)
continue
try:
bars = await ib.reqHistoricalDataAsync(
contract,
endDateTime="",
durationStr="1 D",
barSizeSetting="1 min",
whatToShow="TRADES",
useRTH=True,
formatDate=1,
)
if bars:
last_bar = bars[-1]
chart_state.latest_tick = {
"time": int(last_bar.date.timestamp()),
"open": last_bar.open,
"high": last_bar.high,
"low": last_bar.low,
"close": last_bar.close,
}
except Exception as exc:
logger.warning(f"Historical fallback update failed for {symbol}: {exc}")
await asyncio.sleep(15)
except asyncio.CancelledError:
pass
@router.get("/", response_class=HTMLResponse)
async def index(request: Request, symbol: str = ""):
return templates.TemplateResponse(request, "index.html", {"symbol": symbol})
@@ -115,8 +168,13 @@ async def subscribe(request: Request):
except Exception as e:
print(f"Error cancelling real-time bars: {e}")
await _stop_history_poll_task()
chart_state.current_symbol = symbol
chart_state.latest_tick = None
chart_state.current_minute = None
chart_state.current_ohlc = None
chart_state.last_realtime_update = 0.0
ticker = ib.reqRealTimeBars(qualified[0], barSize=5, whatToShow="TRADES", useRTH=True)
chart_state.current_ticker = ticker
@@ -152,9 +210,13 @@ async def subscribe(request: Request):
"close": bar.close,
}
chart_state.last_realtime_update = time.monotonic()
chart_state.latest_tick = chart_state.current_ohlc.copy()
ticker.updateEvent += on_bar
chart_state.history_poll_task = asyncio.create_task(
_history_fallback_loop(symbol, qualified[0])
)
return {"status": "ok", "symbol": symbol}
except Exception as e:
+4
View File
@@ -1,3 +1,5 @@
import os
from fastapi import APIRouter
from fastapi.responses import JSONResponse
@@ -10,6 +12,7 @@ router = APIRouter()
async def health():
ib_connected = False
account = None
trading_mode = os.getenv("TRADING_MODE", "unknown").split("#", 1)[0].strip() or "unknown"
try:
ib_inst = dependencies.get_ib()
ib_connected = ib_inst.isConnected()
@@ -30,6 +33,7 @@ async def health():
{
"status": "ok" if (ib_connected and db_ok) else "degraded",
"ib_connected": ib_connected,
"trading_mode": trading_mode,
"account": account,
"db_ok": db_ok,
"version": "3.0",
+44 -1
View File
@@ -4,7 +4,7 @@ import os
from datetime import datetime, timezone
from fastapi import APIRouter, HTTPException, Request
from fastapi.responses import HTMLResponse
from fastapi.responses import HTMLResponse, JSONResponse
from fastapi.templating import Jinja2Templates
from ib_async import LimitOrder, MarketOrder, Stock
@@ -34,6 +34,18 @@ def log_trade(symbol, action, quantity, price, cursor, conn, lock):
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()
@@ -145,9 +157,40 @@ async def webhook(request: Request):
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: