from __future__ import annotations import threading from datetime import datetime, timezone from indian_paper_trading_strategy.engine import execution, ledger def test_claim_execution_window_allows_only_one_winner(monkeypatch): claims: set[tuple[str, str, datetime]] = set() lock = threading.Lock() class FakeCursor: def __init__(self): self._result = None def execute(self, sql, params): assert "INSERT INTO execution_claim" in sql key = (params[1], params[2], params[4]) with lock: if key in claims: self._result = None else: claims.add(key) self._result = (params[0],) def fetchone(self): return self._result monkeypatch.setattr(ledger, "get_context", lambda user_id=None, run_id=None: ("user-a", "run-a")) monkeypatch.setattr(ledger, "run_with_retry", lambda op, retries=None, delay=None: op(FakeCursor(), None)) logical_time = datetime(2026, 4, 8, 9, 15, tzinfo=timezone.utc) results: list[bool] = [] def attempt(): results.append(ledger.claim_execution_window(logical_time, mode="LIVE")) threads = [threading.Thread(target=attempt) for _ in range(2)] for thread in threads: thread.start() for thread in threads: thread.join() assert sorted(results) == [False, True] def test_try_execute_sip_live_emits_one_order_batch_when_claim_is_lost(monkeypatch): claim_lock = threading.Lock() claimed = False order_calls: list[tuple[str, str, float]] = [] class FakeBroker: external_orders = True def get_funds(self): return {"cash": 10_000.0} def place_order(self, symbol, side, qty, price, logical_time=None): order_calls.append((symbol, side, qty)) return { "id": f"{symbol}-{len(order_calls)}", "symbol": symbol, "side": side, "status": "COMPLETE", "filled_qty": qty, "average_price": price, "price": price, } def fake_claim_execution_window(logical_time, *, mode=None, cur=None, user_id=None, run_id=None): nonlocal claimed with claim_lock: if claimed: return False claimed = True return True monkeypatch.setattr(execution, "load_state", lambda *args, **kwargs: {"last_sip_ts": None, "last_run": None}) monkeypatch.setattr( execution, "_resolve_timing", lambda state, now_ts, sip_interval: (True, None, datetime(2026, 4, 8, 9, 15, tzinfo=timezone.utc)), ) monkeypatch.setattr(execution, "event_exists", lambda *args, **kwargs: False) monkeypatch.setattr(execution, "claim_execution_window", fake_claim_execution_window) monkeypatch.setattr(execution, "log_event", lambda *args, **kwargs: None) monkeypatch.setattr(execution, "run_with_retry", lambda op, retries=None, delay=None: op(object(), None)) monkeypatch.setattr( execution, "_finalize_live_execution", lambda **kwargs: ({"last_run": kwargs["now_ts"].isoformat()}, bool(kwargs["orders"])), ) results: list[bool] = [] def attempt(): _state, executed = execution._try_execute_sip_live( now=datetime(2026, 4, 8, 14, 45, tzinfo=timezone.utc), market_open=True, sip_interval=120, sip_amount=1000.0, sp_price=125.0, gd_price=250.0, eq_w=0.5, gd_w=0.5, broker=FakeBroker(), mode="LIVE", ) results.append(executed) threads = [threading.Thread(target=attempt) for _ in range(2)] for thread in threads: thread.start() for thread in threads: thread.join() assert sorted(results) == [False, True] assert len(order_calls) == 2 assert {call[0] for call in order_calls} == {"NIFTYBEES.NS", "GOLDBEES.NS"}