Skip to content

Commit eeae241

Browse files
Pigbibicodex
andauthored
fix: claim Firstrade live runs atomically before orders (#249)
Co-authored-by: Codex <noreply@openai.com>
1 parent acf6047 commit eeae241

5 files changed

Lines changed: 94 additions & 1 deletion

File tree

application/rebalance_service.py

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
from application.state_persistence import GcsStateStore, build_gcs_state_store_from_env
3131
from application.strategy_run_persistence import (
3232
build_strategy_run_state,
33+
claim_live_strategy_run,
3334
is_duplicate_live_run,
3435
persist_strategy_run_state,
3536
read_latest_strategy_run_state,
@@ -512,13 +513,26 @@ def log_message(message: str) -> None:
512513
masked_account = mask_account_id(account)
513514
existing_run = None
514515
if persist_strategy_runs and not settings.dry_run_only:
516+
claim_acquired = claim_live_strategy_run(
517+
store=store,
518+
account=masked_account,
519+
strategy_profile=strategy_runtime.profile,
520+
run_period=run_period,
521+
now=now,
522+
)
515523
existing_run = read_latest_strategy_run_state(
516524
store=store,
517525
account=masked_account,
518526
strategy_profile=strategy_runtime.profile,
519527
run_period=run_period,
520528
)
521-
if is_duplicate_live_run(existing_run):
529+
if not claim_acquired and existing_run is None:
530+
existing_run = {
531+
"stage": "PENDING_SUBMISSION",
532+
"as_of": now.isoformat(),
533+
"claim_only": True,
534+
}
535+
if not claim_acquired or is_duplicate_live_run(existing_run):
522536
duplicate_stage = str(existing_run.get("stage") or "NO_ACTION")
523537
duplicate_skipped_orders = [
524538
{

application/state_persistence.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,15 @@ def write_json(self, key: str, payload: dict[str, Any]) -> bool:
7070
raise StatePersistenceError(f"GCS write failed for {key}: {exc}") from exc
7171
return True
7272

73+
def create_json(self, key: str, payload: dict[str, Any]) -> bool:
74+
"""Atomically create a JSON object; return False if it already exists."""
75+
uri = self._object_uri(key)
76+
data = json.dumps(payload, ensure_ascii=False, separators=(",", ":"))
77+
try:
78+
return bool(_object_store().create_text(uri, data, content_type="application/json"))
79+
except Exception as exc:
80+
raise StatePersistenceError(f"GCS atomic create failed for {key}: {exc}") from exc
81+
7382

7483
def build_gcs_state_store_from_env(
7584
env: Callable[[str, str | None], str | None] = os.getenv,

application/strategy_run_persistence.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,37 @@ def strategy_run_history_key(
116116
)
117117

118118

119+
def strategy_run_claim_key(
120+
*, account: str, strategy_profile: str, run_period: str,
121+
) -> str:
122+
"""Permanent live claim key; a failed/unknown submission must remain blocked."""
123+
return (
124+
f"strategy-runs/claims/{safe_key(account)}/{safe_key(strategy_profile)}/"
125+
f"{safe_key(run_period)}.json"
126+
)
127+
128+
129+
def claim_live_strategy_run(
130+
*, store: GcsStateStore, account: str, strategy_profile: str,
131+
run_period: str, now: datetime | None = None,
132+
) -> bool:
133+
"""Acquire the durable pre-order claim using object-store create-if-absent."""
134+
payload = {
135+
"stage": "PENDING_SUBMISSION",
136+
"account": account,
137+
"strategy_profile": strategy_profile,
138+
"run_period": run_period,
139+
"as_of": (now or utcnow()).isoformat(),
140+
"no_order_submitted": True,
141+
}
142+
return store.create_json(
143+
strategy_run_claim_key(
144+
account=account, strategy_profile=strategy_profile, run_period=run_period,
145+
),
146+
payload,
147+
)
148+
149+
119150
def read_latest_strategy_run_state(
120151
*,
121152
store: GcsStateStore,

tests/test_rebalance_service.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,12 @@ def write_json(self, key, payload):
157157
self.writes.append((key, dict(payload)))
158158
return True
159159

160+
def create_json(self, key, payload):
161+
if key in self.payloads:
162+
return False
163+
self.payloads[key] = dict(payload)
164+
return True
165+
160166

161167
def _latest_strategy_run_payloads(store: FakeStateStore) -> list[dict]:
162168
return [payload for key, payload in store.writes if key.endswith("latest.json")]

tests/test_strategy_run_claim.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
from application.strategy_run_persistence import (
2+
claim_live_strategy_run,
3+
strategy_run_claim_key,
4+
)
5+
6+
7+
class AtomicFakeStore:
8+
def __init__(self):
9+
self.payloads = {}
10+
11+
def create_json(self, key, payload):
12+
if key in self.payloads:
13+
return False
14+
self.payloads[key] = dict(payload)
15+
return True
16+
17+
18+
def test_live_claim_is_create_only_and_permanent():
19+
store = AtomicFakeStore()
20+
kwargs = {
21+
"store": store,
22+
"account": "****1234",
23+
"strategy_profile": "tqqq_core",
24+
"run_period": "2026-08",
25+
}
26+
27+
assert claim_live_strategy_run(**kwargs) is True
28+
assert claim_live_strategy_run(**kwargs) is False
29+
key = strategy_run_claim_key(
30+
account="****1234", strategy_profile="tqqq_core", run_period="2026-08"
31+
)
32+
assert store.payloads[key]["stage"] == "PENDING_SUBMISSION"
33+
assert store.payloads[key]["no_order_submitted"] is True

0 commit comments

Comments
 (0)