Spaces:
Running
Running
| import json | |
| import pandas as pd | |
| from scripts.run_multi_factor_finetune_report import ( | |
| _hold_biases, | |
| _score_candidate, | |
| evaluate_configs, | |
| generate_beam_refinement_configs, | |
| generate_finetune_configs, | |
| load_seed_config, | |
| ) | |
| def test_protected_metrics_ranking_preserves_signal_quality_before_accuracy(): | |
| high_accuracy_with_precision_loss = _score_candidate( | |
| { | |
| "accuracy_delta_pp": 4.0, | |
| "direction_accuracy_delta_pp": -0.1, | |
| "buy_precision_delta_pp": -0.1, | |
| "sell_precision_delta_pp": -0.1, | |
| }, | |
| {"accuracy": 44.0}, | |
| 1.0, | |
| ranking_strategy="protected_metrics_first", | |
| min_accuracy_delta_pp=1.0, | |
| min_buy_precision_delta_pp=0.0, | |
| min_direction_accuracy_delta_pp=0.0, | |
| max_sell_precision_drop_pp=0.0, | |
| ) | |
| protected_signal_quality = _score_candidate( | |
| { | |
| "accuracy_delta_pp": 0.5, | |
| "direction_accuracy_delta_pp": 0.1, | |
| "buy_precision_delta_pp": 0.1, | |
| "sell_precision_delta_pp": 0.1, | |
| }, | |
| {"accuracy": 40.5}, | |
| 1.0, | |
| ranking_strategy="protected_metrics_first", | |
| min_accuracy_delta_pp=1.0, | |
| min_buy_precision_delta_pp=0.0, | |
| min_direction_accuracy_delta_pp=0.0, | |
| max_sell_precision_drop_pp=0.0, | |
| ) | |
| assert protected_signal_quality > high_accuracy_with_precision_loss | |
| def test_protected_metrics_ranking_improves_weakest_gate_before_pass_count(): | |
| imbalanced = _score_candidate( | |
| { | |
| "accuracy_delta_pp": 3.0, | |
| "direction_accuracy_delta_pp": 0.5, | |
| "buy_precision_delta_pp": 1.1, | |
| "sell_precision_delta_pp": -1.5, | |
| }, | |
| {"accuracy": 43.0}, | |
| 1.0, | |
| ranking_strategy="protected_metrics_first", | |
| min_accuracy_delta_pp=1.0, | |
| min_buy_precision_delta_pp=1.0, | |
| min_direction_accuracy_delta_pp=0.0, | |
| max_sell_precision_drop_pp=0.0, | |
| ) | |
| balanced = _score_candidate( | |
| { | |
| "accuracy_delta_pp": 2.0, | |
| "direction_accuracy_delta_pp": 0.1, | |
| "buy_precision_delta_pp": 0.4, | |
| "sell_precision_delta_pp": -0.2, | |
| }, | |
| {"accuracy": 42.0}, | |
| 1.0, | |
| ranking_strategy="protected_metrics_first", | |
| min_accuracy_delta_pp=1.0, | |
| min_buy_precision_delta_pp=1.0, | |
| min_direction_accuracy_delta_pp=0.0, | |
| max_sell_precision_drop_pp=0.0, | |
| ) | |
| assert balanced > imbalanced | |
| def test_protected_metrics_ranking_rejects_precision_gain_from_low_coverage(): | |
| common = { | |
| "accuracy_delta_pp": 1.2, | |
| "direction_accuracy_delta_pp": 0.2, | |
| "buy_precision_delta_pp": 1.2, | |
| "sell_precision_delta_pp": 0.2, | |
| } | |
| low_coverage = _score_candidate( | |
| common, | |
| {"accuracy": 41.2}, | |
| 1.0, | |
| signal_ratio=0.5, | |
| ranking_strategy="protected_metrics_first", | |
| min_accuracy_delta_pp=1.0, | |
| min_buy_precision_delta_pp=1.0, | |
| min_direction_accuracy_delta_pp=0.0, | |
| max_sell_precision_drop_pp=0.0, | |
| min_signal_ratio=0.7, | |
| ) | |
| healthy_coverage = _score_candidate( | |
| common, | |
| {"accuracy": 41.2}, | |
| 1.0, | |
| signal_ratio=0.8, | |
| ranking_strategy="protected_metrics_first", | |
| min_accuracy_delta_pp=1.0, | |
| min_buy_precision_delta_pp=1.0, | |
| min_direction_accuracy_delta_pp=0.0, | |
| max_sell_precision_drop_pp=0.0, | |
| min_signal_ratio=0.7, | |
| ) | |
| assert healthy_coverage > low_coverage | |
| def test_load_seed_config_reads_golden_weights(tmp_path): | |
| seed_json = tmp_path / "seed.json" | |
| seed_json.write_text( | |
| json.dumps( | |
| { | |
| "generated_at": "2026-05-23T00:00:00+00:00", | |
| "result": { | |
| "golden_config": { | |
| "config": { | |
| "weights": { | |
| "oldwang_trend": 0.01, | |
| "volume_spike": 0.08, | |
| } | |
| } | |
| } | |
| }, | |
| } | |
| ) | |
| ) | |
| weights, payload = load_seed_config(seed_json) | |
| assert weights == {"oldwang_trend": 0.01, "volume_spike": 0.08} | |
| assert payload["generated_at"] == "2026-05-23T00:00:00+00:00" | |
| def test_load_seed_config_can_use_top_overall_as_explicit_research_seed(tmp_path): | |
| seed_json = tmp_path / "failed_search.json" | |
| seed_json.write_text( | |
| json.dumps( | |
| { | |
| "result": { | |
| "golden_config": None, | |
| "top_overall": [{"config": {"weights": {"trend": 0.02}}}], | |
| } | |
| } | |
| ) | |
| ) | |
| weights, payload = load_seed_config(seed_json, selection="top_overall") | |
| assert weights == {"trend": 0.02} | |
| assert payload["_seed_selection"] == { | |
| "requested": "top_overall", | |
| "selected_from": "top_overall[0]", | |
| "research_seed_only": True, | |
| } | |
| def test_generate_finetune_configs_keeps_every_selected_weight_nonzero(): | |
| configs = generate_finetune_configs( | |
| {"oldwang_trend": 0.01, "volume_spike": 0.08, "trust_flow": 0.02}, | |
| delta_values=[-0.01, 0.0, 0.01], | |
| scale_values=[0.9, 1.0, 1.1], | |
| random_configs=20, | |
| max_configs=30, | |
| weight_step=0.005, | |
| min_weight=0.005, | |
| max_weight=0.10, | |
| random_seed=7, | |
| ) | |
| assert configs[0].weights == {"oldwang_trend": 0.01, "volume_spike": 0.08, "trust_flow": 0.02} | |
| assert 1 < len(configs) <= 30 | |
| assert all(set(config.weights) == {"oldwang_trend", "volume_spike", "trust_flow"} for config in configs) | |
| assert all(all(weight > 0 for weight in config.weights.values()) for config in configs) | |
| assert all(all(0.005 <= weight <= 0.10 for weight in config.weights.values()) for config in configs) | |
| def test_generate_finetune_configs_respects_zero_weight_limit(): | |
| configs = generate_finetune_configs( | |
| {"trend": 0.0, "volume": 0.25, "chip": 0.0, "risk": 0.5}, | |
| delta_values=[-0.01, 0.0, 0.01], | |
| scale_values=[1.0], | |
| random_configs=20, | |
| max_configs=30, | |
| weight_step=0.01, | |
| min_weight=0.0, | |
| max_weight=1.0, | |
| random_seed=7, | |
| max_zero_factor_weights=1, | |
| ) | |
| assert configs | |
| assert all( | |
| sum(1 for weight in cfg.weights.values() if abs(weight) <= 1e-12) <= 1 | |
| for cfg in configs | |
| ) | |
| def test_generate_finetune_configs_lifts_excess_seed_zeros_before_random_search(): | |
| configs = generate_finetune_configs( | |
| {"a": 0.0, "b": 0.0, "c": 0.0, "d": 0.0, "e": 0.5}, | |
| delta_values=[-0.01, 0.0, 0.01], | |
| scale_values=[1.0], | |
| random_configs=20, | |
| max_configs=30, | |
| weight_step=0.01, | |
| min_weight=0.0, | |
| max_weight=1.0, | |
| random_seed=7, | |
| max_zero_factor_weights=1, | |
| ) | |
| assert len(configs) > 5 | |
| assert all(sum(1 for weight in cfg.weights.values() if weight == 0.0) <= 1 for cfg in configs) | |
| assert configs[0].weights == {"a": 0.01, "b": 0.01, "c": 0.01, "d": 0.0, "e": 0.5} | |
| def test_generate_finetune_configs_uses_local_mutations_for_many_low_weights(): | |
| seed = {f"factor_{idx}": 0.01 for idx in range(30)} | |
| configs = generate_finetune_configs( | |
| seed, | |
| delta_values=[-0.01, 0.0, 0.01], | |
| scale_values=[1.0], | |
| random_configs=100, | |
| max_configs=120, | |
| weight_step=0.01, | |
| min_weight=0.0, | |
| max_weight=1.0, | |
| random_seed=7, | |
| max_zero_factor_weights=3, | |
| ) | |
| assert len(configs) >= 100 | |
| assert all(sum(1 for weight in cfg.weights.values() if weight == 0.0) <= 3 for cfg in configs) | |
| def test_generate_beam_refinement_configs_uses_cent_step_and_zero_limit(): | |
| rows = [] | |
| for idx in range(12): | |
| bullish = idx % 2 == 0 | |
| rows.append( | |
| { | |
| "stock": "2330", | |
| "date": f"2025-04-{idx + 1:02d}", | |
| "y_true": 1 if bullish else -1, | |
| "base_pred": 0, | |
| "p_buy": 0.49, | |
| "p_hold": 0.52, | |
| "p_sell": 0.49, | |
| "factor__trend": 2.0 if bullish else -2.0, | |
| "factor__volume": 0.0, | |
| } | |
| ) | |
| configs = generate_beam_refinement_configs( | |
| {"trend": 0.0, "volume": 0.25}, | |
| events=pd.DataFrame(rows), | |
| optimize_ratio=0.5, | |
| weight_step=0.01, | |
| min_weight=0.0, | |
| max_weight=1.0, | |
| beam_width=3, | |
| beam_passes=1, | |
| max_zero_factor_weights=1, | |
| ) | |
| assert configs | |
| assert all( | |
| all(abs((weight * 100) - round(weight * 100)) < 1e-9 for weight in cfg.weights.values()) | |
| for cfg in configs | |
| ) | |
| assert all( | |
| sum(1 for weight in cfg.weights.values() if abs(weight) <= 1e-12) <= 1 | |
| for cfg in configs | |
| ) | |
| def test_generate_beam_refinement_configs_searches_hold_bias_threshold(): | |
| events = pd.DataFrame( | |
| [ | |
| { | |
| "stock": "2330", | |
| "date": f"2025-05-{idx + 1:02d}", | |
| "y_true": 0, | |
| "base_pred": 1, | |
| "p_buy": 0.51, | |
| "p_hold": 0.50, | |
| "p_sell": 0.0, | |
| "factor__trend": 0.0, | |
| } | |
| for idx in range(6) | |
| ] | |
| ) | |
| configs = generate_beam_refinement_configs( | |
| {"trend": 0.01}, | |
| events=events, | |
| optimize_ratio=0.5, | |
| weight_step=0.01, | |
| min_weight=0.0, | |
| max_weight=0.01, | |
| beam_width=2, | |
| beam_passes=1, | |
| max_zero_factor_weights=1, | |
| hold_bias_values=[0.0, 0.02], | |
| ) | |
| assert {cfg.hold_bias for cfg in configs} == {0.0, 0.02} | |
| def test_hold_bias_values_must_be_inside_probability_range(): | |
| assert _hold_biases("0.0,0.05") == [0.0, 0.05] | |
| try: | |
| _hold_biases("1.01") | |
| except ValueError as exc: | |
| assert "inside [0, 1]" in str(exc) | |
| else: | |
| raise AssertionError("Expected invalid hold bias to fail") | |
| def test_evaluate_configs_reports_current_seed_and_best_separately(): | |
| rows = [] | |
| for idx in range(12): | |
| bullish = idx % 2 == 0 | |
| rows.append( | |
| { | |
| "stock": "2330" if idx < 6 else "2317", | |
| "date": f"2025-04-{idx + 1:02d}", | |
| "y_true": 1 if bullish else -1, | |
| "base_pred": 0, | |
| "p_buy": 0.49, | |
| "p_hold": 0.52, | |
| "p_sell": 0.49, | |
| "factor__trend": 2.0 if bullish else -2.0, | |
| "factor__volume": 0.0, | |
| } | |
| ) | |
| configs = generate_finetune_configs( | |
| {"trend": 0.01, "volume": 0.01}, | |
| delta_values=[0.01], | |
| scale_values=[1.0], | |
| random_configs=0, | |
| max_configs=10, | |
| weight_step=0.005, | |
| min_weight=0.005, | |
| max_weight=0.10, | |
| random_seed=1, | |
| ) | |
| result = evaluate_configs( | |
| pd.DataFrame(rows), | |
| configs, | |
| optimize_ratio=0.5, | |
| min_accuracy_delta_pp=0.0, | |
| min_buy_precision_delta_pp=0.0, | |
| min_signal_ratio=0.5, | |
| max_buy_count_ratio=None, | |
| max_sell_precision_drop_pp=None, | |
| max_zero_factor_weights=None, | |
| top_n=5, | |
| ) | |
| assert result["baseline"]["validation"]["accuracy"] == 0.0 | |
| assert result["ranking_source"] == "tune" | |
| assert result["seed_config"]["name"] == "seed" | |
| assert result["seed_config"]["validation"]["metrics"]["accuracy"] == 0.0 | |
| assert result["golden_config"]["validation"]["metrics"]["accuracy"] == 100.0 | |
| assert result["golden_config"]["config"]["weights"]["trend"] == 0.02 | |
| assert result["golden_config"]["gate"]["checks"]["all_factor_weights_nonzero"] is True | |
| assert result["best_vs_seed_deltas"]["accuracy_delta_pp"] == 100.0 | |
| def test_evaluate_configs_reports_per_stock_for_top_candidate_without_golden(): | |
| rows = [] | |
| for idx in range(12): | |
| bullish = idx % 2 == 0 | |
| rows.append( | |
| { | |
| "stock": "2330" if idx < 6 else "2317", | |
| "date": f"2025-05-{idx + 1:02d}", | |
| "y_true": 1 if bullish else -1, | |
| "base_pred": 0, | |
| "p_buy": 0.49, | |
| "p_hold": 0.52, | |
| "p_sell": 0.49, | |
| "factor__trend": 2.0 if bullish else -2.0, | |
| "factor__volume": 0.0, | |
| } | |
| ) | |
| configs = generate_finetune_configs( | |
| {"trend": 0.01, "volume": 0.01}, | |
| delta_values=[0.01], | |
| scale_values=[1.0], | |
| random_configs=0, | |
| max_configs=10, | |
| weight_step=0.005, | |
| min_weight=0.005, | |
| max_weight=0.10, | |
| random_seed=1, | |
| ) | |
| result = evaluate_configs( | |
| pd.DataFrame(rows), | |
| configs, | |
| optimize_ratio=0.5, | |
| min_accuracy_delta_pp=101.0, | |
| min_buy_precision_delta_pp=0.0, | |
| min_signal_ratio=0.5, | |
| max_buy_count_ratio=None, | |
| max_sell_precision_drop_pp=None, | |
| max_zero_factor_weights=None, | |
| top_n=5, | |
| ) | |
| assert result["golden_config"] is None | |
| assert result["per_stock_validation_config_name"] == "candidate_00001" | |
| assert result["per_stock_validation_config_passed"] is False | |
| assert set(result["per_stock_validation"]) == {"2317"} | |
| def test_evaluate_configs_can_retain_all_ranked_combinations(): | |
| rows = [ | |
| { | |
| "stock": "2330", | |
| "date": f"2025-06-{idx + 1:02d}", | |
| "y_true": 1 if idx % 2 == 0 else -1, | |
| "base_pred": 0, | |
| "p_buy": 0.49, | |
| "p_hold": 0.52, | |
| "p_sell": 0.49, | |
| "factor__trend": 2.0 if idx % 2 == 0 else -2.0, | |
| } | |
| for idx in range(12) | |
| ] | |
| configs = generate_finetune_configs( | |
| {"trend": 0.01}, | |
| delta_values=[0.01, 0.02], | |
| scale_values=[1.0], | |
| random_configs=0, | |
| max_configs=10, | |
| weight_step=0.01, | |
| min_weight=0.01, | |
| max_weight=0.10, | |
| random_seed=1, | |
| ) | |
| result = evaluate_configs( | |
| pd.DataFrame(rows), | |
| configs, | |
| optimize_ratio=0.5, | |
| min_accuracy_delta_pp=0.0, | |
| min_buy_precision_delta_pp=0.0, | |
| min_signal_ratio=0.5, | |
| max_buy_count_ratio=None, | |
| max_sell_precision_drop_pp=None, | |
| max_zero_factor_weights=0, | |
| top_n=1, | |
| retain_all_configs=True, | |
| ) | |
| assert len(result["all_configs"]) == len(configs) | |
| assert len(result["top_overall"]) == 1 | |