Skip to content

Commit fa20754

Browse files
Pigbibicodex
andauthored
fix: hand off lifecycle preflight baselines (#233)
* fix: hand off lifecycle preflight baselines Co-Authored-By: Codex <noreply@openai.com> * chore: rerun review gate Co-Authored-By: Codex <noreply@openai.com> * chore: verify review availability handling Co-Authored-By: Codex <noreply@openai.com> * chore: rerun lifecycle review gate Co-Authored-By: Codex <noreply@openai.com> --------- Co-authored-by: Codex <noreply@openai.com>
1 parent 17278db commit fa20754

4 files changed

Lines changed: 85 additions & 7 deletions

File tree

.github/workflows/reusable-drift-check.yml

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,10 @@ on:
1616
required: false
1717
type: string
1818
default: "3.11"
19+
quant_platform_kit_ref:
20+
required: false
21+
type: string
22+
default: "main"
1923
ai_gateway_service_url:
2024
required: false
2125
type: string
@@ -31,6 +35,10 @@ on:
3135
required: false
3236
type: string
3337
default: ""
38+
lifecycle_preflight_artifact:
39+
required: false
40+
type: string
41+
default: ""
3442
secrets:
3543
codex_audit_service_url:
3644
required: false
@@ -131,7 +139,7 @@ jobs:
131139
uses: actions/checkout@v6
132140
with:
133141
repository: QuantStrategyLab/QuantPlatformKit
134-
ref: ${{ github.workflow_sha }}
142+
ref: ${{ inputs.quant_platform_kit_ref }}
135143
path: external/QuantPlatformKit
136144

137145
- name: Set up Python
@@ -146,6 +154,13 @@ jobs:
146154
python -m pip install -e . pandas
147155
python -m pip install --no-deps -e external/QuantPlatformKit
148156
157+
- name: Download lifecycle preflight artifact
158+
if: inputs.lifecycle_preflight_artifact != ''
159+
uses: actions/download-artifact@v4
160+
with:
161+
name: ${{ inputs.lifecycle_preflight_artifact }}
162+
path: ${{ env.LIFECYCLE_LOCAL_ROOT }}
163+
149164
- name: Build lifecycle performance snapshots
150165
run: quant-lifecycle monitor --domain ${{ inputs.strategy_domain }}
151166

src/quant_platform_kit/strategy_lifecycle/performance_store.py

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -102,13 +102,21 @@ def _cloud_path(self, key: str) -> str:
102102
clean = _clean_key(key)
103103
return f"{prefix}/{clean}" if prefix else clean
104104

105+
def _cloud_uri(self, key: str) -> str:
106+
return f"gs://{self.cloud_bucket}/{self._cloud_path(key)}"
107+
108+
def _cloud_key(self, uri: str) -> str:
109+
bucket_prefix = f"gs://{self.cloud_bucket}/"
110+
path = uri[len(bucket_prefix) :] if uri.startswith(bucket_prefix) else uri
111+
prefix = self.cloud_prefix.strip("/")
112+
return path[len(prefix) + 1 :] if prefix and path.startswith(f"{prefix}/") else path
113+
105114
def _read_cloud_json(self, key: str) -> dict[str, Any] | None:
106115
if not self.cloud_bucket:
107116
return None
108117
try:
109118
store = self._object_store()
110-
path = self._cloud_path(key)
111-
raw = store.read_bytes(self.cloud_bucket, path)
119+
raw = store.read_bytes(self._cloud_uri(key))
112120
data = json.loads(raw.decode("utf-8"))
113121
return data if isinstance(data, Mapping) else None
114122
except Exception:
@@ -118,15 +126,17 @@ def _write_cloud_json(self, key: str, payload: Mapping[str, Any]) -> None:
118126
if not self.cloud_bucket:
119127
return
120128
store = self._object_store()
121-
path = self._cloud_path(key)
122-
store.write_bytes(self.cloud_bucket, path, json.dumps(payload, ensure_ascii=False, indent=2).encode("utf-8"))
129+
store.write_bytes(
130+
self._cloud_uri(key),
131+
json.dumps(payload, ensure_ascii=False, indent=2).encode("utf-8"),
132+
)
123133

124134
def _list_cloud_keys(self, prefix: str) -> list[str]:
125135
if not self.cloud_bucket:
126136
return []
127137
try:
128138
store = self._object_store()
129-
return store.list_keys(self.cloud_bucket, self._cloud_path(prefix))
139+
return [self._cloud_key(uri) for uri in store.list(self._cloud_uri(prefix))]
130140
except Exception:
131141
return []
132142

tests/test_lifecycle_performance_store.py

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from pathlib import Path
88
from unittest.mock import patch
99

10+
from quant_platform_kit.strategy_lifecycle.contracts import BacktestResult
1011
from quant_platform_kit.strategy_lifecycle.performance_store import (
1112
DEFAULT_LOCAL_ROOT,
1213
PerformanceStore,
@@ -26,6 +27,53 @@ def test_from_env_uses_default_local_root_without_override(self) -> None:
2627
store = PerformanceStore.from_env()
2728
self.assertEqual(store.local_root, DEFAULT_LOCAL_ROOT)
2829

30+
def test_cloud_backtest_storage_uses_uri_object_store_contract(self) -> None:
31+
class UriObjectStore:
32+
def __init__(self) -> None:
33+
self.objects: dict[str, bytes] = {}
34+
35+
def read_bytes(self, uri: str) -> bytes:
36+
return self.objects[uri]
37+
38+
def write_bytes(self, uri: str, data: bytes, content_type: str = "application/octet-stream") -> str:
39+
self.objects[uri] = data
40+
return uri
41+
42+
def list(self, prefix: str) -> list[str]:
43+
return sorted(uri for uri in self.objects if uri.startswith(prefix))
44+
45+
with tempfile.TemporaryDirectory() as tmp:
46+
object_store = UriObjectStore()
47+
store = PerformanceStore(
48+
cloud_bucket="lifecycle-bucket",
49+
cloud_prefix="production",
50+
local_root=Path(tmp),
51+
)
52+
result = BacktestResult(
53+
strategy_profile="global_etf_rotation",
54+
domain="us_equity",
55+
param_set_id="baseline",
56+
params={},
57+
param_version=1,
58+
sharpe_ratio=1.0,
59+
calmar_ratio=1.0,
60+
max_drawdown=-0.1,
61+
cagr=0.2,
62+
volatility=0.2,
63+
win_rate=0.55,
64+
)
65+
with patch(
66+
"quant_platform_kit.strategy_lifecycle.performance_store.get_object_store",
67+
return_value=object_store,
68+
):
69+
store.save_backtest_result(result)
70+
keys = store._list_cloud_keys("backtest/us_equity/global_etf_rotation/")
71+
payload = store._read_cloud_json(keys[0])
72+
73+
self.assertEqual(len(keys), 1)
74+
self.assertTrue(keys[0].startswith("backtest/us_equity/global_etf_rotation/"))
75+
self.assertEqual(payload["strategy_profile"], "global_etf_rotation")
76+
2977

3078
if __name__ == "__main__":
3179
unittest.main()

tests/test_reusable_drift_workflow.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,11 @@ def test_reusable_drift_workflow_enforces_lifecycle_preflight() -> None:
1515
assert "snapshot_repository_token:" in workflow
1616
assert "snapshot_checkout_path:" in workflow
1717
assert "ai_gateway_service_url:" in workflow
18+
assert "quant_platform_kit_ref:" in workflow
1819
assert "lifecycle_performance_bucket:" in workflow
1920
assert "caller_event_name:" in workflow
2021
assert "caller_pr_head_repository:" in workflow
22+
assert "lifecycle_preflight_artifact:" in workflow
2123
assert "codex_audit_service_url:" in workflow
2224
assert 'python-version: ${{ inputs.python_version }}' in workflow
2325
assert "LIFECYCLE_PERFORMANCE_BUCKET: ${{ inputs.lifecycle_performance_bucket || vars.LIFECYCLE_PERFORMANCE_BUCKET || '' }}" in workflow
@@ -38,7 +40,10 @@ def test_reusable_drift_workflow_enforces_lifecycle_preflight() -> None:
3840
assert 'repository: ${{ inputs.snapshot_repository }}' in workflow
3941
assert 'path: ${{ inputs.snapshot_checkout_path }}' in workflow
4042
assert 'token: ${{ secrets.snapshot_repository_token || github.token }}' in workflow
41-
assert 'ref: ${{ github.workflow_sha }}' in workflow
43+
assert 'ref: ${{ inputs.quant_platform_kit_ref }}' in workflow
44+
assert "Download lifecycle preflight artifact" in workflow
45+
assert "actions/download-artifact@v4" in workflow
46+
assert "name: ${{ inputs.lifecycle_preflight_artifact }}" in workflow
4247
assert 'GH_TOKEN: ${{ github.token }}' in workflow
4348
assert 'os.environ["CODEX_AUDIT_ORG"] = owner' in workflow
4449
assert 'os.environ["CODEX_AUDIT_ORCHESTRATOR_REPO"] = repository' in workflow

0 commit comments

Comments
 (0)