|
| 1 | +from __future__ import annotations |
| 2 | + |
| 3 | +import unittest |
| 4 | +from unittest.mock import patch |
| 5 | + |
| 6 | +import pandas as pd |
| 7 | + |
| 8 | +from src.export import build_live_pool_payload |
| 9 | +from src.ranking import build_final_scores, latest_ranking_snapshot |
| 10 | + |
| 11 | + |
| 12 | +class RankingTieBreakTests(unittest.TestCase): |
| 13 | + def test_tie_break_prefers_confidence_then_liquidity_then_symbol(self) -> None: |
| 14 | + as_of_date = pd.Timestamp("2026-04-01") |
| 15 | + index = pd.MultiIndex.from_tuples( |
| 16 | + [ |
| 17 | + (as_of_date, "AAAUSDT"), |
| 18 | + (as_of_date, "BBBUSDT"), |
| 19 | + (as_of_date, "CCCUSDT"), |
| 20 | + ], |
| 21 | + names=["date", "symbol"], |
| 22 | + ) |
| 23 | + panel = pd.DataFrame( |
| 24 | + { |
| 25 | + "in_universe": [True, True, True], |
| 26 | + "rule_score": [1.0, 0.5, 0.5], |
| 27 | + "linear_score_raw": [0.0, 0.5, 0.5], |
| 28 | + "ml_score_raw": [0.5, 0.5, 0.5], |
| 29 | + "regime": ["risk_off", "risk_off", "risk_off"], |
| 30 | + "liquidity_stability": [0.70, 0.90, 0.80], |
| 31 | + "avg_quote_vol_180": [20_000_000.0, 30_000_000.0, 30_000_000.0], |
| 32 | + }, |
| 33 | + index=index, |
| 34 | + ) |
| 35 | + config = { |
| 36 | + "ensemble": {"default_weights": {"rule_score": 1.0, "linear_score": 1.0, "ml_score": 1.0}}, |
| 37 | + "regime_weights": {}, |
| 38 | + "ranking": {"selected_pool_size": 2}, |
| 39 | + } |
| 40 | + |
| 41 | + with patch("src.ranking.normalize_component_by_date", side_effect=lambda frame, column, mask: frame[column]): |
| 42 | + scored = build_final_scores(panel, config) |
| 43 | + |
| 44 | + snapshot = latest_ranking_snapshot(scored, as_of_date) |
| 45 | + self.assertEqual(snapshot.index.tolist(), ["BBBUSDT", "CCCUSDT", "AAAUSDT"]) |
| 46 | + self.assertEqual(snapshot["current_rank"].tolist(), [1.0, 2.0, 3.0]) |
| 47 | + self.assertEqual(snapshot.loc[snapshot["selected_flag"]].index.tolist(), ["BBBUSDT", "CCCUSDT"]) |
| 48 | + |
| 49 | + def test_live_pool_payload_uses_same_deterministic_tie_break(self) -> None: |
| 50 | + as_of_date = pd.Timestamp("2026-04-01") |
| 51 | + ranking_snapshot = pd.DataFrame( |
| 52 | + { |
| 53 | + "final_score": [0.5, 0.5, 0.5], |
| 54 | + "confidence": [0.6, 0.6, 0.6], |
| 55 | + "liquidity_stability": [0.80, 0.80, 0.80], |
| 56 | + "avg_quote_vol_180": [15_000_000.0, 25_000_000.0, 25_000_000.0], |
| 57 | + }, |
| 58 | + index=pd.Index(["CCCUSDT", "BBBUSDT", "AAAUSDT"], name="symbol"), |
| 59 | + ) |
| 60 | + metadata = pd.DataFrame( |
| 61 | + { |
| 62 | + "symbol": ["AAAUSDT", "BBBUSDT", "CCCUSDT"], |
| 63 | + "base_asset": ["AAA", "BBB", "CCC"], |
| 64 | + } |
| 65 | + ) |
| 66 | + |
| 67 | + payload, _ = build_live_pool_payload( |
| 68 | + ranking_snapshot=ranking_snapshot, |
| 69 | + metadata=metadata, |
| 70 | + as_of_date=as_of_date, |
| 71 | + pool_size=2, |
| 72 | + mode="core_major", |
| 73 | + ) |
| 74 | + |
| 75 | + self.assertEqual(payload["symbols"], ["AAAUSDT", "BBBUSDT"]) |
| 76 | + |
| 77 | + |
| 78 | +if __name__ == "__main__": |
| 79 | + unittest.main() |
0 commit comments