from datetime import datetime, timezone def _base_state(): return { "nifty_units": 0.0, "gold_units": 0.0, "total_invested": 0.0, "last_run": None, "last_sip_ts": None, } class _Broker: def __init__(self, payload): self.payload = payload def refresh_order_status(self, order): merged = dict(order) merged.update(self.payload) return merged def test_reconciliation_applies_late_fill_once(monkeypatch): import indian_paper_trading_strategy.engine.execution as execution logical_time = datetime(2026, 4, 9, 4, 0, tzinfo=timezone.utc) checked_at = datetime(2026, 4, 9, 4, 1, tzinfo=timezone.utc) state = _base_state() saved = {} pending_orders = [ { "local_order_id": "order-1", "logical_time": logical_time, "broker": "ZERODHA", "symbol": "NIFTYBEES.NS", "side": "BUY", "requested_qty": 1.0, "requested_price": 250.0, "status": "PENDING", "created_at": checked_at, } ] cycle_orders = [ { "local_order_id": "order-1", "symbol": "NIFTYBEES.NS", "side": "BUY", "requested_qty": 1.0, "filled_qty": 1.0, "accounted_fill_qty": 1.0, "requested_price": 250.0, "average_price": 251.0, "status": execution.LOCAL_ORDER_FILLED, "needs_reconciliation": False, } ] monkeypatch.setattr(execution, "_normalize_now", lambda value: value) monkeypatch.setattr(execution, "run_with_retry", lambda op, retries=None, delay=None: op(object(), None)) monkeypatch.setattr(execution, "_list_orders_to_reconcile", lambda cur, checked_before: list(pending_orders)) monkeypatch.setattr(execution, "load_state", lambda mode=None, cur=None, for_update=False: state) monkeypatch.setattr(execution, "save_state", lambda snapshot, **kwargs: saved.update(snapshot)) monkeypatch.setattr(execution, "insert_engine_event", lambda *args, **kwargs: None) monkeypatch.setattr(execution, "log_event", lambda *args, **kwargs: None) monkeypatch.setattr(execution, "event_exists", lambda *args, **kwargs: False) monkeypatch.setattr( execution, "_upsert_broker_order_state", lambda cur, broker_name, logical_time, order, checked_at: { "local_order_id": "order-1", "symbol": "NIFTYBEES.NS", "requested_qty": 1.0, "filled_qty": 1.0, "requested_price": 250.0, "average_price": 251.0, "status": execution.LOCAL_ORDER_FILLED, "accounted_fill_qty": 0.0, "delta_fill_qty": 1.0, "needs_reconciliation": False, }, ) monkeypatch.setattr(execution, "_mark_accounted_fill", lambda *args, **kwargs: None) monkeypatch.setattr(execution, "_load_cycle_order_rows", lambda cur, logical_time: list(cycle_orders)) result = execution.reconcile_live_orders( broker=_Broker({"status": "FILLED", "filled_qty": 1.0, "average_price": 251.0}), mode="LIVE", now_ts=checked_at, ) assert result["blocked"] is False assert state["nifty_units"] == 1.0 assert state["total_invested"] == 251.0 assert state["last_sip_ts"] == checked_at.isoformat() assert saved["last_sip_ts"] == checked_at.isoformat() def test_partial_fill_remains_partial_and_does_not_advance_cycle(monkeypatch): import indian_paper_trading_strategy.engine.execution as execution logical_time = datetime(2026, 4, 9, 4, 0, tzinfo=timezone.utc) checked_at = datetime(2026, 4, 9, 4, 1, tzinfo=timezone.utc) state = _base_state() monkeypatch.setattr(execution, "_normalize_now", lambda value: value) monkeypatch.setattr(execution, "run_with_retry", lambda op, retries=None, delay=None: op(object(), None)) monkeypatch.setattr( execution, "_list_orders_to_reconcile", lambda cur, checked_before: [ { "local_order_id": "order-2", "logical_time": logical_time, "broker": "ZERODHA", "symbol": "GOLDBEES.NS", "side": "BUY", "requested_qty": 2.0, "requested_price": 100.0, "status": "PENDING", "created_at": checked_at, } ], ) monkeypatch.setattr(execution, "load_state", lambda mode=None, cur=None, for_update=False: state) monkeypatch.setattr(execution, "save_state", lambda *args, **kwargs: None) monkeypatch.setattr(execution, "insert_engine_event", lambda *args, **kwargs: None) monkeypatch.setattr(execution, "log_event", lambda *args, **kwargs: None) monkeypatch.setattr(execution, "event_exists", lambda *args, **kwargs: False) monkeypatch.setattr( execution, "_upsert_broker_order_state", lambda cur, broker_name, logical_time, order, checked_at: { "local_order_id": "order-2", "symbol": "GOLDBEES.NS", "requested_qty": 2.0, "filled_qty": 1.0, "requested_price": 100.0, "average_price": 101.0, "status": execution.LOCAL_ORDER_PARTIAL, "accounted_fill_qty": 0.0, "delta_fill_qty": 1.0, "needs_reconciliation": False, }, ) monkeypatch.setattr(execution, "_mark_accounted_fill", lambda *args, **kwargs: None) monkeypatch.setattr( execution, "_load_cycle_order_rows", lambda cur, logical_time: [ { "local_order_id": "order-2", "symbol": "GOLDBEES.NS", "requested_qty": 2.0, "filled_qty": 1.0, "requested_price": 100.0, "average_price": 101.0, "status": execution.LOCAL_ORDER_PARTIAL, "needs_reconciliation": False, } ], ) result = execution.reconcile_live_orders( broker=_Broker({"status": "CANCELLED", "filled_qty": 1.0, "average_price": 101.0}), mode="LIVE", now_ts=checked_at, ) assert result["blocked"] is False assert state["gold_units"] == 1.0 assert state["last_sip_ts"] is None assert state["last_run"] == checked_at.isoformat() def test_rejected_order_does_not_mark_cycle_executed(monkeypatch): import indian_paper_trading_strategy.engine.execution as execution logical_time = datetime(2026, 4, 9, 4, 0, tzinfo=timezone.utc) checked_at = datetime(2026, 4, 9, 4, 1, tzinfo=timezone.utc) state = _base_state() monkeypatch.setattr(execution, "_normalize_now", lambda value: value) monkeypatch.setattr(execution, "run_with_retry", lambda op, retries=None, delay=None: op(object(), None)) monkeypatch.setattr( execution, "_list_orders_to_reconcile", lambda cur, checked_before: [ { "local_order_id": "order-3", "logical_time": logical_time, "broker": "ZERODHA", "symbol": "NIFTYBEES.NS", "side": "BUY", "requested_qty": 1.0, "requested_price": 250.0, "status": "PENDING", "created_at": checked_at, } ], ) monkeypatch.setattr(execution, "load_state", lambda mode=None, cur=None, for_update=False: state) monkeypatch.setattr(execution, "save_state", lambda *args, **kwargs: None) monkeypatch.setattr(execution, "insert_engine_event", lambda *args, **kwargs: None) monkeypatch.setattr(execution, "log_event", lambda *args, **kwargs: None) monkeypatch.setattr(execution, "event_exists", lambda *args, **kwargs: False) monkeypatch.setattr( execution, "_upsert_broker_order_state", lambda cur, broker_name, logical_time, order, checked_at: { "local_order_id": "order-3", "symbol": "NIFTYBEES.NS", "requested_qty": 1.0, "filled_qty": 0.0, "requested_price": 250.0, "average_price": 0.0, "status": execution.LOCAL_ORDER_REJECTED, "accounted_fill_qty": 0.0, "delta_fill_qty": 0.0, "needs_reconciliation": False, }, ) monkeypatch.setattr(execution, "_mark_accounted_fill", lambda *args, **kwargs: None) monkeypatch.setattr( execution, "_load_cycle_order_rows", lambda cur, logical_time: [ { "local_order_id": "order-3", "symbol": "NIFTYBEES.NS", "requested_qty": 1.0, "filled_qty": 0.0, "requested_price": 250.0, "average_price": 0.0, "status": execution.LOCAL_ORDER_REJECTED, "needs_reconciliation": False, } ], ) result = execution.reconcile_live_orders( broker=_Broker({"status": "REJECTED", "filled_qty": 0.0}), mode="LIVE", now_ts=checked_at, ) assert result["blocked"] is False assert state["nifty_units"] == 0.0 assert state["last_sip_ts"] is None assert state["last_run"] == checked_at.isoformat() def test_reconciliation_is_repeat_safe(monkeypatch): import indian_paper_trading_strategy.engine.execution as execution logical_time = datetime(2026, 4, 9, 4, 0, tzinfo=timezone.utc) first_check = datetime(2026, 4, 9, 4, 1, tzinfo=timezone.utc) second_check = datetime(2026, 4, 9, 4, 2, tzinfo=timezone.utc) state = _base_state() upserts = [ { "local_order_id": "order-4", "symbol": "NIFTYBEES.NS", "requested_qty": 1.0, "filled_qty": 1.0, "requested_price": 250.0, "average_price": 250.0, "status": execution.LOCAL_ORDER_FILLED, "accounted_fill_qty": 0.0, "delta_fill_qty": 1.0, "needs_reconciliation": False, }, { "local_order_id": "order-4", "symbol": "NIFTYBEES.NS", "requested_qty": 1.0, "filled_qty": 1.0, "requested_price": 250.0, "average_price": 250.0, "status": execution.LOCAL_ORDER_FILLED, "accounted_fill_qty": 1.0, "delta_fill_qty": 0.0, "needs_reconciliation": False, }, ] monkeypatch.setattr(execution, "_normalize_now", lambda value: value) monkeypatch.setattr(execution, "run_with_retry", lambda op, retries=None, delay=None: op(object(), None)) monkeypatch.setattr( execution, "_list_orders_to_reconcile", lambda cur, checked_before: [ { "local_order_id": "order-4", "logical_time": logical_time, "broker": "ZERODHA", "symbol": "NIFTYBEES.NS", "side": "BUY", "requested_qty": 1.0, "requested_price": 250.0, "status": "PENDING", "created_at": first_check, } ], ) monkeypatch.setattr(execution, "load_state", lambda mode=None, cur=None, for_update=False: state) monkeypatch.setattr(execution, "save_state", lambda *args, **kwargs: None) monkeypatch.setattr(execution, "insert_engine_event", lambda *args, **kwargs: None) monkeypatch.setattr(execution, "log_event", lambda *args, **kwargs: None) monkeypatch.setattr(execution, "event_exists", lambda *args, **kwargs: False) monkeypatch.setattr(execution, "_mark_accounted_fill", lambda *args, **kwargs: None) monkeypatch.setattr(execution, "_load_cycle_order_rows", lambda cur, logical_time: [ { "local_order_id": "order-4", "symbol": "NIFTYBEES.NS", "requested_qty": 1.0, "filled_qty": 1.0, "requested_price": 250.0, "average_price": 250.0, "status": execution.LOCAL_ORDER_FILLED, "needs_reconciliation": False, } ]) monkeypatch.setattr( execution, "_upsert_broker_order_state", lambda cur, broker_name, logical_time, order, checked_at: upserts.pop(0), ) broker = _Broker({"status": "FILLED", "filled_qty": 1.0, "average_price": 250.0}) execution.reconcile_live_orders(broker=broker, mode="LIVE", now_ts=first_check) execution.reconcile_live_orders(broker=broker, mode="LIVE", now_ts=second_check) assert state["nifty_units"] == 1.0 assert state["total_invested"] == 250.0