From 24663ebd35bfdeb79e8bad7cbbc44fa0a129f75a Mon Sep 17 00:00:00 2001 From: Gahow Wang Date: Thu, 14 May 2026 12:53:26 +0800 Subject: [PATCH] test: add strategy and integration tests Add tests for trend rider (integration, robustness, v4), US combo sweep, and US fundamentals modules. --- tests/test_trend_rider_integration.py | 51 +++++++++++++++ tests/test_trend_rider_robustness.py | 91 +++++++++++++++++++++++++++ tests/test_trend_rider_v4.py | 43 +++++++++++++ tests/test_us_combo_sweep.py | 65 +++++++++++++++++++ tests/test_us_fundamentals.py | 52 +++++++++++++++ 5 files changed, 302 insertions(+) create mode 100644 tests/test_trend_rider_integration.py create mode 100644 tests/test_trend_rider_robustness.py create mode 100644 tests/test_trend_rider_v4.py create mode 100644 tests/test_us_combo_sweep.py create mode 100644 tests/test_us_fundamentals.py diff --git a/tests/test_trend_rider_integration.py b/tests/test_trend_rider_integration.py new file mode 100644 index 0000000..5a23af1 --- /dev/null +++ b/tests/test_trend_rider_integration.py @@ -0,0 +1,51 @@ +import unittest + +import pandas as pd + +import trader +from strategies.permanent import ( + ETF_UNIVERSE, + GLOBAL_ETF_UNIVERSE, + HK_ETF_UNIVERSE, + TREND_RIDER_V4_UNIVERSE, + TrendRiderV3, + TrendRiderV4, +) + + +class TrendRiderTraderIntegrationTests(unittest.TestCase): + def test_trend_rider_strategies_are_registered(self): + self.assertIsInstance(trader.STRATEGY_REGISTRY["trend_rider_v3_us"](), TrendRiderV3) + self.assertIsInstance(trader.STRATEGY_REGISTRY["trend_rider_v3_global"](), TrendRiderV3) + self.assertIsInstance(trader.STRATEGY_REGISTRY["trend_rider_v3_hk"](), TrendRiderV3) + self.assertIsInstance(trader.STRATEGY_REGISTRY["trend_rider_v4"](), TrendRiderV4) + + def test_strategy_universe_uses_etfs_for_trend_rider(self): + tickers, benchmark = trader.strategy_universe("us", "trend_rider_v3_us") + self.assertEqual(tickers, sorted(ETF_UNIVERSE)) + self.assertEqual(benchmark, "SPY") + self.assertEqual(trader.strategy_data_market("us", "trend_rider_v3_us"), "etfs") + + global_tickers, global_benchmark = trader.strategy_universe("us", "trend_rider_v3_global") + self.assertEqual(global_tickers, sorted(set(GLOBAL_ETF_UNIVERSE))) + self.assertEqual(global_benchmark, "SPY") + + hk_tickers, hk_benchmark = trader.strategy_universe("us", "trend_rider_v3_hk") + self.assertEqual(hk_tickers, sorted(set(HK_ETF_UNIVERSE))) + self.assertEqual(hk_benchmark, "SPY") + + v4_tickers, v4_benchmark = trader.strategy_universe("us", "trend_rider_v4") + self.assertEqual(v4_tickers, sorted(set(TREND_RIDER_V4_UNIVERSE))) + self.assertEqual(v4_benchmark, "SPY") + + def test_filter_tradable_columns_preserves_strategy_assets(self): + close_data = pd.DataFrame(columns=["SPY", "TQQQ", "UPRO", "GLD", "DBC", "SHY"]) + tickers = trader.filter_tradable_tickers(close_data, ["SPY", "TQQQ", "MISSING"]) + self.assertEqual(tickers, ["SPY", "TQQQ"]) + + def test_stock_strategies_keep_market_cache(self): + self.assertEqual(trader.strategy_data_market("us", "recovery_mom_top10"), "us") + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_trend_rider_robustness.py b/tests/test_trend_rider_robustness.py new file mode 100644 index 0000000..1cfc7df --- /dev/null +++ b/tests/test_trend_rider_robustness.py @@ -0,0 +1,91 @@ +import unittest + +import numpy as np +import pandas as pd + +from research import trend_rider_robustness as robustness + + +class TrendRiderRobustnessTests(unittest.TestCase): + def test_evaluate_weights_reports_core_risk_metrics(self): + dates = pd.bdate_range("2024-01-01", periods=6) + prices = pd.DataFrame( + { + "AAA": [100, 110, 105, 120, 118, 130], + "BBB": [50, 49, 51, 50, 52, 53], + }, + index=dates, + ) + weights = pd.DataFrame( + { + "AAA": [0, 1, 1, 0, 0, 1], + "BBB": [0, 0, 0, 1, 1, 0], + }, + index=dates, + ) + + result = robustness.evaluate_weights("synthetic", weights, prices, transaction_cost=0.001) + + self.assertEqual(result.name, "synthetic") + self.assertGreater(result.final_multiple, 1.0) + self.assertLessEqual(result.max_drawdown, 0.0) + self.assertGreater(result.switches, 0) + self.assertGreater(result.avg_daily_turnover, 0.0) + + def test_parameter_sweep_returns_rankable_rows(self): + dates = pd.bdate_range("2023-01-02", periods=320) + trend = np.linspace(100, 180, len(dates)) + prices = pd.DataFrame( + { + "SPY": trend, + "TQQQ": trend * 1.5, + "UPRO": trend * 1.4, + "GLD": np.linspace(100, 105, len(dates)), + "DBC": np.linspace(90, 95, len(dates)), + }, + index=dates, + ) + + sweep = robustness.parameter_sweep( + prices, + variants=[ + {"vol_enter": 0.14, "dd_stop": 0.05, "peak_enter": 0.02, "mom_lookback": 63}, + {"vol_enter": 0.16, "dd_stop": 0.07, "peak_enter": 0.03, "mom_lookback": 84}, + ], + start="2023-01-02", + ) + + self.assertEqual(len(sweep), 2) + self.assertIn("cagr", sweep.columns) + self.assertIn("max_drawdown", sweep.columns) + self.assertTrue(sweep["cagr"].notna().all()) + + def test_candidate_weights_include_v4_and_market_benchmarks(self): + dates = pd.bdate_range("2023-01-02", periods=320) + trend = np.linspace(100, 180, len(dates)) + prices = pd.DataFrame( + { + "SPY": trend, + "QQQ": trend * 1.1, + "SSO": trend * 1.5, + "QLD": trend * 1.6, + "UPRO": trend * 2.0, + "TQQQ": trend * 2.2, + "SHY": np.linspace(100, 103, len(dates)), + "IEF": np.linspace(100, 104, len(dates)), + "TLT": np.linspace(100, 105, len(dates)), + "GLD": np.linspace(100, 115, len(dates)), + "DBC": np.linspace(90, 105, len(dates)), + }, + index=dates, + ) + + candidates = robustness.candidate_weights(prices) + + self.assertIn("TrendRiderV4", candidates) + self.assertIn("SPY Buy&Hold", candidates) + self.assertIn("QQQ Buy&Hold", candidates) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_trend_rider_v4.py b/tests/test_trend_rider_v4.py new file mode 100644 index 0000000..91bee49 --- /dev/null +++ b/tests/test_trend_rider_v4.py @@ -0,0 +1,43 @@ +import unittest + +import numpy as np +import pandas as pd + +from strategies.permanent import TrendRiderV4 + + +class TrendRiderV4Tests(unittest.TestCase): + def test_v4_builds_capped_multi_asset_portfolio(self): + dates = pd.bdate_range("2023-01-02", periods=320) + trend = np.linspace(100.0, 180.0, len(dates)) + prices = pd.DataFrame( + { + "SPY": trend, + "QQQ": trend * 1.10, + "SSO": trend * 1.55, + "QLD": trend * 1.65, + "UPRO": trend * 2.00, + "TQQQ": trend * 2.20, + "SHY": np.linspace(100.0, 103.0, len(dates)), + "IEF": np.linspace(100.0, 104.0, len(dates)), + "TLT": np.linspace(100.0, 105.0, len(dates)), + "GLD": np.linspace(100.0, 115.0, len(dates)), + "DBC": np.linspace(90.0, 105.0, len(dates)), + }, + index=dates, + ) + + strategy = TrendRiderV4(max_single_weight=0.35, max_leveraged_weight=0.50) + weights = strategy.generate_signals(prices) + active = weights[weights.sum(axis=1) > 0.99] + + self.assertFalse(active.empty) + self.assertLessEqual(active.max(axis=1).max(), 0.350001) + self.assertGreaterEqual((active > 0.001).sum(axis=1).min(), 4) + leveraged = [c for c in ["SSO", "QLD", "UPRO", "TQQQ"] if c in active.columns] + self.assertLessEqual(active[leveraged].sum(axis=1).max(), 0.500001) + self.assertTrue(np.allclose(active.sum(axis=1), 1.0)) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_us_combo_sweep.py b/tests/test_us_combo_sweep.py new file mode 100644 index 0000000..053dcdb --- /dev/null +++ b/tests/test_us_combo_sweep.py @@ -0,0 +1,65 @@ +import unittest + +import pandas as pd + + +class USComboSweepTests(unittest.TestCase): + def test_apply_filter_threshold_masks_names_below_rank_cutoff(self): + from research.us_combo_sweep import apply_filter_threshold + + index = pd.DatetimeIndex([pd.Timestamp("2024-01-31")]) + score = pd.DataFrame({"AAA": [0.9], "BBB": [0.8], "CCC": [0.7]}, index=index) + filter_rank = pd.DataFrame({"AAA": [0.2], "BBB": [0.6], "CCC": [0.9]}, index=index) + + filtered = apply_filter_threshold(score, filter_rank, min_rank=0.5) + + self.assertTrue(pd.isna(filtered.iloc[0]["AAA"])) + self.assertEqual(float(filtered.iloc[0]["BBB"]), 0.8) + self.assertEqual(float(filtered.iloc[0]["CCC"]), 0.7) + + def test_run_combo_backtests_returns_candidates_and_yearly_summary(self): + from research.us_combo_sweep import run_combo_backtests + + dates = pd.date_range("2022-01-01", periods=800, freq="D") + close = pd.DataFrame( + { + "AAA": [50.0 + 0.12 * i for i in range(800)], + "BBB": [40.0 + 0.08 * i for i in range(800)], + "CCC": [35.0 + 0.06 * i for i in range(800)], + "DDD": [30.0 + 0.04 * i for i in range(800)], + "EEE": [25.0 + 0.03 * i for i in range(800)], + "FFF": [20.0 + 0.02 * i for i in range(800)], + "GGG": [18.0 + 0.015 * i for i in range(800)], + "HHH": [16.0 + 0.010 * i for i in range(800)], + "III": [14.0 + 0.008 * i for i in range(800)], + "JJJ": [12.0 + 0.005 * i for i in range(800)], + "SPY": [300.0 + 0.20 * i for i in range(800)], + }, + index=dates, + ) + fundamental_score = pd.DataFrame( + { + "AAA": [0.95] * 800, + "BBB": [0.90] * 800, + "CCC": [0.85] * 800, + "DDD": [0.80] * 800, + "EEE": [0.75] * 800, + "FFF": [0.70] * 800, + "GGG": [0.65] * 800, + "HHH": [0.60] * 800, + "III": [0.55] * 800, + "JJJ": [0.50] * 800, + }, + index=dates, + ) + + yearly, summary = run_combo_backtests(close, fundamental_score, top_n=3) + + self.assertIn("Recovery+Mom Top10", yearly.columns) + self.assertIn("rm_fund_tilt_20", yearly.columns) + self.assertIn("rm_fund_filter_50", yearly.columns) + self.assertIn("mega_quality_fund", set(summary["strategy"])) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_us_fundamentals.py b/tests/test_us_fundamentals.py new file mode 100644 index 0000000..aba44b0 --- /dev/null +++ b/tests/test_us_fundamentals.py @@ -0,0 +1,52 @@ +import unittest + +import pandas as pd + + +class USFundamentalTransformsTests(unittest.TestCase): + def test_build_quarterly_factor_pack_derives_expected_signals(self): + from research.us_fundamentals import build_quarterly_factor_pack + + quarter_ends = pd.to_datetime( + ["2023-03-31", "2023-06-30", "2023-09-30", "2023-12-31", "2024-03-31"] + ) + close = pd.DataFrame( + {"AAA": [100.0, 105.0], "BBB": [50.0, 48.0]}, + index=pd.to_datetime(["2024-06-03", "2024-06-04"]), + ) + quarterly = { + "net_income": pd.DataFrame( + {"AAA": [10.0, 11.0, 12.0, 13.0, 14.0], "BBB": [4.0, 4.0, 5.0, 5.0, 5.0]}, + index=quarter_ends, + ), + "gross_profit": pd.DataFrame( + {"AAA": [30.0, 31.0, 32.0, 33.0, 34.0], "BBB": [10.0, 10.0, 11.0, 11.0, 11.0]}, + index=quarter_ends, + ), + "equity": pd.DataFrame( + {"AAA": [200.0, 205.0, 210.0, 215.0, 220.0], "BBB": [80.0, 81.0, 82.0, 83.0, 84.0]}, + index=quarter_ends, + ), + "assets": pd.DataFrame( + {"AAA": [300.0, 305.0, 310.0, 315.0, 320.0], "BBB": [130.0, 131.0, 132.0, 133.0, 134.0]}, + index=quarter_ends, + ), + "shares": pd.DataFrame( + {"AAA": [10.0, 10.0, 10.0, 10.0, 10.0], "BBB": [10.0, 10.0, 11.0, 11.0, 11.0]}, + index=quarter_ends, + ), + } + + factor_pack = build_quarterly_factor_pack(quarterly, close, lag_days=60) + + self.assertIn("composite", factor_pack) + self.assertIn("book_to_market", factor_pack) + self.assertEqual(list(factor_pack["composite"].columns), ["AAA", "BBB"]) + self.assertGreater( + float(factor_pack["composite"].iloc[-1]["AAA"]), + float(factor_pack["composite"].iloc[-1]["BBB"]), + ) + + +if __name__ == "__main__": + unittest.main()