From 9593883c591ffdbe753a041e29a1d2bf2488b532 Mon Sep 17 00:00:00 2001 From: shy3130 <415333856@qq.com> Date: Sun, 30 Aug 2026 19:05:11 +0800 Subject: [PATCH] =?UTF-8?q?refactor(strategy):=20=E5=88=86=E9=92=9F?= =?UTF-8?q?=E7=BA=A27=E7=94=B1=E5=86=85=E7=BD=AE=E7=AD=96=E7=95=A5?= =?UTF-8?q?=E6=94=B9=E4=B8=BA=E8=87=AA=E5=AE=9A=E4=B9=89=E7=AD=96=E7=95=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 按用户要求, 分钟红7不再是产品内置形态, 以自定义策略形态交付: - 移除 app/strategy/builtin/minute_red_streak.py; 运行时副本落在 data/strategies/custom/ (gitignore, 用户可自行修改调参) - 策略 id (minute_red_streak) 不变: 参数覆盖 / 策略池引用 / minute_filter 契约 (daily_history_bars + daily= 注入) 全部不受位置影响 - 仓库保留参考实现作为测试夹具 (tests/fixtures/strategies/), 25 个行为测试改由夹具加载, 引擎加载测试同时断言 source == "custom" - builtin 相关不变量更新: 内置 19 个全部 matrix_native, 无 minute_filter 本地验证: /api/strategies/reload 后 source=custom, 08-25 分区分钟扫描 命中 5 只 (用户实盘参数: 6根开5红+最高2红+20日涨停过+沪深主板50-200亿)。 --- .../tests/backtest/test_matrix_strategy.py | 7 ++----- .../fixtures/strategies}/minute_red_streak.py | 0 backend/tests/test_minute_strategy.py | 19 ++++++++++++++----- backend/tests/test_screener_etf.py | 10 ++++------ 4 files changed, 20 insertions(+), 16 deletions(-) rename backend/{app/strategy/builtin => tests/fixtures/strategies}/minute_red_streak.py (100%) diff --git a/backend/tests/backtest/test_matrix_strategy.py b/backend/tests/backtest/test_matrix_strategy.py index 0f8defd..f0cab1e 100644 --- a/backend/tests/backtest/test_matrix_strategy.py +++ b/backend/tests/backtest/test_matrix_strategy.py @@ -319,13 +319,10 @@ def test_builtin_matrix_strategies_use_their_declared_formula_modules(): path for path in strategy_dir.glob("*.py") if path.name != "__init__.py" ) - # 非 matrix 后端的内置策略白名单 (当前仅分钟形态策略) - non_matrix = {"minute_red_streak"} - assert len(strategy_files) == 19 + len(non_matrix) + # 分钟形态策略 (minute_red_streak) 已迁至自定义策略目录, 内置策略全部 matrix 后端 + assert len(strategy_files) == 19 for strategy_path in strategy_files: strategy = StrategyEngine._load_file(strategy_path) - if strategy_path.stem in non_matrix: - continue assert strategy.execution_backend == "matrix_native" assert strategy.matrix_strategy is not None assert strategy.matrix_strategy.__class__.__module__ == strategy_path.stem diff --git a/backend/app/strategy/builtin/minute_red_streak.py b/backend/tests/fixtures/strategies/minute_red_streak.py similarity index 100% rename from backend/app/strategy/builtin/minute_red_streak.py rename to backend/tests/fixtures/strategies/minute_red_streak.py diff --git a/backend/tests/test_minute_strategy.py b/backend/tests/test_minute_strategy.py index 894f580..44a5198 100644 --- a/backend/tests/test_minute_strategy.py +++ b/backend/tests/test_minute_strategy.py @@ -12,15 +12,24 @@ from __future__ import annotations import datetime as _dt +import importlib.util from datetime import date, datetime from pathlib import Path import polars as pl from app.services.screener import ScreenerService -from app.strategy.builtin import minute_red_streak from app.strategy.engine import StrategyDataContext, StrategyEngine +# 分钟红7 已从内置策略改为自定义策略 (运行时 data/strategies/custom/, 不入库); +# 测试通过仓库内的参考实现夹具加载, 覆盖同一份策略逻辑。 +STRATEGY_FIXTURE_DIR = Path(__file__).resolve().parent / "fixtures" / "strategies" +_spec = importlib.util.spec_from_file_location( + "minute_red_streak_fixture", STRATEGY_FIXTURE_DIR / "minute_red_streak.py" +) +minute_red_streak = importlib.util.module_from_spec(_spec) +_spec.loader.exec_module(minute_red_streak) + def _bars(symbol: str, candles: list[tuple[float, float, float]], start_hour: int = 9) -> pl.DataFrame: """candles: (open, close, high) 序列, 时间从 start_hour:30 起每分钟一根。""" @@ -261,15 +270,15 @@ def test_pattern_limit_up_disabled_ignores_daily(): # ── 引擎加载与运行 ────────────────────────────────────────────────── -def test_builtin_minute_strategy_loads_with_minute_filter_backend(): - engine = StrategyEngine( - strategy_dirs=[Path(__file__).resolve().parent.parent / "app" / "strategy" / "builtin"] - ) +def test_custom_minute_strategy_loads_with_minute_filter_backend(): + # 自定义策略与内置策略共用同一加载器: 夹具目录即一个 custom 目录 + engine = StrategyEngine(strategy_dirs=[STRATEGY_FIXTURE_DIR]) assert not [e for e in engine.load_errors() if "minute" in e["file"]] s = engine.get("minute_red_streak") assert s.execution_backend == "minute_filter" assert s.filter_minute_history_fn is not None assert s.meta["timeframes"] == ["1m"] + assert s.source == "custom" def _minute_code(sid: str, timeframes: str = '["1m"]', extra: str = "") -> str: diff --git a/backend/tests/test_screener_etf.py b/backend/tests/test_screener_etf.py index 32c4de7..7d37ead 100644 --- a/backend/tests/test_screener_etf.py +++ b/backend/tests/test_screener_etf.py @@ -41,8 +41,8 @@ def test_all_builtin_strategies_declare_asset_types_and_timeframes(): assert engine.load_errors() == [] for meta in engine.list_strategies(): assert meta["asset_types"] - # 分钟策略 timeframes 为 ["1m"], 日线内置策略为 ["1d"] - assert meta["timeframes"] in (["1d"], ["1m"]) + # 分钟红7已迁至自定义策略目录, 内置策略均为日线 + assert meta["timeframes"] == ["1d"] def test_all_builtin_strategies_use_matrix_backend_only(): @@ -54,10 +54,8 @@ def test_all_builtin_strategies_use_matrix_backend_only(): assert all(s.matrix_strategy is not None for s in matrix_strategies) assert all(s.filter_fn is None for s in matrix_strategies) assert all(s.filter_history_fn is None for s in matrix_strategies) - # 分钟形态策略 (minute_filter) 不参与日线矩阵不变量 - assert [s.meta["id"] for s in strategies if s.execution_backend == "minute_filter"] == [ - "minute_red_streak" - ] + # 分钟形态策略 (minute_filter) 已迁至自定义策略目录, 不在 builtin 加载范围 + assert [s.meta["id"] for s in strategies if s.execution_backend == "minute_filter"] == [] def test_all_builtin_matrix_formulas_accept_base_market_matrix():