Skip to content

Commit 9b80032

Browse files
Pigbibicodex
andcommitted
feat: publish crypto lifecycle inputs
Co-Authored-By: Codex <noreply@openai.com>
1 parent 4e98f91 commit 9b80032

4 files changed

Lines changed: 172 additions & 0 deletions

File tree

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
name: Publish Lifecycle Inputs
2+
3+
on:
4+
schedule:
5+
- cron: "0 4 * * 1"
6+
workflow_dispatch:
7+
8+
permissions:
9+
contents: read
10+
11+
concurrency:
12+
group: ${{ github.workflow }}-${{ github.ref_name }}
13+
cancel-in-progress: false
14+
15+
jobs:
16+
publish:
17+
if: github.ref == format('refs/heads/{0}', github.event.repository.default_branch)
18+
runs-on: [self-hosted, Linux, X64]
19+
timeout-minutes: 60
20+
steps:
21+
- name: Checkout
22+
uses: actions/checkout@v6
23+
24+
- name: Set up Python
25+
uses: actions/setup-python@v6
26+
with:
27+
python-version: "3.11"
28+
29+
- name: Install dependencies
30+
run: |
31+
set -euo pipefail
32+
python -m pip install --upgrade pip
33+
requirements_file=requirements-lock.txt
34+
if [ ! -f "${requirements_file}" ]; then requirements_file=requirements.txt; fi
35+
python -m pip install -r "${requirements_file}"
36+
37+
- name: Download real Binance history
38+
run: |
39+
set -euo pipefail
40+
python scripts/download_history.py --top-liquid 30 --force-exchange-info
41+
42+
- name: Export production research inputs
43+
run: |
44+
set -euo pipefail
45+
python scripts/export_lifecycle_preflight_inputs.py \
46+
--universe-mode broad_liquid \
47+
--output-dir "${RUNNER_TEMP}/crypto-lifecycle-inputs"
48+
49+
- name: Upload lifecycle inputs
50+
uses: actions/upload-artifact@v7
51+
with:
52+
name: crypto-lifecycle-inputs-${{ github.run_id }}-${{ github.run_attempt }}
53+
path: ${{ runner.temp }}/crypto-lifecycle-inputs
54+
if-no-files-found: error
55+
retention-days: 30
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
#!/usr/bin/env python3
2+
from __future__ import annotations
3+
4+
import argparse
5+
import json
6+
import sys
7+
from pathlib import Path
8+
9+
import pandas as pd
10+
11+
PROJECT_ROOT = Path(__file__).resolve().parents[1]
12+
if str(PROJECT_ROOT) not in sys.path:
13+
sys.path.insert(0, str(PROJECT_ROOT))
14+
15+
from src.config import load_config
16+
from src.pipeline import run_research_pipeline
17+
18+
PANEL_COLUMNS = ("in_universe", "open", "final_score")
19+
COMBO_SYMBOLS = ("BTCUSDT", "ETHUSDT")
20+
21+
22+
def export_lifecycle_inputs(panel: pd.DataFrame, output_dir: Path) -> dict[str, object]:
23+
if list(panel.index.names) != ["date", "symbol"]:
24+
raise ValueError("research panel must use a date/symbol MultiIndex")
25+
missing = sorted(set((*PANEL_COLUMNS, "close")) - set(panel.columns))
26+
if missing:
27+
raise ValueError(f"research panel is missing columns: {', '.join(missing)}")
28+
29+
frame = panel.reset_index().copy()
30+
frame["date"] = pd.to_datetime(frame["date"], errors="coerce").dt.tz_localize(None).dt.normalize()
31+
frame["symbol"] = frame["symbol"].astype(str).str.strip().str.upper()
32+
lifecycle_panel = frame[["date", "symbol", *PANEL_COLUMNS]].dropna(subset=["date", "open", "final_score"])
33+
if lifecycle_panel.empty:
34+
raise ValueError("research panel has no scored lifecycle rows")
35+
36+
market_history = frame.loc[frame["symbol"].isin(COMBO_SYMBOLS), ["date", "symbol", "close"]].dropna()
37+
missing_combo = sorted(set(COMBO_SYMBOLS) - set(market_history["symbol"]))
38+
if missing_combo:
39+
raise ValueError(f"research panel is missing combo symbols: {', '.join(missing_combo)}")
40+
41+
output_dir.mkdir(parents=True, exist_ok=True)
42+
panel_path = output_dir / "research_panel.csv.gz"
43+
market_path = output_dir / "market_history.csv.gz"
44+
manifest_path = output_dir / "manifest.json"
45+
lifecycle_panel.to_csv(panel_path, index=False, compression="gzip")
46+
market_history.to_csv(market_path, index=False, compression="gzip")
47+
manifest = {
48+
"contract_version": "crypto.lifecycle_preflight.v1",
49+
"panel_rows": int(len(lifecycle_panel)),
50+
"panel_symbols": sorted(lifecycle_panel["symbol"].unique().tolist()),
51+
"market_rows": int(len(market_history)),
52+
"market_symbols": sorted(market_history["symbol"].unique().tolist()),
53+
"start_date": lifecycle_panel["date"].min().date().isoformat(),
54+
"end_date": lifecycle_panel["date"].max().date().isoformat(),
55+
}
56+
manifest_path.write_text(json.dumps(manifest, indent=2, sort_keys=True) + "\n", encoding="utf-8")
57+
return manifest
58+
59+
60+
def main() -> int:
61+
parser = argparse.ArgumentParser(description="Export real production research inputs for lifecycle drift preflight.")
62+
parser.add_argument("--config", default="config/default.yaml")
63+
parser.add_argument("--universe-mode", default="broad_liquid")
64+
parser.add_argument("--output-dir", type=Path, required=True)
65+
args = parser.parse_args()
66+
67+
config = load_config(args.config)
68+
result = run_research_pipeline(config, universe_mode=args.universe_mode)
69+
manifest = export_lifecycle_inputs(result["panel"], args.output_dir)
70+
print(manifest)
71+
return 0
72+
73+
74+
if __name__ == "__main__":
75+
raise SystemExit(main())
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
from __future__ import annotations
2+
3+
import json
4+
from pathlib import Path
5+
6+
import pandas as pd
7+
8+
from scripts.export_lifecycle_preflight_inputs import export_lifecycle_inputs
9+
10+
11+
def test_export_lifecycle_inputs_writes_real_panel_contract(tmp_path: Path) -> None:
12+
dates = pd.date_range("2024-01-01", periods=10, freq="D")
13+
symbols = ("BTCUSDT", "ETHUSDT", "SOLUSDT")
14+
index = pd.MultiIndex.from_product([dates, symbols], names=["date", "symbol"])
15+
panel = pd.DataFrame(index=index)
16+
panel["in_universe"] = True
17+
panel["open"] = range(1, len(panel) + 1)
18+
panel["close"] = panel["open"] + 0.5
19+
panel["final_score"] = 0.75
20+
21+
manifest = export_lifecycle_inputs(panel, tmp_path)
22+
23+
exported_panel = pd.read_csv(tmp_path / "research_panel.csv.gz")
24+
market_history = pd.read_csv(tmp_path / "market_history.csv.gz")
25+
persisted_manifest = json.loads((tmp_path / "manifest.json").read_text())
26+
assert set(exported_panel.columns) == {"date", "symbol", "in_universe", "open", "final_score"}
27+
assert set(market_history["symbol"]) == {"BTCUSDT", "ETHUSDT"}
28+
assert manifest == persisted_manifest
29+
assert manifest["contract_version"] == "crypto.lifecycle_preflight.v1"
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
from pathlib import Path
2+
3+
4+
def test_publish_lifecycle_inputs_workflow_uses_real_research_pipeline() -> None:
5+
workflow = (
6+
Path(__file__).resolve().parents[1] / ".github" / "workflows" / "publish-lifecycle-inputs.yml"
7+
).read_text(encoding="utf-8")
8+
9+
assert "scripts/download_history.py --top-liquid 30" in workflow
10+
assert "scripts/export_lifecycle_preflight_inputs.py" in workflow
11+
assert "--universe-mode broad_liquid" in workflow
12+
assert "crypto-lifecycle-inputs-${{ github.run_id }}-${{ github.run_attempt }}" in workflow
13+
assert "if-no-files-found: error" in workflow

0 commit comments

Comments
 (0)