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
12 changes: 12 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,23 @@ It supports the system but does not decide which strategy should be live. Strate

- `python/`: Python tooling (scripts, tests, pyproject.toml) — validation, code generation, deployment scripts.
- `web/`: JavaScript web app (Cloudflare Workers strategy switch console).
- `compat/`: QSL compatibility manifests (bundle matrix and repo tier rules).
- `schemas/`: JSON Schema files shared by both Python and JS.
- `tests/`: JavaScript unit/integration tests.
- `.github/workflows/`: CI, scheduled jobs, release, or deployment workflows.
- `docs/ARCHITECTURE.md`: Detailed architecture documentation.

## QSL compatibility manifest (phase 1)

This repository is the central manifest source for QSL compatibility checks.

- `qsl.toml`: current repo QSL metadata (`tier`, `upgrade_ring`, `bundle`).
- `scripts/check_qsl_compat.py`: validate a repo's QSL compliance in `pyproject.toml`/`uv.lock` against central bundle.
- `scripts/render_qsl_dependency_graph.py`: render dependency graph for review.
- `compat/bundles/2026.07.0.toml`: first compatibility bundle (CalVer).
- `compat/repo-tiers.toml`: repo tier and upgrade ring policy notes.
- [QSL compatibility docs](docs/qsl_compat_upgrade.md)

## Quick start

