feat: Multi-Pair Divergence Selection Strategy
- Extend regime detection to top 10 cryptocurrencies (45 pairs) - Dynamic pair selection based on divergence score (|z_score| * probability) - Universal ML model trained on all pairs - Correlation-based filtering to avoid redundant positions - Funding rate integration from OKX for all 10 assets - ATR-based dynamic stop-loss and take-profit - Walk-forward training with 70/30 split Performance: +35.69% return (vs +28.66% baseline), 63.6% win rate
This commit is contained in:
168
strategies/multi_pair/pair_scanner.py
Normal file
168
strategies/multi_pair/pair_scanner.py
Normal file
@@ -0,0 +1,168 @@
|
||||
"""
|
||||
Pair Scanner for Multi-Pair Divergence Strategy.
|
||||
|
||||
Generates all possible pairs from asset universe and checks tradeability.
|
||||
"""
|
||||
from dataclasses import dataclass
|
||||
from itertools import combinations
|
||||
from typing import Optional
|
||||
|
||||
import ccxt
|
||||
|
||||
from engine.logging_config import get_logger
|
||||
from .config import MultiPairConfig
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
@dataclass
|
||||
class TradingPair:
|
||||
"""
|
||||
Represents a tradeable pair for spread analysis.
|
||||
|
||||
Attributes:
|
||||
base_asset: First asset in the pair (numerator)
|
||||
quote_asset: Second asset in the pair (denominator)
|
||||
pair_id: Unique identifier for the pair
|
||||
is_direct: Whether pair can be traded directly on exchange
|
||||
exchange_symbol: Symbol for direct trading (if available)
|
||||
"""
|
||||
base_asset: str
|
||||
quote_asset: str
|
||||
pair_id: str
|
||||
is_direct: bool = False
|
||||
exchange_symbol: Optional[str] = None
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
"""Human-readable pair name."""
|
||||
return f"{self.base_asset}/{self.quote_asset}"
|
||||
|
||||
def __hash__(self):
|
||||
return hash(self.pair_id)
|
||||
|
||||
def __eq__(self, other):
|
||||
if not isinstance(other, TradingPair):
|
||||
return False
|
||||
return self.pair_id == other.pair_id
|
||||
|
||||
|
||||
class PairScanner:
|
||||
"""
|
||||
Scans and generates tradeable pairs from asset universe.
|
||||
|
||||
Checks OKX for directly tradeable cross-pairs and generates
|
||||
synthetic pairs via USDT for others.
|
||||
"""
|
||||
|
||||
def __init__(self, config: MultiPairConfig):
|
||||
self.config = config
|
||||
self.exchange: Optional[ccxt.Exchange] = None
|
||||
self._available_markets: set[str] = set()
|
||||
|
||||
def _init_exchange(self) -> None:
|
||||
"""Initialize exchange connection for market lookup."""
|
||||
if self.exchange is None:
|
||||
exchange_class = getattr(ccxt, self.config.exchange_id)
|
||||
self.exchange = exchange_class({'enableRateLimit': True})
|
||||
self.exchange.load_markets()
|
||||
self._available_markets = set(self.exchange.symbols)
|
||||
logger.info(
|
||||
"Loaded %d markets from %s",
|
||||
len(self._available_markets),
|
||||
self.config.exchange_id
|
||||
)
|
||||
|
||||
def generate_pairs(self, check_exchange: bool = True) -> list[TradingPair]:
|
||||
"""
|
||||
Generate all unique pairs from asset universe.
|
||||
|
||||
Args:
|
||||
check_exchange: Whether to check OKX for direct trading
|
||||
|
||||
Returns:
|
||||
List of TradingPair objects
|
||||
"""
|
||||
if check_exchange:
|
||||
self._init_exchange()
|
||||
|
||||
pairs = []
|
||||
assets = self.config.assets
|
||||
|
||||
for base, quote in combinations(assets, 2):
|
||||
pair_id = f"{base}__{quote}"
|
||||
|
||||
# Check if directly tradeable as cross-pair on OKX
|
||||
is_direct = False
|
||||
exchange_symbol = None
|
||||
|
||||
if check_exchange:
|
||||
# Check perpetual cross-pair (e.g., ETH/BTC:BTC)
|
||||
# OKX perpetuals are typically quoted in USDT
|
||||
# Cross-pairs like ETH/BTC are less common
|
||||
cross_symbol = f"{base.replace('-USDT', '')}/{quote.replace('-USDT', '')}:USDT"
|
||||
if cross_symbol in self._available_markets:
|
||||
is_direct = True
|
||||
exchange_symbol = cross_symbol
|
||||
|
||||
pair = TradingPair(
|
||||
base_asset=base,
|
||||
quote_asset=quote,
|
||||
pair_id=pair_id,
|
||||
is_direct=is_direct,
|
||||
exchange_symbol=exchange_symbol
|
||||
)
|
||||
pairs.append(pair)
|
||||
|
||||
# Log summary
|
||||
direct_count = sum(1 for p in pairs if p.is_direct)
|
||||
logger.info(
|
||||
"Generated %d pairs: %d direct, %d synthetic",
|
||||
len(pairs), direct_count, len(pairs) - direct_count
|
||||
)
|
||||
|
||||
return pairs
|
||||
|
||||
def get_required_symbols(self, pairs: list[TradingPair]) -> list[str]:
|
||||
"""
|
||||
Get list of symbols needed to calculate all pair spreads.
|
||||
|
||||
For synthetic pairs, we need both USDT pairs.
|
||||
For direct pairs, we still load USDT pairs for simplicity.
|
||||
|
||||
Args:
|
||||
pairs: List of trading pairs
|
||||
|
||||
Returns:
|
||||
List of unique symbols to load (e.g., ['BTC-USDT', 'ETH-USDT'])
|
||||
"""
|
||||
symbols = set()
|
||||
for pair in pairs:
|
||||
symbols.add(pair.base_asset)
|
||||
symbols.add(pair.quote_asset)
|
||||
return list(symbols)
|
||||
|
||||
def filter_by_assets(
|
||||
self,
|
||||
pairs: list[TradingPair],
|
||||
exclude_assets: list[str]
|
||||
) -> list[TradingPair]:
|
||||
"""
|
||||
Filter pairs that contain any of the excluded assets.
|
||||
|
||||
Args:
|
||||
pairs: List of trading pairs
|
||||
exclude_assets: Assets to exclude
|
||||
|
||||
Returns:
|
||||
Filtered list of pairs
|
||||
"""
|
||||
if not exclude_assets:
|
||||
return pairs
|
||||
|
||||
exclude_set = set(exclude_assets)
|
||||
return [
|
||||
p for p in pairs
|
||||
if p.base_asset not in exclude_set
|
||||
and p.quote_asset not in exclude_set
|
||||
]
|
||||
Reference in New Issue
Block a user