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:
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user