```bash
Expand Down
12 changes: 12 additions & 0 deletions compat/bundles/2026.07.0.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
name = "2026.07.0"
description = "Phase-1 QSL compatibility bundle"
created_at = "2026-07-03"

[repos]
QuantPlatformKit = "0063af3b4a974650ea58a7d3f26dd1b94f65d3e8"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Align bundle refs with the current consumer baseline

When this new bundle is used against the current consumers tracked by this repo, it rejects the existing baseline rather than representing it: for example, this sets QuantPlatformKit to 0063af..., while internal_dependency_matrix.json still records active consumers at 6d367..., b0eacd..., d786..., and e86554b. Because this commit does not update those consumers or the existing matrix in the same wave, adding qsl.toml with bundle = "2026.07.0" to any current consumer will immediately produce bundle pin mismatches.

Useful? React with 👍 / 👎.

UsEquityStrategies = "46887bc3f5454d5b59623b1f5efb7c65912c6b8b"
HkEquityStrategies = "61993bf261aeccf64b5a75428b9405f4e1d1d682"
CnEquityStrategies = "ffbdf7303179ba6e7f9d3e28c21202f77e04762c"
CryptoStrategies = "8039ddddde7634ad3615496c9b79d2918996938c"
QuantStrategyPlugins = "6c10aef8989f52d82571e676092b14d315401989"
MarketSignalSources = "ea8771b6b65f356e7edc4a2b08cd621186726646"
29 changes: 29 additions & 0 deletions compat/repo-tiers.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
[tiers]
core = { name = "core", description = "平台最小闭环、运行核心与共享基础设施" }
strategy_lib = { name = "strategy-lib", description = "策略库、插件与市场信号源" }
pipeline = { name = "pipeline", description = "策略研发与快照流水线" }
runtime = { name = "runtime", description = "平台运行时与执行服务" }
ops = { name = "ops/tooling", description = "运维/发布与中央配置仓库" }

[upgrade_rings]
ring_a = "core"
ring_b = "strategy-lib"
ring_c = "pipeline"
ring_d = "runtime"
ring_e = "ops/tooling"

[upgrade_rules]
# 允许上升方向:稳定性从低到高(ops/tooling -> runtime -> pipeline -> strategy-lib -> core)
# 建议原则:较外层可回退引用内层,内层通常不直接引用同级以上的低稳定性 ring。
allow_drift = [
"ops/tooling:runtime",
"ops/tooling:pipeline",
"ops/tooling:strategy-lib",
"ops/tooling:core",
"runtime:pipeline",
"runtime:strategy-lib",
"runtime:core",
"pipeline:strategy-lib",
"pipeline:core",
"strategy-lib:core",
]
57 changes: 57 additions & 0 deletions docs/qsl_compat_upgrade.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
# QSL Compatibility & Upgrade Checklist (Central Manifest Phase-1)

This repository carries the first version of the QSL central compatibility manifest.

## 文件结构

- `compat/bundles/*.toml`
- 每个 bundle 用 Calendar Version 命名(当前:`2026.07.0`)。
- 记录 QSL 管控的固定内部仓库提交。
- `compat/repo-tiers.toml`
- 记录仓库层级(`core/strategy-lib/pipeline/runtime/ops`)与升级 ring 规则。
- `qsl.toml`
- 本仓库自身的 QSL 元信息:`tier`、`compat`/`bundle`、`upgrade_ring`。
- `scripts/check_qsl_compat.py`
- 在任意仓库根目录运行,校验:
- 禁止 `@main`
- 禁止短 SHA
- 禁止 `requirements.txt` / `constraints.txt`(未设置 `allow_legacy=true` 时)
- 内部依赖 Ref 是否与 `compat/bundles/<bundle>.toml` 一致
- `scripts/render_qsl_dependency_graph.py`
- 输出当前仓库的 QSL 依赖图(Markdown / Text)。

## 如何接入一个新仓库

1. 在仓库根目录新建 `qsl.toml`:

```toml
[qsl]
bundle = "2026.07.0" # 选择要对齐的 central bundle
compat = "2026.07.0" # 兼容检查入口(与 bundle 相同)
tier = "ops/tooling"
upgrade_ring = "ring_e"
allow_legacy = false # 需要临时兼容时可先放开
enforce_bundle = true # 过渡仓库可设 false;ref drift 会降级为 warning
```

2. 在 `pyproject.toml`/`uv.lock` 中用完整 SHA 固定 QuantStrategyLab 的内部 git 依赖。

3. 运行自检:

```bash
python scripts/check_qsl_compat.py --repo-root . --non-strict
python scripts/render_qsl_dependency_graph.py --repo-root . --format md
```

说明:默认脚本会输出错误并以非零退出码返回;`--non-strict` 仅用于本地快速预览。

## 当前中心兼容基线

- Bundle: `2026.07.0`
- QPK: `0063af3b4a974650ea58a7d3f26dd1b94f65d3e8`
- UsEquityStrategies: `46887bc3f5454d5b59623b1f5efb7c65912c6b8b`
- HkEquityStrategies: `61993bf261aeccf64b5a75428b9405f4e1d1d682`
- CnEquityStrategies: `ffbdf7303179ba6e7f9d3e28c21202f77e04762c`
- CryptoStrategies: `8039ddddde7634ad3615496c9b79d2918996938c`
- QuantStrategyPlugins: `6c10aef8989f52d82571e676092b14d315401989`
- MarketSignalSources: `ea8771b6b65f356e7edc4a2b08cd621186726646`
22 changes: 11 additions & 11 deletions internal_dependency_matrix.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,14 +34,14 @@
"path": "requirements.txt",
"package": "us-equity-strategies",
"source_repo": "UsEquityStrategies",
"ref": "6568c315ce3be6f7ae5b799374cf7fb44232c170"
"ref": "46887bc3f5454d5b59623b1f5efb7c65912c6b8b"
},
{
"consumer_repo": "CharlesSchwabPlatform",
"path": "requirements.txt",
"package": "quant-platform-kit",
"source_repo": "QuantPlatformKit",
"ref": "b0eacd2fe4884f7f2447b704a232e9a121f396c4"
"ref": "0063af3b4a974650ea58a7d3f26dd1b94f65d3e8"
},
{
"consumer_repo": "CnEquitySnapshotPipelines",
Expand All @@ -55,21 +55,21 @@
"path": "pyproject.toml",
"package": "quant-platform-kit",
"source_repo": "QuantPlatformKit",
"ref": "b0eacd2fe4884f7f2447b704a232e9a121f396c4"
"ref": "0063af3b4a974650ea58a7d3f26dd1b94f65d3e8"
},
{
"consumer_repo": "FirstradePlatform",
"path": "requirements.txt",
"package": "us-equity-strategies",
"source_repo": "UsEquityStrategies",
"ref": "6568c315ce3be6f7ae5b799374cf7fb44232c170"
"ref": "46887bc3f5454d5b59623b1f5efb7c65912c6b8b"
},
{
"consumer_repo": "FirstradePlatform",
"path": "requirements.txt",
"package": "quant-platform-kit",
"source_repo": "QuantPlatformKit",
"ref": "b0eacd2fe4884f7f2447b704a232e9a121f396c4"
"ref": "0063af3b4a974650ea58a7d3f26dd1b94f65d3e8"
},
{
"consumer_repo": "HkEquitySnapshotPipelines",
Expand All @@ -83,42 +83,42 @@
"path": "requirements.txt",
"package": "hk-equity-strategies",
"source_repo": "HkEquityStrategies",
"ref": "e9e3058c1eaf3f43b25d50df5eb14442816e568e"
"ref": "61993bf261aeccf64b5a75428b9405f4e1d1d682"
},
{
"consumer_repo": "InteractiveBrokersPlatform",
"path": "requirements.txt",
"package": "us-equity-strategies",
"source_repo": "UsEquityStrategies",
"ref": "6568c315ce3be6f7ae5b799374cf7fb44232c170"
"ref": "46887bc3f5454d5b59623b1f5efb7c65912c6b8b"
},
{
"consumer_repo": "InteractiveBrokersPlatform",
"path": "requirements.txt",
"package": "quant-platform-kit",
"source_repo": "QuantPlatformKit",
"ref": "b0eacd2fe4884f7f2447b704a232e9a121f396c4"
"ref": "0063af3b4a974650ea58a7d3f26dd1b94f65d3e8"
},
{
"consumer_repo": "LongBridgePlatform",
"path": "requirements.txt",
"package": "hk-equity-strategies",
"source_repo": "HkEquityStrategies",
"ref": "e9e3058c1eaf3f43b25d50df5eb14442816e568e"
"ref": "61993bf261aeccf64b5a75428b9405f4e1d1d682"
},
{
"consumer_repo": "LongBridgePlatform",
"path": "requirements.txt",
"package": "quant-platform-kit",
"source_repo": "QuantPlatformKit",
"ref": "b0eacd2fe4884f7f2447b704a232e9a121f396c4"
"ref": "0063af3b4a974650ea58a7d3f26dd1b94f65d3e8"
},
{
"consumer_repo": "LongBridgePlatform",
"path": "requirements.txt",
"package": "us-equity-strategies",
"source_repo": "UsEquityStrategies",
"ref": "6568c315ce3be6f7ae5b799374cf7fb44232c170"
"ref": "46887bc3f5454d5b59623b1f5efb7c65912c6b8b"
},
{
"consumer_repo": "QmtPlatform",
Expand Down
69 changes: 56 additions & 13 deletions python/scripts/check_internal_dependency_matrix.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@
r"(?P<source_repo>[A-Za-z0-9_.-]+)\.git@(?P<ref>[A-Za-z0-9_.-]+)"
)
TRACKED_DEPENDENCY_PATHS = ("requirements.txt", "requirements-lock.txt", "pyproject.toml")
LEGACY_DEPENDENCY_PATHS = ("requirements.txt", "requirements-lock.txt")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Do not treat lockfiles as legacy fallbacks

Including requirements-lock.txt here makes _fallback_path_for_legacy_requirements() accept a missing lockfile whenever the repo still has matching pins in pyproject.toml, so the Validate workflow's check_internal_dependency_matrix.py --strict --require-consumer-files step no longer reports that required consumer lockfile as missing. For any consumer that accidentally deletes requirements-lock.txt after migrating direct deps to pyproject, CI would pass while the production lock pins tracked by the matrix have disappeared.

Useful? React with 👍 / 👎.

PYPROJECT_FALLBACK_PATH = "pyproject.toml"


def _sort_dependency_pins(pins: list[DependencyPin]) -> list[DependencyPin]:
Expand Down Expand Up @@ -126,6 +128,40 @@ def parse_dependency_pins(consumer_repo: str, path: str, text: str) -> list[Depe
]


def _parse_repo_pins(projects_root: Path, consumer_repo: str, relative_path: str) -> list[DependencyPin]:
path = projects_root / consumer_repo / relative_path
if not path.is_file():
return []
return parse_dependency_pins(consumer_repo, relative_path, path.read_text(encoding="utf-8"))


def _fallback_path_for_legacy_requirements(projects_root: Path, consumer_repo: str, expected_path: str) -> str | None:
if expected_path not in LEGACY_DEPENDENCY_PATHS:
return None
pyproject_path = projects_root / consumer_repo / PYPROJECT_FALLBACK_PATH
if pyproject_path.is_file():
return PYPROJECT_FALLBACK_PATH
return None


def _compare_dependency_pins(
*, expected_pins: list[DependencyPin], actual_pins: list[DependencyPin], issues: list[str]
) -> None:
actual_by_key = {pin.key: pin for pin in actual_pins}
expected_by_key = {pin.key: pin for pin in expected_pins}

for key, expected in sorted(expected_by_key.items()):
actual = actual_by_key.get(key)
if actual is None:
issues.append(f"missing {expected.label()} expected @{expected.ref}")
elif actual.ref != expected.ref:
issues.append(f"ref mismatch {expected.label()}: expected @{expected.ref}, found @{actual.ref}")

for key, actual in sorted(actual_by_key.items()):
if key not in expected_by_key:
issues.append(f"untracked internal dependency {actual.label()} @{actual.ref}")


def check_matrix(*, matrix_pins: list[DependencyPin], projects_root: Path) -> MatrixReport:
expected_by_file: dict[tuple[str, str], list[DependencyPin]] = {}
for pin in matrix_pins:
Expand All @@ -137,23 +173,30 @@ def check_matrix(*, matrix_pins: list[DependencyPin], projects_root: Path) -> Ma
for (consumer_repo, relative_path), expected_pins in sorted(expected_by_file.items()):
path = projects_root / consumer_repo / relative_path
if not path.exists():
fallback_path = _fallback_path_for_legacy_requirements(projects_root, consumer_repo, relative_path)
if fallback_path is not None:
fallback_pins = _parse_repo_pins(projects_root, consumer_repo, fallback_path)
if fallback_pins:
_compare_dependency_pins(
expected_pins=[
DependencyPin(
consumer_repo=pin.consumer_repo,
path=fallback_path,
package=pin.package,
source_repo=pin.source_repo,
ref=pin.ref,
)
for pin in expected_pins
],
actual_pins=fallback_pins,
issues=issues,
Comment on lines +189 to +192

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Limit pyproject fallbacks to migrated pins

When a repo already has matrix rows for pyproject.toml and a missing legacy file falls back to the same pyproject, this comparison checks the entire pyproject contents against only the legacy file's expected pins. The real pyproject rows are checked separately later, so valid pyproject-only internal dependencies are reported as untracked during the fallback path, causing strict validation to fail for mixed migration repos.

Useful? React with 👍 / 👎.

)
continue
missing_files.append(f"{consumer_repo}/{relative_path}")
continue
checked_files += 1
actual_pins = parse_dependency_pins(consumer_repo, relative_path, path.read_text(encoding="utf-8"))
actual_by_key = {pin.key: pin for pin in actual_pins}
expected_by_key = {pin.key: pin for pin in expected_pins}

for key, expected in sorted(expected_by_key.items()):
actual = actual_by_key.get(key)
if actual is None:
issues.append(f"missing {expected.label()} expected @{expected.ref}")
elif actual.ref != expected.ref:
issues.append(f"ref mismatch {expected.label()}: expected @{expected.ref}, found @{actual.ref}")

for key, actual in sorted(actual_by_key.items()):
if key not in expected_by_key:
issues.append(f"untracked internal dependency {actual.label()} @{actual.ref}")
_compare_dependency_pins(expected_pins=expected_pins, actual_pins=actual_pins, issues=issues)

return MatrixReport(checked_files=checked_files, missing_files=missing_files, issues=issues)

Expand Down
25 changes: 25 additions & 0 deletions python/tests/test_internal_dependency_matrix.py
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,31 @@ def test_require_consumer_files_treats_missing_paths_as_issues(self):
self.assertEqual(report.missing_files, ["ExamplePlatform/requirements.txt"])
self.assertEqual(report.issues, [])

def test_check_matrix_accepts_pyproject_for_migrated_legacy_requirements(self):
projects_root = self._make_projects_root(
{
"ExamplePlatform/pyproject.toml": (
"quant-platform-kit @ git+https://github.com/QuantStrategyLab/"
"QuantPlatformKit.git@v0.7.35"
)
}
)
expected = [
check_internal_dependency_matrix.DependencyPin(
consumer_repo="ExamplePlatform",
path="requirements.txt",
package="quant-platform-kit",
source_repo="QuantPlatformKit",
ref="v0.7.35",
)
]

report = check_internal_dependency_matrix.check_matrix(matrix_pins=expected, projects_root=projects_root)

self.assertEqual(report.checked_files, 0)
self.assertEqual(report.missing_files, [])
self.assertEqual(report.issues, [])

def test_collect_dependency_pins_from_projects(self):
projects_root = self._make_projects_root(
{
Expand Down
8 changes: 8 additions & 0 deletions qsl.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
[qsl]
# QuantRuntimeSettings 作为 QSL 中央仓库
repo = "QuantRuntimeSettings"
bundle = "2026.07.0"
compat = "2026.07.0"
tier = "ops/tooling"
upgrade_ring = "ring_e"
allow_legacy = false
Loading
Loading