import unittest from lib.hub.hub_divergence_scan_lib import ( analyze_ohlcv_bars, build_symbol_scan_row, compute_confluence, detect_latest_macd_divergence, filter_tab_items, ) def _synthetic_bull_div_closes(n: int = 120) -> list[float]: """价格双底 + MACD 抬高 → 底背离。""" closes = [100.0] * n # 下跌 for i in range(20, 40): closes[i] = 100 - (i - 20) * 0.8 # 反弹 for i in range(40, 55): closes[i] = closes[39] + (i - 40) * 0.5 # 再跌略破前低 for i in range(55, 75): closes[i] = closes[54] - (i - 55) * 0.35 # 末尾企稳略抬 for i in range(75, n): closes[i] = closes[74] + (i - 75) * 0.02 return closes class TestHubDivergenceScanLib(unittest.TestCase): def test_compute_confluence_three_same(self): tf = { "4h": {"direction": "bull"}, "1d": {"direction": "bull"}, "1w": {"direction": "bull"}, } c = compute_confluence(tf) self.assertEqual(c["confluence"], 3) self.assertEqual(c["confluence_css"], "c3") self.assertFalse(c["is_split"]) def test_compute_confluence_split(self): tf = { "4h": {"direction": "bull"}, "1d": {"direction": "bear"}, "1w": {"direction": None}, } c = compute_confluence(tf) self.assertTrue(c["is_split"]) self.assertEqual(c["confluence_kind"], "分歧") self.assertEqual(c["confluence_css"], "split") self.assertIn("4h底", c["split_detail"]) def test_filter_tab_items_only_matching_tf(self): items = [ build_symbol_scan_row( rank=1, symbol="AAA/USDT", volume_label="1M", tf_hits={ "4h": {"direction": "bull", "bars_ago": 2, "open_time_ms": 1}, "1d": {"direction": None}, "1w": {"direction": None}, }, ), build_symbol_scan_row( rank=2, symbol="BBB/USDT", volume_label="2M", tf_hits={ "4h": {"direction": None}, "1d": {"direction": "bear", "bars_ago": 1, "open_time_ms": 2}, "1w": {"direction": None}, }, ), ] f4 = filter_tab_items(items, "4h") self.assertEqual(len(f4), 1) self.assertEqual(f4[0]["symbol"], "AAA/USDT") f1d = filter_tab_items(items, "1d") self.assertEqual(len(f1d), 1) self.assertEqual(f1d[0]["symbol"], "BBB/USDT") def test_detect_macd_divergence_may_hit_on_synthetic(self): closes = _synthetic_bull_div_closes() hit = detect_latest_macd_divergence(closes) # 合成数据不保证必中,但函数应正常返回 self.assertIn(hit.get("direction"), (None, "bull", "bear")) def test_analyze_ohlcv_bars_from_rows(self): closes = [float(100 + i * 0.1) for i in range(80)] bars = [ {"open_time_ms": i * 3600000, "close": c, "open": c, "high": c, "low": c} for i, c in enumerate(closes) ] out = analyze_ohlcv_bars(bars) self.assertIn("direction", out) if __name__ == "__main__": unittest.main()