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:
51
tests/test_trend_rider_integration.py
Normal file
51
tests/test_trend_rider_integration.py
Normal 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()
|
||||
91
tests/test_trend_rider_robustness.py
Normal file
91
tests/test_trend_rider_robustness.py
Normal 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()
|
||||
43
tests/test_trend_rider_v4.py
Normal file
43
tests/test_trend_rider_v4.py
Normal 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()
|
||||
65
tests/test_us_combo_sweep.py
Normal file
65
tests/test_us_combo_sweep.py
Normal 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()
|
||||
52
tests/test_us_fundamentals.py
Normal file
52
tests/test_us_fundamentals.py
Normal 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()
|
||||
Reference in New Issue
Block a user