@ -446,6 +446,35 @@ class PaperTradingEngine:
)
return None , reason
def _poll_exit_fill (
self , order_id : str , symbol : str , timeout_sec : float = 30.0
) - > float | None :
""" Poll a submitted close/exit order until `filled_avg_price` is populated.
Returns the actual Alpaca fill price or None on timeout . Callers must
handle the None case ( fall back to simulated price and log drift ) .
"""
deadline = time . time ( ) + timeout_sec
while time . time ( ) < deadline :
try :
o = self . _broker . get_order ( order_id )
if o and o . filled_avg_price :
return float ( o . filled_avg_price )
if o and o . status in ( " canceled " , " rejected " , " expired " ) :
logger . warning (
" paper_engine_exit_terminal " ,
symbol = symbol , order_id = order_id , alpaca_status = o . status ,
)
return None
except Exception as exc :
logger . debug ( " paper_engine_poll_exit_retry " , order_id = order_id , error = str ( exc ) )
time . sleep ( 1.0 )
logger . warning (
" paper_engine_exit_fill_timeout " ,
symbol = symbol , order_id = order_id , timeout_sec = timeout_sec ,
)
return None
def _is_market_open ( self ) - > bool :
""" Return True if US equity market is currently open.
@ -583,11 +612,24 @@ class PaperTradingEngine:
close_qty = None
if filled_trade . shares < alpaca_pos . qty :
close_qty = filled_trade . shares
self . _broker . close_position ( sym , qty = close_qty , fill_price = filled_trade . exit_price )
close_order = self . _broker . close_position ( sym , qty = close_qty )
actual_fill = self . _poll_exit_fill ( close_order . id , sym )
if actual_fill is not None :
exit_price_used = actual_fill
drift = actual_fill - filled_trade . exit_price
if abs ( drift ) / max ( abs ( filled_trade . exit_price ) , 1e-9 ) > 0.005 :
logger . warning (
" paper_engine_exit_fill_drift " ,
symbol = sym , simulated = filled_trade . exit_price ,
actual = actual_fill , drift_pct = round ( 100 * drift / filled_trade . exit_price , 2 ) ,
)
else :
exit_price_used = filled_trade . exit_price
logger . info (
" paper_engine_exit " ,
symbol = sym ,
reason = filled_trade . exit_reason . value ,
exit_price = exit_price_used ,
pnl = filled_trade . net_pnl ,
)
except Exception as exc :
@ -595,6 +637,15 @@ class PaperTradingEngine:
continue
self . _state . close_strategy_state ( session_id , sym )
if exit_price_used != filled_trade . exit_price :
direction_sign = - 1.0 if ( ss . trade_direction or " long " ) == " short " else 1.0
actual_pnl = (
( exit_price_used - alpaca_pos . avg_entry_price )
* filled_trade . shares
* direction_sign
)
else :
actual_pnl = filled_trade . net_pnl
self . _state . close_trade (
session_id = session_id ,
symbol = sym ,
@ -603,14 +654,14 @@ class PaperTradingEngine:
entry_date = ss . entry_date ,
exit_date = today . isoformat ( ) ,
entry_price = alpaca_pos . avg_entry_price ,
exit_price = filled_trade. exit_price,
exit_price = exit_price_used ,
exit_reason = filled_trade . exit_reason . value ,
shares = filled_trade . shares ,
net_pnl = filled_trade. net _pnl,
net_pnl = actual _pnl,
r_multiple = filled_trade . r_multiple ,
holding_days = ss . days_held ,
)
net_pnl_today + = filled_trade. net _pnl
net_pnl_today + = actual _pnl
# Update consecutive losses / cooldown
if filled_trade . net_pnl < 0 :
@ -2313,7 +2364,11 @@ class PaperTradingEngine:
""" 모니터링 루프에서 포지션 청산 처리. """
session_id = self . _session . session_id
try :
self . _broker . close_position ( pos . symbol , fill_price = price )
close_order = self . _broker . close_position ( pos . symbol )
actual_fill = self . _poll_exit_fill ( close_order . id , pos . symbol )
exit_price_used = actual_fill if actual_fill is not None else price
direction_sign = - 1.0 if ( ss . trade_direction or " long " ) == " short " else 1.0
actual_pnl = ( exit_price_used - pos . avg_entry_price ) * pos . qty * direction_sign
self . _state . close_strategy_state ( session_id , pos . symbol )
self . _state . close_trade (
session_id = session_id ,
@ -2323,10 +2378,10 @@ class PaperTradingEngine:
entry_date = ss . entry_date ,
exit_date = dt . date . today ( ) . isoformat ( ) ,
entry_price = pos . avg_entry_price ,
exit_price = price,
exit_price = exit_ price_used ,
exit_reason = reason ,
shares = pos . qty ,
net_pnl = ( price - pos . avg_entry_price ) * pos . qty ,
net_pnl = actual_pnl ,
r_multiple = 0.0 ,
holding_days = ss . days_held ,
)
@ -2468,10 +2523,24 @@ class PaperTradingEngine:
if filled_trade is not None :
is_partial = filled_trade . shares < alpaca_pos . qty
try :
self . _broker . close_position ( sym , qty = filled_trade . shares if is_partial else None , fill_price = filled_trade . exit_price )
close_order = self . _broker . close_position (
sym , qty = filled_trade . shares if is_partial else None
)
actual_fill = self . _poll_exit_fill ( close_order . id , sym )
exit_price_used = actual_fill if actual_fill is not None else filled_trade . exit_price
if actual_fill is not None :
drift = actual_fill - filled_trade . exit_price
if abs ( drift ) / max ( abs ( filled_trade . exit_price ) , 1e-9 ) > 0.005 :
logger . warning (
" paper_engine_exit_fill_drift " ,
symbol = sym , simulated = filled_trade . exit_price ,
actual = actual_fill ,
drift_pct = round ( 100 * drift / filled_trade . exit_price , 2 ) ,
)
logger . info (
" paper_engine_exit " ,
symbol = sym , reason = filled_trade . exit_reason . value , pnl = filled_trade . net_pnl ,
symbol = sym , reason = filled_trade . exit_reason . value ,
exit_price = exit_price_used , pnl = filled_trade . net_pnl ,
partial = is_partial ,
)
except Exception as exc :
@ -2489,17 +2558,26 @@ class PaperTradingEngine:
)
else :
self . _state . close_strategy_state ( session_id , sym )
if exit_price_used != filled_trade . exit_price :
direction_sign = - 1.0 if ( ss . trade_direction or " long " ) == " short " else 1.0
actual_pnl = (
( exit_price_used - alpaca_pos . avg_entry_price )
* filled_trade . shares
* direction_sign
)
else :
actual_pnl = filled_trade . net_pnl
self . _state . close_trade (
session_id = session_id , symbol = sym ,
engine_id = ss . engine_id ,
capital_bucket_id = self . _get_strategy_state_capital_bucket_id ( ss ) ,
entry_date = ss . entry_date , exit_date = today . isoformat ( ) ,
entry_price = alpaca_pos . avg_entry_price , exit_price = filled_trade . exit_price ,
entry_price = alpaca_pos . avg_entry_price , exit_price = exit_price_used ,
exit_reason = filled_trade . exit_reason . value , shares = filled_trade . shares ,
net_pnl = filled_trade . net_pnl , r_multiple = filled_trade . r_multiple ,
net_pnl = actual _pnl, r_multiple = filled_trade . r_multiple ,
holding_days = ss . days_held ,
)
if filled_trade. net _pnl < 0 :
if actual _pnl < 0 :
session_st . consecutive_losses + = 1
streak = self . _config . risk . cooldown_after_loss_streak
if streak > 0 and session_st . consecutive_losses > = streak :
@ -2509,8 +2587,8 @@ class PaperTradingEngine:
session_st . consecutive_losses = 0
exits . append ( {
" symbol " : sym , " reason " : filled_trade . exit_reason . value ,
" pnl " : filled_trade. net _pnl, " r_multiple " : filled_trade . r_multiple ,
" shares " : filled_trade . shares , " exit_price " : filled_trade. exit_price,
" pnl " : actual _pnl, " r_multiple " : filled_trade . r_multiple ,
" shares " : filled_trade . shares , " exit_price " : exit_price_used ,
} )
else :
self . _state . update_strategy_state (