mirror of
https://ghfast.top/https://github.com/aeroxw/tick-stock-panel.git
synced 2026-09-12 23:44:16 +08:00
- 因子平台: /factors 一级页(检验/因子库/编辑器/组合/挖掘), DSL 公式因子(25 算子点选、双语字段、我的因子模板、脏公式守卫), 版本与生命周期, 自动挖掘 L1 统计筛选 - 因子↔策略四条桥: 触发器 Zap 快建因子条件信号、因子一键生成排名策略、自定义信号 AI 提示词接入因子分组、策略回测因子归因(胜/败单入场信号日因子均值, 独立 tab, 双语因子名) - 回测: 统计卡新增盈亏比(≥1 红/<1 绿), 蒙卡回撤合并为中位/95% 双值卡(自适应字号), 高级设置基础过滤与策略编辑器参数对齐(5 组区间) - 信号库独立页 /signals(原设置 tab 迁出), 持仓提醒入导航; 挖掘并入因子页第 5 tab, /mining 旧链接重定向 - 研究线配套: 因子目录 61→77(评分/矩阵双内核), stats_v2(Newey-West/BH-FDR/DSR), enriched 管道与异动/报价服务配套调整 - 文档: README 导航与特性表、features.md 因子平台章节、操作说明书 9.2、factor-platform-plan 执行状态与 §5、二开文档桥接说明; 交流与支持节改版 - 版本 0.2.2 → 0.2.3; 后端全量 1625 passed(1 例环境性跳过), 前端 build 通过
93 lines
3.6 KiB
Python
93 lines
3.6 KiB
Python
"""自定义信号因子条件 — 字段白名单动态化 + 因子列物化链路。
|
|
|
|
因子接入规则 (P3): 条件可引用注册表因子 (虚拟/自定义/复合), 历史路径由
|
|
materialize_factor_columns 复用评分物化管线补算, 与检验/评分同一条计算逻辑。
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
from datetime import date, timedelta
|
|
|
|
import polars as pl
|
|
import pytest
|
|
|
|
from app.strategy import custom_signals
|
|
|
|
|
|
def _frame(n_days: int = 30) -> pl.DataFrame:
|
|
"""两标的收盘价缓涨; ma20 手工预置为 0.9 倍滚动均值 (保证乖离恒为正)。"""
|
|
rows = []
|
|
end = date(2026, 8, 31)
|
|
for symbol_id, base in (("A", 100.0), ("B", 50.0)):
|
|
for i in range(n_days):
|
|
rows.append({
|
|
"symbol": symbol_id,
|
|
"date": end - timedelta(days=n_days - 1 - i),
|
|
"close": base * (1.0 + 0.001 * i),
|
|
})
|
|
df = pl.DataFrame(rows).sort(["symbol", "date"])
|
|
return df.with_columns(
|
|
(pl.col("close").rolling_mean(20).over("symbol") * 0.9).alias("ma20")
|
|
)
|
|
|
|
|
|
def test_allowed_fields_union_registry() -> None:
|
|
allowed = custom_signals.allowed_fields()
|
|
assert "close" in allowed # 物化白名单保留
|
|
assert "ma20_bias" in allowed # 虚拟因子
|
|
assert "turnover_z_60d" in allowed # 虚拟因子 (61 日预热)
|
|
assert "momentum_20d" in allowed # 既是白名单列也是基础因子
|
|
assert "nope_col" not in allowed
|
|
# 静态白名单不受污染 (供 monitor_rules 等仍按物化列口径使用)
|
|
assert "ma20_bias" not in custom_signals.ALLOWED_FIELDS
|
|
|
|
|
|
def test_validate_accepts_and_rejects_factor_fields() -> None:
|
|
ok = {
|
|
"id": "t_bias_low", "name": "乖离超卖", "kind": "entry",
|
|
"conditions": [{"left": "ma20_bias", "op": "<", "right": "-0.05", "leftDays": 0, "rightDays": 0}],
|
|
}
|
|
custom_signals.validate(ok) # 不抛错即通过
|
|
bad = {
|
|
"id": "t_bad", "name": "x", "kind": "entry",
|
|
"conditions": [{"left": "not_a_field", "op": "<", "right": "1"}],
|
|
}
|
|
with pytest.raises(ValueError):
|
|
custom_signals.validate(bad)
|
|
|
|
|
|
def test_materialize_factor_columns_and_inject() -> None:
|
|
sig = {
|
|
"id": "bias_high", "name": "乖离偏高", "kind": "entry", "enabled": True,
|
|
"conditions": [{"left": "ma20_bias", "op": ">", "right": "0", "leftDays": 0, "rightDays": 0}],
|
|
}
|
|
exprs = custom_signals.build_expressions([sig])
|
|
col = custom_signals.column_name("bias_high")
|
|
assert col in exprs
|
|
|
|
df = _frame()
|
|
assert "ma20_bias" not in df.columns
|
|
df2 = custom_signals.materialize_factor_columns(df, exprs)
|
|
assert "ma20_bias" in df2.columns # 复用评分物化路径补算
|
|
|
|
# ma20 = 0.9 x 滚动均值 → 窗口内乖离恒 > 0
|
|
warm = df2.filter(pl.col("ma20_bias").is_not_null())
|
|
assert warm.height > 0
|
|
assert (warm["ma20_bias"] > 0).all()
|
|
|
|
injected = custom_signals.inject(df2, exprs)
|
|
assert col in injected.columns
|
|
hit = injected.filter(pl.col("ma20_bias").is_not_null())
|
|
assert hit[col].all() # 条件在窗口内全部成立
|
|
|
|
|
|
def test_materialize_skips_unknown_columns() -> None:
|
|
"""非注册表缺失列: 物化不处理不报错, 由 inject 缺列告警跳过。"""
|
|
sig = {
|
|
"id": "t_unknown", "name": "x", "kind": "entry", "enabled": True,
|
|
"conditions": [{"left": "not_a_field", "op": "<", "right": "1", "leftDays": 0, "rightDays": 0}],
|
|
}
|
|
exprs = custom_signals.build_expressions([sig]) # 编译不做白名单校验 (validate 负责)
|
|
df = _frame()
|
|
df2 = custom_signals.materialize_factor_columns(df, exprs)
|
|
assert df2.columns == df.columns
|