test: add strategy and integration tests

Add tests for trend rider (integration, robustness, v4),
US combo sweep, and US fundamentals modules.
This commit is contained in:
2026-05-14 12:53:26 +08:00
parent 541f7bcf5b
commit 24663ebd35
5 changed files with 302 additions and 0 deletions

View File

@@ -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()

View File

@@ -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()

View File

@@ -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()

View File

@@ -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()

View File

@@ -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()