mirror of
https://github.com/shy3130/tick-stock-panel.git
synced 2026-11-02 10:26:13 +00:00
- 策略: 引擎只加载 data/strategies/{custom,ai,composite} 与研究模板目录,
原 25 个内置策略一次性迁移为用户自定义文件(ID 不变, 引用零迁移);
研究模板移至 app/strategy/research, 打包 spec 同步; 新装环境策略库为空
- 信号: 管线不再硬编码任何信号表达式; 涨跌停判定价成为运行时列
limit_up_price/limit_down_price(生效价∓0.005 容差), 20 个信号转为
data/user_data/custom_signals 定义(显式 column 沿用 signal_* 原列名);
依赖解析/注入顺序/当日路径 prev-join 全部定义驱动; 语义统一为历史路径
口径(顺带修正当日路径 BOLL 比较符与翘板/炸板空值两处既有偏差)
- 前端: 信号库单列表(全部可编辑), SignalPicker/监控选项/信号标签全走
定义接口; 回测页新增免责声明(模拟性质/数据源准确性不作保证/不构成
投资建议)
- 测试: 25 个策略夹具与 20 个信号种子入库 tests/fixtures, 受影响测试
全部自包含化(不依赖运行目录用户数据); 77 万行 MA 死叉新旧表达式对拍
逐行一致; 回测实跑 45 笔卖点信号出场验证矩阵链路
- 同批包含工作区既有进度: 策略结果 ext 列同日投影修复(含测试)、数据源插件、fuyao/auth/api_gateway/data_integrity 调整、README 信号库
描述与 strategy.md 导入路径补充
验证: 后端 2592 passed(空数据目录 CI 模拟 2588 passed, 4 例为模拟环境
伪影复验排除); 前端 vitest 125 passed + tsc 0 错 + vite build 通过
132 lines
4.6 KiB
Python
132 lines
4.6 KiB
Python
from datetime import date
|
|
from types import SimpleNamespace
|
|
|
|
import polars as pl
|
|
import pytest
|
|
from fastapi import HTTPException
|
|
|
|
from app.api import strategy as strategy_api
|
|
from app.strategy import config as strategy_config
|
|
from app.strategy.engine import StrategyDataContext, StrategyDef, StrategyEngine
|
|
|
|
|
|
def _make_engine() -> tuple[StrategyEngine, StrategyDataContext]:
|
|
df = pl.DataFrame({"symbol": ["A", "B", "C"], "value": [1, 2, 3]})
|
|
engine = StrategyEngine(strategy_dirs=[])
|
|
engine._strategies["saved_params"] = StrategyDef(
|
|
meta={"id": "saved_params", "scoring": {}, "limit": 100},
|
|
basic_filter={"enabled": False},
|
|
entry_signals=[],
|
|
exit_signals=[],
|
|
stop_loss=None,
|
|
trailing_stop=None,
|
|
trailing_take_profit_activate=None,
|
|
trailing_take_profit_drawdown=None,
|
|
max_hold_days=None,
|
|
filter_fn=lambda _df, params: pl.col("value") >= params.get("min_value", 1),
|
|
filter_history_fn=None,
|
|
lookback_days=1,
|
|
source="custom",
|
|
)
|
|
return engine, StrategyDataContext(
|
|
asset_type="stock",
|
|
timeframe="1d",
|
|
as_of=date(2026, 7, 15),
|
|
current=df,
|
|
)
|
|
|
|
|
|
def test_run_applies_saved_strategy_params():
|
|
engine, context = _make_engine()
|
|
result = engine.run(
|
|
"saved_params",
|
|
context,
|
|
overrides={"params": {"min_value": 2}},
|
|
)
|
|
|
|
assert [row["symbol"] for row in result.rows] == ["B", "C"]
|
|
|
|
|
|
def test_explicit_params_override_saved_strategy_params():
|
|
engine, context = _make_engine()
|
|
result = engine.run(
|
|
"saved_params",
|
|
context,
|
|
params={"min_value": 3},
|
|
overrides={"params": {"min_value": 2}},
|
|
)
|
|
|
|
assert [row["symbol"] for row in result.rows] == ["C"]
|
|
|
|
|
|
def test_patch_config_preserves_other_user_overrides(tmp_path):
|
|
engine, _ = _make_engine()
|
|
request = SimpleNamespace(app=SimpleNamespace(state=SimpleNamespace(
|
|
strategy_engine=engine,
|
|
repo=SimpleNamespace(store=SimpleNamespace(data_dir=tmp_path)),
|
|
)))
|
|
strategy_config.save_override(tmp_path, "saved_params", {
|
|
"params": {"min_value": 2},
|
|
"stop_loss": -0.05,
|
|
})
|
|
|
|
strategy_api.patch_config(strategy_api.SaveConfigRequest(
|
|
strategy_id="saved_params",
|
|
overrides={
|
|
"scoring": {"rsi_14": 1.0},
|
|
"scoring_directions": {"rsi_14": "low"},
|
|
"scoring_replace": True,
|
|
},
|
|
), request)
|
|
|
|
saved = strategy_config.load_override(tmp_path, "saved_params")
|
|
assert saved["params"] == {"min_value": 2}
|
|
assert saved["stop_loss"] == -0.05
|
|
assert saved["scoring"] == {"rsi_14": 1.0}
|
|
assert saved["scoring_directions"] == {"rsi_14": "low"}
|
|
|
|
|
|
def test_strategy_detail_echoes_saved_risk_overrides():
|
|
"""风控字段 (止盈/移动止损/回撤止盈) 的 override 优先回显 — 策略设置保存后,
|
|
详情与回测页默认 overrides 都要能看到, 与 stop_loss 同口径。"""
|
|
engine, _ = _make_engine()
|
|
s = engine.get("saved_params")
|
|
detail = strategy_api._strategy_detail(s, {
|
|
"stop_loss": -0.05,
|
|
"take_profit": 0.15,
|
|
"trailing_stop": -0.08,
|
|
"trailing_take_profit_activate": 0.2,
|
|
"trailing_take_profit_drawdown": 0.05,
|
|
}, engine)
|
|
assert detail["stop_loss"] == -0.05
|
|
assert detail["take_profit"] == 0.15
|
|
assert detail["trailing_stop"] == -0.08
|
|
assert detail["trailing_take_profit_activate"] == 0.2
|
|
assert detail["trailing_take_profit_drawdown"] == 0.05
|
|
|
|
|
|
def test_strategy_detail_risk_falls_back_to_definition():
|
|
"""无 override 时回退策略定义里的风控值 (模块属性)。"""
|
|
engine, _ = _make_engine()
|
|
s = engine.get("saved_params")
|
|
s.take_profit = 0.1 # StrategyDef 无该字段定义, 模拟模块属性 (getattr 路径)
|
|
detail = strategy_api._strategy_detail(s, None, engine)
|
|
assert detail["take_profit"] == 0.1
|
|
detail_no_override = strategy_api._strategy_detail(s, {"stop_loss": -0.03}, engine)
|
|
assert detail_no_override["take_profit"] == 0.1
|
|
assert detail_no_override["stop_loss"] == -0.03
|
|
|
|
|
|
def test_save_config_rejects_invalid_scoring_direction(tmp_path):
|
|
engine, _ = _make_engine()
|
|
request = SimpleNamespace(app=SimpleNamespace(state=SimpleNamespace(
|
|
strategy_engine=engine,
|
|
repo=SimpleNamespace(store=SimpleNamespace(data_dir=tmp_path)),
|
|
)))
|
|
|
|
with pytest.raises(HTTPException, match="方向无效"):
|
|
strategy_api.save_config(strategy_api.SaveConfigRequest(
|
|
strategy_id="saved_params",
|
|
overrides={"scoring_directions": {"rsi_14": "sideways"}},
|
|
), request)
|