From 0a2d646b26f34ffe6e1864b88f6666b16dd873dc Mon Sep 17 00:00:00 2001 From: Gahow Wang Date: Thu, 14 May 2026 12:52:49 +0800 Subject: [PATCH] feat: enhance trader with expanded capabilities --- trader.py | 102 +++++++++++++++++++++++++++++++++++++++++------------- 1 file changed, 77 insertions(+), 25 deletions(-) diff --git a/trader.py b/trader.py index 78d7e6b..f22379b 100644 --- a/trader.py +++ b/trader.py @@ -44,6 +44,14 @@ from strategies.factor_combo import FactorComboStrategy from strategies.inverse_vol import InverseVolatilityStrategy from strategies.momentum import MomentumStrategy from strategies.momentum_quality import MomentumQualityStrategy +from strategies.permanent import ( + ETF_UNIVERSE, + GLOBAL_ETF_UNIVERSE, + HK_ETF_UNIVERSE, + TREND_RIDER_V4_UNIVERSE, + TrendRiderV3, + TrendRiderV4, +) from strategies.recovery_momentum import RecoveryMomentumStrategy from strategies.trend_following import TrendFollowingStrategy from universe import UNIVERSES @@ -107,8 +115,57 @@ STRATEGY_REGISTRY = { "fc_up_cap_mom_gap_weekly": lambda **kw: FactorComboStrategy("up_cap+mom_gap", rebal_freq=5), "fc_up_cap_mom_gap_biweekly": lambda **kw: FactorComboStrategy("up_cap+mom_gap", rebal_freq=10), "fc_up_cap_mom_gap_monthly": lambda **kw: FactorComboStrategy("up_cap+mom_gap", rebal_freq=21), + # --- ETF tactical allocation strategies --- + "trend_rider_v3_us": lambda **kw: TrendRiderV3(), + "trend_rider_v3_global": lambda **kw: TrendRiderV3( + risk_on=("TQQQ", "UPRO", "YINN", "CHAU"), + risk_off=("GLD", "DBC"), + ), + "trend_rider_v3_hk": lambda **kw: TrendRiderV3( + risk_on=("7200.HK", "7500.HK"), + risk_off=("GLD", "DBC"), + ), + "trend_rider_v4": lambda **kw: TrendRiderV4(), } +ETF_STRATEGY_UNIVERSES = { + "trend_rider_v3_us": sorted(set(ETF_UNIVERSE)), + "trend_rider_v3_global": sorted(set(GLOBAL_ETF_UNIVERSE)), + "trend_rider_v3_hk": sorted(set(HK_ETF_UNIVERSE)), + "trend_rider_v4": sorted(set(TREND_RIDER_V4_UNIVERSE)), +} + +DEFAULT_MONITOR_STRATEGIES = [ + name for name in STRATEGY_REGISTRY + if name not in ETF_STRATEGY_UNIVERSES +] + + +def strategy_universe(market: str, strategy_name: str) -> tuple[list[str], str]: + """Return tradable tickers and benchmark for a strategy. + + Stock strategies use the market's dynamic universe. TrendRider variants + trade fixed USD/HK ETF baskets and use SPY as the regime benchmark. + """ + base_name = strategy_name.removeprefix("sim_") + if base_name in ETF_STRATEGY_UNIVERSES: + return ETF_STRATEGY_UNIVERSES[base_name], "SPY" + + universe = UNIVERSES[market] + tickers = universe["fetch"]() + return tickers, universe["benchmark"] + + +def strategy_data_market(market: str, strategy_name: str) -> str: + """Return the cache namespace used for a strategy's price data.""" + base_name = strategy_name.removeprefix("sim_") + return "etfs" if base_name in ETF_STRATEGY_UNIVERSES else market + + +def filter_tradable_tickers(price_data: pd.DataFrame, tickers: list[str]) -> list[str]: + """Keep requested tickers that are present in a downloaded price panel.""" + return [t for t in tickers if t in price_data.columns] + # --------------------------------------------------------------------------- # Persistent state @@ -383,9 +440,8 @@ def cmd_morning(args): """Morning: download open prices, generate today's trade orders.""" market = args.market strategy_name = args.strategy - universe = UNIVERSES[market] - tickers = universe["fetch"]() - benchmark = universe["benchmark"] + tickers, benchmark = strategy_universe(market, strategy_name) + data_market = strategy_data_market(market, strategy_name) all_tickers = sorted(set(tickers + [benchmark])) # Load or init state @@ -395,8 +451,8 @@ def cmd_morning(args): print(f"--- Initialized new portfolio: ${args.capital:,.0f} cash ---") # Download data (close + open) - close_data, open_data = data_manager.update(market, all_tickers, with_open=True) - tickers = [t for t in tickers if t in close_data.columns] + close_data, open_data = data_manager.update(data_market, all_tickers, with_open=True) + tickers = filter_tradable_tickers(close_data, tickers) today = open_data.index[-1] today_str = str(today.date()) @@ -473,9 +529,8 @@ def cmd_evening(args): """Evening: record execution at close prices, update portfolio.""" market = args.market strategy_name = args.strategy - universe = UNIVERSES[market] - tickers = universe["fetch"]() - benchmark = universe["benchmark"] + tickers, benchmark = strategy_universe(market, strategy_name) + data_market = strategy_data_market(market, strategy_name) all_tickers = sorted(set(tickers + [benchmark])) state = load_state(market, strategy_name) @@ -495,8 +550,8 @@ def cmd_evening(args): return # Get close prices - close_data = data_manager.update(market, all_tickers) - tickers = [t for t in tickers if t in close_data.columns] + close_data = data_manager.update(data_market, all_tickers) + tickers = filter_tradable_tickers(close_data, tickers) target_date = pd.Timestamp(trade_date) all_held = list(set( @@ -577,11 +632,10 @@ def cmd_status(args): return # Get latest prices - universe = UNIVERSES[market] - tickers = universe["fetch"]() - benchmark = universe["benchmark"] + tickers, benchmark = strategy_universe(market, strategy_name) + data_market = strategy_data_market(market, strategy_name) all_tickers = sorted(set(tickers + [benchmark])) - close_data = data_manager.update(market, all_tickers) + close_data = data_manager.update(data_market, all_tickers) last_date = close_data.index[-1] all_held = list(state["holdings"].keys()) @@ -883,14 +937,13 @@ def cmd_simulate(args): """Simulate day-by-day over a date range.""" market = args.market strategy_name = args.strategy - universe = UNIVERSES[market] - tickers = universe["fetch"]() - benchmark = universe["benchmark"] + tickers, benchmark = strategy_universe(market, strategy_name) + data_market = strategy_data_market(market, strategy_name) all_tickers = sorted(set(tickers + [benchmark])) # Load both open and close data - close_data, open_data = data_manager.update(market, all_tickers, with_open=True) - tickers = [t for t in tickers if t in close_data.columns] + close_data, open_data = data_manager.update(data_market, all_tickers, with_open=True) + tickers = filter_tradable_tickers(close_data, tickers) # Date range start = pd.Timestamp(args.start) @@ -1259,9 +1312,8 @@ def cmd_auto(args): market = args.market strategy_name = args.strategy - universe = UNIVERSES[market] - tickers = universe["fetch"]() - benchmark = universe["benchmark"] + tickers, benchmark = strategy_universe(market, strategy_name) + data_market = strategy_data_market(market, strategy_name) all_tickers = sorted(set(tickers + [benchmark])) # Load or init state @@ -1271,8 +1323,8 @@ def cmd_auto(args): print(f"[auto] Initialized new portfolio: ${args.capital:,.0f} cash") # Download data (close + open) - close_data, open_data = data_manager.update(market, all_tickers, with_open=True) - tickers = [t for t in tickers if t in close_data.columns] + close_data, open_data = data_manager.update(data_market, all_tickers, with_open=True) + tickers = filter_tradable_tickers(close_data, tickers) today = close_data.index[-1] today_str = str(today.date()) @@ -1398,7 +1450,7 @@ def main(): help="Markets to monitor (default: ALL)") p_monitor.add_argument("--strategy", nargs="+", choices=list(STRATEGY_REGISTRY.keys()), - default=list(STRATEGY_REGISTRY.keys()), + default=DEFAULT_MONITOR_STRATEGIES, help="Strategies to run (default: ALL)") p_monitor.add_argument("--capital", type=float, default=10_000) p_monitor.add_argument("--tx-cost", type=float, default=0.001,