SIP_GoldBees_Backend/backend/tests/test_execution_claims.py

121 lines
4.0 KiB
Python

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"}