Skip to content

Commit efd9069

Browse files
Pigbibicodex
andcommitted
chore: converge qmt runtime dependencies
Co-Authored-By: Codex <noreply@openai.com>
1 parent 720ba26 commit efd9069

15 files changed

Lines changed: 235 additions & 305 deletions

.env.example

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,3 @@ STRATEGY_PROFILE=cn_industry_etf_rotation
33
ACCOUNT_PREFIX=QMT
44
ACCOUNT_REGION=CN
55
QMT_MARKET_HISTORY_PATH=data/fixtures/market_history.sample.csv
6-
QMT_FEATURE_SNAPSHOT_PATH=data/fixtures/dividend_quality/cn_dividend_quality_snapshot_factor_snapshot_latest.csv
7-
QMT_FEATURE_SNAPSHOT_MANIFEST_PATH=data/fixtures/dividend_quality/cn_dividend_quality_snapshot_factor_snapshot_latest.csv.manifest.json

.github/workflows/ci.yml

Lines changed: 1 addition & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -30,19 +30,6 @@ jobs:
3030
ref="$(grep -Eo 'CnEquityStrategies\.git@[0-9a-f]+' pyproject.toml | head -n1 | sed 's/.*@//')"
3131
echo "ref=${ref}" >> "$GITHUB_OUTPUT"
3232
33-
- name: Resolve CnEquitySnapshotPipelines ref
34-
id: cn-equity-snapshot-pipelines-ref
35-
run: |
36-
set -euo pipefail
37-
ref="main"
38-
for candidate in "${GITHUB_HEAD_REF:-}" "${GITHUB_BASE_REF:-}"; do
39-
if [ -n "$candidate" ] && git ls-remote --exit-code --heads https://github.com/QuantStrategyLab/CnEquitySnapshotPipelines.git "$candidate" >/dev/null 2>&1; then
40-
ref="$candidate"
41-
break
42-
fi
43-
done
44-
echo "ref=${ref}" >> "$GITHUB_OUTPUT"
45-
4633
- name: Checkout QuantPlatformKit
4734
uses: actions/checkout@v6
4835
with:
@@ -57,13 +44,6 @@ jobs:
5744
ref: ${{ steps.cn-equity-strategies-ref.outputs.ref }}
5845
path: external/CnEquityStrategies
5946

60-
- name: Checkout CnEquitySnapshotPipelines
61-
uses: actions/checkout@v6
62-
with:
63-
repository: QuantStrategyLab/CnEquitySnapshotPipelines
64-
ref: ${{ steps.cn-equity-snapshot-pipelines-ref.outputs.ref }}
65-
path: external/CnEquitySnapshotPipelines
66-
6747
- name: Setup Python
6848
uses: actions/setup-python@v6
6949
with:
@@ -74,7 +54,7 @@ jobs:
7454
set -euo pipefail
7555
python -m pip install --upgrade pip
7656
python -m pip install -e '.[test]' ruff
77-
python -m pip install --no-deps -e external/QuantPlatformKit -e external/CnEquityStrategies -e external/CnEquitySnapshotPipelines
57+
python -m pip install --no-deps -e external/QuantPlatformKit -e external/CnEquityStrategies
7858
7959
- name: Run ruff
8060
run: python -m ruff check . --extend-exclude external

README.md

Lines changed: 8 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -6,21 +6,21 @@
66
- **Layer**: `runtime-platform`.
77
- **Responsibility**: QMT / miniQMT A-share execution runtime.
88
- **Owns**: QMT runtime controls and A-share platform integration.
9-
- **Consumes**: CnEquityStrategies, CnEquitySnapshotPipelines artifacts, QuantPlatformKit, QuantRuntimeSettings.
9+
- **Consumes**: CnEquityStrategies, QuantPlatformKit, QuantRuntimeSettings, market-history inputs.
1010
- **Must not**: own strategy research logic or publish snapshot artifacts.
1111

1212
A-share quant platform layer for **miniQMT / QMT**, built on `QuantPlatformKit` and `CnEquityStrategies`.
1313

