Add check_symbols.py for ETH perpetuals filtering and enhance backtester with size handling

- Introduced `check_symbols.py` to load and filter ETH perpetual markets from the OKX exchange using CCXT.
- Updated the backtester to normalize signals to a 5-tuple format, incorporating size management for trades.
- Enhanced portfolio functions to support variable size and leverage adjustments based on initial capital.
- Added a new method in `CryptoQuantClient` for chunked historical data fetching to avoid API limits.
- Improved market symbol normalization in `market.py` to handle different formats.
- Updated regime strategy parameters based on recent research findings for optimal performance.
This commit is contained in:
2026-01-14 09:46:51 +08:00
parent 10bb371054
commit 1e4cb87da3
8 changed files with 617 additions and 111 deletions

View File

@@ -132,9 +132,22 @@ class Backtester:
**strategy_params
)
# Normalize signals to 4-tuple format
# Normalize signals to 5-tuple format
signals = self._normalize_signals(signals, close_price, market_config)
long_entries, long_exits, short_entries, short_exits = signals
long_entries, long_exits, short_entries, short_exits, size = signals
# Default size if None
if size is None:
size = 1.0
# Convert leverage multiplier to raw value (USD) for vbt
# This works around "SizeType.Percent reversal" error
# Effectively "Fixed Fractional" sizing based on Initial Capital
# (Does not compound, but safe for backtesting)
if isinstance(size, pd.Series):
size = size * init_cash
else:
size = size * init_cash
# Process liquidations - inject forced exits at liquidation points
liquidation_events: list[LiquidationEvent] = []
@@ -164,7 +177,8 @@ class Backtester:
long_entries, long_exits,
short_entries, short_exits,
init_cash, effective_fees, slippage, timeframe,
sl_stop, tp_stop, sl_trail, effective_leverage
sl_stop, tp_stop, sl_trail, effective_leverage,
size=size
)
# Calculate adjusted returns accounting for liquidation losses
@@ -242,39 +256,45 @@ class Backtester:
market_config
) -> tuple:
"""
Normalize strategy signals to 4-tuple format.
Normalize strategy signals to 5-tuple format.
Handles backward compatibility with 2-tuple (long-only) returns.
Returns:
(long_entries, long_exits, short_entries, short_exits, size)
"""
# Default size is None (will be treated as 1.0 or default later)
size = None
if len(signals) == 2:
long_entries, long_exits = signals
short_entries = BaseStrategy.create_empty_signals(long_entries)
short_exits = BaseStrategy.create_empty_signals(long_entries)
return long_entries, long_exits, short_entries, short_exits
return long_entries, long_exits, short_entries, short_exits, size
if len(signals) == 4:
long_entries, long_exits, short_entries, short_exits = signals
elif len(signals) == 5:
long_entries, long_exits, short_entries, short_exits, size = signals
else:
raise ValueError(
f"Strategy must return 2, 4, or 5 signal arrays, got {len(signals)}"
)
# Warn and clear short signals on spot markets
if not market_config.supports_short:
has_shorts = (
short_entries.any().any()
if hasattr(short_entries, 'any')
else short_entries.any()
# Warn and clear short signals on spot markets
if not market_config.supports_short:
has_shorts = (
short_entries.any().any()
if hasattr(short_entries, 'any')
else short_entries.any()
)
if has_shorts:
logger.warning(
"Short signals detected but market type is SPOT. "
"Short signals will be ignored."
)
if has_shorts:
logger.warning(
"Short signals detected but market type is SPOT. "
"Short signals will be ignored."
)
short_entries = BaseStrategy.create_empty_signals(long_entries)
short_exits = BaseStrategy.create_empty_signals(long_entries)
return long_entries, long_exits, short_entries, short_exits
raise ValueError(
f"Strategy must return 2 or 4 signal arrays, got {len(signals)}"
)
short_entries = BaseStrategy.create_empty_signals(long_entries)
short_exits = BaseStrategy.create_empty_signals(long_entries)
return long_entries, long_exits, short_entries, short_exits, size
def _run_portfolio(
self,
@@ -289,7 +309,8 @@ class Backtester:
sl_stop: float | None,
tp_stop: float | None,
sl_trail: bool,
leverage: int
leverage: int,
size: pd.Series | float = 1.0
) -> vbt.Portfolio:
"""Select and run appropriate portfolio simulation."""
has_shorts = (
@@ -304,14 +325,18 @@ class Backtester:
long_entries, long_exits,
short_entries, short_exits,
init_cash, fees, slippage, freq,
sl_stop, tp_stop, sl_trail, leverage
sl_stop, tp_stop, sl_trail, leverage,
size=size
)
return run_long_only_portfolio(
close,
long_entries, long_exits,
init_cash, fees, slippage, freq,
sl_stop, tp_stop, sl_trail, leverage
sl_stop, tp_stop, sl_trail, leverage,
# Long-only doesn't support variable size in current implementation
# without modification, but we can add it if needed.
# For now, only regime strategy uses it, which is Long/Short.
)
def run_wfa(