@ -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
CONFIRM_SECS = 30
for attempt in range ( 1 , MAX_ATTEMPTS + 1 ) :
if attempt > 1 :
bars = self . _broker . get_latest_bars ( [ sym ] )
if sym not in bars :
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 :
try :
order = self . _broker . submit_market_buy ( sym , qty )
order = self . _broker . submit_market_buy ( sym , qty )
for _ in range ( 5 ) :
except Exception as e :
time . sleep ( 1 )
logger . warning ( " parking_buy_submit_failed " , error = str ( e ) , attempt = attempt )
filled = self . _broker . get_order ( order . id )
if attempt < MAX_ATTEMPTS :
if filled and filled . filled_avg_price :
await asyncio . sleep ( 5 )
avg_price = filled . filled_avg_price
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 (
self . _state . save_parking_state (
session_id , sym , today , qty , avg_price , avg_price * qty ,
session_id , sym , today , qty , filled_price, filled _price * qty ,
peak_price = avg_price , gate_in_sgov = 1 if target == " sgov " else 0 ,
peak_price = filled _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 ( ) , filled _price, qty ,
)
)
logger . info ( " parking_filled " , symbol = sym , qty = qty , price = round ( avg_price , 2 ) )
logger . info ( " parking_filled " , symbol = sym , qty = qty , price = round ( filled _price, 2 ) )
return
return
logger . warning ( " parking_buy_timeout " , symbol = sym , order_id = order . id )
except Exception as e :
logger . warning ( " parking_buy_timeout " , symbol = sym , order_id = order . id , attempt = attempt )
logger . warning ( " parking_buy_failed " , error = str ( e ) )
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 (
session_id , sym , today , qty , o . filled_avg_price , o . filled_avg_price * qty ,
peak_price = o . filled_avg_price , gate_in_sgov = 1 if target == " sgov " else 0 ,
committed_target = target ,
)
self . _state . open_trade (
session_id , sym , " parking " , None ,
today . isoformat ( ) , o . filled_avg_price , qty ,
)
logger . info ( " parking_filled_after_cancel_fail " , symbol = sym )
return
if attempt < MAX_ATTEMPTS :
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