diff --git a/bridge.py b/bridge.py index 7173af2..66c4484 100644 --- a/bridge.py +++ b/bridge.py @@ -24,6 +24,8 @@ import universe_history as uh from main import backtest from research.permanent_yearly import equity_with_cashflows from trader import ( + strategy_extra_tickers, + strategy_supports_market, ETF_STRATEGY_UNIVERSES, LIVE_EXCLUDED_STRATEGIES, MARKET_FEE_TIERED, @@ -698,7 +700,7 @@ def cmd_list() -> None: "label": meta["label"], "category": meta["category"], "params": meta["params"], - "markets": meta["markets"], + "markets": [m for m in meta["markets"] if strategy_supports_market(name, m)], }) json.dump(strategies, sys.stdout, indent=2) @@ -754,10 +756,12 @@ def cmd_backtest(args: argparse.Namespace) -> None: with _redirect_stdout_to_stderr(): pit_intervals = uh.load_sp500_history() historical_tickers = uh.all_tickers_ever(pit_intervals) - all_tickers = sorted(set(tickers + historical_tickers + [benchmark_ticker])) + all_tickers = sorted(set(tickers + historical_tickers + [benchmark_ticker]) + | {t for n in stock_strategies for t in strategy_extra_tickers(n)}) _log(f"PIT universe: {len(all_tickers)} tickers (current + historical members)") else: - all_tickers = sorted(set(tickers + [benchmark_ticker])) + all_tickers = sorted(set(tickers + [benchmark_ticker]) + | {t for n in stock_strategies for t in strategy_extra_tickers(n)}) _log(f"WARNING: {market.upper()} market lacks PIT membership history; " "backtest uses current index constituents (survivorship bias possible)") @@ -794,9 +798,12 @@ def cmd_backtest(args: argparse.Namespace) -> None: # Registry defaults are the single source of strategy parameters, # shared with the live trader. strategy = STRATEGY_REGISTRY[name]() + # Mixed stock+ETF strategies also read their fixed ETFs. + cols = tickers + [t for t in strategy_extra_tickers(name) + if t in data.columns and t not in tickers] equity = backtest( - strategy, data[tickers], + strategy, data[cols], initial_capital=capital, transaction_cost=tx_cost, fixed_fee=fixed_fee, @@ -805,8 +812,8 @@ def cmd_backtest(args: argparse.Namespace) -> None: ) # Get weight signals for trade extraction - weights = strategy.generate_signals(data[tickers]) - weights = weights.reindex(data[tickers].index).fillna(0.0) + weights = strategy.generate_signals(data[cols]) + weights = weights.reindex(data[cols].index).fillna(0.0) trade_events = compute_rebalance_trades(weights) result_entry = _build_strategy_result( diff --git a/trader.py b/trader.py index b251976..a4c6ee1 100644 --- a/trader.py +++ b/trader.py @@ -268,6 +268,11 @@ DEFAULT_MONITOR_STRATEGIES = [ ] +def strategy_extra_tickers(strategy_name: str) -> list[str]: + """Fixed ETF tickers a mixed stock+ETF strategy needs beside its stock universe.""" + return list(MIXED_STRATEGY_EXTRA_TICKERS.get(strategy_name.removeprefix("sim_"), [])) + + def strategy_supports_market(strategy_name: str, market: str) -> bool: """ETF-based strategies trade USD-listed ETFs, so they only run in the US book.""" base_name = strategy_name.removeprefix("sim_")