Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions .github/workflows/drift-check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ jobs:
uses: actions/checkout@v6
with:
repository: QuantStrategyLab/QuantPlatformKit
ref: b5654244aa5d08bce2b4b4f931436268d57216df
ref: 5488048cc0fc8e818b8c4be7a9729f6f1b5fdfdd
path: external/QuantPlatformKit

- name: Set up Python
Expand Down Expand Up @@ -194,7 +194,7 @@ jobs:
contents: read
issues: write
id-token: write
uses: QuantStrategyLab/QuantPlatformKit/.github/workflows/reusable-drift-check.yml@b5654244aa5d08bce2b4b4f931436268d57216df
uses: QuantStrategyLab/QuantPlatformKit/.github/workflows/reusable-drift-check.yml@5488048cc0fc8e818b8c4be7a9729f6f1b5fdfdd
with:
strategy_domain: cn_equity
caller_event_name: ${{ github.event_name }}
Expand All @@ -203,7 +203,7 @@ jobs:
snapshot_checkout_path: external/CnEquitySnapshotPipelines
snapshot_repository_ref: ${{ needs.preflight_backtests.outputs.snapshot_repository_ref }}
ai_gateway_service_url: ${{ vars.AI_GATEWAY_SERVICE_URL }}
quant_platform_kit_ref: b5654244aa5d08bce2b4b4f931436268d57216df
quant_platform_kit_ref: 5488048cc0fc8e818b8c4be7a9729f6f1b5fdfdd
lifecycle_preflight_artifact: lifecycle-preflight-${{ github.run_id }}-${{ github.run_attempt }}
secrets:
codex_audit_service_url: ${{ secrets.CODEX_AUDIT_SERVICE_URL }}
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ readme = "README.md"
requires-python = ">=3.11"
dependencies = [
"pandas>=2.0",
"quant-platform-kit @ git+https://github.com/QuantStrategyLab/QuantPlatformKit.git@b5654244aa5d08bce2b4b4f931436268d57216df",
"quant-platform-kit @ git+https://github.com/QuantStrategyLab/QuantPlatformKit.git@5488048cc0fc8e818b8c4be7a9729f6f1b5fdfdd",
]

