import pandas as pd from scripts.search_multi_factor_weight_config import ( DEFAULT_FACTORS, MultiFactorConfig, SUPPORTED_FACTORS, add_intraday_60k_factor_inputs, add_raw_factor_inputs, apply_multi_factor_config, build_promotion_decision, build_signed_factor_frame, iter_multi_factor_grid, search_multi_factor_configs, _needs_intraday_60k, _score_row, ) def test_build_signed_factor_frame_combines_bull_and_bear_components(): features = pd.DataFrame( { "oldwang_triple_bull": [1.0, 0.0], "oldwang_triple_bear": [0.0, 1.0], "oldwang_volume_high_break": [1.0, 0.0], "oldwang_volume_low_break": [0.0, 1.0], "factor_intraday_60k_volume_low_guard": [1.0, 0.0], "factor_intraday_60k_volume_low_break": [0.0, 1.0], "big_player_buy_1m_4m": [1_000_000.0, 500_000.0], "big_player_sell_1m_4m": [500_000.0, 2_000_000.0], "big_player_buy_gt_4m": [4_000_000.0, 1_500_000.0], "big_player_sell_gt_4m": [1_500_000.0, 3_000_000.0], "big_player_power_1m_4m": [0.7, -0.4], "big_player_power_gt_4m": [0.2, -0.8], "factor_lower_shadow_ratio": [0.5, 0.1], "factor_upper_shadow_ratio": [0.1, 0.6], "factor_body_ratio": [0.5, -0.5], "factor_high_base_score": [3.0, 0.0], "factor_short_term_battle": [1.0, 0.0], "factor_high_volume_upper_shadow": [0.7, 0.0], "factor_volume_z20_prior": [2.0, -1.0], "foreign_net_vol_ratio": [0.2, -0.2], "trust_net_vol_ratio": [0.1, -0.1], "dealer_net_vol_ratio": [0.05, -0.05], "chip_score": [0.5, -0.25], "return_20d": [0.10, -0.10], "ma_bull_alignment": [1.0, 0.0], "ma_bear_alignment": [0.0, 1.0], "macd_hist_norm": [0.02, -0.02], "macd_above_zero": [1.0, 0.0], "rsi": [25.0, 75.0], "bb_pct_b": [0.10, 0.90], "foreign_trust_alignment": [1.0, -1.0], "institutional_streak": [5.0, -5.0], "institutional_20d_zscore": [2.0, -2.0], "margin_5d_change_pct": [-0.2, 0.2], "short_balance_5d_change": [-0.1, 0.1], "short_margin_ratio": [0.1, 0.4], "sbl_balance_5d_change": [0.0, 0.6], "sbl_balance_ratio": [0.0, 0.2], "margin_buy_pressure": [0.7, 0.3], "volume_ratio": [2.0, 0.6], "factor_volume_contraction_after_spike": [1.0, -1.0], "amihud_zscore": [-2.0, 2.0], "volatility_20d": [0.01, 0.08], "atr_ratio": [0.01, 0.08], "return_60d": [0.2, -0.2], "return_120d": [0.1, -0.1], "mom_minus_reversal": [0.3, -0.3], } ) factors = build_signed_factor_frame( features, [ "oldwang_trend", "oldwang_volume", "intraday_60k_volume_low_guard", "intraday_60k_volume_low_break", "big_player_buy", "big_player_sell", "big_player_buy_ratio_1m_4m", "big_player_sell_ratio_1m_4m", "big_player_buy_ratio_gt_4m", "big_player_sell_ratio_gt_4m", "big_player_power_1m_4m", "big_player_power_gt_4m", "volume_expansion", "volume_spike", "volume_surge", "volume_contraction_after_spike", "candle_shadow", "upper_shadow", "lower_shadow", "bullish_upper_shadow", "high_base", "high_volume_upper_shadow", "short_term_battle", "foreign_flow", "trust_flow", "dealer_flow", "chip_score", "momentum_20d", "ma_alignment", "macd_momentum", "rsi_reversal", "bb_reversion", "institutional_flow", "margin_risk", "short_pressure", "liquidity_quality", "volatility_risk", "momentum_60d", "momentum_120d", "momentum_20_60_120", "medium_momentum", "reversal_pressure", ], ) assert factors["oldwang_trend"].tolist() == [1.0, -1.0] assert factors["oldwang_volume"].tolist() == [1.0, -1.0] assert factors["intraday_60k_volume_low_guard"].tolist() == [1.0, 0.0] assert factors["intraday_60k_volume_low_break"].tolist() == [-0.0, -1.0] assert factors["big_player_buy"].tolist() == [1.0, 0.4] assert factors["big_player_sell"].tolist() == [-0.4, -1.0] assert [round(value, 4) for value in factors["big_player_buy_ratio_1m_4m"].tolist()] == [0.6667, 0.2] assert [round(value, 4) for value in factors["big_player_sell_ratio_1m_4m"].tolist()] == [-0.3333, -0.8] assert [round(value, 4) for value in factors["big_player_buy_ratio_gt_4m"].tolist()] == [0.7273, 0.3333] assert [round(value, 4) for value in factors["big_player_sell_ratio_gt_4m"].tolist()] == [-0.2727, -0.6667] assert factors["big_player_power_1m_4m"].tolist() == [0.7, -0.4] assert factors["big_player_power_gt_4m"].tolist() == [0.2, -0.8] assert factors["volume_expansion"].iloc[0] > 0 assert factors["volume_expansion"].iloc[1] < 0 assert factors["volume_spike"].tolist() == [1.0, 0.0] assert factors["volume_surge"].iloc[0] > 0 assert factors["volume_surge"].iloc[1] < 0 assert factors["volume_contraction_after_spike"].tolist() == [1.0, -1.0] assert [round(value, 3) for value in factors["candle_shadow"].tolist()] == [0.075, -0.565] assert factors["upper_shadow"].tolist() == [-0.1, -0.6] assert [round(value, 3) for value in factors["lower_shadow"].tolist()] == [0.175, 0.035] assert factors["bullish_upper_shadow"].iloc[0] < 0 assert factors["high_base"].tolist() == [-0.75, -0.0] assert factors["high_volume_upper_shadow"].iloc[0] < 0 assert factors["high_volume_upper_shadow"].iloc[1] == 0 assert factors["short_term_battle"].tolist() == [-1.0, -0.0] assert factors["foreign_flow"].iloc[0] > 0 assert factors["foreign_flow"].iloc[1] < 0 assert factors["trust_flow"].iloc[0] > 0 assert factors["trust_flow"].iloc[1] < 0 assert factors["dealer_flow"].iloc[0] > 0 assert factors["dealer_flow"].iloc[1] < 0 assert factors["chip_score"].tolist() == [0.5, -0.25] assert factors["momentum_20d"].tolist() == [0.4, -0.4] assert factors["ma_alignment"].tolist() == [1.0, -1.0] assert factors["macd_momentum"].iloc[0] > 0 assert factors["macd_momentum"].iloc[1] < 0 assert factors["rsi_reversal"].iloc[0] > 0 assert factors["rsi_reversal"].iloc[1] < 0 assert factors["bb_reversion"].iloc[0] > 0 assert factors["bb_reversion"].iloc[1] < 0 assert factors["institutional_flow"].iloc[0] > 0 assert factors["institutional_flow"].iloc[1] < 0 assert factors["margin_risk"].tolist() == [0.5, -0.5] assert factors["short_pressure"].iloc[0] <= 0 assert factors["short_pressure"].iloc[1] < factors["short_pressure"].iloc[0] assert factors["liquidity_quality"].iloc[0] > 0 assert factors["liquidity_quality"].iloc[1] < 0 assert factors["volatility_risk"].iloc[0] < 0 assert factors["volatility_risk"].iloc[1] < factors["volatility_risk"].iloc[0] assert factors["momentum_60d"].iloc[0] > 0 assert factors["momentum_120d"].iloc[1] < 0 assert factors["momentum_20_60_120"].iloc[0] > 0 assert factors["medium_momentum"].iloc[0] > 0 assert factors["medium_momentum"].iloc[1] < 0 assert factors["reversal_pressure"].tolist() == [0.3, -0.3] def test_add_raw_factor_inputs_derives_volume_and_shadow_inputs(): df = pd.DataFrame( { "open": [10.0] * 25 + [11.0], "high": [11.0] * 25 + [13.0], "low": [9.0] * 25 + [10.5], "close": [10.5] * 25 + [11.2], "volume": [1000.0] * 25 + [5000.0], } ) features = pd.DataFrame(index=df.index) out = add_raw_factor_inputs(features, df) assert out["factor_upper_shadow_ratio"].iloc[-1] > out["factor_lower_shadow_ratio"].iloc[-1] assert out["factor_volume_z20_prior"].iloc[-1] > 0 assert "factor_high_base_score" in out.columns assert "factor_short_term_battle" in out.columns assert "factor_high_volume_upper_shadow" in out.columns assert "factor_volume_contraction_after_spike" in out.columns def test_sell_specific_breakdown_patterns_are_signed_risk_factors(): rows = [] for _ in range(25): rows.append({"open": 100.2, "high": 101.0, "low": 99.0, "close": 100.0, "volume": 1000.0}) rows.extend( [ {"open": 100.0, "high": 100.5, "low": 98.0, "close": 98.5, "volume": 1100.0}, {"open": 98.7, "high": 99.0, "low": 96.0, "close": 96.5, "volume": 1200.0}, {"open": 96.7, "high": 97.0, "low": 93.5, "close": 94.0, "volume": 3500.0}, {"open": 94.2, "high": 95.0, "low": 92.0, "close": 93.0, "volume": 2800.0}, {"open": 93.0, "high": 94.5, "low": 92.5, "close": 94.0, "volume": 900.0}, ] ) df = pd.DataFrame(rows) enriched = add_raw_factor_inputs(pd.DataFrame(index=df.index), df) assert enriched["factor_san_sheng_wu_nai_breakdown"].iloc[27] == 1.0 assert enriched["factor_downside_follow_through"].iloc[28] == 1.0 assert enriched["factor_weak_rebound_after_breakdown"].iloc[29] == 1.0 assert enriched["factor_high_volume_downside_follow_through"].iloc[27] > 0.0 factors = build_signed_factor_frame( enriched, [ "san_sheng_wu_nai_breakdown", "downside_follow_through", "weak_rebound_after_breakdown", "high_volume_downside_follow_through", ], ) assert factors["san_sheng_wu_nai_breakdown"].iloc[27] < 0 assert factors["downside_follow_through"].iloc[28] < 0 assert factors["weak_rebound_after_breakdown"].iloc[29] < 0 assert factors["high_volume_downside_follow_through"].iloc[27] < 0 def test_sell_specific_daily_patterns_do_not_require_intraday_60k_fetch(): assert not _needs_intraday_60k( [ "san_sheng_wu_nai_breakdown", "downside_follow_through", "weak_rebound_after_breakdown", "high_volume_downside_follow_through", ] ) assert _needs_intraday_60k(["intraday_60k_volume_low_guard"]) assert _needs_intraday_60k(["intraday_60k_stale_guard_risk"]) def test_add_intraday_60k_factor_inputs_maps_latest_huge_volume_low_to_daily_rows(): daily = pd.DataFrame( { "date": ["2026-05-26", "2026-05-27"], "close": [101.0, 97.0], } ) intraday = pd.DataFrame( { "date": ["2026-05-26"] * 21 + ["2026-05-27"], "low": [99.0] * 20 + [100.0, 100.0], "close": [101.0] * 22, "volume": [1000.0] * 20 + [2500.0, 1000.0], } ) out = add_intraday_60k_factor_inputs( pd.DataFrame(index=daily.index), daily, intraday_df=intraday, ) assert out["factor_intraday_60k_volume_low"].tolist() == [100.0, 100.0] assert out["factor_intraday_60k_volume_low_available"].tolist() == [1.0, 1.0] assert out["factor_intraday_60k_volume_low_age_sessions"].tolist() == [0.0, 1.0] assert out["factor_intraday_60k_volume_low_guard"].tolist() == [1.0, 0.0] assert out["factor_intraday_60k_volume_low_break"].tolist() == [0.0, 1.0] def test_high_base_and_short_term_battle_are_signed_risk_factors(): closes = [50.0] * 100 + [55.0, 58.0, 61.0, 64.0, 67.0, 70.0, 72.0, 74.0, 76.0, 79.0, 82.0, 86.0, 90.0, 94.0, 98.0, 102.0, 106.0, 110.0, 114.0, 118.0, 120.0] opens = [close - 0.5 for close in closes] highs = [close + 1.0 for close in closes] lows = [close - 1.0 for close in closes] opens[-1] = 119.5 highs[-1] = 125.0 lows[-1] = 118.8 closes[-1] = 120.0 df = pd.DataFrame( { "open": opens, "high": highs, "low": lows, "close": closes, "volume": [10000.0] * (len(closes) - 1) + [40000.0], } ) features = add_raw_factor_inputs(pd.DataFrame(index=df.index), df) factors = build_signed_factor_frame(features, ["high_base", "high_volume_upper_shadow", "short_term_battle", "doji_battle"]) assert factors["high_base"].iloc[-1] < 0 assert factors["high_volume_upper_shadow"].iloc[-1] < 0 assert factors["short_term_battle"].iloc[-1] == -1.0 assert factors["doji_battle"].iloc[-1] == factors["short_term_battle"].iloc[-1] def test_yin_gao_pao_shadow_inputs_follow_article_rules(): df = pd.DataFrame( { "open": [100.0, 98.0, 98.0], "high": [102.0, 100.0, 100.0], "low": [92.0, 93.0, 91.0], "close": [97.0, 98.0, 92.0], "volume": [10000.0, 10000.0, 10000.0], } ) features = add_raw_factor_inputs(pd.DataFrame(index=df.index), df) factors = build_signed_factor_frame(features, ["lower_shadow", "candle_shadow"]) assert features["factor_yin_gao_pao_raw"].tolist() == [1.0, 0.0, 0.0] assert features["factor_yin_gao_pao_hold"].iloc[1] == 1.0 assert features["factor_yin_gao_pao_failed"].iloc[2] == 1.0 assert factors["lower_shadow"].iloc[1] > factors["lower_shadow"].iloc[2] def test_businesstoday_kline_candidate_factors_are_signed(): df = pd.DataFrame( { "open": [100.0, 91.0, 96.0, 100.0, 109.0, 101.0, 106.0, 100.0, 94.0, 100.0], "high": [102.0, 97.0, 104.0, 111.0, 110.0, 106.0, 107.0, 108.0, 101.0, 106.0], "low": [89.0, 90.0, 95.0, 99.0, 103.0, 99.0, 100.0, 95.0, 93.0, 99.0], "close": [90.0, 96.0, 103.0, 110.0, 104.0, 105.0, 101.0, 107.0, 100.0, 106.0], "volume": [10000.0] * 10, } ) features = add_raw_factor_inputs(pd.DataFrame(index=df.index), df) factors = build_signed_factor_frame( features, [ "bt_yi_yin_ya_breakout", "bt_yi_yang_ding_breakdown", "bt_yang_gao_pao", "bt_bull_engulf", "bt_bull_engulf_bear", "bt_bull_bear_engulf_ratio", "bt_double_bull_engulf_one_bear", "oldwang_guard_support", "oldwang_guard_break_risk", "candle_lower_support", "candle_upper_pressure", "yin_gao_pao_support", "yin_gao_pao_risk", "bt_yang_gao_pao_risk", "bt_bull_engulf_risk", ], ) assert factors["bt_yi_yin_ya_breakout"].iloc[2] > 0 assert factors["bt_yi_yang_ding_breakdown"].iloc[4] < 0 assert factors["bt_double_bull_engulf_one_bear"].iloc[7] > 0 assert factors["bt_bull_engulf"].iloc[7] > 0 assert factors["bt_bull_engulf_risk"].iloc[7] < 0 assert factors["bt_bull_engulf_bear"].iloc[4] < 0 assert factors["bt_bull_bear_engulf_ratio"].iloc[7] > 0 assert factors["bt_yang_gao_pao"].iloc[9] > 0 assert factors["bt_yang_gao_pao_risk"].iloc[9] < 0 def test_shadow_cluster_support_resistance_factor_breaks_and_bounces(): df = pd.DataFrame( { "open": [100.0, 100.0, 101.0, 100.0, 100.0, 94.0], "high": [110.0, 110.0, 112.0, 102.0, 102.0, 96.0], "low": [99.0, 99.0, 100.0, 90.0, 90.0, 89.0], "close": [101.0, 101.0, 111.0, 95.0, 96.0, 89.5], "volume": [10000.0] * 6, } ) features = add_raw_factor_inputs(pd.DataFrame(index=df.index), df) factors = build_signed_factor_frame(features, ["bt_shadow_cluster_support_resistance"]) assert factors["bt_shadow_cluster_support_resistance"].iloc[2] > 0 assert factors["bt_shadow_cluster_support_resistance"].iloc[5] < 0 def test_tej_lstm_candidate_factors_capture_trend_and_chop(): closes = [100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119] df = pd.DataFrame( { "open": [value - 0.4 for value in closes], "high": [value + 1.0 for value in closes], "low": [value - 1.0 for value in closes], "close": closes, "volume": [10000.0] * len(closes), } ) features = pd.DataFrame( { "rsi": [55.0, 56.0, 57.0, 58.0, 59.0, 60.0, 61.0, 62.0, 63.0, 64.0, 65.0, 66.0, 67.0, 68.0, 69.0, 70.0, 71.0, 72.0, 73.0, 74.0], "macd_hist_norm": [0.001 * idx for idx in range(len(closes))], "k": [60.0 + idx for idx in range(len(closes))], "d": [55.0 + idx for idx in range(len(closes))], }, index=df.index, ) enriched = add_raw_factor_inputs(features, df) factors = build_signed_factor_frame(enriched, ["tej_lstm_trend_signal", "tej_lstm_chop_filter"]) assert factors["tej_lstm_trend_signal"].iloc[-1] > 0.4 assert factors["tej_lstm_chop_filter"].iloc[-1] > 0 def test_tej_macro_risk_proxy_is_signed_from_market_context(): features = pd.DataFrame( { "taiex_return_5d": [0.03, -0.03], "taiex_return_20d": [0.06, -0.06], "taiex_ma20_ratio": [1.04, 0.96], "usdtwd_return_5d": [-0.01, 0.02], "sox_ret_5d": [0.04, -0.04], "sox_ma20_ratio": [1.03, 0.97], "tnx_change_5d": [-0.10, 0.35], "vix_level": [16.0, 30.0], "vix_change_5d": [-2.0, 5.0], } ) factors = build_signed_factor_frame(features, ["tej_macro_risk_proxy", "tej_macro_risk_contrarian"]) assert factors["tej_macro_risk_proxy"].iloc[0] > 0 assert factors["tej_macro_risk_proxy"].iloc[1] < 0 assert factors["tej_macro_risk_contrarian"].iloc[0] < 0 assert factors["tej_macro_risk_contrarian"].iloc[1] > 0 def test_regime_reversal_research_factors_are_signed_and_gated(): features = pd.DataFrame( { "oldwang_triple_bull": [1.0, 0.0], "oldwang_triple_bear": [0.0, 1.0], "oldwang_ma5_hold": [1.0, 1.0], "oldwang_trust_ma10_guard": [1.0, 1.0], "oldwang_foreign_ma20_guard": [1.0, 1.0], "factor_high_base_score": [0.0, 4.0], "factor_intraday_60k_volume_low_guard": [1.0, 1.0], "factor_intraday_60k_volume_low_age_sessions": [2.0, 8.0], "macd_hist_norm": [0.01, -0.01], "macd_above_zero": [1.0, 0.0], "rsi": [70.0, 30.0], "factor_bt_yi_yin_ya_breakout": [1.0, 0.0], "factor_bt_yi_yin_ya_failed": [0.0, 1.0], } ) factors = build_signed_factor_frame( features, [ "oldwang_trend_reversal_pressure", "oldwang_guard_lowbase_support", "oldwang_guard_highbase_risk", "intraday_60k_fresh_low_guard", "intraday_60k_stale_guard_risk", "macd_reversal_pressure", "rsi_trend_reversal", "bt_yi_yin_ya_breakout_risk", ], ) assert factors["oldwang_trend_reversal_pressure"].iloc[0] < 0 assert factors["oldwang_trend_reversal_pressure"].iloc[1] > 0 assert factors["oldwang_guard_lowbase_support"].iloc[0] > 0 assert factors["oldwang_guard_lowbase_support"].iloc[1] == 0 assert factors["oldwang_guard_highbase_risk"].iloc[0] == 0 assert factors["oldwang_guard_highbase_risk"].iloc[1] < 0 assert factors["intraday_60k_fresh_low_guard"].iloc[0] > 0 assert factors["intraday_60k_fresh_low_guard"].iloc[1] == 0 assert factors["intraday_60k_stale_guard_risk"].iloc[0] == 0 assert factors["intraday_60k_stale_guard_risk"].iloc[1] < 0 assert factors["macd_reversal_pressure"].iloc[0] < 0 assert factors["macd_reversal_pressure"].iloc[1] > 0 assert factors["rsi_trend_reversal"].iloc[0] < 0 assert factors["rsi_trend_reversal"].iloc[1] > 0 assert factors["bt_yi_yin_ya_breakout_risk"].iloc[0] < 0 assert factors["bt_yi_yin_ya_breakout_risk"].iloc[1] > 0 def test_apply_multi_factor_config_pushes_buy_or_sell_from_signed_values(): events = pd.DataFrame( { "p_buy": [0.44, 0.40], "p_hold": [0.48, 0.48], "p_sell": [0.08, 0.12], "factor__a": [2.0, -2.0], "factor__b": [0.0, 0.0], } ) pred = apply_multi_factor_config( events, MultiFactorConfig(weights={"a": 0.25, "b": 0.0}), ) assert pred.tolist() == [1, -1] def test_iter_multi_factor_grid_builds_each_factor_weight_combination(): configs = iter_multi_factor_grid( ["a", "b"], grid_values=[0.0, 0.1], hold_bias_values=[0.0, 0.02], ) assert len(configs) == 8 assert configs[-1].weights == {"a": 0.1, "b": 0.1} assert configs[-1].hold_bias == 0.02 def test_iter_multi_factor_grid_samples_when_candidate_space_is_too_large(): configs = iter_multi_factor_grid( ["a", "b", "c", "d"], grid_values=[0.01, 0.02, 0.03, 0.04], hold_bias_values=[0.0], search_mode="auto", max_configs=10, random_seed=7, ) assert len(configs) == 10 assert all(all(weight > 0 for weight in cfg.weights.values()) for cfg in configs) def test_iter_multi_factor_grid_samples_with_zero_weight_limit(): configs = iter_multi_factor_grid( ["a", "b", "c", "d", "e"], grid_values=[0.0, 0.25, 0.5], hold_bias_values=[0.0], search_mode="sampled", max_configs=20, random_seed=7, max_zero_factor_weights=1, ) constrained = [ cfg for cfg in configs if any(abs(weight) <= 1e-12 for weight in cfg.weights.values()) and not all(abs(weight) <= 1e-12 for weight in cfg.weights.values()) ] assert constrained assert all( sum(1 for weight in cfg.weights.values() if abs(weight) <= 1e-12) <= 1 for cfg in constrained ) def test_search_multi_factor_configs_finds_passing_golden_config(): rows = [] for idx in range(12): is_late = idx >= 6 bullish = idx in {2, 3, 4, 8, 9, 10} bearish = idx in {0, 1, 6, 7} rows.append( { "stock": "2330", "date": f"2025-04-{idx + 1:02d}", "y_true": 1 if bullish else (-1 if bearish else 0), "base_pred": 0, "p_buy": 0.44, "p_hold": 0.48, "p_sell": 0.08, "factor__trend": 1.0 if bullish else (-1.0 if bearish else 0.0), "factor__volume": 0.0, } ) result = search_multi_factor_configs( pd.DataFrame(rows), factor_names=["trend", "volume"], grid_values=[0.0, 0.05], hold_bias_values=[0.0], optimize_ratio=0.5, min_accuracy_delta_pp=10.0, min_buy_precision_delta_pp=0.0, min_sell_precision_delta_pp=None, min_direction_accuracy_delta_pp=0.0, min_signal_ratio=0.5, max_buy_count_ratio=None, require_nonzero_weights=False, require_all_factor_weights=False, top_n=5, ) assert result["golden_config"]["config"]["weights"]["trend"] == 0.05 assert result["golden_config"]["validation"]["deltas"]["accuracy_delta_pp"] > 0 assert result["passing_count"] >= 1 def test_search_multi_factor_configs_can_reject_all_zero_noop_config(): rows = [] for idx in range(8): rows.append( { "stock": "2330", "date": f"2025-04-{idx + 1:02d}", "y_true": 0, "base_pred": 0, "p_buy": 0.20, "p_hold": 0.60, "p_sell": 0.20, "factor__trend": 0.0, } ) result = search_multi_factor_configs( pd.DataFrame(rows), factor_names=["trend"], grid_values=[0.0], hold_bias_values=[0.0], optimize_ratio=0.5, min_accuracy_delta_pp=0.0, min_buy_precision_delta_pp=0.0, min_sell_precision_delta_pp=None, min_direction_accuracy_delta_pp=0.0, min_signal_ratio=0.5, max_buy_count_ratio=None, require_nonzero_weights=True, require_all_factor_weights=False, top_n=5, ) assert result["golden_config"] is None assert result["passing_count"] == 0 assert result["top_overall"][0]["gate"]["checks"]["nonzero_weights"] is False def test_search_multi_factor_configs_can_require_every_factor_weight_nonzero(): rows = [] for idx in range(8): rows.append( { "stock": "2330", "date": f"2025-05-{idx + 1:02d}", "y_true": 1 if idx % 2 == 0 else -1, "base_pred": 0, "p_buy": 0.44, "p_hold": 0.48, "p_sell": 0.08, "factor__trend": 1.0 if idx % 2 == 0 else -1.0, "factor__volume": 0.0, } ) result = search_multi_factor_configs( pd.DataFrame(rows), factor_names=["trend", "volume"], grid_values=[0.0, 0.05], hold_bias_values=[0.0], optimize_ratio=0.5, min_accuracy_delta_pp=0.0, min_buy_precision_delta_pp=0.0, min_sell_precision_delta_pp=None, min_direction_accuracy_delta_pp=0.0, min_signal_ratio=0.5, max_buy_count_ratio=None, require_nonzero_weights=False, require_all_factor_weights=True, top_n=5, ) assert result["golden_config"]["config"]["weights"] == {"trend": 0.05, "volume": 0.05} assert result["golden_config"]["gate"]["checks"]["all_factor_weights_nonzero"] is True def test_search_multi_factor_configs_limits_zero_weight_count(): rows = [] for idx in range(12): bullish = idx in {2, 3, 4, 8, 9, 10} bearish = idx in {0, 1, 6, 7} rows.append( { "stock": "2330", "date": f"2025-05-{idx + 1:02d}", "y_true": 1 if bullish else (-1 if bearish else 0), "base_pred": 0, "p_buy": 0.44, "p_hold": 0.48, "p_sell": 0.08, "factor__trend": 1.0 if bullish else (-1.0 if bearish else 0.0), "factor__volume": 0.0, "factor__chip": 0.0, } ) result = search_multi_factor_configs( pd.DataFrame(rows), factor_names=["trend", "volume", "chip"], grid_values=[0.0, 0.05], hold_bias_values=[0.0], optimize_ratio=0.5, min_accuracy_delta_pp=0.0, min_buy_precision_delta_pp=0.0, min_sell_precision_delta_pp=None, min_direction_accuracy_delta_pp=0.0, min_signal_ratio=0.5, max_buy_count_ratio=None, require_nonzero_weights=True, require_all_factor_weights=False, max_zero_factor_weights=1, top_n=5, ) assert result["golden_config"]["gate"]["zero_factor_weight_count"] <= 1 assert result["golden_config"]["gate"]["checks"]["max_zero_factor_weights"] is True def test_search_multi_factor_configs_can_gate_sell_precision_drop(): rows = [] for idx in range(12): bullish = idx in {2, 3, 4, 8, 9, 10} bearish = idx in {0, 1, 6, 7} rows.append( { "stock": "2330", "date": f"2025-06-{idx + 1:02d}", "y_true": 1 if bullish else (-1 if bearish else 0), "base_pred": -1 if bearish else 0, "p_buy": 0.44, "p_hold": 0.48, "p_sell": 0.08, "factor__trend": 1.0 if bullish else 0.0, } ) result = search_multi_factor_configs( pd.DataFrame(rows), factor_names=["trend"], grid_values=[0.05], hold_bias_values=[0.0], optimize_ratio=0.5, min_accuracy_delta_pp=0.0, min_buy_precision_delta_pp=0.0, min_sell_precision_delta_pp=0.0, min_direction_accuracy_delta_pp=0.0, min_signal_ratio=0.5, max_buy_count_ratio=None, require_nonzero_weights=True, require_all_factor_weights=True, top_n=5, ) assert result["golden_config"] is None assert result["top_overall"][0]["gate"]["checks"]["sell_precision_delta"] is False def test_protected_objective_ranks_signal_quality_before_label_accuracy(): quality_preserved = _score_row( { "accuracy_delta_pp": 0.4, "buy_precision_delta_pp": 0.0, "sell_precision_delta_pp": 0.0, "direction_accuracy_delta_pp": 0.0, }, {"accuracy": 40.7}, 1.0, min_accuracy_delta_pp=1.0, min_buy_precision_delta_pp=0.0, min_sell_precision_delta_pp=0.0, min_direction_accuracy_delta_pp=0.0, ) accuracy_only = _score_row( { "accuracy_delta_pp": 2.1, "buy_precision_delta_pp": -0.2, "sell_precision_delta_pp": -3.0, "direction_accuracy_delta_pp": -1.3, }, {"accuracy": 42.3}, 0.8, min_accuracy_delta_pp=1.0, min_buy_precision_delta_pp=0.0, min_sell_precision_delta_pp=0.0, min_direction_accuracy_delta_pp=0.0, ) assert quality_preserved > accuracy_only def test_default_factor_set_uses_nonzero_full_participation_weights(): assert "oldwang_trend" in DEFAULT_FACTORS assert "intraday_60k_volume_low_guard" in DEFAULT_FACTORS assert "intraday_60k_volume_low_break" in DEFAULT_FACTORS assert "big_player_buy" in DEFAULT_FACTORS assert "big_player_sell" in DEFAULT_FACTORS assert "big_player_buy_ratio_1m_4m" not in DEFAULT_FACTORS assert "big_player_sell_ratio_1m_4m" not in DEFAULT_FACTORS assert "big_player_buy_ratio_gt_4m" not in DEFAULT_FACTORS assert "big_player_sell_ratio_gt_4m" not in DEFAULT_FACTORS assert "big_player_power_1m_4m" not in DEFAULT_FACTORS assert "big_player_power_gt_4m" not in DEFAULT_FACTORS assert "foreign_flow" in DEFAULT_FACTORS assert "short_pressure" in DEFAULT_FACTORS assert "momentum_120d" in DEFAULT_FACTORS assert "bt_yi_yin_ya_breakout" in DEFAULT_FACTORS assert "bt_yi_yang_ding_breakdown" in DEFAULT_FACTORS assert "bt_shadow_cluster_support_resistance" in DEFAULT_FACTORS assert "high_base" in DEFAULT_FACTORS assert "high_volume_upper_shadow" in DEFAULT_FACTORS assert "short_term_battle" in DEFAULT_FACTORS assert "bt_bull_engulf" in DEFAULT_FACTORS assert "bt_bull_engulf_bear" in DEFAULT_FACTORS assert "bt_double_bull_engulf_one_bear" not in DEFAULT_FACTORS assert "oldwang_guard_support" not in DEFAULT_FACTORS assert "oldwang_guard_lowbase_support" not in DEFAULT_FACTORS assert "macd_reversal_pressure" not in DEFAULT_FACTORS assert "rsi_trend_reversal" not in DEFAULT_FACTORS assert "bt_yang_gao_pao_risk" not in DEFAULT_FACTORS for sell_pattern in [ "san_sheng_wu_nai_breakdown", "downside_follow_through", "weak_rebound_after_breakdown", "high_volume_downside_follow_through", ]: assert sell_pattern in SUPPORTED_FACTORS assert sell_pattern not in DEFAULT_FACTORS assert "tej_lstm_trend_signal" in DEFAULT_FACTORS assert "tej_lstm_chop_filter" in DEFAULT_FACTORS assert "tej_macro_risk_proxy" in DEFAULT_FACTORS def test_promotion_decision_rejects_smoke_even_with_golden_config(): result = { "thresholds": {"min_signal_ratio": 0.7}, "search_mode": "exhaustive", "golden_config": { "config": {"weights": {"trend": 0.05, "volume": 0.05}, "hold_bias": 0.0}, "gate": {"signal_ratio": 1.0}, "validation": { "deltas": { "accuracy_delta_pp": 2.0, "buy_precision_delta_pp": 0.0, "sell_precision_delta_pp": 0.0, } }, }, } decision = build_promotion_decision( result=result, factor_names=["trend", "volume"], covered_stocks=["2330"], months=12, periods=20, min_stocks=0, ) assert decision["passed"] is False assert decision["checks"]["all_factor_weights_nonzero"] is True assert decision["checks"]["not_tiny_stock_sample"] is False assert decision["checks"]["history_at_least_3y"] is False assert not any(name.startswith("periods_at_least") for name in decision["checks"])