1414
Current scope is **dry-run first**: evaluate strategy targets and preview orders without submitting to the broker.
1515

16-
## Supported profiles (P0)
16+
## Supported dry-run profiles
1717

1818
| Profile | Input mode |
1919
|---|---|
2020
| `cn_industry_etf_rotation` | `market_history` (**主轨,runtime_enabled**) |
21-
| `cn_industry_etf_rotation_aggressive` | `market_history` (**optional target,vol25%**) |
22-
| `cn_dividend_quality_snapshot` | `feature_snapshot` (requires snapshot path) |
23-
| `cn_index_etf_tactical_rotation` | `market_history` (legacy / research_backtest_only) |
21+
| `cn_industry_etf_rotation_aggressive` | `market_history` (**optional target,live_candidate**) |
22+
23+
Research-only profiles such as `cn_dividend_quality_snapshot` and `cn_index_etf_tactical_rotation` are intentionally rejected by QMT runtime settings until they are promoted in `CnEquityStrategies`.
2424

2525
## Quick start
2626

@@ -34,6 +34,7 @@ export STRATEGY_PROFILE=cn_industry_etf_rotation
3434
export QMT_DRY_RUN_ONLY=true
3535
export QMT_MARKET_HISTORY_PATH=data/fixtures/market_history.sample.csv
3636

37+
python3 scripts/preflight_qmt_runtime.py
3738
python3 main.py
3839
curl http://127.0.0.1:8080/probe
3940
curl http://127.0.0.1:8080/dry-run
@@ -52,35 +53,9 @@ curl http://127.0.0.1:8080/dry-run
5253

5354
Runtime target example: `QuantRuntimeSettings/examples/targets/qmt/industry_etf_aggressive_dry_run.example.json`
5455

55-
### Legacy index ETF tactical rotation (research only)
56-
57-
```bash
58-
export STRATEGY_PROFILE=cn_index_etf_tactical_rotation
59-
export QMT_MARKET_HISTORY_PATH=data/fixtures/market_history.sample.csv
60-
python3 main.py
61-
```
62-
63-
### Dividend quality snapshot (feature snapshot)
64-
65-
```bash
66-
python3 -m pip install --no-deps -e ../CnEquitySnapshotPipelines
67-
68-
# Regenerate fixtures (uses sample factor CSV; sets as_of to today)
69-
python3 scripts/build_fixtures.py
70-
71-
export STRATEGY_PROFILE=cn_dividend_quality_snapshot
72-
export QMT_DRY_RUN_ONLY=true
73-
export QMT_FEATURE_SNAPSHOT_PATH=data/fixtures/dividend_quality/cn_dividend_quality_snapshot_factor_snapshot_latest.csv
74-
export QMT_FEATURE_SNAPSHOT_MANIFEST_PATH=data/fixtures/dividend_quality/cn_dividend_quality_snapshot_factor_snapshot_latest.csv.manifest.json
75-
76-
python3 main.py
77-
curl http://127.0.0.1:8080/dry-run
78-
```
79-
8056
End-to-end smoke (stage/build/run):
8157

8258
```bash
83-
python3 scripts/smoke_cn_dividend_quality_dry_run_e2e.py
8459
python3 scripts/smoke_cn_industry_etf_rotation_dry_run_e2e.py
8560
python3 scripts/smoke_cn_industry_etf_rotation_aggressive_dry_run_e2e.py
8661
```
@@ -90,13 +65,11 @@ python3 scripts/smoke_cn_industry_etf_rotation_aggressive_dry_run_e2e.py
9065
| Variable | Default | Description |
9166
|---|---|---|
9267
| `QMT_DRY_RUN_ONLY` | `true` | When true, never submit live orders |
93-
| `STRATEGY_PROFILE` | required | Strategy profile id |
68+
| `STRATEGY_PROFILE` | required | QMT-enabled strategy profile id |
9469
| `QMT_MARKET_HISTORY_PATH` || CSV with `date,symbol,close` for direct strategies |
95-
| `QMT_FEATURE_SNAPSHOT_PATH` || Snapshot CSV for feature-snapshot strategies |
96-
| `QMT_FEATURE_SNAPSHOT_MANIFEST_PATH` || Snapshot manifest JSON (required for dividend quality) |
9770
| `RUNTIME_TARGET_JSON` || Optional runtime target override from QuantRuntimeSettings |
9871

