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
36 changes: 36 additions & 0 deletions .cursor/plans/20260810-11_拆分CI依赖.plan.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# 拆分 CI 依赖

## 背景 / 目标

当前 Python Tests 对所有改动都安装完整 Torch / Hugging Face / MCP 依赖,而完整测试执行仅约 14 秒。目标是在保持 `requirements.txt` 全功能安装合同和模型相关变更覆盖的前提下,缩短普通 PR 的依赖安装时间并消除重复触发。

## 任务分解(checklist)

- [ ] 拆分基础运行、Kronos 模型、MCP 与 CI 测试依赖。
- [ ] 保留 `requirements.txt` 作为完整功能安装入口。
- [ ] 普通 Python Tests 仅安装 CI 依赖,并启用 pip 缓存。
- [ ] 限制常规触发为 main push 与面向 main 的 PR,避免 feature push + PR 重复运行。
- [ ] 新增模型路径专用 workflow,安装模型依赖并验证导入。
- [ ] 删除当前仓库无引用的 scikit-learn / lightgbm 依赖。
- [ ] 更新 README 安装档位说明。
- [ ] 在隔离环境验证基础安装不包含模型/MCP 包且 API 可启动、完整测试通过。
- [ ] 提交、推送、创建 PR,等待 CI 通过后合并并关闭 Issue #5。

## 验收标准

- `requirements.txt` 递归安装基础、Kronos 与 MCP 全部功能依赖。
- `requirements-ci.txt` 不包含 torch、Hugging Face、MCP、scikit-learn 或 lightgbm。
- 普通 CI 在缺少可选包的环境中完成 API 启动与完整 pytest。
- 模型代码或依赖变化会触发独立 model-import 检查。
- workflow YAML 可解析,GitHub Actions 绿灯。

## 风险与回滚

- 风险:隐藏的顶层导入导致基础 CI 无法启动;用全新虚拟环境实际安装并启动 API 验证。
- 风险:模型代码失去覆盖;以 paths 触发的专用 workflow 保留依赖和导入检查。
- 回滚:恢复单一 requirements 与原 workflow 安装命令,不涉及数据库和生产数据。

## 关键决策记录

- 不把模型依赖从完整安装入口移除,只让普通 CI 使用轻量档位。
- benchmark 不纳入普通 pytest;模型 workflow 只做确定性 import/构造检查,不下载远端权重。
67 changes: 67 additions & 0 deletions .github/workflows/model-tests.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
name: Kronos Model Imports

on:
push:
branches: [main]
paths:
- 'app/services/kronos_model/**'
- 'app/services/kronos_predict_service.py'
- 'app/services/first_limit_alpha_service.py'
- 'strategy/first_limit_alpha/train_sequence.py'
- 'tests/test_first_limit_alpha.py'
- 'requirements*.txt'
- '.github/workflows/model-tests.yml'
pull_request:
branches: [main]
paths:
- 'app/services/kronos_model/**'
- 'app/services/kronos_predict_service.py'
- 'app/services/first_limit_alpha_service.py'
- 'strategy/first_limit_alpha/train_sequence.py'
- 'tests/test_first_limit_alpha.py'
- 'requirements*.txt'
- '.github/workflows/model-tests.yml'
workflow_dispatch:

permissions:
contents: read

concurrency:
group: kronos-imports-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true

jobs:
import-model-runtime:
runs-on: ubuntu-24.04

steps:
- name: Checkout
uses: actions/checkout@v4

- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: '3.11'
cache: pip
cache-dependency-path: |
requirements*.txt

- name: Install model runtime
run: |
python -m pip install --upgrade pip
pip install -r requirements-ci.txt -r requirements-kronos.txt
pip check

- name: Import Kronos runtime
run: |
python - <<'PY'
from app.services.kronos_model import Kronos, KronosPredictor, KronosTokenizer

assert Kronos is not None
assert KronosPredictor is not None
assert KronosTokenizer is not None
print("Kronos runtime imports: ok")
PY

- name: Test Torch sequence training
run: python -m pytest tests/test_first_limit_alpha.py -q -k sequence
21 changes: 16 additions & 5 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,21 @@ name: Python Tests

on:
push:
branches: ["**"]
branches: [main]
pull_request:
branches: ["**"]
branches: [main]
workflow_dispatch:

permissions:
contents: read

concurrency:
group: python-tests-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true

jobs:
test:
runs-on: ubuntu-latest
runs-on: ubuntu-24.04

