fix(backtest): give mixed stock+ETF strategies their ETFs; derive markets from trader

trend_rider_v6 backtests failed ('SPY' not in data) because only the stock
universe was passed. Strategy markets in 'list' now follow the trader's
eligibility rule, so v6 is no longer offered for CN.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-25 02:16:48 +08:00
parent 69636c3fae
commit 6dc623b4a1
2 changed files with 18 additions and 6 deletions

View File

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

View File

@@ -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_")