File size: 3,348 Bytes
e610a2f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
import json

import numpy as np
import pandas as pd

from scripts.run_ml_method_ladder import (
    _date_group_sizes,
    _ranking_metrics,
    _ranker_relevance,
    _split_panel,
    build_sequence_features,
    render_html_report,
)


def test_build_sequence_features_uses_current_and_prior_rows_only():
    df = pd.DataFrame(
        {
            "close": [10, 11, 12, 11, 13, 14, 15, 15, 16, 18, 19, 20],
            "high": [11, 12, 13, 12, 14, 15, 16, 16, 17, 19, 20, 21],
            "low": [9, 10, 11, 10, 12, 13, 14, 14, 15, 17, 18, 19],
            "volume": [100, 110, 120, 130, 140, 160, 180, 190, 200, 220, 240, 260],
        }
    )

    seq = build_sequence_features(df)

    assert "seq_ret_5_sum" in seq
    assert "seq_sax_up_ratio_10" in seq
    assert len(seq) == len(df)
    assert np.isfinite(seq.to_numpy()).all()


def test_split_panel_embargoes_label_horizon_between_train_and_validation():
    rows = []
    for stock in ["2330", "2454"]:
        for idx in range(180):
            rows.append(
                {
                    "stock": stock,
                    "date": f"2026-01-{(idx % 28) + 1:02d}",
                    "row_idx": idx,
                    "label": 1 if idx % 3 == 0 else 0,
                    "f": float(idx),
                }
            )
    panel = pd.DataFrame(rows)

    train, valid, split = _split_panel(panel, train_ratio=0.7)

    assert not train.empty
    assert not valid.empty
    for stock, info in split["stocks"].items():
        assert info["validation_start_idx"] - info["train_end_idx"] > 1
        assert train[train["stock"] == stock]["row_idx"].max() < valid[valid["stock"] == stock]["row_idx"].min()


def test_ranking_metrics_scores_top_k_hits_by_date():
    frame = pd.DataFrame(
        {
            "date": ["2026-01-01"] * 4 + ["2026-01-02"] * 4,
            "stock": list("ABCDEFGH"),
            "label": [1, 0, -1, 1, 0, 1, -1, 1],
            "p_buy": [0.9, 0.8, 0.1, 0.2, 0.2, 0.95, 0.1, 0.8],
            "pred": [1, 1, -1, 0, 0, 1, -1, 1],
        }
    )

    metrics = _ranking_metrics(frame, top_k=2)

    assert metrics["dates"] == 2
    assert metrics["hit_rate"] == 75.0


def test_ranker_helpers_build_date_groups_and_non_negative_relevance():
    frame = pd.DataFrame(
        {
            "date": ["2026-01-01", "2026-01-01", "2026-01-02"],
            "label": [-1, 0, 1],
        }
    )

    assert _date_group_sizes(frame) == [2, 1]
    assert _ranker_relevance(frame["label"]).to_list() == [0, 1, 2]


def test_render_html_report_contains_method_rows():
    payload = {
        "generated_at": "2026-05-21T00:00:00+00:00",
        "stocks": ["2330"],
        "split": {"validation_date_range": ["2026-01-01", "2026-02-01"]},
        "methods": {
            "panel_global_model": {
                "baseline": {"accuracy": 40, "up_precision": 50},
                "candidate": {"accuracy": 44, "up_precision": 51, "n_signals": 10},
                "deltas": {"accuracy_delta_pp": 4, "up_precision_delta_pp": 1},
                "gate": {"passed": True},
            },
            "ranking_from_panel": {"ranking": {"baseline": {}, "candidate": {}, "deltas": {}}},
        },
    }

    html = render_html_report(payload)

    assert "ML Method Ladder Validation" in html
    assert "panel_global_model" in html
    assert "PASS" in html