- 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
169 lines
5.1 KiB
Python
169 lines
5.1 KiB
Python
"""
|
|
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
|
|
]
|