@@ -273,6 +273,8 @@ pub fn persist_checkpoint(storage: &Storage, engine: &GridEngine, phase: &str) {
273273}
274274
275275pub fn record_fill_ledger ( storage : & Storage , session_id : & str , fill : & grid_engine:: FillEvent , pnl : Decimal ) {
276+ // `pnl` is net of fees from the engine; ledger stores gross separately.
277+ let gross = pnl + fill. fee ;
276278 let notional = fill. price * fill. size ;
277279 let row = FillLedgerRow {
278280 session_id : session_id. to_string ( ) ,
@@ -291,7 +293,7 @@ pub fn record_fill_ledger(storage: &Storage, session_id: &str, fill: &grid_engin
291293 crossed : fill. crossed ,
292294 fee : fill. fee ,
293295 fee_token : fill. fee_token . clone ( ) ,
294- gross_closed_pnl : fill . closed_pnl . unwrap_or ( pnl ) ,
296+ gross_closed_pnl : gross ,
295297 position_before : None ,
296298 position_after : None ,
297299 source : "exchange" . into ( ) ,
@@ -301,6 +303,36 @@ pub fn record_fill_ledger(storage: &Storage, session_id: &str, fill: &grid_engin
301303 }
302304}
303305
306+ fn sort_fills_chronologically ( fills : & mut [ grid_engine:: FillEvent ] ) {
307+ fills. sort_by_key ( |f| f. exchange_time_ms . unwrap_or ( 0 ) ) ;
308+ }
309+
310+ /// Apply exchange fills in time order. Returns replenish intents from successful fills.
311+ fn apply_fills_to_engine (
312+ storage : & Storage ,
313+ engine : & mut GridEngine ,
314+ session_id : & str ,
315+ mut fills : Vec < grid_engine:: FillEvent > ,
316+ record_ledger : bool ,
317+ ) -> Vec < grid_engine:: OrderIntent > {
318+ sort_fills_chronologically ( & mut fills) ;
319+ let mut replenish = Vec :: new ( ) ;
320+ for fill in fills {
321+ match engine. on_fill ( fill. clone ( ) ) {
322+ Ok ( ( pnl, intent) ) => {
323+ if record_ledger {
324+ record_fill_ledger ( storage, session_id, & fill, pnl) ;
325+ }
326+ if let Some ( i) = intent {
327+ replenish. push ( i) ;
328+ }
329+ }
330+ Err ( e) => warn ! ( "fill apply failed: {e}" ) ,
331+ }
332+ }
333+ replenish
334+ }
335+
304336pub fn record_equity ( storage : & Storage , engine : & GridEngine ) {
305337 let snap = engine. snapshot ( ) ;
306338 let sid = engine. session_id ( ) . to_string ( ) ;
@@ -615,8 +647,8 @@ pub async fn execute_recenter(
615647}
616648
617649/// Sync engine (+ HL cache) open orders from exchange.
618- /// Local-only phantoms are treated as fills (so reverse replenish runs), then
619- /// missing grid levels are repaired up toward `target_resting` .
650+ /// Stale local orders missing on the exchange are dropped (not simulated as fills).
651+ /// Missing grid levels are repaired only when the book is fully in sync .
620652async fn sync_open_orders_from_exchange (
621653 st : & mut AppState ,
622654 symbol : & str ,
@@ -640,34 +672,58 @@ async fn sync_open_orders_from_exchange(
640672 let Some ( engine) = st. engine . as_mut ( ) else {
641673 return Ok ( vec ! [ ] ) ;
642674 } ;
643- let local = engine. live_orders ( ) . to_vec ( ) ;
644675 let session_id = engine. session_id ( ) . to_string ( ) ;
645676
646- let mut replenish = Vec :: new ( ) ;
677+ // Drain exchange fills we may have missed before inferring phantoms.
678+ let mut replenish = if st. mode == RunMode :: Simulation {
679+ vec ! [ ]
680+ } else {
681+ let live = engine. live_orders ( ) . to_vec ( ) ;
682+ if let Some ( hl) = st. hl . as_mut ( ) {
683+ hl. restore_tracked_orders ( & live) ;
684+ }
685+ let fills = st
686+ . hl
687+ . as_mut ( )
688+ . unwrap ( )
689+ . drain_fills ( )
690+ . await
691+ . unwrap_or_default ( ) ;
692+ apply_fills_to_engine ( & st. storage , engine, & session_id, fills, true )
693+ } ;
694+
695+ let local = engine. live_orders ( ) . to_vec ( ) ;
696+
697+ // Exchange says no open orders but we still track locals → desync (2nd instance,
698+ // API glitch). Do not phantom-fill, replace with empty, or repair — that triggers
699+ // a burst of replenish orders (see repair_hole after replace_open_orders([])).
700+ if exchange. is_empty ( ) && !local. is_empty ( ) {
701+ warn ! (
702+ "open-order sync {symbol}: exchange=0 local={} — skipping sync (desync)" ,
703+ local. len( )
704+ ) ;
705+ let _ = st. storage . record_event (
706+ "order_sync_desync" ,
707+ & format ! ( "{symbol}: exchange=0 local={} sync_skipped" , local. len( ) ) ,
708+ ) ;
709+ return Ok ( replenish) ;
710+ }
711+
647712 let local_only: Vec < _ > = local
648713 . iter ( )
649714 . filter ( |l| !exchange. iter ( ) . any ( |e| orders_same ( l, e) ) )
650715 . cloned ( )
651716 . collect ( ) ;
652717 for phantom in & local_only {
653- let fill = GridEngine :: synthetic_fill_from_order ( phantom) ;
654- match engine. on_fill ( fill. clone ( ) ) {
655- Ok ( ( pnl, intent) ) => {
656- record_fill_ledger ( & st. storage , & session_id, & fill, pnl) ;
657- warn ! (
658- "phantom order treated as fill {:?} {} @ {}" ,
659- fill. side, fill. size, fill. price
660- ) ;
661- let _ = st. storage . record_event (
662- "phantom_fill" ,
663- & format ! ( "{:?} {} @ {}" , fill. side, fill. size, fill. price) ,
664- ) ;
665- if let Some ( i) = intent {
666- replenish. push ( i) ;
667- }
668- }
669- Err ( e) => warn ! ( "phantom fill apply failed: {e}" ) ,
670- }
718+ // Drop stale local entries only — never simulate fills or replenish from phantoms.
719+ warn ! (
720+ "local order missing on exchange (not a fill): {:?} {} @ {}" ,
721+ phantom. side, phantom. size, phantom. price
722+ ) ;
723+ let _ = st. storage . record_event (
724+ "phantom_drop" ,
725+ & format ! ( "{:?} {} @ {}" , phantom. side, phantom. size, phantom. price) ,
726+ ) ;
671727 }
672728
673729 let local_after = engine. live_orders ( ) . to_vec ( ) ;
@@ -711,18 +767,27 @@ async fn sync_open_orders_from_exchange(
711767 let mid = engine
712768 . mid_price
713769 . unwrap_or_else ( || ( engine. active_bounds ( ) . 0 + engine. active_bounds ( ) . 1 ) / Decimal :: from ( 2 ) ) ;
714- match engine. repair_hole_intents ( mid) {
715- Ok ( extra) if !extra. is_empty ( ) => {
716- info ! (
717- "repairing {} missing grid level(s); open={} target={}" ,
718- extra. len( ) ,
719- engine. live_orders( ) . len( ) ,
720- engine. target_resting
721- ) ;
722- replenish. extend ( extra) ;
770+ // Only repair when exchange confirmed open orders — never fill the whole grid in one
771+ // burst after a partial desync (local_only non-empty but exchange had some orders).
772+ if local_only. is_empty ( ) {
773+ match engine. repair_hole_intents ( mid) {
774+ Ok ( extra) if !extra. is_empty ( ) => {
775+ info ! (
776+ "repairing {} missing grid level(s); open={} target={}" ,
777+ extra. len( ) ,
778+ engine. live_orders( ) . len( ) ,
779+ engine. target_resting
780+ ) ;
781+ replenish. extend ( extra) ;
782+ }
783+ Ok ( _) => { }
784+ Err ( e) => warn ! ( "repair_hole_intents: {e}" ) ,
723785 }
724- Ok ( _) => { }
725- Err ( e) => warn ! ( "repair_hole_intents: {e}" ) ,
786+ } else if !local_only. is_empty ( ) {
787+ warn ! (
788+ "open-order sync {symbol}: {} local-only order(s) dropped; skipping repair this tick" ,
789+ local_only. len( )
790+ ) ;
726791 }
727792
728793 if let Some ( hl) = st. hl . as_mut ( ) {
@@ -852,7 +917,7 @@ async fn sync_fills_and_position(
852917 symbol : & str ,
853918 session_id : & str ,
854919) -> Result < ( ) , String > {
855- let fills = if st. mode == RunMode :: Simulation {
920+ let mut fills = if st. mode == RunMode :: Simulation {
856921 st. sim
857922 . as_mut ( )
858923 . unwrap ( )
@@ -875,6 +940,7 @@ async fn sync_fills_and_position(
875940 . await
876941 . unwrap_or_default ( )
877942 } ;
943+ sort_fills_chronologically ( & mut fills) ;
878944 for fill in fills {
879945 if let Some ( engine) = st. engine . as_mut ( ) {
880946 match engine. on_fill ( fill. clone ( ) ) {
@@ -1132,7 +1198,7 @@ async fn process_fills_and_replenish(
11321198 symbol : & str ,
11331199 session_id : & str ,
11341200) -> Result < ( ) , String > {
1135- let fills = if st. mode == RunMode :: Simulation {
1201+ let mut fills = if st. mode == RunMode :: Simulation {
11361202 st. sim
11371203 . as_mut ( )
11381204 . unwrap ( )
@@ -1158,6 +1224,7 @@ async fn process_fills_and_replenish(
11581224
11591225 let mut replenish_intents = Vec :: new ( ) ;
11601226 let mut risk_exit: Option < String > = None ;
1227+ sort_fills_chronologically ( & mut fills) ;
11611228 for fill in fills {
11621229 match st. engine . as_mut ( ) . unwrap ( ) . on_fill ( fill. clone ( ) ) {
11631230 Ok ( ( pnl, replenish) ) => {
0 commit comments