[project.optional-dependencies]
Expand Down
2 changes: 1 addition & 1 deletion qsl.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,5 @@ upgrade_ring = "ring_b"
bundle = "2026.09.1"
requires = [
"pandas>=2.0",
"quant-platform-kit @ git+https://github.com/QuantStrategyLab/QuantPlatformKit.git@b5654244aa5d08bce2b4b4f931436268d57216df",
"quant-platform-kit @ git+https://github.com/QuantStrategyLab/QuantPlatformKit.git@5488048cc0fc8e818b8c4be7a9729f6f1b5fdfdd",
]
37 changes: 33 additions & 4 deletions src/cn_equity_strategies/backtest/index_etf_research_job.py
Original file line number Diff line number Diff line change
Expand Up @@ -386,7 +386,9 @@ def run_index_etf_research_job(
development_start: date, development_end: date, folds,
locked_oos_start: date, locked_oos_end: date, purge_days: int, embargo_days: int,
code_revision: str, ticket_dir, store_root, as_of, drift_score, source_revision,
record_shadow, sync_console, diagnose=None, pull_console=None, admit_new_research=None, read_pending_shadow=None,
record_shadow, sync_console, diagnose=None, summarize=None, pull_console=None, admit_new_research=None,
read_pending_shadow=None,
research_owner=None,
config=None, cost_model=None, evaluation_date=None,
) -> dict[str, Any]:
"""Owning watcher's frozen job -> QPK's existing durable cycle.
Expand Down Expand Up @@ -436,16 +438,43 @@ def run_index_etf_research_job(
)
# A dependency without the persisted interface fails before any remote call.
import inspect
if not {"research_identity", "admit_new_research", "read_pending_shadow"} <= set(inspect.signature(run_actionable_research_promotion).parameters):
required_qpk_parameters = {"research_identity", "admit_new_research", "read_pending_shadow"}
if summarize is not None:
required_qpk_parameters.add("summarize")
if research_owner is not None:
required_qpk_parameters.add("research_owner")
if not required_qpk_parameters <= set(inspect.signature(run_actionable_research_promotion).parameters):
raise ValueError("cn_research_persistent_qpk_required")
result = run_actionable_research_promotion(
promotion_kwargs = dict(
strategy_profile=PROFILE, domain="cn_equity", as_of=as_of, drift_score=drift_score,
source_revision=source_revision, evaluation_date=evaluation_date,
optimize=optimize, enforce_backtest_gates=gate, record_shadow=record_shadow, sync_console=sync_console,
diagnose=diagnose, pull_console=pull_console, research_identity=identity, ticket_dir=ticket_dir,
admit_new_research=admit_new_research, read_pending_shadow=read_pending_shadow,
research_owner=research_owner,
)
return {**result, "trial_records_path": str(trial_path), "experiment_store_root": str(experiment_store_root),
if summarize is not None:
promotion_kwargs["summarize"] = summarize
result = dict(run_actionable_research_promotion(**promotion_kwargs))
# QPK may reuse or archive a saved ticket before invoking either callback.
# Do not report the newly derived experiment hash as executed evidence in
# that case; the durable ticket is the only accepted identity.
ticket_path = result.get("ticket_path")
ticket_stem = Path(ticket_path).stem if isinstance(ticket_path, str) else ""
if not ticket_stem:
ticket = result.get("ticket")
ticket_id = ticket.get("ticket_id") if isinstance(ticket, dict) else ""
ticket_stem = str(ticket_id or "")
if ticket_stem:
if (ticket_stem.startswith("rpt_") and len(ticket_stem) == 68
and all(char in "0123456789abcdef" for char in ticket_stem[4:])):
result["research_key"] = ticket_stem[4:]
if result.get("reason") in {"saved_research_ticket_reused", "saved_research_ticket_terminal",
"research_scope_archived"} and not trial_path.exists():
result["trial_records_path"] = None
result["experiment_store_root"] = None
return {**result, "trial_records_path": result.get("trial_records_path", str(trial_path)),
"experiment_store_root": result.get("experiment_store_root", str(experiment_store_root)),
"research_identity": identity,
"benchmark_method": "monthly_target_510300_same_constraints",
"live_ready": False, "size_zero_required": True, "no_order": True}
6 changes: 3 additions & 3 deletions tests/test_drift_workflow_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@ def test_drift_workflow_wires_real_snapshot_history_and_preflight_bundle() -> No
assert '"conclusion": "success"' in workflow
assert "cn_etf_market_history.csv" in workflow
assert "repository: QuantStrategyLab/QuantPlatformKit" in workflow
assert "ref: b5654244aa5d08bce2b4b4f931436268d57216df" in workflow
assert "quant_platform_kit_ref: b5654244aa5d08bce2b4b4f931436268d57216df" in workflow
assert "ref: 5488048cc0fc8e818b8c4be7a9729f6f1b5fdfdd" in workflow
assert "quant_platform_kit_ref: 5488048cc0fc8e818b8c4be7a9729f6f1b5fdfdd" in workflow
assert "python -m pip install --no-deps -e external/QuantPlatformKit" in workflow
assert "scripts/run_walk_forward_backtest.py" in workflow
assert '"--list-profiles"' in workflow
Expand All @@ -30,7 +30,7 @@ def test_drift_workflow_wires_real_snapshot_history_and_preflight_bundle() -> No
assert "Upload lifecycle preflight artifact" in workflow
assert "actions/upload-artifact@v7" in workflow
assert workflow.count("github.ref == format('refs/heads/{0}', github.event.repository.default_branch)") == 2
assert "uses: QuantStrategyLab/QuantPlatformKit/.github/workflows/reusable-drift-check.yml@b5654244aa5d08bce2b4b4f931436268d57216df" in workflow
assert "uses: QuantStrategyLab/QuantPlatformKit/.github/workflows/reusable-drift-check.yml@5488048cc0fc8e818b8c4be7a9729f6f1b5fdfdd" in workflow
assert "strategy_domain: cn_equity" in workflow
assert "caller_event_name: ${{ github.event_name }}" in workflow
assert "caller_pr_head_repository: ${{ github.event.pull_request.head.repo.full_name || '' }}" in workflow
Expand Down
56 changes: 53 additions & 3 deletions tests/test_index_etf_strict_runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -330,6 +330,51 @@ def test_full_job_refuses_unapproved_or_synthetic_roots_before_any_callback(tmp_
assert not list(tmp_path.rglob("*.json"))


def test_full_job_forwards_optional_summary_callback_to_qpk(tmp_path, monkeypatch):
import cn_equity_strategies.backtest.index_etf_research_job as job
import sys
import types

identity = {field: "a" * 64 for field in (
"code_revision", "input_revision", "param_space_revision", "cost_model_revision", "validator_revision",
)}
def summary(context):
return {"status": "available", "text": "合成说明", "provider": "codex", "model": "test"}
observed = {}
owner = {"repository": "QuantStrategyLab/CnEquityStrategies", "issue_number": 123,
"watcher_issue_key": "watcher-key-123"}

monkeypatch.setattr(job, "preflight_index_etf_research_job", lambda **_: identity)
monkeypatch.setattr(job, "make_strict_index_etf_optimizer", lambda **_: "optimize")
monkeypatch.setattr(job, "make_index_etf_promotion_gate", lambda **_: "gate")

def checked_cycle(*, research_identity, admit_new_research, read_pending_shadow, summarize, research_owner, **kwargs):
observed.update({"research_identity": research_identity, "admit_new_research": admit_new_research,
"read_pending_shadow": read_pending_shadow, "summarize": summarize,
"research_owner": research_owner, **kwargs})
return {"status": "parked", "reason": "research_scope_archived"}

monkeypatch.setitem(
sys.modules,
"quant_platform_kit.strategy_lifecycle.promotion_actionable_runner",
types.SimpleNamespace(run_actionable_research_promotion=checked_cycle),
)
result = job.run_index_etf_research_job(
development_input=object(), validation_input=object(), trusted_input_roots={"development": "dev", "validation": "val"},
development_start=date(2024, 1, 1), development_end=date(2024, 1, 2), folds=(),
locked_oos_start=date(2025, 1, 1), locked_oos_end=date(2025, 1, 2), purge_days=1, embargo_days=1,
code_revision="c" * 40, ticket_dir=tmp_path / "tickets", store_root=tmp_path / "store",
as_of=date(2026, 9, 13), drift_score=.8, source_revision="d" * 40,
record_shadow=lambda *_: None, sync_console=lambda *_: None, summarize=summary,
research_owner=owner,
config=object(), cost_model=object(),
)

assert result["reason"] == "research_scope_archived"
assert observed["summarize"] is summary
assert observed["research_owner"] == owner


def test_benchmark_retries_monthly_target_under_same_volume_constraint():
from cn_equity_strategies.backtest.index_etf_research_job import BASELINE_PARAMS
days = list(pd.bdate_range("2024-01-02", periods=290).strftime("%Y-%m-%d"))
Expand Down Expand Up @@ -396,10 +441,15 @@ def admit(*_):
old_root = Path(first["trial_records_path"]).parent
before = {p: p.read_bytes() for p in old_root.rglob("*.json")}
third = run_index_etf_research_job(**{**kwargs, "as_of": date(2026, 9, 8), "source_revision": "e" * 40})
assert third["research_key"] != first["research_key"]
assert third["trial_records_path"] != first["trial_records_path"]
# A changed observation revision does not create a new research scope. The
# terminal ticket remains the durable admission record, so no optimizer or
# diagnosis call is repeated.
assert third["reason"] == "saved_research_ticket_reused"
assert third["research_key"] == first["research_key"]
assert third["trial_records_path"] is None
assert third["experiment_store_root"] is None
assert all(p.read_bytes() == content for p, content in before.items())
assert calls == ["admit", "diagnose", "admit", "diagnose"]
assert calls == ["admit", "diagnose"]


def test_malformed_license_record_has_fixed_failure_reason():
Expand Down
2 changes: 1 addition & 1 deletion tests/test_qsl_compat_metadata.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@


ROOT = Path(__file__).resolve().parents[1]
QPK_REVISION = "b5654244aa5d08bce2b4b4f931436268d57216df"
QPK_REVISION = "5488048cc0fc8e818b8c4be7a9729f6f1b5fdfdd"
QPK_URL = (
"quant-platform-kit @ git+https://github.com/QuantStrategyLab/"
f"QuantPlatformKit.git@{QPK_REVISION}"
Expand Down
4 changes: 2 additions & 2 deletions uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.