Add READ_ONLY_API environment variable and enhance trading error handling
This commit is contained in:
@@ -7,6 +7,7 @@ services:
|
||||
- ${COMPOSE_APP_ENV_FILE:-docker.env}
|
||||
environment:
|
||||
- TRADING_MODE=${TRADING_MODE:-paper}
|
||||
- READ_ONLY_API=${READ_ONLY_API:-no}
|
||||
- TWOFA_TIMEOUT_ACTION=${TWOFA_TIMEOUT_ACTION:-restart}
|
||||
- AUTO_RESTART_TIME=${AUTO_RESTART_TIME:-11:59 PM}
|
||||
- RELOGIN_AFTER_TWOFA_TIMEOUT=${RELOGIN_AFTER_TWOFA_TIMEOUT:-yes}
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
@@ -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:
|
||||
|
||||
@@ -28,7 +28,9 @@
|
||||
grid: { vertLines: { color: '#f0f0f0' }, horzLines: { color: '#f0f0f0' } },
|
||||
timeScale: { timeVisible: true, secondsVisible: false }
|
||||
});
|
||||
const series = chart.addCandlestickSeries();
|
||||
const series = typeof chart.addCandlestickSeries === 'function'
|
||||
? chart.addCandlestickSeries()
|
||||
: chart.addSeries(LightweightCharts.CandlestickSeries, {});
|
||||
|
||||
let eventSource = null;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user