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}
|
- ${COMPOSE_APP_ENV_FILE:-docker.env}
|
||||||
environment:
|
environment:
|
||||||
- TRADING_MODE=${TRADING_MODE:-paper}
|
- TRADING_MODE=${TRADING_MODE:-paper}
|
||||||
|
- READ_ONLY_API=${READ_ONLY_API:-no}
|
||||||
- TWOFA_TIMEOUT_ACTION=${TWOFA_TIMEOUT_ACTION:-restart}
|
- TWOFA_TIMEOUT_ACTION=${TWOFA_TIMEOUT_ACTION:-restart}
|
||||||
- AUTO_RESTART_TIME=${AUTO_RESTART_TIME:-11:59 PM}
|
- AUTO_RESTART_TIME=${AUTO_RESTART_TIME:-11:59 PM}
|
||||||
- RELOGIN_AFTER_TWOFA_TIMEOUT=${RELOGIN_AFTER_TWOFA_TIMEOUT:-yes}
|
- RELOGIN_AFTER_TWOFA_TIMEOUT=${RELOGIN_AFTER_TWOFA_TIMEOUT:-yes}
|
||||||
|
|||||||
@@ -24,13 +24,66 @@ class ChartState:
|
|||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.latest_tick = None
|
self.latest_tick = None
|
||||||
self.current_ticker = None
|
self.current_ticker = None
|
||||||
|
self.current_symbol = None
|
||||||
self.current_minute = None
|
self.current_minute = None
|
||||||
self.current_ohlc = None
|
self.current_ohlc = None
|
||||||
|
self.history_poll_task = None
|
||||||
|
self.last_realtime_update = 0.0
|
||||||
|
|
||||||
|
|
||||||
chart_state = ChartState()
|
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)
|
@router.get("/", response_class=HTMLResponse)
|
||||||
async def index(request: Request, symbol: str = ""):
|
async def index(request: Request, symbol: str = ""):
|
||||||
return templates.TemplateResponse(request, "index.html", {"symbol": symbol})
|
return templates.TemplateResponse(request, "index.html", {"symbol": symbol})
|
||||||
@@ -115,8 +168,13 @@ async def subscribe(request: Request):
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Error cancelling real-time bars: {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_minute = None
|
||||||
chart_state.current_ohlc = None
|
chart_state.current_ohlc = None
|
||||||
|
chart_state.last_realtime_update = 0.0
|
||||||
|
|
||||||
ticker = ib.reqRealTimeBars(qualified[0], barSize=5, whatToShow="TRADES", useRTH=True)
|
ticker = ib.reqRealTimeBars(qualified[0], barSize=5, whatToShow="TRADES", useRTH=True)
|
||||||
chart_state.current_ticker = ticker
|
chart_state.current_ticker = ticker
|
||||||
@@ -152,9 +210,13 @@ async def subscribe(request: Request):
|
|||||||
"close": bar.close,
|
"close": bar.close,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
chart_state.last_realtime_update = time.monotonic()
|
||||||
chart_state.latest_tick = chart_state.current_ohlc.copy()
|
chart_state.latest_tick = chart_state.current_ohlc.copy()
|
||||||
|
|
||||||
ticker.updateEvent += on_bar
|
ticker.updateEvent += on_bar
|
||||||
|
chart_state.history_poll_task = asyncio.create_task(
|
||||||
|
_history_fallback_loop(symbol, qualified[0])
|
||||||
|
)
|
||||||
|
|
||||||
return {"status": "ok", "symbol": symbol}
|
return {"status": "ok", "symbol": symbol}
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import os
|
||||||
|
|
||||||
from fastapi import APIRouter
|
from fastapi import APIRouter
|
||||||
from fastapi.responses import JSONResponse
|
from fastapi.responses import JSONResponse
|
||||||
|
|
||||||
@@ -10,6 +12,7 @@ router = APIRouter()
|
|||||||
async def health():
|
async def health():
|
||||||
ib_connected = False
|
ib_connected = False
|
||||||
account = None
|
account = None
|
||||||
|
trading_mode = os.getenv("TRADING_MODE", "unknown").split("#", 1)[0].strip() or "unknown"
|
||||||
try:
|
try:
|
||||||
ib_inst = dependencies.get_ib()
|
ib_inst = dependencies.get_ib()
|
||||||
ib_connected = ib_inst.isConnected()
|
ib_connected = ib_inst.isConnected()
|
||||||
@@ -30,6 +33,7 @@ async def health():
|
|||||||
{
|
{
|
||||||
"status": "ok" if (ib_connected and db_ok) else "degraded",
|
"status": "ok" if (ib_connected and db_ok) else "degraded",
|
||||||
"ib_connected": ib_connected,
|
"ib_connected": ib_connected,
|
||||||
|
"trading_mode": trading_mode,
|
||||||
"account": account,
|
"account": account,
|
||||||
"db_ok": db_ok,
|
"db_ok": db_ok,
|
||||||
"version": "3.0",
|
"version": "3.0",
|
||||||
|
|||||||
+44
-1
@@ -4,7 +4,7 @@ import os
|
|||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
|
|
||||||
from fastapi import APIRouter, HTTPException, Request
|
from fastapi import APIRouter, HTTPException, Request
|
||||||
from fastapi.responses import HTMLResponse
|
from fastapi.responses import HTMLResponse, JSONResponse
|
||||||
from fastapi.templating import Jinja2Templates
|
from fastapi.templating import Jinja2Templates
|
||||||
from ib_async import LimitOrder, MarketOrder, Stock
|
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}")
|
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)
|
@router.get("/tradelog", response_class=HTMLResponse)
|
||||||
async def tradelog(request: Request):
|
async def tradelog(request: Request):
|
||||||
cursor = dependencies.get_db_cursor()
|
cursor = dependencies.get_db_cursor()
|
||||||
@@ -145,9 +157,40 @@ async def webhook(request: Request):
|
|||||||
primary_trade = trades_list[0]
|
primary_trade = trades_list[0]
|
||||||
for _ in range(60):
|
for _ in range(60):
|
||||||
await asyncio.sleep(0.5)
|
await asyncio.sleep(0.5)
|
||||||
|
status = _normalized_trade_status(primary_trade)
|
||||||
|
if status in {"ValidationError", "Cancelled", "ApiCancelled", "Inactive"}:
|
||||||
|
break
|
||||||
if primary_trade.isDone():
|
if primary_trade.isDone():
|
||||||
break
|
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:
|
if not primary_trade.isDone() or not primary_trade.fills:
|
||||||
for t in trades_list:
|
for t in trades_list:
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -28,7 +28,9 @@
|
|||||||
grid: { vertLines: { color: '#f0f0f0' }, horzLines: { color: '#f0f0f0' } },
|
grid: { vertLines: { color: '#f0f0f0' }, horzLines: { color: '#f0f0f0' } },
|
||||||
timeScale: { timeVisible: true, secondsVisible: false }
|
timeScale: { timeVisible: true, secondsVisible: false }
|
||||||
});
|
});
|
||||||
const series = chart.addCandlestickSeries();
|
const series = typeof chart.addCandlestickSeries === 'function'
|
||||||
|
? chart.addCandlestickSeries()
|
||||||
|
: chart.addSeries(LightweightCharts.CandlestickSeries, {});
|
||||||
|
|
||||||
let eventSource = null;
|
let eventSource = null;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user