From ae7fba35d3b5ca8349f5a067bb6e981ddd4b005b Mon Sep 17 00:00:00 2001 From: Pigbibi <20649888+Pigbibi@users.noreply.github.com> Date: Wed, 19 Aug 2026 00:20:43 +0800 Subject: [PATCH 1/2] chore(deps): align QSL 2026.08 runtime bundle Co-Authored-By: Codex --- pyproject.toml | 6 +++--- qsl.toml | 6 +++--- tests/test_qsl_metadata.py | 2 +- uv.lock | 10 +++++----- 4 files changed, 12 insertions(+), 12 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 837981e..9cea5a0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -18,8 +18,8 @@ dependencies = [ "pytest", "pytz", "requests", - "quant-platform-kit @ git+https://github.com/QuantStrategyLab/QuantPlatformKit.git@92458590a463e7219f0369a3505031ee74414135", - "us-equity-strategies @ git+https://github.com/QuantStrategyLab/UsEquityStrategies.git@17ddb86c72d44b2c7b78ba7a10d8f71b21180166", + "quant-platform-kit @ git+https://github.com/QuantStrategyLab/QuantPlatformKit.git@3acab1923a97b805b077c85c6c19657be0143bac", + "us-equity-strategies @ git+https://github.com/QuantStrategyLab/UsEquityStrategies.git@be1a2c9f7c388d4dd78bda6c2dd7ccad0d4b13b4", ] license = "MIT" authors = [ @@ -82,5 +82,5 @@ show_missing = true [tool.uv] override-dependencies = [ - "quant-platform-kit @ git+https://github.com/QuantStrategyLab/QuantPlatformKit.git@92458590a463e7219f0369a3505031ee74414135", + "quant-platform-kit @ git+https://github.com/QuantStrategyLab/QuantPlatformKit.git@3acab1923a97b805b077c85c6c19657be0143bac", ] diff --git a/qsl.toml b/qsl.toml index 177dd2b..197f5b7 100644 --- a/qsl.toml +++ b/qsl.toml @@ -5,8 +5,8 @@ upgrade_ring = "ring_d" allow_legacy = false [qsl.requires] -quant_platform_kit = "92458590a463e7219f0369a3505031ee74414135" -us_equity_strategies = "17ddb86c72d44b2c7b78ba7a10d8f71b21180166" +quant_platform_kit = "3acab1923a97b805b077c85c6c19657be0143bac" +us_equity_strategies = "be1a2c9f7c388d4dd78bda6c2dd7ccad0d4b13b4" [qsl.compat] -bundle = "2026.07.4" +bundle = "2026.08.0" diff --git a/tests/test_qsl_metadata.py b/tests/test_qsl_metadata.py index 1d4cd51..dab5a3d 100644 --- a/tests/test_qsl_metadata.py +++ b/tests/test_qsl_metadata.py @@ -12,7 +12,7 @@ def test_qsl_metadata_has_runtime_platform_fields() -> None: assert qsl["tier"] == "runtime" assert qsl["upgrade_ring"] == "ring_d" assert qsl.get("repo") == "FirstradePlatform" - assert qsl["compat"]["bundle"] == "2026.07.4" + assert qsl["compat"]["bundle"] == "2026.08.0" requires = qsl["requires"] assert "quant_platform_kit" in requires assert "us_equity_strategies" in requires diff --git a/uv.lock b/uv.lock index 292d618..78ed1a2 100644 --- a/uv.lock +++ b/uv.lock @@ -17,7 +17,7 @@ resolution-markers = [ ] [manifest] -overrides = [{ name = "quant-platform-kit", git = "https://github.com/QuantStrategyLab/QuantPlatformKit.git?rev=92458590a463e7219f0369a3505031ee74414135" }] +overrides = [{ name = "quant-platform-kit", git = "https://github.com/QuantStrategyLab/QuantPlatformKit.git?rev=3acab1923a97b805b077c85c6c19657be0143bac" }] [[package]] name = "blinker" @@ -446,10 +446,10 @@ requires-dist = [ { name = "pytest", marker = "extra == 'test'" }, { name = "pytest-cov", marker = "extra == 'test'" }, { name = "pytz" }, - { name = "quant-platform-kit", git = "https://github.com/QuantStrategyLab/QuantPlatformKit.git?rev=92458590a463e7219f0369a3505031ee74414135" }, + { name = "quant-platform-kit", git = "https://github.com/QuantStrategyLab/QuantPlatformKit.git?rev=3acab1923a97b805b077c85c6c19657be0143bac" }, { name = "requests" }, { name = "ruff", marker = "extra == 'test'" }, - { name = "us-equity-strategies", git = "https://github.com/QuantStrategyLab/UsEquityStrategies.git?rev=17ddb86c72d44b2c7b78ba7a10d8f71b21180166" }, + { name = "us-equity-strategies", git = "https://github.com/QuantStrategyLab/UsEquityStrategies.git?rev=be1a2c9f7c388d4dd78bda6c2dd7ccad0d4b13b4" }, ] provides-extras = ["test"] @@ -1102,7 +1102,7 @@ wheels = [ [[package]] name = "quant-platform-kit" version = "0.10.0" -source = { git = "https://github.com/QuantStrategyLab/QuantPlatformKit.git?rev=92458590a463e7219f0369a3505031ee74414135#92458590a463e7219f0369a3505031ee74414135" } +source = { git = "https://github.com/QuantStrategyLab/QuantPlatformKit.git?rev=3acab1923a97b805b077c85c6c19657be0143bac#3acab1923a97b805b077c85c6c19657be0143bac" } [[package]] name = "requests" @@ -1237,7 +1237,7 @@ wheels = [ [[package]] name = "us-equity-strategies" version = "0.7.60" -source = { git = "https://github.com/QuantStrategyLab/UsEquityStrategies.git?rev=17ddb86c72d44b2c7b78ba7a10d8f71b21180166#17ddb86c72d44b2c7b78ba7a10d8f71b21180166" } +source = { git = "https://github.com/QuantStrategyLab/UsEquityStrategies.git?rev=be1a2c9f7c388d4dd78bda6c2dd7ccad0d4b13b4#be1a2c9f7c388d4dd78bda6c2dd7ccad0d4b13b4" } dependencies = [ { name = "pandas" }, { name = "pytz" }, From 1e7c7f389cc919097a7e419cee0108a6460d3511 Mon Sep 17 00:00:00 2001 From: Pigbibi <20649888+Pigbibi@users.noreply.github.com> Date: Mon, 24 Aug 2026 03:25:05 +0800 Subject: [PATCH 2/2] fix: claim Firstrade live runs atomically before orders Co-Authored-By: Codex --- application/rebalance_service.py | 16 +++++++++++- application/state_persistence.py | 9 +++++++ application/strategy_run_persistence.py | 31 +++++++++++++++++++++++ tests/test_rebalance_service.py | 6 +++++ tests/test_strategy_run_claim.py | 33 +++++++++++++++++++++++++ 5 files changed, 94 insertions(+), 1 deletion(-) create mode 100644 tests/test_strategy_run_claim.py diff --git a/application/rebalance_service.py b/application/rebalance_service.py index aee3fc9..bcd5fc6 100644 --- a/application/rebalance_service.py +++ b/application/rebalance_service.py @@ -30,6 +30,7 @@ from application.state_persistence import GcsStateStore, build_gcs_state_store_from_env from application.strategy_run_persistence import ( build_strategy_run_state, + claim_live_strategy_run, is_duplicate_live_run, persist_strategy_run_state, read_latest_strategy_run_state, @@ -512,13 +513,26 @@ def log_message(message: str) -> None: masked_account = mask_account_id(account) existing_run = None if persist_strategy_runs and not settings.dry_run_only: + claim_acquired = claim_live_strategy_run( + store=store, + account=masked_account, + strategy_profile=strategy_runtime.profile, + run_period=run_period, + now=now, + ) existing_run = read_latest_strategy_run_state( store=store, account=masked_account, strategy_profile=strategy_runtime.profile, run_period=run_period, ) - if is_duplicate_live_run(existing_run): + if not claim_acquired and existing_run is None: + existing_run = { + "stage": "PENDING_SUBMISSION", + "as_of": now.isoformat(), + "claim_only": True, + } + if not claim_acquired or is_duplicate_live_run(existing_run): duplicate_stage = str(existing_run.get("stage") or "NO_ACTION") duplicate_skipped_orders = [ { diff --git a/application/state_persistence.py b/application/state_persistence.py index 255fd09..1e636f3 100644 --- a/application/state_persistence.py +++ b/application/state_persistence.py @@ -70,6 +70,15 @@ def write_json(self, key: str, payload: dict[str, Any]) -> bool: raise StatePersistenceError(f"GCS write failed for {key}: {exc}") from exc return True + def create_json(self, key: str, payload: dict[str, Any]) -> bool: + """Atomically create a JSON object; return False if it already exists.""" + uri = self._object_uri(key) + data = json.dumps(payload, ensure_ascii=False, separators=(",", ":")) + try: + return bool(_object_store().create_text(uri, data, content_type="application/json")) + except Exception as exc: + raise StatePersistenceError(f"GCS atomic create failed for {key}: {exc}") from exc + def build_gcs_state_store_from_env( env: Callable[[str, str | None], str | None] = os.getenv, diff --git a/application/strategy_run_persistence.py b/application/strategy_run_persistence.py index 0143f74..8066d69 100644 --- a/application/strategy_run_persistence.py +++ b/application/strategy_run_persistence.py @@ -116,6 +116,37 @@ def strategy_run_history_key( ) +def strategy_run_claim_key( + *, account: str, strategy_profile: str, run_period: str, +) -> str: + """Permanent live claim key; a failed/unknown submission must remain blocked.""" + return ( + f"strategy-runs/claims/{safe_key(account)}/{safe_key(strategy_profile)}/" + f"{safe_key(run_period)}.json" + ) + + +def claim_live_strategy_run( + *, store: GcsStateStore, account: str, strategy_profile: str, + run_period: str, now: datetime | None = None, +) -> bool: + """Acquire the durable pre-order claim using object-store create-if-absent.""" + payload = { + "stage": "PENDING_SUBMISSION", + "account": account, + "strategy_profile": strategy_profile, + "run_period": run_period, + "as_of": (now or utcnow()).isoformat(), + "no_order_submitted": True, + } + return store.create_json( + strategy_run_claim_key( + account=account, strategy_profile=strategy_profile, run_period=run_period, + ), + payload, + ) + + def read_latest_strategy_run_state( *, store: GcsStateStore, diff --git a/tests/test_rebalance_service.py b/tests/test_rebalance_service.py index 7dced56..18b3182 100644 --- a/tests/test_rebalance_service.py +++ b/tests/test_rebalance_service.py @@ -157,6 +157,12 @@ def write_json(self, key, payload): self.writes.append((key, dict(payload))) return True + def create_json(self, key, payload): + if key in self.payloads: + return False + self.payloads[key] = dict(payload) + return True + def _latest_strategy_run_payloads(store: FakeStateStore) -> list[dict]: return [payload for key, payload in store.writes if key.endswith("latest.json")] diff --git a/tests/test_strategy_run_claim.py b/tests/test_strategy_run_claim.py new file mode 100644 index 0000000..845496b --- /dev/null +++ b/tests/test_strategy_run_claim.py @@ -0,0 +1,33 @@ +from application.strategy_run_persistence import ( + claim_live_strategy_run, + strategy_run_claim_key, +) + + +class AtomicFakeStore: + def __init__(self): + self.payloads = {} + + def create_json(self, key, payload): + if key in self.payloads: + return False + self.payloads[key] = dict(payload) + return True + + +def test_live_claim_is_create_only_and_permanent(): + store = AtomicFakeStore() + kwargs = { + "store": store, + "account": "****1234", + "strategy_profile": "tqqq_core", + "run_period": "2026-08", + } + + assert claim_live_strategy_run(**kwargs) is True + assert claim_live_strategy_run(**kwargs) is False + key = strategy_run_claim_key( + account="****1234", strategy_profile="tqqq_core", run_period="2026-08" + ) + assert store.payloads[key]["stage"] == "PENDING_SUBMISSION" + assert store.payloads[key]["no_order_submitted"] is True