steps:
- name: Checkout
Expand All @@ -18,12 +26,15 @@ jobs:
uses: actions/setup-python@v5
with:
python-version: '3.11'
cache: pip
cache-dependency-path: |
requirements*.txt

- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
pip install pytest
pip install -r requirements-ci.txt
pip check

- name: Start API for regression tests
env:
Expand Down
14 changes: 13 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -513,9 +513,22 @@ Alpha/
### 安装依赖

```bash
# 完整安装:基础 API / 数据源 + Kronos + MCP + 训练后端
pip3 install -r requirements.txt
```

也可以按部署角色选择更小的依赖档位:

| 安装档位 | 命令 | 用途 |
|----------|------|------|
| 基础运行 | `pip3 install -r requirements-base.txt` | FastAPI、行情数据、策略与本地缓存;Kronos / MCP 保持惰性可选 |
| Kronos | `pip3 install -r requirements-base.txt -r requirements-kronos.txt` | 增加 Torch、Hugging Face 与 Kronos 推理能力 |
| MCP | `pip3 install -r requirements-base.txt -r requirements-mcp.txt` | 增加独立 MCP Server |
| 策略训练 | `pip3 install -r requirements-base.txt -r requirements-training.txt` | 增加 FirstLimit Alpha 首选的 LightGBM 训练后端;未安装时自动使用 RandomForest |
| 完整功能 | `pip3 install -r requirements.txt` | 安装上述全部能力 |

CI 使用 `requirements-ci.txt` 执行基础单元/API 回归;只有模型代码或模型依赖变化时,才运行独立的 Kronos 导入与 Torch 序列训练测试。模型权重仍在第一次真实预测时按需下载,不进入仓库或 CI 缓存。

### 启动服务

