feat(tick_precision): Added exchange-derived per symbol tick-precision. Adjusted BULL/BEAR comparison to account for per-symbol, exchange limited precision.

This commit is contained in:
2025-09-01 21:42:18 +02:00
parent c9cffb4b53
commit af0bfbe100

View File

@@ -36,6 +36,8 @@ STRUCTURED_TOPIC = "SILOQY.STRUCTURED.TICKS"
REGIME_TOPIC = "DOLPHIN.REGIME.RESULTS" REGIME_TOPIC = "DOLPHIN.REGIME.RESULTS"
SYMBOLS_DISCOVERED_TOPIC = "SILOQY.SYMBOLS.DISCOVERED" SYMBOLS_DISCOVERED_TOPIC = "SILOQY.SYMBOLS.DISCOVERED"
CANDLES_INITIAL_TOPIC = "SILOQY.CANDLES.INITIAL" CANDLES_INITIAL_TOPIC = "SILOQY.CANDLES.INITIAL"
# ADDED LINE 18:
TICK_SIZES_TOPIC = "SILOQY.TICK.SIZES"
# Rate limiting constant # Rate limiting constant
MIN_INTERVAL = 2.5 # seconds between API batches MIN_INTERVAL = 2.5 # seconds between API batches
@@ -351,6 +353,7 @@ class SILOQYSymbolDiscoveryActor(Actor):
self.symbols = list(config.symbols) if config.symbols else [] self.symbols = list(config.symbols) if config.symbols else []
self.candle_interval_ms = config.candle_interval_ms self.candle_interval_ms = config.candle_interval_ms
self.active_candles = {} self.active_candles = {}
self.tick_sizes = {}
# Process management configuration # Process management configuration
self.throttle_mode = config.throttle_mode self.throttle_mode = config.throttle_mode
@@ -416,11 +419,37 @@ class SILOQYSymbolDiscoveryActor(Actor):
if response.status_code == 200: if response.status_code == 200:
self.log.info("Successfully received exchange info") self.log.info("Successfully received exchange info")
data = response.json() data = response.json()
# Get all trading symbols (USDT pairs for example)
full_symbols = [ # Combined symbol discovery and tick size extraction
s['symbol'] for s in data['symbols'] self.log.info("Processing symbols and extracting tick sizes...")
if s['status'] == 'TRADING' and s['symbol'].endswith('USDT') full_symbols = []
] for symbol_info in data['symbols']:
if symbol_info['status'] == 'TRADING' and symbol_info['symbol'].endswith('USDT'):
symbol = symbol_info['symbol']
full_symbols.append(symbol)
# Extract tick size while processing # Extract tick size while processing
tick_size = None
for filter_info in symbol_info['filters']:
if filter_info['filterType'] == 'PRICE_FILTER':
tick_size = float(filter_info['tickSize'])
break
# If no PRICE_FILTER found, try other filter types
if tick_size is None:
for filter_info in symbol_info['filters']:
if filter_info['filterType'] == 'TICK_SIZE':
tick_size = float(filter_info['tickSize'])
break
# Fallback to default if still not found
if tick_size is None:
tick_size = 1e-8 # Default fallback
self.log.warning(f"No tick size found for {symbol}, using fallback {tick_size}")
self.tick_sizes[symbol] = tick_size
self.log.info(f"Processed {len(full_symbols)} symbols, extracted {len(self.tick_sizes)} tick sizes")
# Apply throttle mode symbol limiting # Apply throttle mode symbol limiting
if self.throttle_mode: if self.throttle_mode:
@@ -574,8 +603,10 @@ class SILOQYSymbolDiscoveryActor(Actor):
# Publish symbols and candles as tuples # Publish symbols and candles as tuples
self.msgbus.publish(SYMBOLS_DISCOVERED_TOPIC, (self.symbols, int(time.time_ns()))) self.msgbus.publish(SYMBOLS_DISCOVERED_TOPIC, (self.symbols, int(time.time_ns())))
self.msgbus.publish(CANDLES_INITIAL_TOPIC, (self.active_candles, int(time.time_ns()))) self.msgbus.publish(CANDLES_INITIAL_TOPIC, (self.active_candles, int(time.time_ns())))
self.msgbus.publish(TICK_SIZES_TOPIC, (self.tick_sizes, int(time.time_ns())))
self.log.info(f"Nautilus ActorExecutor: Published {len(self.symbols)} symbols and {len(self.active_candles)} candles") self.log.info(f"Nautilus ActorExecutor: Published {len(self.symbols)} symbols and {len(self.active_candles)} candles")
self.log.info(f"Nautilus ActorExecutor: Published {len(self.tick_sizes)} tick sizes")
self.log.info("Nautilus ActorExecutor: Discovery phase complete - other actors can now start processing") self.log.info("Nautilus ActorExecutor: Discovery phase complete - other actors can now start processing")
else: else:
self.log.warning("Nautilus ActorExecutor: msgbus not available for publishing") self.log.warning("Nautilus ActorExecutor: msgbus not available for publishing")
@@ -868,6 +899,7 @@ class DOLPHINRegimeActor(Actor):
self.low_prices = np.zeros(self.max_symbols, dtype=np.float64) self.low_prices = np.zeros(self.max_symbols, dtype=np.float64)
self.volumes = np.zeros(self.max_symbols, dtype=np.float64) self.volumes = np.zeros(self.max_symbols, dtype=np.float64)
self.last_update = np.zeros(self.max_symbols, dtype=np.int64) self.last_update = np.zeros(self.max_symbols, dtype=np.int64)
self.tick_sizes = np.full(self.max_symbols, 1e-8, dtype=np.float64) # Default fallback
# PRESERVED: All original mapping and state # PRESERVED: All original mapping and state
self.symbol_to_idx = {} self.symbol_to_idx = {}
@@ -904,6 +936,30 @@ class DOLPHINRegimeActor(Actor):
self.log.info("Nautilus ActorExecutor: DOLPHINRegimeActor stopping") self.log.info("Nautilus ActorExecutor: DOLPHINRegimeActor stopping")
# Nautilus kernel handles executor shutdown - no manual cleanup needed # Nautilus kernel handles executor shutdown - no manual cleanup needed
def handle_tick_sizes(self, data):
"""Handle tick sizes from discovery actor"""
try:
tick_sizes, timestamp = data
self.log.info(f"Nautilus ActorExecutor: Received {len(tick_sizes)} tick sizes from discovery actor")
# Update the pre-allocated array with actual tick sizes
updated_count = 0
for symbol, tick_size in tick_sizes.items():
if symbol in self.symbol_to_idx:
idx = self.symbol_to_idx[symbol]
# Validate tick size
if tick_size <= 0 or tick_size > 1.0:
self.log.warning(f"Invalid tick size {tick_size} for {symbol}, using fallback")
self.tick_sizes[idx] = 1e-8 # Use fallback
else:
self.tick_sizes[idx] = tick_size
updated_count += 1
self.log.info(f"Nautilus ActorExecutor: Updated {updated_count} tick sizes in pre-allocated array")
except Exception as e:
self.log.error(f"Nautilus ActorExecutor: Error handling tick sizes: {e}")
def handle_raw_tick(self, data): def handle_raw_tick(self, data):
""" """
PRESERVED EXACTLY: All original zero-allocation tick processing PRESERVED EXACTLY: All original zero-allocation tick processing
@@ -1002,12 +1058,15 @@ class DOLPHINRegimeActor(Actor):
analyzed += 1 analyzed += 1
# NEW: Direct price comparison with epsilon for precision
EPSILON = 1e-10 # Very small tolerance to capture any meaningful price change
# Check if prices are effectively equal # NEW: HFT-grade tick-size based comparison
if abs(close_price - open_price) <= EPSILON: tick_size = self.tick_sizes[idx]
# Prices are effectively equal equality_threshold = tick_size / 2 # Half tick size standard
price_diff = abs(close_price - open_price)
# Check if prices are effectively equal within tick size tolerance
if price_diff <= equality_threshold:
# Prices are effectively equal (within tick size tolerance)
symbol_pattern.append(f"S{close_price:.2f}={open_price:.2f}") symbol_pattern.append(f"S{close_price:.2f}={open_price:.2f}")
elif close_price > open_price: elif close_price > open_price:
# Bullish: close > open # Bullish: close > open
@@ -1241,7 +1300,6 @@ def test_siloqy_actors_with_nautilus_process_management():
node = TradingNode(config=trading_config) node = TradingNode(config=trading_config)
try: try:
node.build() node.build()
print("Node built successfully with Nautilus built-in process management") print("Node built successfully with Nautilus built-in process management")