diff --git a/backend/app/strategy/builtin/boll_breakout.py b/backend/app/strategy/builtin/boll_breakout.py index cb86588..0c6623e 100644 --- a/backend/app/strategy/builtin/boll_breakout.py +++ b/backend/app/strategy/builtin/boll_breakout.py @@ -7,6 +7,10 @@ META = { "description": "突破布林上轨 + 放量, 强势加速信号", "tags": ["布林", "突破"], "params": [ + {"id": "require_boll_breakout", "label": "要求突破布林上轨", "type": "bool", + "default": True}, + {"id": "use_volume_filter", "label": "启用量比过滤", "type": "bool", + "default": True}, {"id": "vol_ratio_min", "label": "最低量比", "type": "float", "default": 1.5, "min": 0.5, "max": 5.0, "step": 0.1}, ], @@ -25,7 +29,9 @@ ALERTS = [] def filter(df: pl.DataFrame, params: dict) -> pl.Expr: vol_min = params.get("vol_ratio_min", 1.5) - return ( - pl.col("signal_boll_breakout_upper").fill_null(False) - & (pl.col("vol_ratio_5d") >= vol_min) - ) + expr = pl.col("symbol").is_not_null() | pl.col("symbol").is_null() + if params.get("require_boll_breakout", True): + expr = expr & pl.col("signal_boll_breakout_upper").fill_null(False) + if params.get("use_volume_filter", True): + expr = expr & (pl.col("vol_ratio_5d") >= vol_min) + return expr diff --git a/backend/app/strategy/builtin/broken_board_recovery.py b/backend/app/strategy/builtin/broken_board_recovery.py index 4fd9b5e..69c4735 100644 --- a/backend/app/strategy/builtin/broken_board_recovery.py +++ b/backend/app/strategy/builtin/broken_board_recovery.py @@ -7,8 +7,14 @@ META = { "description": "连板≥2后断板1-2天, 出现放量反包信号", "tags": ["涨停", "反包"], "params": [ + {"id": "require_limit_up", "label": "要求当日涨停", "type": "bool", + "default": True}, + {"id": "use_volume_filter", "label": "启用量比过滤", "type": "bool", + "default": True}, {"id": "vol_ratio_min", "label": "最低量比", "type": "float", "default": 1.5, "min": 0.5, "max": 5.0, "step": 0.1}, + {"id": "use_change_filter", "label": "启用涨幅过滤", "type": "bool", + "default": True}, {"id": "change_pct_min", "label": "最低涨幅", "type": "float", "default": 0.03, "min": 0.01, "max": 0.10, "step": 0.01}, ], @@ -28,8 +34,11 @@ ALERTS = [] def filter(df: pl.DataFrame, params: dict) -> pl.Expr: vol_min = params.get("vol_ratio_min", 1.5) chg_min = params.get("change_pct_min", 0.03) - return ( - pl.col("signal_limit_up").fill_null(False) - & (pl.col("vol_ratio_5d") >= vol_min) - & (pl.col("change_pct") > chg_min) - ) + expr = pl.col("symbol").is_not_null() | pl.col("symbol").is_null() + if params.get("require_limit_up", True): + expr = expr & pl.col("signal_limit_up").fill_null(False) + if params.get("use_volume_filter", True): + expr = expr & (pl.col("vol_ratio_5d") >= vol_min) + if params.get("use_change_filter", True): + expr = expr & (pl.col("change_pct") > chg_min) + return expr diff --git a/backend/app/strategy/builtin/bullish_alignment.py b/backend/app/strategy/builtin/bullish_alignment.py index 8dc3fc1..0aff8f2 100644 --- a/backend/app/strategy/builtin/bullish_alignment.py +++ b/backend/app/strategy/builtin/bullish_alignment.py @@ -6,7 +6,12 @@ META = { "name": "均线多头", "description": "MA5>MA10>MA20>MA60多头排列 + 短期动量为正", "tags": ["均线", "多头"], - "params": [], + "params": [ + {"id": "require_ma_alignment", "label": "要求均线多头排列", "type": "bool", + "default": True}, + {"id": "require_positive_momentum", "label": "要求20日动量为正", "type": "bool", + "default": True}, + ], "scoring": {"momentum_60d": 0.4, "momentum_20d": 0.3, "turnover_rate": 0.3}, "order_by": "score", "descending": True, @@ -21,9 +26,14 @@ ALERTS = [] def filter(df: pl.DataFrame, params: dict) -> pl.Expr: - return ( - (pl.col("ma5") > pl.col("ma10")) - & (pl.col("ma10") > pl.col("ma20")) - & (pl.col("ma20") > pl.col("ma60")) - & (pl.col("momentum_20d") > 0) - ) + expr = pl.col("symbol").is_not_null() | pl.col("symbol").is_null() + if params.get("require_ma_alignment", True): + expr = ( + expr + & (pl.col("ma5") > pl.col("ma10")) + & (pl.col("ma10") > pl.col("ma20")) + & (pl.col("ma20") > pl.col("ma60")) + ) + if params.get("require_positive_momentum", True): + expr = expr & (pl.col("momentum_20d") > 0) + return expr diff --git a/backend/app/strategy/builtin/consecutive_limit_ups.py b/backend/app/strategy/builtin/consecutive_limit_ups.py index 4e864ac..eef4444 100644 --- a/backend/app/strategy/builtin/consecutive_limit_ups.py +++ b/backend/app/strategy/builtin/consecutive_limit_ups.py @@ -7,6 +7,10 @@ META = { "description": "当日涨停且连续涨停≥2天, 强势追涨", "tags": ["涨停", "连板"], "params": [ + {"id": "require_limit_up", "label": "要求当日涨停", "type": "bool", + "default": True}, + {"id": "use_boards_filter", "label": "启用连板数过滤", "type": "bool", + "default": True}, {"id": "min_boards", "label": "最少连板数", "type": "int", "default": 2, "min": 1, "max": 20, "step": 1}, ], @@ -25,7 +29,9 @@ ALERTS = [] def filter(df: pl.DataFrame, params: dict) -> pl.Expr: min_boards = params.get("min_boards", 2) - return ( - pl.col("signal_limit_up").fill_null(False) - & (pl.col("consecutive_limit_ups") >= min_boards) - ) + expr = pl.col("symbol").is_not_null() | pl.col("symbol").is_null() + if params.get("require_limit_up", True): + expr = expr & pl.col("signal_limit_up").fill_null(False) + if params.get("use_boards_filter", True): + expr = expr & (pl.col("consecutive_limit_ups") >= min_boards) + return expr diff --git a/backend/app/strategy/builtin/high_turnover_surge.py b/backend/app/strategy/builtin/high_turnover_surge.py index 53b8276..807de60 100644 --- a/backend/app/strategy/builtin/high_turnover_surge.py +++ b/backend/app/strategy/builtin/high_turnover_surge.py @@ -7,8 +7,12 @@ META = { "description": "换手率 > 5% 且涨幅 > 3%, 资金活跃", "tags": ["换手率", "放量", "资金"], "params": [ + {"id": "use_turnover_filter", "label": "启用换手率过滤", "type": "bool", + "default": True}, {"id": "min_turnover", "label": "最低换手率%", "type": "float", "default": 5.0, "min": 1.0, "max": 20.0, "step": 0.5}, + {"id": "use_change_filter", "label": "启用涨幅过滤", "type": "bool", + "default": True}, {"id": "min_change", "label": "最低涨幅%", "type": "float", "default": 3.0, "min": 1.0, "max": 10.0, "step": 0.5}, ], @@ -28,7 +32,9 @@ ALERTS = [] def filter(df: pl.DataFrame, params: dict) -> pl.Expr: min_to = params.get("min_turnover", 5.0) / 100.0 min_chg = params.get("min_change", 3.0) / 100.0 - return ( - (pl.col("turnover_rate") > min_to) - & (pl.col("change_pct") > min_chg) - ) + expr = pl.col("symbol").is_not_null() | pl.col("symbol").is_null() + if params.get("use_turnover_filter", True): + expr = expr & (pl.col("turnover_rate") > min_to) + if params.get("use_change_filter", True): + expr = expr & (pl.col("change_pct") > min_chg) + return expr diff --git a/backend/app/strategy/builtin/limit_up_momentum.py b/backend/app/strategy/builtin/limit_up_momentum.py index 65afe29..6b653a8 100644 --- a/backend/app/strategy/builtin/limit_up_momentum.py +++ b/backend/app/strategy/builtin/limit_up_momentum.py @@ -7,8 +7,12 @@ META = { "description": "连板股 + 今日涨幅 > 5%, 连板接力追踪", "tags": ["涨停", "连板", "接力"], "params": [ + {"id": "use_change_filter", "label": "启用涨幅过滤", "type": "bool", + "default": True}, {"id": "min_change", "label": "最低涨幅%", "type": "float", "default": 5.0, "min": 2.0, "max": 15.0, "step": 0.5}, + {"id": "use_boards_filter", "label": "启用连板数过滤", "type": "bool", + "default": True}, {"id": "min_boards", "label": "最少连板", "type": "int", "default": 1, "min": 1, "max": 10, "step": 1}, ], @@ -28,7 +32,9 @@ ALERTS = [] def filter(df: pl.DataFrame, params: dict) -> pl.Expr: min_chg = params.get("min_change", 5.0) / 100.0 min_boards = params.get("min_boards", 1) - return ( - (pl.col("change_pct") > min_chg) - & (pl.col("consecutive_limit_ups") >= min_boards) - ) + expr = pl.col("symbol").is_not_null() | pl.col("symbol").is_null() + if params.get("use_change_filter", True): + expr = expr & (pl.col("change_pct") > min_chg) + if params.get("use_boards_filter", True): + expr = expr & (pl.col("consecutive_limit_ups") >= min_boards) + return expr diff --git a/backend/app/strategy/builtin/low_volatility_leader.py b/backend/app/strategy/builtin/low_volatility_leader.py index f32a3a8..7232514 100644 --- a/backend/app/strategy/builtin/low_volatility_leader.py +++ b/backend/app/strategy/builtin/low_volatility_leader.py @@ -7,8 +7,14 @@ META = { "description": "20日动量为正 + 年化波动 < 30% + MA20上方", "tags": ["低波动", "龙头"], "params": [ + {"id": "require_positive_momentum", "label": "要求20日动量为正", "type": "bool", + "default": True}, + {"id": "use_volatility_filter", "label": "启用波动率过滤", "type": "bool", + "default": True}, {"id": "vol_max", "label": "最大年化波动", "type": "float", "default": 0.30, "min": 0.05, "max": 1.0, "step": 0.01}, + {"id": "require_above_ma20", "label": "要求收盘价在MA20上方", "type": "bool", + "default": True}, ], "scoring": {"momentum_60d": 0.4, "momentum_20d": 0.3, "turnover_rate": 0.3}, "order_by": "score", @@ -25,8 +31,11 @@ ALERTS = [] def filter(df: pl.DataFrame, params: dict) -> pl.Expr: vol_max = params.get("vol_max", 0.30) - return ( - (pl.col("momentum_20d") > 0) - & (pl.col("annual_vol_20d") < vol_max) - & (pl.col("close") > pl.col("ma20")) - ) + expr = pl.col("symbol").is_not_null() | pl.col("symbol").is_null() + if params.get("require_positive_momentum", True): + expr = expr & (pl.col("momentum_20d") > 0) + if params.get("use_volatility_filter", True): + expr = expr & (pl.col("annual_vol_20d") < vol_max) + if params.get("require_above_ma20", True): + expr = expr & (pl.col("close") > pl.col("ma20")) + return expr diff --git a/backend/app/strategy/builtin/ma_golden_cross.py b/backend/app/strategy/builtin/ma_golden_cross.py index 81b81fb..6fb90ee 100644 --- a/backend/app/strategy/builtin/ma_golden_cross.py +++ b/backend/app/strategy/builtin/ma_golden_cross.py @@ -7,8 +7,14 @@ META = { "description": "MA5上穿MA20当日触发, 量能配合", "tags": ["均线", "金叉"], "params": [ + {"id": "require_ma_golden", "label": "要求MA5上穿MA20", "type": "bool", + "default": True}, + {"id": "use_volume_filter", "label": "启用量比过滤", "type": "bool", + "default": True}, {"id": "vol_ratio_min", "label": "最低量比", "type": "float", "default": 1.2, "min": 0.5, "max": 5.0, "step": 0.1}, + {"id": "require_above_ma60", "label": "要求收盘价在MA60上方", "type": "bool", + "default": True}, ], "scoring": {"momentum_20d": 0.5, "vol_ratio_5d": 0.3, "change_pct": 0.2}, "order_by": "score", @@ -25,8 +31,11 @@ ALERTS = [] def filter(df: pl.DataFrame, params: dict) -> pl.Expr: vol_min = params.get("vol_ratio_min", 1.2) - return ( - pl.col("signal_ma_golden_5_20").fill_null(False) - & (pl.col("vol_ratio_5d") >= vol_min) - & (pl.col("close") > pl.col("ma60")) - ) + expr = pl.col("symbol").is_not_null() | pl.col("symbol").is_null() + if params.get("require_ma_golden", True): + expr = expr & pl.col("signal_ma_golden_5_20").fill_null(False) + if params.get("use_volume_filter", True): + expr = expr & (pl.col("vol_ratio_5d") >= vol_min) + if params.get("require_above_ma60", True): + expr = expr & (pl.col("close") > pl.col("ma60")) + return expr diff --git a/backend/app/strategy/builtin/macd_golden.py b/backend/app/strategy/builtin/macd_golden.py index 2ea8006..42a05cb 100644 --- a/backend/app/strategy/builtin/macd_golden.py +++ b/backend/app/strategy/builtin/macd_golden.py @@ -7,6 +7,10 @@ META = { "description": "MACD金叉当日 + 量能放大", "tags": ["MACD", "金叉", "放量"], "params": [ + {"id": "require_macd_golden", "label": "要求MACD金叉", "type": "bool", + "default": True}, + {"id": "use_volume_filter", "label": "启用量比过滤", "type": "bool", + "default": True}, {"id": "vol_ratio_min", "label": "最低量比", "type": "float", "default": 1.5, "min": 0.5, "max": 5.0, "step": 0.1}, ], @@ -25,7 +29,9 @@ ALERTS = [] def filter(df: pl.DataFrame, params: dict) -> pl.Expr: vol_min = params.get("vol_ratio_min", 1.5) - return ( - pl.col("signal_macd_golden").fill_null(False) - & (pl.col("vol_ratio_5d") >= vol_min) - ) + expr = pl.col("symbol").is_not_null() | pl.col("symbol").is_null() + if params.get("require_macd_golden", True): + expr = expr & pl.col("signal_macd_golden").fill_null(False) + if params.get("use_volume_filter", True): + expr = expr & (pl.col("vol_ratio_5d") >= vol_min) + return expr diff --git a/backend/app/strategy/builtin/n_day_low_reversal.py b/backend/app/strategy/builtin/n_day_low_reversal.py index 11219c2..662c31a 100644 --- a/backend/app/strategy/builtin/n_day_low_reversal.py +++ b/backend/app/strategy/builtin/n_day_low_reversal.py @@ -7,6 +7,12 @@ META = { "description": "触及60日新低后当日收阳放量, 反转信号", "tags": ["反转", "新低"], "params": [ + {"id": "require_n_day_low", "label": "要求60日新低", "type": "bool", + "default": True}, + {"id": "require_bullish_candle", "label": "要求收阳", "type": "bool", + "default": True}, + {"id": "use_volume_filter", "label": "启用量比过滤", "type": "bool", + "default": True}, {"id": "vol_ratio_min", "label": "最低量比", "type": "float", "default": 1.5, "min": 0.5, "max": 5.0, "step": 0.1}, ], @@ -25,8 +31,11 @@ ALERTS = [] def filter(df: pl.DataFrame, params: dict) -> pl.Expr: vol_min = params.get("vol_ratio_min", 1.5) - return ( - pl.col("signal_n_day_low").fill_null(False) - & (pl.col("close") > pl.col("open")) - & (pl.col("vol_ratio_5d") >= vol_min) - ) + expr = pl.col("symbol").is_not_null() | pl.col("symbol").is_null() + if params.get("require_n_day_low", True): + expr = expr & pl.col("signal_n_day_low").fill_null(False) + if params.get("require_bullish_candle", True): + expr = expr & (pl.col("close") > pl.col("open")) + if params.get("use_volume_filter", True): + expr = expr & (pl.col("vol_ratio_5d") >= vol_min) + return expr diff --git a/backend/app/strategy/builtin/near_limit_up.py b/backend/app/strategy/builtin/near_limit_up.py index 4442e06..fec68e2 100644 --- a/backend/app/strategy/builtin/near_limit_up.py +++ b/backend/app/strategy/builtin/near_limit_up.py @@ -27,8 +27,12 @@ META = { "description": "涨幅 > 7% 且距涨停 < 3%, 追涨信号", "tags": ["涨停", "追涨"], "params": [ + {"id": "use_change_filter", "label": "启用涨幅过滤", "type": "bool", + "default": True}, {"id": "min_change", "label": "最低涨幅%", "type": "float", "default": 7.0, "min": 3.0, "max": 15.0, "step": 1.0}, + {"id": "use_limit_gap_filter", "label": "启用距涨停空间过滤", "type": "bool", + "default": True}, {"id": "limit_gap", "label": "距涨停空间%", "type": "float", "default": 3.0, "min": 1.0, "max": 10.0, "step": 0.5}, ], @@ -49,7 +53,9 @@ def filter(df: pl.DataFrame, params: dict) -> pl.Expr: min_chg = params.get("min_change", 7.0) / 100.0 gap = params.get("limit_gap", 3.0) / 100.0 lp = _limit_pct() - return ( - (pl.col("change_pct") > min_chg) - & (pl.col("change_pct") < lp - gap) - ) + expr = pl.col("symbol").is_not_null() | pl.col("symbol").is_null() + if params.get("use_change_filter", True): + expr = expr & (pl.col("change_pct") > min_chg) + if params.get("use_limit_gap_filter", True): + expr = expr & (pl.col("change_pct") < lp - gap) + return expr diff --git a/backend/app/strategy/builtin/oversold_bounce.py b/backend/app/strategy/builtin/oversold_bounce.py index 8adc124..e66fb49 100644 --- a/backend/app/strategy/builtin/oversold_bounce.py +++ b/backend/app/strategy/builtin/oversold_bounce.py @@ -7,8 +7,14 @@ META = { "description": "RSI14 < 30超卖区 + 当日收阳 + 放量, 抄底信号", "tags": ["超跌", "反弹", "RSI"], "params": [ + {"id": "use_rsi_filter", "label": "启用RSI过滤", "type": "bool", + "default": True}, {"id": "rsi_max", "label": "RSI上限", "type": "float", "default": 30.0, "min": 10.0, "max": 50.0, "step": 1.0}, + {"id": "require_bullish_candle", "label": "要求收阳", "type": "bool", + "default": True}, + {"id": "use_volume_filter", "label": "启用量比过滤", "type": "bool", + "default": True}, {"id": "vol_ratio_min", "label": "最低量比", "type": "float", "default": 1.2, "min": 0.5, "max": 5.0, "step": 0.1}, ], @@ -30,8 +36,11 @@ ALERTS = [ def filter(df: pl.DataFrame, params: dict) -> pl.Expr: rsi_max = params.get("rsi_max", 30.0) vol_min = params.get("vol_ratio_min", 1.2) - return ( - (pl.col("rsi_14") < rsi_max) - & (pl.col("close") > pl.col("open")) - & (pl.col("vol_ratio_5d") >= vol_min) - ) + expr = pl.col("symbol").is_not_null() | pl.col("symbol").is_null() + if params.get("use_rsi_filter", True): + expr = expr & (pl.col("rsi_14") < rsi_max) + if params.get("require_bullish_candle", True): + expr = expr & (pl.col("close") > pl.col("open")) + if params.get("use_volume_filter", True): + expr = expr & (pl.col("vol_ratio_5d") >= vol_min) + return expr diff --git a/backend/app/strategy/builtin/oversold_reversal.py b/backend/app/strategy/builtin/oversold_reversal.py index ba31a6b..5c1b382 100644 --- a/backend/app/strategy/builtin/oversold_reversal.py +++ b/backend/app/strategy/builtin/oversold_reversal.py @@ -7,10 +7,16 @@ META = { "description": "RSI14 < 30超卖 + 涨幅 > 1% + 站上MA5, 超卖反转信号", "tags": ["超跌", "反弹", "RSI"], "params": [ + {"id": "use_rsi_filter", "label": "启用RSI过滤", "type": "bool", + "default": True}, {"id": "rsi_max", "label": "RSI上限", "type": "float", "default": 30.0, "min": 10.0, "max": 50.0, "step": 1.0}, + {"id": "use_change_filter", "label": "启用涨幅过滤", "type": "bool", + "default": True}, {"id": "min_change", "label": "最低涨幅%", "type": "float", "default": 1.0, "min": 0.5, "max": 5.0, "step": 0.5}, + {"id": "require_above_ma5", "label": "要求收盘价在MA5上方", "type": "bool", + "default": True}, ], "scoring": {"change_pct": 0.4, "rsi_14": 0.3, "vol_ratio_5d": 0.3}, "order_by": "score", @@ -30,8 +36,11 @@ ALERTS = [ def filter(df: pl.DataFrame, params: dict) -> pl.Expr: rsi_max = params.get("rsi_max", 30.0) min_chg = params.get("min_change", 1.0) / 100.0 - return ( - (pl.col("rsi_14") < rsi_max) - & (pl.col("change_pct") > min_chg) - & (pl.col("close") > pl.col("ma5")) - ) + expr = pl.col("symbol").is_not_null() | pl.col("symbol").is_null() + if params.get("use_rsi_filter", True): + expr = expr & (pl.col("rsi_14") < rsi_max) + if params.get("use_change_filter", True): + expr = expr & (pl.col("change_pct") > min_chg) + if params.get("require_above_ma5", True): + expr = expr & (pl.col("close") > pl.col("ma5")) + return expr diff --git a/backend/app/strategy/builtin/pullback_ma20_bounce.py b/backend/app/strategy/builtin/pullback_ma20_bounce.py index 9c56adb..3ab19ad 100644 --- a/backend/app/strategy/builtin/pullback_ma20_bounce.py +++ b/backend/app/strategy/builtin/pullback_ma20_bounce.py @@ -7,8 +7,14 @@ META = { "description": "价格在MA20附近(±2%)且MA5>MA20>MA60多头排列, 回踩买入", "tags": ["回踩", "均线", "反弹"], "params": [ + {"id": "use_ma20_proximity", "label": "启用MA20附近过滤", "type": "bool", + "default": True}, {"id": "ma_proximity", "label": "MA偏离度%", "type": "float", "default": 2.0, "min": 0.5, "max": 5.0, "step": 0.5}, + {"id": "require_ma_alignment", "label": "要求MA5>MA20>MA60", "type": "bool", + "default": True}, + {"id": "require_positive_change", "label": "要求当日上涨", "type": "bool", + "default": True}, ], "scoring": {"momentum_60d": 0.4, "change_pct": 0.3, "momentum_20d": 0.3}, "order_by": "score", @@ -25,10 +31,15 @@ ALERTS = [] def filter(df: pl.DataFrame, params: dict) -> pl.Expr: proximity = params.get("ma_proximity", 2.0) / 100.0 - return ( - (pl.col("close") > pl.col("ma20") * (1 - proximity)) - & (pl.col("close") < pl.col("ma20") * (1 + proximity)) - & (pl.col("ma5") > pl.col("ma20")) - & (pl.col("ma20") > pl.col("ma60")) - & (pl.col("change_pct") > 0) - ) + expr = pl.col("symbol").is_not_null() | pl.col("symbol").is_null() + if params.get("use_ma20_proximity", True): + expr = ( + expr + & (pl.col("close") > pl.col("ma20") * (1 - proximity)) + & (pl.col("close") < pl.col("ma20") * (1 + proximity)) + ) + if params.get("require_ma_alignment", True): + expr = expr & (pl.col("ma5") > pl.col("ma20")) & (pl.col("ma20") > pl.col("ma60")) + if params.get("require_positive_change", True): + expr = expr & (pl.col("change_pct") > 0) + return expr diff --git a/backend/app/strategy/builtin/pullback_to_support.py b/backend/app/strategy/builtin/pullback_to_support.py index 36fc51a..0bed03f 100644 --- a/backend/app/strategy/builtin/pullback_to_support.py +++ b/backend/app/strategy/builtin/pullback_to_support.py @@ -7,10 +7,18 @@ META = { "description": "回踩MA20附近 + 缩量 + 中期趋势向上", "tags": ["回踩", "支撑"], "params": [ + {"id": "use_ma20_proximity", "label": "启用MA20附近过滤", "type": "bool", + "default": True}, {"id": "ma_proximity", "label": "均线偏离度", "type": "float", "default": 0.02, "min": 0.01, "max": 0.05, "step": 0.005}, + {"id": "use_volume_filter", "label": "启用缩量过滤", "type": "bool", + "default": True}, {"id": "vol_ratio_max", "label": "最大量比", "type": "float", "default": 0.8, "min": 0.2, "max": 1.5, "step": 0.1}, + {"id": "require_above_ma60", "label": "要求收盘价在MA60上方", "type": "bool", + "default": True}, + {"id": "require_positive_momentum", "label": "要求20日动量为正", "type": "bool", + "default": True}, ], "scoring": {"momentum_60d": 0.4, "momentum_20d": 0.3, "turnover_rate": 0.3}, "order_by": "score", @@ -28,10 +36,17 @@ ALERTS = [] def filter(df: pl.DataFrame, params: dict) -> pl.Expr: proximity = params.get("ma_proximity", 0.02) vol_max = params.get("vol_ratio_max", 0.8) - return ( - (pl.col("close") > pl.col("ma20") * (1 - proximity)) - & (pl.col("close") < pl.col("ma20") * (1 + proximity)) - & (pl.col("vol_ratio_5d") < vol_max) - & (pl.col("close") > pl.col("ma60")) - & (pl.col("momentum_20d") > 0) - ) + expr = pl.col("symbol").is_not_null() | pl.col("symbol").is_null() + if params.get("use_ma20_proximity", True): + expr = ( + expr + & (pl.col("close") > pl.col("ma20") * (1 - proximity)) + & (pl.col("close") < pl.col("ma20") * (1 + proximity)) + ) + if params.get("use_volume_filter", True): + expr = expr & (pl.col("vol_ratio_5d") < vol_max) + if params.get("require_above_ma60", True): + expr = expr & (pl.col("close") > pl.col("ma60")) + if params.get("require_positive_momentum", True): + expr = expr & (pl.col("momentum_20d") > 0) + return expr diff --git a/backend/app/strategy/builtin/strong_open.py b/backend/app/strategy/builtin/strong_open.py index 918842b..7d3dd98 100644 --- a/backend/app/strategy/builtin/strong_open.py +++ b/backend/app/strategy/builtin/strong_open.py @@ -7,8 +7,14 @@ META = { "description": "高开 > 3% 且收盘高于开盘价, 集合竞价强势", "tags": ["高开", "强势"], "params": [ + {"id": "use_open_gap_filter", "label": "启用高开过滤", "type": "bool", + "default": True}, {"id": "min_open_gap", "label": "最低高开%", "type": "float", "default": 3.0, "min": 1.0, "max": 10.0, "step": 0.5}, + {"id": "require_close_above_open", "label": "要求收盘高于开盘", "type": "bool", + "default": True}, + {"id": "use_change_filter", "label": "启用涨幅过滤", "type": "bool", + "default": True}, {"id": "min_change", "label": "最低涨幅%", "type": "float", "default": 3.0, "min": 1.0, "max": 10.0, "step": 0.5}, ], @@ -28,8 +34,11 @@ ALERTS = [] def filter(df: pl.DataFrame, params: dict) -> pl.Expr: min_gap = params.get("min_open_gap", 3.0) / 100.0 min_chg = params.get("min_change", 3.0) / 100.0 - return ( - (pl.col("open") > pl.col("prev_close") * (1 + min_gap)) - & (pl.col("close") > pl.col("open")) - & (pl.col("change_pct") > min_chg) - ) + expr = pl.col("symbol").is_not_null() | pl.col("symbol").is_null() + if params.get("use_open_gap_filter", True): + expr = expr & (pl.col("open") > pl.col("prev_close") * (1 + min_gap)) + if params.get("require_close_above_open", True): + expr = expr & (pl.col("close") > pl.col("open")) + if params.get("use_change_filter", True): + expr = expr & (pl.col("change_pct") > min_chg) + return expr diff --git a/backend/app/strategy/builtin/trend_breakout.py b/backend/app/strategy/builtin/trend_breakout.py index eb95cca..c00c325 100644 --- a/backend/app/strategy/builtin/trend_breakout.py +++ b/backend/app/strategy/builtin/trend_breakout.py @@ -15,6 +15,12 @@ META = { "exclude_new_days": 60, }, "params": [ + {"id": "require_above_ma60", "label": "要求收盘价在MA60上方", "type": "bool", + "default": True}, + {"id": "require_n_day_high", "label": "要求60日新高", "type": "bool", + "default": True}, + {"id": "use_volume_filter", "label": "启用量比过滤", "type": "bool", + "default": True}, {"id": "vol_ratio_min", "label": "最低量比", "type": "float", "default": 2.0, "min": 0.5, "max": 10.0, "step": 0.1}, ], @@ -35,8 +41,11 @@ ALERTS = [ def filter(df: pl.DataFrame, params: dict) -> pl.Expr: vol_min = params.get("vol_ratio_min", 2.0) - return ( - (pl.col("close") > pl.col("ma60")) - & pl.col("signal_n_day_high").fill_null(False) - & (pl.col("vol_ratio_5d") >= vol_min) - ) + expr = pl.col("symbol").is_not_null() | pl.col("symbol").is_null() + if params.get("require_above_ma60", True): + expr = expr & (pl.col("close") > pl.col("ma60")) + if params.get("require_n_day_high", True): + expr = expr & pl.col("signal_n_day_high").fill_null(False) + if params.get("use_volume_filter", True): + expr = expr & (pl.col("vol_ratio_5d") >= vol_min) + return expr diff --git a/backend/app/strategy/builtin/volume_price_surge.py b/backend/app/strategy/builtin/volume_price_surge.py index c3573e4..47dc475 100644 --- a/backend/app/strategy/builtin/volume_price_surge.py +++ b/backend/app/strategy/builtin/volume_price_surge.py @@ -7,8 +7,14 @@ META = { "description": "突破MA20 + 放量 + 收阳", "tags": ["量价", "突破"], "params": [ + {"id": "require_ma20_breakout", "label": "要求突破MA20", "type": "bool", + "default": True}, + {"id": "use_volume_filter", "label": "启用量比过滤", "type": "bool", + "default": True}, {"id": "vol_ratio_min", "label": "最低量比", "type": "float", "default": 2.0, "min": 0.5, "max": 10.0, "step": 0.1}, + {"id": "require_bullish_candle", "label": "要求收阳", "type": "bool", + "default": True}, ], "scoring": {"vol_ratio_5d": 0.4, "change_pct": 0.3, "momentum_20d": 0.3}, "order_by": "score", @@ -25,8 +31,11 @@ ALERTS = [] def filter(df: pl.DataFrame, params: dict) -> pl.Expr: vol_min = params.get("vol_ratio_min", 2.0) - return ( - pl.col("signal_ma20_breakout").fill_null(False) - & (pl.col("vol_ratio_5d") >= vol_min) - & (pl.col("close") > pl.col("open")) - ) + expr = pl.col("symbol").is_not_null() | pl.col("symbol").is_null() + if params.get("require_ma20_breakout", True): + expr = expr & pl.col("signal_ma20_breakout").fill_null(False) + if params.get("use_volume_filter", True): + expr = expr & (pl.col("vol_ratio_5d") >= vol_min) + if params.get("require_bullish_candle", True): + expr = expr & (pl.col("close") > pl.col("open")) + return expr diff --git a/frontend/src/pages/backtest/StrategyBacktest.tsx b/frontend/src/pages/backtest/StrategyBacktest.tsx index 633b7f6..1b2b8bb 100644 --- a/frontend/src/pages/backtest/StrategyBacktest.tsx +++ b/frontend/src/pages/backtest/StrategyBacktest.tsx @@ -149,6 +149,10 @@ const strategyDefaultParams = (detail: StrategyDetail) => { }) return values } +const mergeStrategyParams = (detail: StrategyDetail, values?: Record | null) => ({ + ...strategyDefaultParams(detail), + ...(values ?? {}), +}) const buildDefaultOverrides = (detail: StrategyDetail) => ({ basic_filter: { ...detail.basic_filter }, entry_signals: detail.entry_signals.map(toSignalId), @@ -745,7 +749,7 @@ export function StrategyBacktest() { if (!detail || loadedStrategyRef.current === detail.id) return loadedStrategyRef.current = detail.id if (saved?.selectedStrategy === detail.id && (saved.params || saved.overrides)) { - setStrategyParams(saved.params ?? strategyDefaultParams(detail)) + setStrategyParams(mergeStrategyParams(detail, saved.params)) setOverrides(saved.overrides ?? buildDefaultOverrides(detail)) return }