99-
See `.env.example` and `CnEquityStrategies/docs/platform_integration.md`.
72+
Use `.env.example` as the dry-run configuration template. Run `python3 scripts/preflight_qmt_runtime.py` before starting the service; it validates the selected profile and required input paths without touching any live account credentials.
10073

10174
## HTTP endpoints
10275

README.zh-CN.md

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,32 +6,36 @@
66
- **层级**`执行平台`
77
- **职责**:QMT / miniQMT A 股执行运行时。
88
- **事实源/归属**:QMT runtime 控制和 A 股平台集成。
9-
- **消费对象**:CnEquityStrategies、CnEquitySnapshotPipelines artifacts、QuantPlatformKit、QuantRuntimeSettings。
9+
- **消费对象**:CnEquityStrategies、QuantPlatformKit、QuantRuntimeSettings、market-history 输入
1010
- **禁止事项**:承载策略研究逻辑或发布 snapshot artifacts。
1111

1212
A 股量化平台层,基于 `QuantPlatformKit``CnEquityStrategies` 构建,对接 **miniQMT / QMT**
1313

1414
当前范围为**仅干跑**:评估策略目标并预览订单,不向券商提交实盘。
1515

16-
## 支持策略
16+
## 支持 dry-run 策略
1717

1818
| Profile | 输入模式 |
1919
|---|---|
2020
| `cn_industry_etf_rotation` | `market_history`(主轨,runtime_enabled) |
21-
| `cn_industry_etf_rotation_aggressive` | `market_history`(可选目标,vol25%|
22-
| `cn_dividend_quality_snapshot` | `feature_snapshot`(需快照路径) |
23-
| `cn_index_etf_tactical_rotation` | `market_history`(旧版/research_backtest_only) |
21+
| `cn_industry_etf_rotation_aggressive` | `market_history`(可选目标,live_candidate|
22+
23+
`cn_dividend_quality_snapshot``cn_index_etf_tactical_rotation` 等 research-only profile 不会进入 QMT runtime 配置;只有在 `CnEquityStrategies` 中完成提级后才允许启用。
2424

2525
## 快速开始
2626

2727
```bash
2828
python3 -m pip install -e '.[test]'
2929
export STRATEGY_PROFILE=cn_industry_etf_rotation
3030
export QMT_DRY_RUN_ONLY=true
31+
export QMT_MARKET_HISTORY_PATH=data/fixtures/market_history.sample.csv
32+
python3 scripts/preflight_qmt_runtime.py
3133
python3 main.py
3234
curl http://127.0.0.1:8080/probe
3335
```
3436

37+
`.env.example` 是当前 dry-run 配置模板;preflight 只校验 profile 与输入文件路径,不读取或写入账号、密码、token。
38+
3539
详见 [README.md](README.md)(英文)。
3640

3741
## 许可证

pyproject.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ dependencies = [
1414
"flask>=3.0",
1515
"pandas>=2.0",
1616
"quant-platform-kit @ git+https://github.com/QuantStrategyLab/QuantPlatformKit.git@37c81901160c5b31127a27dba1c63944933fb6bf",
17-
"cn-equity-strategies @ git+https://github.com/QuantStrategyLab/CnEquityStrategies.git@8dfadcf8a4dc6cc516f27a4013248474603d8ce2",
17+
"cn-equity-strategies @ git+https://github.com/QuantStrategyLab/CnEquityStrategies.git@73844e92a8570a61e5a9dc6c245809d0b27b89bc",
1818
]
1919

2020
[project.optional-dependencies]
@@ -23,6 +23,7 @@ test = ["pytest>=8"]
2323
[tool.setuptools]
2424
py-modules = [
2525
"main",
26+
"runtime_preflight",
2627
"runtime_config_support",
2728
"strategy_loader",
2829
"strategy_registry",

qsl.toml

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,11 @@ repo = "QmtPlatform"
33
tier = "runtime-platform"
44
ring = 3
55
allow_legacy = false
6-
enforce_bundle = false
7-
owner = "runtime-platform"
8-
expires_at = "2026-09-30"
9-
next_action = "finish dependency pin convergence and restore enforce_bundle=true"
6+
enforce_bundle = true
107

118
[qsl.requires]
129
quant_platform_kit = "37c81901160c5b31127a27dba1c63944933fb6bf"
13-
cn_equity_strategies = "8dfadcf8a4dc6cc516f27a4013248474603d8ce2"
10+
cn_equity_strategies = "73844e92a8570a61e5a9dc6c245809d0b27b89bc"
1411

1512
[qsl.compat]
1613
bundle = "2026.07.2"

runtime_preflight.py

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
from __future__ import annotations
2+
3+
from dataclasses import dataclass
4+
from pathlib import Path
5+
from typing import Any
6+
7+
from runtime_config_support import load_platform_runtime_settings
8+
from strategy_loader import load_strategy_entrypoint_for_profile
9+
10+
11+
@dataclass(frozen=True)
12+
class PreflightIssue:
13+
code: str
14+
message: str
15+
16+
17+
@dataclass(frozen=True)
18+
class QmtPreflightReport:
19+
status: str
20+
strategy_profile: str | None
21+
dry_run_only: bool | None
22+
required_inputs: tuple[str, ...]
23+
issues: tuple[PreflightIssue, ...]
24+
25+
def to_payload(self) -> dict[str, Any]:
26+
return {
27+
"status": self.status,
28+
"strategy_profile": self.strategy_profile,
29+
"dry_run_only": self.dry_run_only,
30+
"required_inputs": list(self.required_inputs),
31+
"issues": [
32+
{"code": issue.code, "message": issue.message}
33+
for issue in self.issues
34+
],
35+
}
36+
37+
38+
def run_preflight(*, allow_non_dry_run: bool = False) -> QmtPreflightReport:
39+
issues: list[PreflightIssue] = []
40+
try:
41+
settings = load_platform_runtime_settings()
42+
entrypoint = load_strategy_entrypoint_for_profile(settings.strategy_profile)
43+
except Exception as exc:
44+
return QmtPreflightReport(
45+
status="error",
46+
strategy_profile=None,
47+
dry_run_only=None,
48+
required_inputs=(),
49+
issues=(PreflightIssue("runtime_config_error", str(exc)),),
50+
)
51+
52+
required_inputs = tuple(sorted(frozenset(entrypoint.manifest.required_inputs)))
53+
if not settings.dry_run_only and not allow_non_dry_run:
54+
issues.append(
55+
PreflightIssue(
56+
"non_dry_run_blocked",
57+
"QMT_DRY_RUN_ONLY=false is not accepted by preflight; live QMT submission is not wired in this repo.",
58+
)
59+
)
60+
61+
if "market_history" in required_inputs:
62+
_check_existing_path(
63+
settings.market_history_path,
64+
code="missing_market_history",
65+
label="QMT_MARKET_HISTORY_PATH",
66+
issues=issues,
67+
)
68+
if "feature_snapshot" in required_inputs:
69+
_check_existing_path(
70+
settings.feature_snapshot_path,
71+
code="missing_feature_snapshot",
72+
label="QMT_FEATURE_SNAPSHOT_PATH",
73+
issues=issues,
74+
)
75+
_check_existing_path(
76+
settings.feature_snapshot_manifest_path,
77+
code="missing_feature_snapshot_manifest",
78+
label="QMT_FEATURE_SNAPSHOT_MANIFEST_PATH",
79+
issues=issues,
80+
)
81+
82+
return QmtPreflightReport(
83+
status="ok" if not issues else "error",
84+
strategy_profile=settings.strategy_profile,
85+
dry_run_only=settings.dry_run_only,
86+
required_inputs=required_inputs,
87+
issues=tuple(issues),
88+
)
89+
90+
91+
def _check_existing_path(value: str | None, *, code: str, label: str, issues: list[PreflightIssue]) -> None:
92+
if not value:
93+
issues.append(PreflightIssue(code, f"{label} is required."))
94+
return
95+
if not Path(value).expanduser().exists():
96+
issues.append(PreflightIssue(code, f"{label} does not exist."))

scripts/build_fixtures.py

Lines changed: 1 addition & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -4,20 +4,13 @@
44
from __future__ import annotations
55

66
import argparse
7-
import subprocess
8-
import sys
9-
from datetime import datetime, timezone
107
from pathlib import Path
118

129
import pandas as pd
1310

1411
ROOT = Path(__file__).resolve().parents[1]
1512
FIXTURES = ROOT / "data" / "fixtures"
1613
MARKET_HISTORY = FIXTURES / "market_history.sample.csv"
17-
DIVIDEND_DIR = FIXTURES / "dividend_quality"
18-
SAMPLE_FACTOR = (
19-
ROOT.parent / "CnEquitySnapshotPipelines" / "examples" / "dividend_quality" / "factor_snapshot.sample.csv"
20-
)
2114

2215

2316
def _build_market_history(path: Path, *, use_akshare: bool) -> None:
@@ -58,67 +51,17 @@ def _build_market_history(path: Path, *, use_akshare: bool) -> None:
5851
pd.DataFrame(rows).to_csv(path, index=False)
5952

6053

61-
def _build_dividend_factor_csv(*, output_path: Path, as_of: str, use_akshare: bool, sample_factor: Path) -> Path:
62-
if use_akshare:
63-
from cn_equity_snapshot_pipelines.akshare_staging import write_staging_factor_snapshot
64-
65-
write_staging_factor_snapshot(
66-
output_path=output_path,
67-
sample_fallback_path=sample_factor,
68-
as_of=as_of,
69-
)
70-
return output_path
71-
return sample_factor
72-
73-
74-
def _build_dividend_snapshot(*, as_of: str, sample_factor: Path, output_dir: Path) -> None:
75-
command = [
76-
sys.executable,
77-
"-m",
78-
"cn_equity_snapshot_pipelines.dividend_quality",
79-
"--factor-snapshot",
80-
str(sample_factor),
81-
"--output-dir",
82-
str(output_dir),
83-
"--as-of",
84-
as_of,
85-
]
86-
subprocess.run(command, check=True)
87-
88-
8954
def main(argv: list[str] | None = None) -> int:
9055
parser = argparse.ArgumentParser()
91-
parser.add_argument(
92-
"--as-of",
93-
default=datetime.now(timezone.utc).date().isoformat(),
94-
help="Snapshot as_of date for dividend quality fixture (YYYY-MM-DD).",
95-
)
96-
parser.add_argument(
97-
"--sample-factor",
98-
type=Path,
99-
default=SAMPLE_FACTOR,
100-
help="Factor CSV used to build the dividend quality snapshot fixture.",
101-
)
10256
parser.add_argument(
10357
"--use-akshare",
10458
action="store_true",
105-
help="Fetch real ETF market history and dividend factor fields via AkShare when available.",
59+
help="Fetch real ETF market history via AkShare when available.",
10660
)
10761
args = parser.parse_args(argv)
10862

10963
_build_market_history(MARKET_HISTORY, use_akshare=args.use_akshare)
110-
if not args.sample_factor.exists() and not args.use_akshare:
111-
raise FileNotFoundError(f"sample factor CSV not found: {args.sample_factor}")
112-
factor_path = _build_dividend_factor_csv(
113-
output_path=FIXTURES / "staging" / "dividend_quality" / "factor_snapshot.latest.csv",
114-
as_of=args.as_of,
115-
use_akshare=args.use_akshare,
116-
sample_factor=args.sample_factor,
117-
)
118-
_build_dividend_snapshot(as_of=args.as_of, sample_factor=factor_path, output_dir=DIVIDEND_DIR)
119-
12064
print(f"market_history={MARKET_HISTORY}")
121-
print(f"dividend_snapshot_dir={DIVIDEND_DIR}")
12265
return 0
12366

12467

0 commit comments

Comments
 (0)