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:
19
bridge.py
19
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(
|
||||
|
||||
@@ -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_")
|
||||
|
||||
Reference in New Issue
Block a user