Fix: parking buy fill confirmation blocks event loop with 5s hard timeout

Make _parking_buy async (asyncio.sleep instead of time.sleep) and extend
confirmation from 5s to 30s per attempt with up to 3 retries on timeout.
On timeout, cancels the stale order before retrying with fresh price data.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
main
I Luk Kim 4 months ago
parent 0cae86aa87
commit 41462c9e2c

@ -5,6 +5,7 @@ Order execution (HOW to execute) is done via Alpaca Paper Trading API.
""" """
from __future__ import annotations from __future__ import annotations
import asyncio
import datetime as dt import datetime as dt
import json import json
import math import math
@ -1157,7 +1158,7 @@ class PaperTradingEngine:
# CASH PARKING: buy with idle cash (after all entries) # CASH PARKING: buy with idle cash (after all entries)
# ============================================================ # ============================================================
if self._config.risk.cash_parking_enabled and not parking_sold_today: if self._config.risk.cash_parking_enabled and not parking_sold_today:
self._parking_buy(session_id, today) await self._parking_buy(session_id, today)
summary = self._finalize_day(today, session_st, exits, entries, rejected, len(candidate_rows)) summary = self._finalize_day(today, session_st, exits, entries, rejected, len(candidate_rows))
summary["reconciliation"] = recon summary["reconciliation"] = recon
@ -1741,7 +1742,7 @@ class PaperTradingEngine:
session_cash = max(0.0, session_equity - session_mv) session_cash = max(0.0, session_equity - session_mv)
return session_equity, session_cash return session_equity, session_cash
def _parking_buy(self, session_id: str, today: dt.date) -> None: async def _parking_buy(self, session_id: str, today: dt.date) -> None:
"""Buy or top-up parking with idle cash after all entries are done.""" """Buy or top-up parking with idle cash after all entries are done."""
risk = self._config.risk risk = self._config.risk
parking_st = self._state.get_parking_state(session_id) parking_st = self._state.get_parking_state(session_id)
@ -1810,28 +1811,79 @@ class PaperTradingEngine:
if qty <= 0: if qty <= 0:
return return
logger.info("parking_buy", symbol=sym, qty=qty, price=round(price, 2)) MAX_ATTEMPTS = 3
try: CONFIRM_SECS = 30
order = self._broker.submit_market_buy(sym, qty)
for _ in range(5): for attempt in range(1, MAX_ATTEMPTS + 1):
time.sleep(1) if attempt > 1:
filled = self._broker.get_order(order.id) bars = self._broker.get_latest_bars([sym])
if filled and filled.filled_avg_price: if sym not in bars:
avg_price = filled.filled_avg_price return
price = bars[sym].close
if price <= 0 or investable < price:
return
qty = int(investable / price)
if qty <= 0:
return
logger.info("parking_buy", symbol=sym, qty=qty, price=round(price, 2), attempt=attempt)
try:
order = self._broker.submit_market_buy(sym, qty)
except Exception as e:
logger.warning("parking_buy_submit_failed", error=str(e), attempt=attempt)
if attempt < MAX_ATTEMPTS:
await asyncio.sleep(5)
continue
filled_price: float | None = None
for _ in range(CONFIRM_SECS):
await asyncio.sleep(1)
o = self._broker.get_order(order.id)
if o and o.filled_avg_price:
filled_price = o.filled_avg_price
break
if filled_price is None:
o = self._broker.get_order(order.id)
if o and o.filled_avg_price:
filled_price = o.filled_avg_price
if filled_price is not None:
self._state.save_parking_state(
session_id, sym, today, qty, filled_price, filled_price * qty,
peak_price=filled_price, gate_in_sgov=1 if target == "sgov" else 0,
committed_target=target,
)
self._state.open_trade(
session_id, sym, "parking", None,
today.isoformat(), filled_price, qty,
)
logger.info("parking_filled", symbol=sym, qty=qty, price=round(filled_price, 2))
return
logger.warning("parking_buy_timeout", symbol=sym, order_id=order.id, attempt=attempt)
try:
self._broker.cancel_order(order.id)
except Exception:
# cancel may fail if order filled between final check and cancel call
o = self._broker.get_order(order.id)
if o and o.filled_avg_price:
self._state.save_parking_state( self._state.save_parking_state(
session_id, sym, today, qty, avg_price, avg_price * qty, session_id, sym, today, qty, o.filled_avg_price, o.filled_avg_price * qty,
peak_price=avg_price, gate_in_sgov=1 if target == "sgov" else 0, peak_price=o.filled_avg_price, gate_in_sgov=1 if target == "sgov" else 0,
committed_target=target, committed_target=target,
) )
self._state.open_trade( self._state.open_trade(
session_id, sym, "parking", None, session_id, sym, "parking", None,
today.isoformat(), avg_price, qty, today.isoformat(), o.filled_avg_price, qty,
) )
logger.info("parking_filled", symbol=sym, qty=qty, price=round(avg_price, 2)) logger.info("parking_filled_after_cancel_fail", symbol=sym)
return return
logger.warning("parking_buy_timeout", symbol=sym, order_id=order.id)
except Exception as e: if attempt < MAX_ATTEMPTS:
logger.warning("parking_buy_failed", error=str(e)) await asyncio.sleep(5)
logger.error("parking_buy_all_attempts_failed", symbol=sym)
# ------------------------------------------------------------------ # # ------------------------------------------------------------------ #
# Phased execution: reaction_close / next_open / monitor # Phased execution: reaction_close / next_open / monitor
@ -2093,7 +2145,7 @@ class PaperTradingEngine:
# CASH PARKING: buy with remaining idle cash after entries # CASH PARKING: buy with remaining idle cash after entries
if self._config.risk.cash_parking_enabled and not parking_sold_today: if self._config.risk.cash_parking_enabled and not parking_sold_today:
self._parking_buy(session_id, today) await self._parking_buy(session_id, today)
summary = self._finalize_day(today, session_st, exits, entries, rejected, len(next_open_rows)) summary = self._finalize_day(today, session_st, exits, entries, rejected, len(next_open_rows))
summary["phase"] = phase summary["phase"] = phase

Loading…
Cancel
Save