```bash
Expand Down Expand Up @@ -553,7 +566,6 @@ pip3 install -r requirements.txt

日志文件:默认 `logs/server-18890.log`,随 `PORT` 变化。


### 行情缓存维护

历史解析缺陷或错误补数可能污染 `data/market_kline.db`。维护命令默认只做只读预检;只有显式传入 `--apply` 才会先通过 SQLite backup API 创建并验证完整备份,再用单一事务清理早于 2000 年或无法解析的交易日期及其关联同步状态。
Expand Down
5 changes: 4 additions & 1 deletion app/services/first_limit_alpha_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@
TrainingConfig,
)
from strategy.first_limit_alpha.feature_store import build_version_dir, latest_child, read_dataframe, read_json, write_json
from strategy.first_limit_alpha.train_sequence import FirstLimitSequenceTrainer

DEFAULT_GRAPHIC_CONFIG: dict[str, Any] = {
"threshold_candidate": 6.0,
Expand Down Expand Up @@ -168,6 +167,10 @@ def train_baseline(self, training_cfg: TrainingConfig | None = None) -> dict[str
return result

def train_sequence(self, sequence_cfg: SequenceConfig | None = None) -> dict[str, Any]:
# Keep the core API usable without the optional Torch profile. Sequence
# training imports its backend only when this endpoint is invoked.
from strategy.first_limit_alpha.train_sequence import FirstLimitSequenceTrainer

samples = self._load_latest_samples()
out_dir = build_version_dir(self.root, "sequence_models", prefix="gru")
baseline_meta: dict[str, Any] = {}
Expand Down
13 changes: 13 additions & 0 deletions requirements-base.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# Core API and market-data runtime.
akshare>=1.16.0
tushare>=1.4.0
fastapi>=0.115.0
uvicorn[standard]>=0.30.0
pandas>=2.2.0
pydantic>=2.8.0
requests>=2.31.0
httpx>=0.27.0
tqdm>=4.60.0
numpy>=1.24.0
joblib>=1.4.0
scikit-learn>=1.5.0
3 changes: 3 additions & 0 deletions requirements-ci.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Deterministic unit/API regression environment; optional model/MCP imports stay lazy.
-r requirements-base.txt
pytest>=8.0.0
5 changes: 5 additions & 0 deletions requirements-kronos.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Optional Kronos model runtime. Model weights are downloaded lazily.
torch>=2.0.0
einops>=0.8.1
huggingface_hub>=0.33.0
safetensors>=0.6.0
2 changes: 2 additions & 0 deletions requirements-mcp.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# Optional standalone MCP server runtime.
mcp>=1.27.0
2 changes: 2 additions & 0 deletions requirements-training.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# Optional preferred backend for FirstLimit Alpha training.
lightgbm>=4.3.0
22 changes: 5 additions & 17 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1,17 +1,5 @@
akshare>=1.16.0
tushare>=1.4.0
fastapi>=0.115.0
uvicorn[standard]>=0.30.0
pandas>=2.2.0
pydantic>=2.8.0
requests>=2.31.0
httpx>=0.27.0
torch>=2.0.0
einops>=0.8.1
huggingface_hub>=0.33.0
safetensors>=0.6.0
tqdm>=4.60.0
numpy>=1.24.0
mcp>=1.27.0
scikit-learn>=1.5.0
lightgbm>=4.3.0
# Full Alpha installation: API/data runtime + Kronos + MCP + training backends.
-r requirements-base.txt
-r requirements-kronos.txt
-r requirements-mcp.txt
-r requirements-training.txt
63 changes: 63 additions & 0 deletions tests/test_dependency_profiles.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
from pathlib import Path


ROOT = Path(__file__).resolve().parents[1]


def _requirement_lines(name: str) -> set[str]:
return {
line.strip()
for line in (ROOT / name).read_text(encoding="utf-8").splitlines()
if line.strip() and not line.lstrip().startswith("#")
}


def test_full_install_includes_all_supported_profiles():
assert _requirement_lines("requirements.txt") == {
"-r requirements-base.txt",
"-r requirements-kronos.txt",
"-r requirements-mcp.txt",
"-r requirements-training.txt",
}


def test_ci_profile_excludes_optional_heavy_dependencies():
ci_lines = _requirement_lines("requirements-ci.txt")
base_lines = _requirement_lines("requirements-base.txt")
combined = "\n".join(sorted(ci_lines | base_lines)).lower()

assert "-r requirements-base.txt" in ci_lines
assert "pytest>=8.0.0" in ci_lines
for package in (
"torch",
"einops",
"huggingface_hub",
"safetensors",
"mcp",
"lightgbm",
):
assert package not in combined


def test_model_workflow_tracks_model_paths_and_dependencies():
workflow = (ROOT / ".github/workflows/model-tests.yml").read_text(
encoding="utf-8"
)

assert "app/services/kronos_model/**" in workflow
assert "app/services/kronos_predict_service.py" in workflow
assert "strategy/first_limit_alpha/train_sequence.py" in workflow
assert "requirements*.txt" in workflow
assert "requirements-ci.txt" in workflow
assert "requirements-kronos.txt" in workflow
assert "from app.services.kronos_model import" in workflow
assert "-k sequence" in workflow


def test_core_service_lazy_loads_torch_sequence_backend():
service = (ROOT / "app/services/first_limit_alpha_service.py").read_text(
encoding="utf-8"
)

import_line = "from strategy.first_limit_alpha.train_sequence import FirstLimitSequenceTrainer"
assert service.index(import_line) > service.index("def train_sequence(")
8 changes: 7 additions & 1 deletion tests/test_first_limit_alpha.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,14 @@
from pathlib import Path

import pandas as pd
import pytest

from app.services.first_limit_alpha_service import FirstLimitAlphaService
from app.services.kline_store import KlineSQLiteStore
from strategy.first_limit_alpha.data_builder import FirstLimitAlphaDataBuilder
from strategy.first_limit_alpha.features import FirstLimitFeatureBuilder
from strategy.first_limit_alpha.modeling import FirstLimitBaselineTrainer
from strategy.first_limit_alpha.schema import LabelConfig, SampleBuildConfig, SequenceConfig, TrainingConfig
from strategy.first_limit_alpha.train_sequence import FirstLimitSequenceTrainer


def _symbol_rows(base_close: float, event_mode: str, start: str = "2026-01-01") -> list[dict]:
Expand Down Expand Up @@ -181,6 +181,9 @@ def test_baseline_trainer_excludes_future_columns(tmp_path: Path):


def test_sequence_trainer_small_sample_returns_graceful_result(tmp_path: Path):
pytest.importorskip("torch")
from strategy.first_limit_alpha.train_sequence import FirstLimitSequenceTrainer

store = KlineSQLiteStore(str(tmp_path / "market_kline.db"))
store.upsert_symbol_klines("600001", _symbol_rows(10.0, "strong"), "2026-04-22T11:00:00+08:00")
samples = pd.DataFrame(
Expand All @@ -200,6 +203,9 @@ def test_sequence_trainer_small_sample_returns_graceful_result(tmp_path: Path):


def test_sequence_trainer_outputs_metrics_and_predictions(tmp_path: Path):
pytest.importorskip("torch")
from strategy.first_limit_alpha.train_sequence import FirstLimitSequenceTrainer

store = KlineSQLiteStore(str(tmp_path / "market_kline.db"))
name_map = {}
for idx in range(40):
Expand Down
Loading