mirror of
https://ghfast.top/https://github.com/aeroxw/tick-stock-panel.git
synced 2026-09-12 16:44:15 +08:00
feat(strategy): 新增 7 个矩阵原生内置策略, 公开计数 18→25
- 趋势/形态: 均线粘合突破 · 平台整理突破 · 放量创60日新高 - 量价/涨停: 涨停基因活跃股 (max_change_pct 参数过滤当日已大涨) - 反转/波动: MACD 零下回升 · 长下影反击 (close_position>=0.5 兼容假阴线) · RSI 中轴回踩 - 阈值统一小数制口径 (change_pct/momentum 除以 100), 一致性测试计数 26
This commit is contained in:
@@ -43,7 +43,7 @@
|
||||
| 模块 | 一句话 | 详见 |
|
||||
| :--------------- | :--------------------------------------------------------------------- | :-------------------------------- |
|
||||
| 🔀 **能力路由** | 多数据集(日K/除权/实时/分钟/盘口/财务,持续扩展)按源能力独立路由,任选组合 | [custom-data-source.md](./docs/custom-data-source.md) |
|
||||
| 🔍 **选股引擎** | 18 个内置策略 + 分钟策略 + 自定义信号 + AI 生成,Polars 毫秒级扫全 A 股 | [strategy.md](./docs/strategy.md) |
|
||||
| 🔍 **选股引擎** | 25 个内置策略 + 分钟策略 + 自定义信号 + AI 生成,Polars 毫秒级扫全 A 股 | [strategy.md](./docs/strategy.md) |
|
||||
| 📊 **指标流水线** | MA/EMA/MACD/RSI/KDJ/布林/量比等 68 列指标与信号,一次扫表落盘 enriched Parquet | [features.md](./docs/features.md) |
|
||||
| 🧪 **回测研究** | 因子/策略/分钟回测 + 财务快照因子(点时口径),T+1/费用/滑点约束,结果可导出 | [features.md](./docs/features.md) |
|
||||
| ⛏️ **因子挖掘** | 嵌套样本外搜索多因子排名组合,与自有策略对照,候选库显式发布、永不自动上线 | [mining.md](./docs/mining.md) |
|
||||
@@ -335,7 +335,7 @@ PORT=3018 # 服务端口
|
||||
| [docs/configuration.md](./docs/configuration.md) | 所有 `.env` 配置项详解(数据源、AI、服务、密码、数据目录) |
|
||||
| [docs/features.md](./docs/features.md) | 各功能模块详细说明(选股/指标/回测/监控/个股分析/数据扩展) |
|
||||
| [docs/custom-data-source.md](./docs/custom-data-source.md) | 自定义数据源接入、能力路由契约、YAML 配置与 mock 联调示例 |
|
||||
| [docs/strategy.md](./docs/strategy.md) | 策略体系(18 内置策略 + 三种扩展方式 + 文件结构) |
|
||||
| [docs/strategy.md](./docs/strategy.md) | 策略体系(25 内置策略 + 三种扩展方式 + 文件结构) |
|
||||
| [docs/mining.md](./docs/mining.md) | 因子与策略挖掘口径、防泄漏、任务隔离和发布边界 |
|
||||
| [docs/market-phase.md](./docs/market-phase.md) | 市场情绪周期 6 阶段与概念/行业主线识别的口径与设计 |
|
||||
| [docs/plugin-development.md](./docs/plugin-development.md) | 数据源插件开发规范(以 stock-sdk / fuyao 为参考实现) |
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
"""涨停基因活跃股 — 近期多次涨停的活跃标的池 (股性筛选, 非追板)"""
|
||||
|
||||
import numpy as np
|
||||
|
||||
from app.backtest.matrix import (
|
||||
MarketDataMatrix,
|
||||
SignalMatrix,
|
||||
make_signal_matrix,
|
||||
matrix_feature,
|
||||
)
|
||||
|
||||
META = {
|
||||
"id": "active_limit_gene",
|
||||
"name": "涨停基因活跃股",
|
||||
"description": "近60日涨停次数达标的活跃标的, 且当日未涨停、缩量休整 — 股性筛选池",
|
||||
"tags": ["涨停", "活跃", "股性"],
|
||||
"asset_types": ["stock"],
|
||||
"timeframes": ["1d"],
|
||||
"params": [
|
||||
{
|
||||
"id": "min_limit_count",
|
||||
"label": "近60日最少涨停次数",
|
||||
"type": "int",
|
||||
"default": 3,
|
||||
"min": 2,
|
||||
"max": 10,
|
||||
"step": 1,
|
||||
},
|
||||
{
|
||||
"id": "vol_ratio_max",
|
||||
"label": "当日量比上限 (休整)",
|
||||
"type": "float",
|
||||
"default": 1.2,
|
||||
"min": 0.5,
|
||||
"max": 3.0,
|
||||
"step": 0.1,
|
||||
},
|
||||
{
|
||||
"id": "max_change_pct",
|
||||
"label": "当日涨幅上限% (排除涨停)",
|
||||
"type": "float",
|
||||
"default": 7.0,
|
||||
"min": 3.0,
|
||||
"max": 15.0,
|
||||
"step": 0.5,
|
||||
},
|
||||
],
|
||||
"scoring": {"limit_up_count_60d": 0.4, "momentum_20d": 0.3, "turnover_ratio_5d": 0.3},
|
||||
"order_by": "score",
|
||||
"descending": True,
|
||||
"limit": 100,
|
||||
}
|
||||
|
||||
EXECUTION_BACKEND = "matrix_native"
|
||||
ENTRY_SIGNALS = ["signal_active_limit_gene"]
|
||||
EXIT_SIGNALS = ["signal_active_gene_cool"]
|
||||
STOP_LOSS = -0.08
|
||||
MAX_HOLD_DAYS = 25
|
||||
|
||||
|
||||
class ActiveLimitGeneMatrixStrategy:
|
||||
def required_fields(self) -> frozenset[str]:
|
||||
return frozenset({"close", "volume"})
|
||||
|
||||
def required_warmup_bars(self, params: dict) -> int:
|
||||
del params
|
||||
return 70
|
||||
|
||||
def compute_signals(self, market: MarketDataMatrix, params: dict) -> SignalMatrix:
|
||||
entry = matrix_feature(market, "limit_up_count_60d") >= int(params.get("min_limit_count", 3))
|
||||
# 当日未涨停 (休整日而非情绪顶点) 且量能收敛
|
||||
entry &= matrix_feature(market, "change_pct") < float(params.get("max_change_pct", 7.0)) / 100.0
|
||||
entry &= matrix_feature(market, "vol_ratio_5d") <= float(params.get("vol_ratio_max", 1.2))
|
||||
|
||||
# 出场: 股性冷却 (60日涨停计数回落到阈值下)
|
||||
exit_ = matrix_feature(market, "limit_up_count_60d") < int(params.get("min_limit_count", 3)) - 1
|
||||
return make_signal_matrix(
|
||||
market.shape,
|
||||
entry=entry.astype(np.uint8),
|
||||
exit=exit_.astype(np.uint8),
|
||||
entry_signal_code=np.where(entry, 0, -1).astype(np.int16),
|
||||
exit_signal_code=np.where(exit_, 0, -1).astype(np.int16),
|
||||
entry_signal_ids=("signal_active_limit_gene",),
|
||||
exit_signal_ids=("signal_active_gene_cool",),
|
||||
)
|
||||
|
||||
|
||||
MATRIX_STRATEGY = ActiveLimitGeneMatrixStrategy()
|
||||
@@ -0,0 +1,83 @@
|
||||
"""放量创60日新高 — 突破前期高点平台 + 量能确认"""
|
||||
|
||||
import numpy as np
|
||||
|
||||
from app.backtest.matrix import (
|
||||
MarketDataMatrix,
|
||||
SignalMatrix,
|
||||
make_signal_matrix,
|
||||
matrix_feature,
|
||||
)
|
||||
from app.backtest.matrix import (
|
||||
valid_shift as shift,
|
||||
)
|
||||
|
||||
META = {
|
||||
"id": "breakout_new_high_60d",
|
||||
"name": "放量创60日新高",
|
||||
"description": "收盘突破前60日最高价, 量比配合, 趋势强度确认",
|
||||
"tags": ["突破", "新高"],
|
||||
"asset_types": ["stock", "etf"],
|
||||
"timeframes": ["1d"],
|
||||
"params": [
|
||||
{"id": "require_volume", "label": "要求量比配合", "type": "bool", "default": True},
|
||||
{
|
||||
"id": "vol_ratio_min",
|
||||
"label": "最低量比",
|
||||
"type": "float",
|
||||
"default": 1.3,
|
||||
"min": 0.5,
|
||||
"max": 5.0,
|
||||
"step": 0.1,
|
||||
},
|
||||
{
|
||||
"id": "min_change_pct",
|
||||
"label": "最低当日涨幅%",
|
||||
"type": "float",
|
||||
"default": 2.0,
|
||||
"min": 0.0,
|
||||
"max": 10.0,
|
||||
"step": 0.5,
|
||||
},
|
||||
],
|
||||
"scoring": {"momentum_20d": 0.4, "vol_ratio_5d": 0.3, "change_pct": 0.3},
|
||||
"order_by": "score",
|
||||
"descending": True,
|
||||
"limit": 100,
|
||||
}
|
||||
|
||||
EXECUTION_BACKEND = "matrix_native"
|
||||
ENTRY_SIGNALS = ["signal_breakout_high_60d"]
|
||||
EXIT_SIGNALS = ["signal_break_ma20_lose"]
|
||||
STOP_LOSS = -0.07
|
||||
MAX_HOLD_DAYS = 20
|
||||
|
||||
|
||||
class BreakoutNewHigh60dMatrixStrategy:
|
||||
def required_fields(self) -> frozenset[str]:
|
||||
return frozenset({"close", "volume"})
|
||||
|
||||
def required_warmup_bars(self, params: dict) -> int:
|
||||
del params
|
||||
return 70
|
||||
|
||||
def compute_signals(self, market: MarketDataMatrix, params: dict) -> SignalMatrix:
|
||||
prior_high = shift(matrix_feature(market, "high_60d"), 1) # 昨日及以前的60日高点
|
||||
entry = market.close > prior_high
|
||||
entry &= matrix_feature(market, "change_pct") >= float(params.get("min_change_pct", 2.0)) / 100.0
|
||||
if params.get("require_volume", True):
|
||||
entry &= matrix_feature(market, "vol_ratio_5d") >= float(params.get("vol_ratio_min", 1.3))
|
||||
ma20 = matrix_feature(market, "ma20")
|
||||
exit_ = market.close < ma20
|
||||
return make_signal_matrix(
|
||||
market.shape,
|
||||
entry=entry.astype(np.uint8),
|
||||
exit=exit_.astype(np.uint8),
|
||||
entry_signal_code=np.where(entry, 0, -1).astype(np.int16),
|
||||
exit_signal_code=np.where(exit_, 0, -1).astype(np.int16),
|
||||
entry_signal_ids=("signal_breakout_high_60d",),
|
||||
exit_signal_ids=("signal_break_ma20_lose",),
|
||||
)
|
||||
|
||||
|
||||
MATRIX_STRATEGY = BreakoutNewHigh60dMatrixStrategy()
|
||||
@@ -0,0 +1,90 @@
|
||||
"""长下影反击 — 超跌后放量收长下影线, 多头承接确认"""
|
||||
|
||||
import numpy as np
|
||||
|
||||
from app.backtest.matrix import (
|
||||
MarketDataMatrix,
|
||||
SignalMatrix,
|
||||
make_signal_matrix,
|
||||
matrix_feature,
|
||||
)
|
||||
from app.backtest.matrix import (
|
||||
valid_shift as shift,
|
||||
)
|
||||
|
||||
META = {
|
||||
"id": "long_lower_shadow_reversal",
|
||||
"name": "长下影反击",
|
||||
"description": "近5日超跌后收长下影线且收盘收复实体, 下方承接强势的反转信号",
|
||||
"tags": ["K线形态", "超跌", "反转"],
|
||||
"asset_types": ["stock"],
|
||||
"timeframes": ["1d"],
|
||||
"params": [
|
||||
{
|
||||
"id": "shadow_pct_min",
|
||||
"label": "下影线最低长度% (相对昨收)",
|
||||
"type": "float",
|
||||
"default": 3.0,
|
||||
"min": 1.0,
|
||||
"max": 10.0,
|
||||
"step": 0.5,
|
||||
},
|
||||
{
|
||||
"id": "drop_pct_max",
|
||||
"label": "近5日累计跌幅下限% (负值)",
|
||||
"type": "float",
|
||||
"default": -5.0,
|
||||
"min": -30.0,
|
||||
"max": 0.0,
|
||||
"step": 1.0,
|
||||
},
|
||||
{"id": "require_recovery", "label": "要求收盘收复上半区", "type": "bool", "default": True},
|
||||
],
|
||||
"scoring": {"momentum_20d": 0.3, "vol_ratio_5d": 0.4, "change_pct": 0.3},
|
||||
"order_by": "score",
|
||||
"descending": True,
|
||||
"limit": 100,
|
||||
}
|
||||
|
||||
EXECUTION_BACKEND = "matrix_native"
|
||||
ENTRY_SIGNALS = ["signal_long_lower_shadow"]
|
||||
EXIT_SIGNALS = ["signal_ma5_lose_after_shadow"]
|
||||
STOP_LOSS = -0.05
|
||||
MAX_HOLD_DAYS = 10
|
||||
|
||||
|
||||
class LongLowerShadowReversalMatrixStrategy:
|
||||
def required_fields(self) -> frozenset[str]:
|
||||
return frozenset({"open", "high", "low", "close", "volume"})
|
||||
|
||||
def required_warmup_bars(self, params: dict) -> int:
|
||||
del params
|
||||
return 20
|
||||
|
||||
def compute_signals(self, market: MarketDataMatrix, params: dict) -> SignalMatrix:
|
||||
prev_close = matrix_feature(market, "prev_close")
|
||||
body_bot = np.minimum(market.open, market.close)
|
||||
shadow_pct = (body_bot - market.low) / prev_close * 100.0
|
||||
|
||||
entry = shadow_pct >= float(params.get("shadow_pct_min", 3.0))
|
||||
entry &= matrix_feature(market, "momentum_5d") <= float(params.get("drop_pct_max", -5.0)) / 100.0
|
||||
entry &= matrix_feature(market, "vol_ratio_5d") >= 1.2 # 承接需有量
|
||||
if params.get("require_recovery", True):
|
||||
# 收盘位于当日振幅上半部 (close_position ∈ [0,1]): 长下影反击常为低开回拉,
|
||||
# 收盘仍可能低于开盘 (假阴线), 用位置而非阴阳判定收复力度
|
||||
entry &= matrix_feature(market, "close_position") >= 0.5
|
||||
|
||||
ma5 = matrix_feature(market, "ma5")
|
||||
exit_ = market.close < ma5
|
||||
return make_signal_matrix(
|
||||
market.shape,
|
||||
entry=entry.astype(np.uint8),
|
||||
exit=exit_.astype(np.uint8),
|
||||
entry_signal_code=np.where(entry, 0, -1).astype(np.int16),
|
||||
exit_signal_code=np.where(exit_, 0, -1).astype(np.int16),
|
||||
entry_signal_ids=("signal_long_lower_shadow",),
|
||||
exit_signal_ids=("signal_ma5_lose_after_shadow",),
|
||||
)
|
||||
|
||||
|
||||
MATRIX_STRATEGY = LongLowerShadowReversalMatrixStrategy()
|
||||
@@ -0,0 +1,105 @@
|
||||
"""均线粘合突破 — MA5/10/20 挤压收敛后放量向上突破 (变盘启动点)"""
|
||||
|
||||
import numpy as np
|
||||
|
||||
from app.backtest.matrix import (
|
||||
MarketDataMatrix,
|
||||
SignalMatrix,
|
||||
make_signal_matrix,
|
||||
matrix_feature,
|
||||
)
|
||||
from app.backtest.matrix import (
|
||||
valid_shift as shift,
|
||||
)
|
||||
|
||||
META = {
|
||||
"id": "ma_convergence_breakout",
|
||||
"name": "均线粘合突破",
|
||||
"description": "MA5/10/20 粘合收敛后放量突破, 挤压释放的变盘启动点",
|
||||
"tags": ["均线", "粘合", "突破"],
|
||||
"asset_types": ["stock", "etf"],
|
||||
"timeframes": ["1d"],
|
||||
"params": [
|
||||
{
|
||||
"id": "spread_pct_max",
|
||||
"label": "粘合带宽上限% (三线极差/MA20)",
|
||||
"type": "float",
|
||||
"default": 2.5,
|
||||
"min": 0.5,
|
||||
"max": 8.0,
|
||||
"step": 0.5,
|
||||
},
|
||||
{
|
||||
"id": "squeeze_days",
|
||||
"label": "粘合持续天数",
|
||||
"type": "int",
|
||||
"default": 5,
|
||||
"min": 3,
|
||||
"max": 15,
|
||||
"step": 1,
|
||||
},
|
||||
{
|
||||
"id": "min_change_pct",
|
||||
"label": "突破日最低涨幅%",
|
||||
"type": "float",
|
||||
"default": 2.0,
|
||||
"min": 0.0,
|
||||
"max": 10.0,
|
||||
"step": 0.5,
|
||||
},
|
||||
{"id": "require_volume", "label": "要求量比配合", "type": "bool", "default": True},
|
||||
],
|
||||
"scoring": {"vol_ratio_5d": 0.4, "change_pct": 0.3, "momentum_20d": 0.3},
|
||||
"order_by": "score",
|
||||
"descending": True,
|
||||
"limit": 100,
|
||||
}
|
||||
|
||||
EXECUTION_BACKEND = "matrix_native"
|
||||
ENTRY_SIGNALS = ["signal_ma_convergence_breakout"]
|
||||
EXIT_SIGNALS = ["signal_ma10_lose"]
|
||||
STOP_LOSS = -0.06
|
||||
MAX_HOLD_DAYS = 15
|
||||
|
||||
|
||||
class MAConvergenceBreakoutMatrixStrategy:
|
||||
def required_fields(self) -> frozenset[str]:
|
||||
return frozenset({"close", "volume"})
|
||||
|
||||
def required_warmup_bars(self, params: dict) -> int:
|
||||
return 20 + int(params.get("squeeze_days", 5))
|
||||
|
||||
def compute_signals(self, market: MarketDataMatrix, params: dict) -> SignalMatrix:
|
||||
ma5 = matrix_feature(market, "ma5")
|
||||
ma10 = matrix_feature(market, "ma10")
|
||||
ma20 = matrix_feature(market, "ma20")
|
||||
band_top = np.maximum(np.maximum(ma5, ma10), ma20)
|
||||
band_bot = np.minimum(np.minimum(ma5, ma10), ma20)
|
||||
spread = np.abs(band_top - band_bot) / ma20 * 100.0
|
||||
|
||||
# 粘合: 近 squeeze_days 日 (含昨日, 不含今日) 带宽持续低于阈值
|
||||
days = max(2, int(params.get("squeeze_days", 5)))
|
||||
tight = spread <= float(params.get("spread_pct_max", 2.5))
|
||||
squeeze = shift(tight, 1).astype(bool)
|
||||
for k in range(2, days + 1):
|
||||
squeeze &= shift(tight, k).astype(bool)
|
||||
|
||||
# 今日变盘: 放量阳线脱离粘合带, 收盘站上三线上方
|
||||
entry = squeeze & (market.close > band_top)
|
||||
entry &= matrix_feature(market, "change_pct") >= float(params.get("min_change_pct", 2.0)) / 100.0
|
||||
if params.get("require_volume", True):
|
||||
entry &= matrix_feature(market, "vol_ratio_5d") >= 1.2
|
||||
|
||||
exit_ = market.close < ma10
|
||||
return make_signal_matrix(
|
||||
market.shape,
|
||||
entry=entry.astype(np.uint8),
|
||||
exit=exit_.astype(np.uint8),
|
||||
entry_signal_code=np.where(entry, 0, -1).astype(np.int16),
|
||||
exit_signal_code=np.where(exit_, 0, -1).astype(np.int16),
|
||||
entry_signal_ids=("signal_ma_convergence_breakout",),
|
||||
exit_signal_ids=("signal_ma10_lose",),
|
||||
)
|
||||
|
||||
|
||||
MATRIX_STRATEGY = MAConvergenceBreakoutMatrixStrategy()
|
||||
@@ -0,0 +1,89 @@
|
||||
"""MACD 零下回升 — 股价新低而 DIF 拒绝新低 (底背离简化形态)"""
|
||||
|
||||
import numpy as np
|
||||
|
||||
from app.backtest.matrix import (
|
||||
MarketDataMatrix,
|
||||
SignalMatrix,
|
||||
make_signal_matrix,
|
||||
matrix_feature,
|
||||
)
|
||||
from app.backtest.matrix import (
|
||||
valid_shift as shift,
|
||||
)
|
||||
|
||||
META = {
|
||||
"id": "macd_below_zero_revival",
|
||||
"name": "MACD 零下回升",
|
||||
"description": "股价创阶段新低而 MACD DIF 拒绝新低并回升 (底背离简化), 零轴下方动能修复",
|
||||
"tags": ["MACD", "背离", "超跌"],
|
||||
"asset_types": ["stock"],
|
||||
"timeframes": ["1d"],
|
||||
"params": [
|
||||
{
|
||||
"id": "low_window",
|
||||
"label": "新低回看天数",
|
||||
"type": "int",
|
||||
"default": 20,
|
||||
"min": 10,
|
||||
"max": 60,
|
||||
"step": 5,
|
||||
},
|
||||
{
|
||||
"id": "revive_days",
|
||||
"label": "DIF 回升对比天数",
|
||||
"type": "int",
|
||||
"default": 10,
|
||||
"min": 5,
|
||||
"max": 20,
|
||||
"step": 1,
|
||||
},
|
||||
],
|
||||
"scoring": {"momentum_20d": 0.4, "change_pct": 0.3, "vol_ratio_5d": 0.3},
|
||||
"order_by": "score",
|
||||
"descending": True,
|
||||
"limit": 100,
|
||||
}
|
||||
|
||||
EXECUTION_BACKEND = "matrix_native"
|
||||
ENTRY_SIGNALS = ["signal_macd_below_zero_revival"]
|
||||
EXIT_SIGNALS = ["signal_macd_golden_above_zero"]
|
||||
STOP_LOSS = -0.06
|
||||
MAX_HOLD_DAYS = 20
|
||||
|
||||
|
||||
class MACDBelowZeroRevivalMatrixStrategy:
|
||||
def required_fields(self) -> frozenset[str]:
|
||||
return frozenset({"close", "volume"})
|
||||
|
||||
def required_warmup_bars(self, params: dict) -> int:
|
||||
return int(params.get("low_window", 20)) + 40
|
||||
|
||||
def compute_signals(self, market: MarketDataMatrix, params: dict) -> SignalMatrix:
|
||||
win = max(5, int(params.get("low_window", 20)))
|
||||
revive = max(3, int(params.get("revive_days", 10)))
|
||||
dif = matrix_feature(market, "macd_dif")
|
||||
|
||||
# 阶段新低: 今日收盘 <= 前 win 日 (不含今日) 的最低收盘
|
||||
prior_min = shift(market.close, 1)
|
||||
for k in range(2, win + 1):
|
||||
prior_min = np.fmin(prior_min, shift(market.close, k))
|
||||
new_low = market.close <= prior_min
|
||||
|
||||
# 零下 + DIF 较 revive 日前抬升 (动能拒绝新低)
|
||||
entry = new_low & (dif < 0) & (dif > shift(dif, revive))
|
||||
|
||||
# 出场: DIF 上穿零轴 (修复完成)
|
||||
exit_ = (dif > 0) & (shift(dif, 1) <= 0)
|
||||
return make_signal_matrix(
|
||||
market.shape,
|
||||
entry=entry.astype(np.uint8),
|
||||
exit=exit_.astype(np.uint8),
|
||||
entry_signal_code=np.where(entry, 0, -1).astype(np.int16),
|
||||
exit_signal_code=np.where(exit_, 0, -1).astype(np.int16),
|
||||
entry_signal_ids=("signal_macd_below_zero_revival",),
|
||||
exit_signal_ids=("signal_macd_golden_above_zero",),
|
||||
)
|
||||
|
||||
|
||||
MATRIX_STRATEGY = MACDBelowZeroRevivalMatrixStrategy()
|
||||
@@ -0,0 +1,99 @@
|
||||
"""平台缩量整理突破 — 窄幅横盘蓄势后放量突破平台上沿"""
|
||||
|
||||
import numpy as np
|
||||
|
||||
from app.backtest.matrix import (
|
||||
MarketDataMatrix,
|
||||
SignalMatrix,
|
||||
make_signal_matrix,
|
||||
matrix_feature,
|
||||
)
|
||||
from app.backtest.matrix import (
|
||||
valid_shift as shift,
|
||||
)
|
||||
|
||||
META = {
|
||||
"id": "platform_consolidation_breakout",
|
||||
"name": "平台整理突破",
|
||||
"description": "近N日窄幅横盘 (振幅收敛) 后放量突破平台上沿, 蓄势变盘",
|
||||
"tags": ["形态", "平台", "突破"],
|
||||
"asset_types": ["stock"],
|
||||
"timeframes": ["1d"],
|
||||
"params": [
|
||||
{
|
||||
"id": "platform_days",
|
||||
"label": "平台天数",
|
||||
"type": "int",
|
||||
"default": 10,
|
||||
"min": 5,
|
||||
"max": 30,
|
||||
"step": 1,
|
||||
},
|
||||
{
|
||||
"id": "range_pct_max",
|
||||
"label": "平台振幅上限%",
|
||||
"type": "float",
|
||||
"default": 8.0,
|
||||
"min": 3.0,
|
||||
"max": 20.0,
|
||||
"step": 0.5,
|
||||
},
|
||||
{
|
||||
"id": "vol_ratio_min",
|
||||
"label": "突破日最低量比",
|
||||
"type": "float",
|
||||
"default": 1.5,
|
||||
"min": 1.0,
|
||||
"max": 5.0,
|
||||
"step": 0.1,
|
||||
},
|
||||
],
|
||||
"scoring": {"vol_ratio_5d": 0.4, "momentum_20d": 0.3, "change_pct": 0.3},
|
||||
"order_by": "score",
|
||||
"descending": True,
|
||||
"limit": 100,
|
||||
}
|
||||
|
||||
EXECUTION_BACKEND = "matrix_native"
|
||||
ENTRY_SIGNALS = ["signal_platform_breakout"]
|
||||
EXIT_SIGNALS = ["signal_platform_fail_ma20"]
|
||||
STOP_LOSS = -0.06
|
||||
MAX_HOLD_DAYS = 15
|
||||
|
||||
|
||||
class PlatformConsolidationBreakoutMatrixStrategy:
|
||||
def required_fields(self) -> frozenset[str]:
|
||||
return frozenset({"high", "low", "close", "volume"})
|
||||
|
||||
def required_warmup_bars(self, params: dict) -> int:
|
||||
return int(params.get("platform_days", 10)) + 10
|
||||
|
||||
def compute_signals(self, market: MarketDataMatrix, params: dict) -> SignalMatrix:
|
||||
days = max(4, int(params.get("platform_days", 10)))
|
||||
|
||||
# 平台区间: 前 days 日 (不含今日) 的最高/最低 (滚动窗口平移)
|
||||
prior_high = shift(market.high, 1)
|
||||
prior_low = shift(market.low, 1)
|
||||
for k in range(2, days + 1):
|
||||
prior_high = np.fmax(prior_high, shift(market.high, k))
|
||||
prior_low = np.fmin(prior_low, shift(market.low, k))
|
||||
range_pct = (prior_high - prior_low) / market.close * 100.0
|
||||
|
||||
# 平台成立 + 今日放量突破平台上沿
|
||||
entry = range_pct <= float(params.get("range_pct_max", 8.0))
|
||||
entry &= market.close > prior_high
|
||||
entry &= matrix_feature(market, "vol_ratio_5d") >= float(params.get("vol_ratio_min", 1.5))
|
||||
|
||||
exit_ = market.close < matrix_feature(market, "ma20")
|
||||
return make_signal_matrix(
|
||||
market.shape,
|
||||
entry=entry.astype(np.uint8),
|
||||
exit=exit_.astype(np.uint8),
|
||||
entry_signal_code=np.where(entry, 0, -1).astype(np.int16),
|
||||
exit_signal_code=np.where(exit_, 0, -1).astype(np.int16),
|
||||
entry_signal_ids=("signal_platform_breakout",),
|
||||
exit_signal_ids=("signal_platform_fail_ma20",),
|
||||
)
|
||||
|
||||
|
||||
MATRIX_STRATEGY = PlatformConsolidationBreakoutMatrixStrategy()
|
||||
@@ -0,0 +1,98 @@
|
||||
"""RSI 中轴回踩 — 强趋势中 RSI 回落至 50 中轴附近而不破, 低吸点"""
|
||||
|
||||
import numpy as np
|
||||
|
||||
from app.backtest.matrix import (
|
||||
MarketDataMatrix,
|
||||
SignalMatrix,
|
||||
make_signal_matrix,
|
||||
matrix_feature,
|
||||
)
|
||||
from app.backtest.matrix import (
|
||||
valid_shift as shift,
|
||||
)
|
||||
|
||||
META = {
|
||||
"id": "rsi_midline_pullback",
|
||||
"name": "RSI 中轴回踩",
|
||||
"description": "多头趋势 (MA60 上方) 中 RSI 回落至 50 中轴区间企稳, 强势股低吸",
|
||||
"tags": ["RSI", "趋势", "低吸"],
|
||||
"asset_types": ["stock", "etf"],
|
||||
"timeframes": ["1d"],
|
||||
"params": [
|
||||
{
|
||||
"id": "rsi_period",
|
||||
"label": "RSI 周期",
|
||||
"type": "int",
|
||||
"default": 14,
|
||||
"min": 6,
|
||||
"max": 24,
|
||||
"step": 2,
|
||||
},
|
||||
{
|
||||
"id": "mid_low",
|
||||
"label": "中轴区间下沿",
|
||||
"type": "float",
|
||||
"default": 45.0,
|
||||
"min": 30.0,
|
||||
"max": 55.0,
|
||||
"step": 1.0,
|
||||
},
|
||||
{
|
||||
"id": "mid_high",
|
||||
"label": "中轴区间上沿",
|
||||
"type": "float",
|
||||
"default": 60.0,
|
||||
"min": 45.0,
|
||||
"max": 70.0,
|
||||
"step": 1.0,
|
||||
},
|
||||
],
|
||||
"scoring": {"momentum_20d": 0.4, "up_days_20d": 0.3, "vol_ratio_5d": 0.3},
|
||||
"order_by": "score",
|
||||
"descending": True,
|
||||
"limit": 100,
|
||||
}
|
||||
|
||||
EXECUTION_BACKEND = "matrix_native"
|
||||
ENTRY_SIGNALS = ["signal_rsi_midline_pullback"]
|
||||
EXIT_SIGNALS = ["signal_rsi_midline_fail"]
|
||||
STOP_LOSS = -0.06
|
||||
MAX_HOLD_DAYS = 15
|
||||
|
||||
|
||||
class RSIMidlinePullbackMatrixStrategy:
|
||||
def required_fields(self) -> frozenset[str]:
|
||||
return frozenset({"close", "volume"})
|
||||
|
||||
def required_warmup_bars(self, params: dict) -> int:
|
||||
return int(params.get("rsi_period", 14)) + 60
|
||||
|
||||
def compute_signals(self, market: MarketDataMatrix, params: dict) -> SignalMatrix:
|
||||
rsi = matrix_feature(market, f"rsi_{int(params.get('rsi_period', 14))}")
|
||||
lo = float(params.get("mid_low", 45.0))
|
||||
hi = float(params.get("mid_high", 60.0))
|
||||
|
||||
# 趋势前提: MA60 上方; 今日 RSI 落在中轴区间
|
||||
entry = market.close > matrix_feature(market, "ma60")
|
||||
entry &= (rsi >= lo) & (rsi <= hi)
|
||||
|
||||
# 回踩而非走坏: 近5日内出现过 RSI > hi+5 (强势记忆), 且今日未破中轴下沿
|
||||
was_strong = shift(rsi, 1) > hi + 5
|
||||
for k in range(2, 6):
|
||||
was_strong |= shift(rsi, k) > hi + 5
|
||||
entry &= was_strong
|
||||
|
||||
exit_ = market.close < matrix_feature(market, "ma20")
|
||||
return make_signal_matrix(
|
||||
market.shape,
|
||||
entry=entry.astype(np.uint8),
|
||||
exit=exit_.astype(np.uint8),
|
||||
entry_signal_code=np.where(entry, 0, -1).astype(np.int16),
|
||||
exit_signal_code=np.where(exit_, 0, -1).astype(np.int16),
|
||||
entry_signal_ids=("signal_rsi_midline_pullback",),
|
||||
exit_signal_ids=("signal_rsi_midline_fail",),
|
||||
)
|
||||
|
||||
|
||||
MATRIX_STRATEGY = RSIMidlinePullbackMatrixStrategy()
|
||||
@@ -320,7 +320,7 @@ def test_builtin_matrix_strategies_use_their_declared_formula_modules():
|
||||
)
|
||||
|
||||
# 分钟形态策略 (minute_red_streak) 已迁至自定义策略目录, 内置策略全部 matrix 后端
|
||||
assert len(strategy_files) == 19
|
||||
assert len(strategy_files) == 26
|
||||
for strategy_path in strategy_files:
|
||||
strategy = StrategyEngine._load_file(strategy_path)
|
||||
assert strategy.execution_backend == "matrix_native"
|
||||
@@ -790,7 +790,7 @@ def test_registered_builtin_matrix_strategies_share_one_cache_profile():
|
||||
if s.execution_backend != "minute_filter"
|
||||
)
|
||||
|
||||
assert len(strategies) == 19
|
||||
assert len(strategies) == 26
|
||||
assert all(strategy.execution_backend == "matrix_native" for strategy in strategies)
|
||||
assert profile.warmup_bars > 0
|
||||
assert profile.forward_bars == max(int(strategy.max_hold_days or 0) for strategy in strategies)
|
||||
|
||||
+4
-4
@@ -8,13 +8,13 @@
|
||||
|
||||
## 内置策略
|
||||
|
||||
**18 个内置策略**,每个策略一个独立 Python 文件,基于 Polars 表达式向量化实现(`backend/app/strategy/builtin/`):
|
||||
**25 个内置策略**,每个策略一个独立 Python 文件,基于 Polars 表达式向量化实现(`backend/app/strategy/builtin/`):
|
||||
|
||||
| 类型 | 代表策略 |
|
||||
| :---------- | :------------------------------------------------------- |
|
||||
| 趋势 / 形态 | 趋势突破 · 均线多头 · MA 金叉 · MACD 金叉放量 · 布林突破 |
|
||||
| 量价 / 涨停 | 量价齐升 · 高换手强势 · 连板股 · 断板反包 · 涨停动量 · 接近涨停 |
|
||||
| 反转 / 波动 | 超跌反弹 · 超卖反转 · 新低反转 · 低波动龙头 · 回踩 MA20 · 回踩支撑 · 强势开盘 |
|
||||
| 趋势 / 形态 | 趋势突破 · 均线多头 · MA 金叉 · MACD 金叉放量 · 布林突破 · 均线粘合突破 · 平台整理突破 · 放量创60日新高 |
|
||||
| 量价 / 涨停 | 量价齐升 · 高换手强势 · 连板股 · 断板反包 · 涨停动量 · 接近涨停 · 涨停基因活跃股 |
|
||||
| 反转 / 波动 | 超跌反弹 · 超卖反转 · 新低反转 · 低波动龙头 · 回踩 MA20 · 回踩支撑 · 强势开盘 · MACD 零下回升 · 长下影反击 · RSI 中轴回踩 |
|
||||
|
||||
内置目录 `backend/app/strategy/builtin/` 还包含一个仅供挖掘 worker 使用的受控因子排名研究模板。它不出现在普通选股列表,也不能通过普通策略 API 直接运行或保存 override;挖掘结果发布时会生成独立策略。详见 [因子与策略挖掘](./mining.md)。
|
||||
|
||||
|
||||
Reference in New Issue
Block a user