#!/usr/bin/env python3 # -*- coding: utf-8 -*- import sys if hasattr(sys.stdout, 'reconfigure'): sys.stdout.reconfigure(encoding='utf-8') """ BTC 5/15-min Live Trading Bot with Real-time Dashboard Single file that combines: - Visual terminal dashboard (rich) - Real order execution - Hedge management - Auto-redemption - Telegram notifications Usage: python main.py """ import asyncio import json import time import csv import math import statistics import logging import signal import sys from datetime import datetime, timezone from collections import deque from dataclasses import dataclass, field from typing import Optional, Dict, List, Any from pathlib import Path import aiohttp import websockets from rich.console import Console from rich.live import Live from rich.panel import Panel from rich.layout import Layout import os from dotenv import load_dotenv # noqa: E402 # Setup logging Path("logs").mkdir(exist_ok=True) # Pre-load .env EARLY so proxy detection can read HTTP_PROXY from it _PROJECT_ROOT = Path(__file__).parent load_dotenv(_PROJECT_ROOT / ".env") # Shared proxy helpers (HTTP CONNECT tunnel for websockets 13.x) from src.proxy_util import ( # noqa: E402 apply_proxy_env, get_proxy_url, ws_connect, ws_connect_kwargs as _ws_connect_kwargs, ) _PROXY_URL = apply_proxy_env() if _PROXY_URL: print(f"[proxy] Detected proxy: {_PROXY_URL}") print(f"[proxy] WebSocket via HTTP CONNECT tunnel; aiohttp via trust_env/proxy=") print(f"[proxy] websockets={getattr(websockets, '__version__', '?')}") else: print("[proxy] No HTTP_PROXY / HTTPS_PROXY env var detected. " "If you are in a restricted region, set them in .env.") def _aiohttp_session(**kwargs) -> aiohttp.ClientSession: """Create an aiohttp ClientSession that respects env proxy settings.""" timeout = kwargs.pop("timeout", None) or aiohttp.ClientTimeout(total=30) return aiohttp.ClientSession( timeout=timeout, trust_env=True, **kwargs ) # Main logger logging.basicConfig( level=logging.INFO, format='%(asctime)s [%(name)s] %(levelname)s: %(message)s', datefmt='%Y-%m-%d %H:%M:%S', handlers=[logging.FileHandler('logs/bot.log')] ) logger = logging.getLogger("btc_live") # Detailed order execution logger order_logger = logging.getLogger("btc_live.orders") order_handler = logging.FileHandler('logs/orders.log') order_handler.setFormatter(logging.Formatter( '%(asctime)s %(message)s', datefmt='%Y-%m-%d %H:%M:%S' )) order_logger.addHandler(order_handler) order_logger.setLevel(logging.DEBUG) # Detailed hedge logger hedge_logger = logging.getLogger("btc_live.hedges") hedge_handler = logging.FileHandler('logs/hedges.log') hedge_handler.setFormatter(logging.Formatter( '%(asctime)s %(message)s', datefmt='%Y-%m-%d %H:%M:%S' )) hedge_logger.addHandler(hedge_handler) hedge_logger.setLevel(logging.DEBUG) # Signals logger signal_logger = logging.getLogger("btc_live.signals") signal_handler = logging.FileHandler('logs/signals.log') signal_handler.setFormatter(logging.Formatter( '%(asctime)s %(message)s', datefmt='%Y-%m-%d %H:%M:%S' )) signal_logger.addHandler(signal_handler) signal_logger.setLevel(logging.DEBUG) # Project imports from src.config_loader import load_config, validate_config from src.web_dashboard import WebSnapshotHolder, start_web_dashboard from src.order_executor import OrderExecutor, ExecutionConfig from src.hedge_manager import HedgeManager, HedgeConfig as HedgeManagerConfig, HedgeResult from src.auto_redeemer import AsyncAutoRedeemer from src.telegram_notifier import TelegramNotifier from src.user_websocket import UserWebSocket from src.simulation_history import SimulationHistoryLogger # Constants GAMMA_API = "https://gamma-api.polymarket.com" WSS_URL = "wss://ws-subscriptions-clob.polymarket.com/ws/market" RTDS_URL = "wss://ws-live-data.polymarket.com" console = Console() # ============================================================================= # DATA CLASSES # ============================================================================= @dataclass class Trade: """Single trade record""" timestamp: float price: float size: float side: str @dataclass class TokenData: """Data for a single token (Up or Down)""" token_id: str name: str best_bid: float = 0.0 best_bid_size: float = 0.0 best_ask: float = 0.0 best_ask_size: float = 0.0 trades: deque = field(default_factory=lambda: deque(maxlen=5000)) last_price: float = 0.0 last_trade_time: float = 0.0 trade_count: int = 0 volume_total: float = 0.0 volume_buy: float = 0.0 volume_sell: float = 0.0 def reset(self): self.best_bid = 0.0 self.best_bid_size = 0.0 self.best_ask = 0.0 self.best_ask_size = 0.0 self.trades.clear() self.last_price = 0.0 self.last_trade_time = 0.0 self.trade_count = 0 self.volume_total = 0.0 self.volume_buy = 0.0 self.volume_sell = 0.0 @dataclass class MarketState: """Current market state""" market_id: str = "" condition_id: str = "" slug: str = "" end_time: float = 0.0 up_token: Optional[TokenData] = None down_token: Optional[TokenData] = None connected: bool = False last_update: float = 0.0 # Chainlink BTC/USD price tracking btc_anchor_price: float = 0.0 # Price at market start btc_current_price: float = 0.0 # Latest Chainlink price btc_last_update: float = 0.0 # Timestamp of last price update btc_connected: bool = False # RTDS connection status @dataclass class Position: """Current open position""" token_name: str token_id: str opposite_token_id: str entry_price: float contracts: int entry_time: float market_slug: str hedged: bool = False hedge_contracts: int = 0 hedge_price: float = 0.0 min_price_seen: float = 0.0 # Lowest price after entry (for drawdown tracking) @dataclass class TradeRecord: """Completed trade record""" market_slug: str token_name: str entry_price: float exit_price: float contracts: int pnl: float won: bool timestamp: float max_drawdown_abs: float = 0.0 # Max absolute price drop from entry max_drawdown_pct: float = 0.0 # Max percentage drawdown from entry # ============================================================================= # UTILITIES # ============================================================================= class IndicatorCalculator: @staticmethod def get_trades_in_window(trades: deque, window_seconds: float) -> List[Trade]: now = time.time() cutoff = now - window_seconds return [t for t in trades if t.timestamp >= cutoff] @staticmethod def calc_vwap(trades: List[Trade]) -> float: if not trades: return 0.0 total_value = sum(t.price * t.size for t in trades) total_volume = sum(t.size for t in trades) return total_value / total_volume if total_volume > 0 else 0.0 @staticmethod def calc_deviation(current_price: float, vwap: float) -> float: if vwap == 0: return 0.0 return ((current_price - vwap) / vwap) * 100 @staticmethod def calc_momentum(trades: deque, current_price: float, window: float = 120, avg_band: float = 1.5) -> Optional[float]: """ Price change vs average price ~window seconds ago. Takes all trades in [now-window-avg_band, now-window+avg_band] (3s band), computes arithmetic mean, returns % change from that to current_price. Returns None if no trades found in the band (not enough history). """ now = time.time() band_start = now - window - avg_band band_end = now - window + avg_band band_prices = [t.price for t in trades if band_start <= t.timestamp <= band_end] if not band_prices: return None avg_price_ago = sum(band_prices) / len(band_prices) if avg_price_ago == 0: return None return ((current_price - avg_price_ago) / avg_price_ago) * 100 @staticmethod def calc_zscore(trades: deque, current_price: float, window: float = 5) -> float: now = time.time() recent = [t for t in trades if t.timestamp >= now - window] if len(recent) < 2: return 0.0 prices = [t.price for t in recent] mean_price = statistics.mean(prices) std_price = statistics.stdev(prices) if len(prices) > 1 else 0.001 return (current_price - mean_price) / std_price if std_price > 0 else 0.0 class WinRateTable: def __init__(self, csv_path: str): self.data = {} self.price_ranges = [] self._load(csv_path) def _load(self, csv_path): try: with open(csv_path, 'r') as f: reader = csv.reader(f) next(reader) # Skip header for row in reader: if not row or not row[0]: continue price_range = row[0] self.price_ranges.append(price_range) self.data[price_range] = {} for i, val in enumerate(row[1:], start=0): if val: try: self.data[price_range][i] = float(val) except ValueError: pass except Exception as e: logger.warning(f"Could not load win_rate.csv: {e}") def get_winrate(self, price: float, minute: int, interval_minutes: int = 15) -> Optional[float]: price_range = None for pr in self.price_ranges: try: low, high = pr.split('-') if float(low) <= price <= float(high): price_range = pr break except: continue if not price_range and price > 0.99 and self.price_ranges: price_range = self.price_ranges[-1] if not price_range: return None cap = max(0, interval_minutes - 1) minute = max(0, min(cap, minute)) return self.data.get(price_range, {}).get(minute) # ============================================================================= # TRADING STATS # ============================================================================= class TradingStats: def __init__(self, log_file: str = "logs/trading_log.json"): self.log_file = Path(log_file) self.position: Optional[Position] = None self.trades: List[TradeRecord] = [] self.markets_seen: int = 0 self.current_market_slug: str = "" self.position_closed_this_market: bool = False self.entry_blocked: bool = False # Блокировка повторных попыток после таймаута self._load() def _load(self): try: if self.log_file.exists(): with open(self.log_file, 'r') as f: data = json.load(f) self.trades = [TradeRecord(**t) for t in data.get('trades', [])] self.markets_seen = data.get('markets_seen', 0) except Exception: pass def summary_dict(self) -> Dict[str, Any]: """Aggregates for dashboards and simulation summary files.""" tc = len(self.trades) wins = sum(1 for t in self.trades if t.won) losses = tc - wins total = sum(t.pnl for t in self.trades) pnls = [t.pnl for t in self.trades] wr = (wins / tc * 100.0) if tc else 0.0 return { "total_pnl_usd": round(total, 6), "trade_count": tc, "wins": wins, "losses": losses, "win_rate_pct": round(wr, 4), "avg_trade_pnl_usd": round(total / tc, 6) if tc else 0.0, "best_trade_pnl_usd": round(max(pnls), 6) if pnls else None, "worst_trade_pnl_usd": round(min(pnls), 6) if pnls else None, "last_close_unix": max((t.timestamp for t in self.trades), default=None), } def _save(self): try: self.log_file.parent.mkdir(parents=True, exist_ok=True) data = { 'trades': [t.__dict__ for t in self.trades], 'markets_seen': self.markets_seen, 'summary': self.summary_dict(), } with open(self.log_file, 'w') as f: json.dump(data, f, indent=2) except Exception: pass def new_market(self, slug: str): if slug != self.current_market_slug: self.current_market_slug = slug self.markets_seen += 1 self.position = None self.position_closed_this_market = False self.entry_blocked = False # Сброс блокировки для нового рынка self._save() def can_enter(self) -> bool: return self.position is None and not self.position_closed_this_market and not self.entry_blocked def block_entry(self, reason: str = ""): """Блокирует повторные попытки входа на текущем рынке.""" self.entry_blocked = True if reason: logger.warning(f"Entry blocked: {reason}") def record_entry(self, token_name: str, token_id: str, opposite_token_id: str, price: float, contracts: int, market_slug: str): self.position = Position( token_name=token_name, token_id=token_id, opposite_token_id=opposite_token_id, entry_price=price, contracts=contracts, entry_time=time.time(), market_slug=market_slug, min_price_seen=price # Start tracking from entry price ) def record_hedge(self, contracts: int, price: float): if self.position: self.position.hedged = True self.position.hedge_contracts = contracts self.position.hedge_price = price def update_drawdown(self, current_price: float): """Track minimum price seen since entry for drawdown calculation.""" if self.position and current_price > 0: if current_price < self.position.min_price_seen: self.position.min_price_seen = current_price def close_position(self, final_price: float) -> Optional[TradeRecord]: if not self.position: return None won = final_price >= 0.70 # Win threshold entry_cost = self.position.contracts * self.position.entry_price if won: pnl = self.position.contracts - entry_cost else: pnl = -entry_cost # Calculate max drawdown from entry dd_abs = max(0, self.position.entry_price - self.position.min_price_seen) dd_pct = (dd_abs / self.position.entry_price * 100) if self.position.entry_price > 0 else 0 record = TradeRecord( market_slug=self.position.market_slug, token_name=self.position.token_name, entry_price=self.position.entry_price, exit_price=final_price, contracts=self.position.contracts, pnl=pnl, won=won, timestamp=time.time(), max_drawdown_abs=dd_abs, max_drawdown_pct=dd_pct, ) self.trades.append(record) self.position = None self.position_closed_this_market = True self._save() return record @property def total_pnl(self) -> float: return sum(t.pnl for t in self.trades) @property def win_count(self) -> int: return sum(1 for t in self.trades if t.won) @property def trade_count(self) -> int: return len(self.trades) @property def win_rate(self) -> float: if not self.trades: return 0.0 return (self.win_count / self.trade_count) * 100 # ============================================================================= # WEBSOCKET CLIENT # ============================================================================= class WebSocketClient: def __init__(self, state: MarketState): self.state = state self.running = False self._tokens_validated = False self._ws: Optional[websockets.WebSocketClientProtocol] = None def _validate_tokens(self): """Log token prices after first WebSocket data received. NOTE: Token swap logic was REMOVED because it was buggy. The API token assignment should be trusted. """ if self._tokens_validated: return up = self.state.up_token down = self.state.down_token if not up or not down: return up_price = up.best_bid or up.best_ask or up.last_price down_price = down.best_bid or down.best_ask or down.last_price # Only log once we have valid prices if up_price > 0.05 and down_price > 0.05: price_sum = up_price + down_price logger.info(f"Tokens validated: UP={up_price:.2f}, DOWN={down_price:.2f}, sum={price_sum:.2f}") self._tokens_validated = True async def connect(self): self.running = True while self.running: try: async with ws_connect(WSS_URL, **_ws_connect_kwargs()) as ws: self._ws = ws self.state.connected = True token_ids = [] if self.state.up_token: token_ids.append(self.state.up_token.token_id) if self.state.down_token: token_ids.append(self.state.down_token.token_id) # Log exact token_ids being subscribed logger.info(f"WebSocket subscribing to tokens:") logger.info(f" UP: {self.state.up_token.token_id[:40]}..." if self.state.up_token else " UP: None") logger.info(f" DOWN: {self.state.down_token.token_id[:40]}..." if self.state.down_token else " DOWN: None") await ws.send(json.dumps({"assets_ids": token_ids, "type": "market"})) async for message in ws: if not self.running: break await self._handle_message(message) self._ws = None except websockets.ConnectionClosed: self._ws = None self.state.connected = False if self.running: await asyncio.sleep(1) except Exception: self._ws = None self.state.connected = False if self.running: await asyncio.sleep(2) async def disconnect(self): """Gracefully close WebSocket connection with code 1000 (normal closure).""" self.running = False if self._ws: try: await self._ws.close(code=1000, reason="Normal shutdown") logger.info("WebSocket closed gracefully (code 1000)") except Exception as e: logger.warning(f"Error during WebSocket close: {e}") finally: self._ws = None self.state.connected = False async def _handle_message(self, message: str): try: data = json.loads(message) if isinstance(data, list): for item in data: if isinstance(item, dict): await self._process_item(item) elif isinstance(data, dict): await self._process_item(data) self.state.last_update = time.time() # Validate tokens after receiving price data if not self._tokens_validated: self._validate_tokens() except Exception: pass async def _process_item(self, data: dict): event_type = data.get("event_type", "") if event_type == "last_trade_price": asset_id = data.get("asset_id") token = self._get_token(asset_id) if not token and asset_id: # Asset ID doesn't match our tokens - might indicate subscription issue logger.warning(f"Received price for unknown asset: {asset_id[:30]}...") logger.warning(f" Our UP token: {self.state.up_token.token_id[:30] if self.state.up_token else 'None'}...") logger.warning(f" Our DOWN token: {self.state.down_token.token_id[:30] if self.state.down_token else 'None'}...") if token: price = float(data.get("price", 0)) size = float(data.get("size", 0)) side = data.get("side", "BUY") if price > 0 and size > 0: token.last_price = price token.last_trade_time = time.time() token.trades.append(Trade(time.time(), price, size, side)) token.trade_count += 1 token.volume_total += size if side == "BUY": token.volume_buy += size else: token.volume_sell += size elif event_type == "price_change": for change in data.get("price_changes", []): token = self._get_token(change.get("asset_id")) if token: if change.get("best_bid"): token.best_bid = float(change["best_bid"]) if change.get("best_ask"): token.best_ask = float(change["best_ask"]) elif event_type == "book": token = self._get_token(data.get("asset_id")) if token: bids = data.get("bids", []) if bids: bids.sort(key=lambda x: float(x["price"]), reverse=True) token.best_bid = float(bids[0]["price"]) token.best_bid_size = float(bids[0]["size"]) asks = data.get("asks", []) if asks: asks.sort(key=lambda x: float(x["price"])) token.best_ask = float(asks[0]["price"]) token.best_ask_size = float(asks[0]["size"]) def _get_token(self, asset_id: str) -> Optional[TokenData]: if self.state.up_token and asset_id == self.state.up_token.token_id: return self.state.up_token elif self.state.down_token and asset_id == self.state.down_token.token_id: return self.state.down_token return None def stop(self): """Stop WebSocket (sync version - just sets flag).""" self.running = False async def stop_graceful(self): """Stop WebSocket gracefully with proper close.""" await self.disconnect() # ============================================================================= # CHAINLINK BTC PRICE CLIENT # ============================================================================= class ChainlinkPriceClient: """ Always-on BTC/USD price stream from Polymarket RTDS (Chainlink source). Connects to wss://ws-live-data.polymarket.com and subscribes to crypto_prices_chainlink for btc/usd. Autonomously tracks market boundaries (epoch-aligned to interval length) and snapshots the anchor price at the exact boundary crossing, independent of the bot's market finding flow. This ensures the anchor is captured within ~1 second of the real boundary, not 5-15s later. """ def __init__(self, state: 'MarketState', market_duration_sec: int): self.state = state self._market_duration = int(market_duration_sec) if self._market_duration <= 0: self._market_duration = 900 self.running = False self._ws = None self._ping_task: Optional[asyncio.Task] = None # Track which window the current anchor belongs to self._current_window: int = 0 # Buffer: last price before boundary (for most accurate anchor) self._last_price_before_boundary: float = 0.0 self._last_price_ts: float = 0.0 def _get_window(self, ts: float) -> int: """Window start timestamp (epoch) for the configured interval.""" d = self._market_duration return int(ts) // d * d DATA_TIMEOUT = 30 # seconds without any message → force reconnect async def connect(self): """Connect to RTDS and subscribe to Chainlink BTC/USD prices. Always on.""" self.running = True self._last_msg_time = time.time() while self.running: try: async with ws_connect(RTDS_URL, **_ws_connect_kwargs()) as ws: self._ws = ws self.state.btc_connected = True self._last_msg_time = time.time() logger.info("RTDS Chainlink connected") # Subscribe to chainlink prices (all symbols, filter in code) subscribe_msg = json.dumps({ "action": "subscribe", "subscriptions": [{ "topic": "crypto_prices_chainlink", "type": "*", "filters": "" }] }) await ws.send(subscribe_msg) # Start ping task and watchdog self._ping_task = asyncio.create_task(self._ping_loop(ws)) watchdog_task = asyncio.create_task(self._watchdog(ws)) try: async for message in ws: if not self.running: break self._last_msg_time = time.time() self._handle_message(message) finally: watchdog_task.cancel() try: await watchdog_task except asyncio.CancelledError: pass self._ws = None except websockets.ConnectionClosed: self._ws = None self.state.btc_connected = False if self.running: logger.warning("RTDS Chainlink disconnected, reconnecting in 2s...") await asyncio.sleep(2) except Exception as e: self._ws = None self.state.btc_connected = False if self.running: logger.warning( f"RTDS Chainlink error: {type(e).__name__}: {e or repr(e)}, " f"reconnecting in 5s..." ) await asyncio.sleep(5) finally: if self._ping_task and not self._ping_task.done(): self._ping_task.cancel() try: await self._ping_task except: pass self._ping_task = None async def _watchdog(self, ws): """Force-close WebSocket if no messages received for DATA_TIMEOUT seconds.""" try: while self.running: await asyncio.sleep(5) silence = time.time() - self._last_msg_time if silence > self.DATA_TIMEOUT: logger.warning( f"RTDS Chainlink watchdog: no data for {silence:.0f}s, forcing reconnect" ) self.state.btc_connected = False await ws.close() break except asyncio.CancelledError: pass def _handle_message(self, message: str): """Parse incoming Chainlink price message and auto-detect market boundaries.""" try: if not isinstance(message, str) or not message.strip(): return data = json.loads(message) topic = data.get("topic", "") if topic != "crypto_prices_chainlink": return payload = data.get("payload", {}) symbol = payload.get("symbol", "") if symbol != "btc/usd": return price = float(payload.get("value", 0)) if price <= 0: return # Use Chainlink's own timestamp (ms) for precise boundary detection chainlink_ts_ms = payload.get("timestamp", 0) if chainlink_ts_ms: price_ts = chainlink_ts_ms / 1000.0 else: price_ts = time.time() now = time.time() # Update current price (always) self.state.btc_current_price = price self.state.btc_last_update = now # === CALIBRATION LOG: every tick within [-15s..+5s] of any boundary === price_window = self._get_window(price_ts) next_boundary = price_window + self._market_duration secs_to_next = next_boundary - price_ts secs_from_prev = price_ts - price_window # Log if within 15s before next boundary OR 5s after current boundary start if secs_to_next <= 15.0 or secs_from_prev <= 5.0: cl_time = datetime.fromtimestamp(price_ts, tz=timezone.utc).strftime('%H:%M:%S.%f')[:-3] local_time = datetime.fromtimestamp(now, tz=timezone.utc).strftime('%H:%M:%S.%f')[:-3] if secs_from_prev <= 5.0: offset_str = f"+{secs_from_prev:.3f}s after {datetime.fromtimestamp(price_window, tz=timezone.utc).strftime('%H:%M:%S')}" else: offset_str = f"-{secs_to_next:.3f}s before {datetime.fromtimestamp(next_boundary, tz=timezone.utc).strftime('%H:%M:%S')}" logger.info( f"BTC_TICK {cl_time} (local {local_time}) ${price:,.2f} [{offset_str}]" ) # Detect window boundary crossing if self._current_window == 0: # First price ever — initialize self._current_window = price_window self.state.btc_anchor_price = price logger.info( f"BTC Chainlink init: ${price:,.2f} " f"(window {self._current_window}, " f"ts={datetime.fromtimestamp(price_ts, tz=timezone.utc).strftime('%H:%M:%S.%f')[:-3]})" ) elif price_window != self._current_window: # === NEW WINDOW === use FIRST tick of new window as anchor # Calibrated: reference program uses the first tick AT or AFTER boundary old_anchor = self.state.btc_anchor_price old_window = self._current_window self.state.btc_anchor_price = price # First tick of new window self._current_window = price_window boundary_time = datetime.fromtimestamp(price_window, tz=timezone.utc).strftime('%H:%M:%S') price_time = datetime.fromtimestamp(price_ts, tz=timezone.utc).strftime('%H:%M:%S.%f')[:-3] delay_ms = (price_ts - price_window) * 1000 logger.info( f"BTC anchor reset: ${self.state.btc_anchor_price:,.2f} " f"(boundary {boundary_time}, first tick at {price_time}, " f"delay {delay_ms:.0f}ms, prev anchor ${old_anchor:,.2f})" ) # Always buffer the latest price for next boundary crossing self._last_price_before_boundary = price self._last_price_ts = price_ts except (json.JSONDecodeError, ValueError, KeyError): pass async def _ping_loop(self, ws): """Send ping every 5 seconds to keep connection alive.""" try: while self.running: await asyncio.sleep(5) try: await ws.ping() except Exception: break except asyncio.CancelledError: pass async def disconnect(self): """Gracefully close RTDS WebSocket connection.""" self.running = False if self._ping_task and not self._ping_task.done(): self._ping_task.cancel() try: await self._ping_task except: pass self._ping_task = None if self._ws: try: # Unsubscribe before closing unsub_msg = json.dumps({ "action": "unsubscribe", "subscriptions": [{ "topic": "crypto_prices_chainlink", "type": "*", "filters": "" }] }) await self._ws.send(unsub_msg) await self._ws.close(code=1000, reason="Normal shutdown") logger.info("RTDS Chainlink closed gracefully") except Exception as e: logger.warning(f"RTDS close error: {e}") finally: self._ws = None self.state.btc_connected = False # ============================================================================= # DASHBOARD # ============================================================================= class Dashboard: def __init__(self, state: MarketState, stats: TradingStats, config: Any): self.state = state self.stats = stats self.config = config self.calc = IndicatorCalculator() win_rate_path = Path(__file__).parent / config.strategy.win_rate_csv self.winrate_table = WinRateTable(str(win_rate_path)) self.last_signal = "" self.entry_flash = False self.hedge_flash = False def _fmt_price(self, price: float) -> str: if price >= 0.6: return f"[green]{price:.3f}[/green]" elif price <= 0.4: return f"[red]{price:.3f}[/red]" return f"[yellow]{price:.3f}[/yellow]" def _fmt_dev(self, dev: float) -> str: if dev > 5: return f"[bold green]+{dev:.1f}%[/bold green]" elif dev > 0: return f"[green]+{dev:.1f}%[/green]" elif dev < -5: return f"[bold red]{dev:.1f}%[/bold red]" elif dev < 0: return f"[red]{dev:.1f}%[/red]" return f"{dev:+.1f}%" def _fmt_zscore(self, z: float) -> str: if z > 2: return f"[bold magenta]+{z:.2f}[/bold magenta] ⚡" elif z > 1: return f"[magenta]+{z:.2f}[/magenta]" elif z < -2: return f"[bold cyan]{z:.2f}[/bold cyan] ⚡" elif z < -1: return f"[cyan]{z:.2f}[/cyan]" return f"{z:+.2f}" def create_header(self) -> Panel: now = time.time() time_left = max(0, self.state.end_time - now) minutes = int(time_left // 60) seconds = int(time_left % 60) if time_left < 60: timer = f"[bold red]⏱️ {seconds}s[/bold red]" elif time_left < 180: timer = f"[yellow]⏱️ {minutes}:{seconds:02d}[/yellow]" else: timer = f"[green]⏱️ {minutes}:{seconds:02d}[/green]" status = "[green]● LIVE[/green]" if self.state.connected else "[red]○ DISCONNECTED[/red]" if getattr(self.config, "simulation", None) and self.config.simulation.enabled: mode = "[bold yellow]SIMULATION (no real orders)[/bold yellow]" else: mode = "[bold cyan]REAL TRADING[/bold cyan]" header = f"{timer} | {self.state.slug} | {status} | {mode}" im = self.config.market.interval_minutes return Panel(header, title=f"[bold]BTC {im}-Min Live Bot[/bold]") def create_token_panel(self, token: TokenData, label: str) -> Panel: if not token: return Panel("No data", title=label) lines = [] if token.best_ask > 0: lines.append(f"[red]ASK {token.best_ask:.3f}[/red] | {token.best_ask_size:.0f}") else: lines.append(f"[red]ASK ---[/red]") lines.append("─" * 20) lines.append(f"[bold white]LAST {token.last_price:.3f}[/bold white]") if token.best_ask > 0 and token.best_bid > 0: spread = token.best_ask - token.best_bid lines.append(f"[dim]Spread: {spread:.3f}[/dim]") lines.append("─" * 20) if token.best_bid > 0: lines.append(f"[green]BID {token.best_bid:.3f}[/green] | {token.best_bid_size:.0f}") else: lines.append(f"[green]BID ---[/green]") return Panel( "\n".join(lines), title=f"[bold]{label}[/bold] - {self._fmt_price(token.last_price)}", border_style="green" if "Up" in label else "red" ) def _fmt_momentum(self, m: Optional[float]) -> str: if m is None: return "[dim]N/A[/dim]" if m > 0: return f"[green]+{m:.2f}%[/green]" elif m < 0: return f"[red]{m:.2f}%[/red]" return f"[cyan]0.00%[/cyan]" def create_indicators_panel(self, token: TokenData, label: str) -> Panel: if not token or not token.trades: return Panel("Waiting for data...", title=f"{label} Indicators") mom_window = self.config.strategy.momentum_window_sec vwap_window = self.config.strategy.vwap_window_sec vwap = self.calc.calc_vwap(self.calc.get_trades_in_window(token.trades, vwap_window)) deviation = self.calc.calc_deviation(token.last_price, vwap) zscore = self.calc.calc_zscore(token.trades, token.last_price, window=5) momentum = self.calc.calc_momentum(token.trades, token.last_price, window=mom_window) def fmt_vol(v): if v >= 1_000_000: return f"{v/1_000_000:.1f}M" elif v >= 1_000: return f"{v/1_000:.1f}K" return f"{v:.0f}" lines = [ f"VWAP {vwap_window}s: {vwap:.4f}", f"Deviation: {self._fmt_dev(deviation)}", f"Z-Score 5s: {self._fmt_zscore(zscore)}", f"Mom {mom_window}s: {self._fmt_momentum(momentum)}", "", f"Trades: {token.trade_count}", f"Volume: {fmt_vol(token.volume_total)}", f" Buy: [green]{fmt_vol(token.volume_buy)}[/green]", f" Sell: [red]{fmt_vol(token.volume_sell)}[/red]", ] return Panel("\n".join(lines), title=f"{label} Indicators", border_style="blue") def create_strategy_panel(self) -> Panel: if not self.state.up_token or not self.state.down_token: return Panel("Waiting for data...", title="Strategy Signal") up = self.state.up_token down = self.state.down_token vwap_window = self.config.strategy.vwap_window_sec up_vwap = self.calc.calc_vwap(self.calc.get_trades_in_window(up.trades, vwap_window)) down_vwap = self.calc.calc_vwap(self.calc.get_trades_in_window(down.trades, vwap_window)) up_dev = self.calc.calc_deviation(up.last_price, up_vwap) down_dev = self.calc.calc_deviation(down.last_price, down_vwap) mom_window = self.config.strategy.momentum_window_sec up_mom = self.calc.calc_momentum(up.trades, up.last_price, window=mom_window) down_mom = self.calc.calc_momentum(down.trades, down.last_price, window=mom_window) time_left = max(0, self.state.end_time - time.time()) time_minutes = time_left / 60 span = self.config.market.interval_minutes time_bin = int((span - 1) - time_minutes) time_bin = max(0, min(time_bin, span - 1)) if up.last_price > down.last_price: fav_name = "UP" fav_price = up.last_price fav_dev = up_dev fav_mom = up_mom else: fav_name = "DOWN" fav_price = down.last_price fav_dev = down_dev fav_mom = down_mom base_wr = self.winrate_table.get_winrate(fav_price, time_bin, span) wr_str = f"{base_wr:.1f}%" if base_wr else "N/A" min_price = self.config.strategy.min_price max_price = self.config.strategy.max_price min_elapsed = self.config.strategy.min_elapsed_sec min_dev = self.config.strategy.min_deviation_pct max_dev = self.config.strategy.max_deviation_pct no_entry_cutoff = self.config.strategy.no_entry_before_end_sec elapsed_sec = self.config.market.duration_sec - time_left price_ok = min_price <= fav_price <= max_price time_ok = elapsed_sec >= min_elapsed dev_ok = fav_dev > min_dev and fav_dev < max_dev mom_ok = fav_mom is not None and fav_mom > 5 time_cutoff_ok = time_left > no_entry_cutoff signal = "⏳ WAIT" signal_color = "yellow" if not time_cutoff_ok: signal = f"🚫 NO ENTRY (< {no_entry_cutoff}s left)" signal_color = "red" self.last_signal = "" elif price_ok and time_ok and dev_ok and mom_ok: signal = f"✅ BUY {fav_name}" signal_color = "bold green" self.last_signal = f"BUY_{fav_name}" elif fav_price >= 0.70 and time_ok: if not mom_ok: signal = "🟡 ALMOST (need Mom>0%)" elif fav_dev >= max_dev: signal = f"🟡 ALMOST (Dev≥{max_dev}%)" else: signal = "🟡 ALMOST (need dev)" self.last_signal = "" else: self.last_signal = "" if not time_ok: signal = f"⏳ WAIT (elapsed<{min_elapsed}s)" elif not price_ok: signal = f"⏳ WAIT (P not in range)" elif not dev_ok: if fav_dev >= max_dev: signal = f"⏳ WAIT (Dev≥{max_dev}%)" else: signal = f"⏳ WAIT (Dev<{min_dev}%)" elif not mom_ok: signal = f"⏳ WAIT (Mom≤0%)" lines = [ f"Favorite: [{signal_color}]{fav_name} ({fav_price:.3f})[/{signal_color}] — WR: [cyan]{wr_str}[/cyan]", f"Signal: [{signal_color}][bold]{signal}[/bold][/{signal_color}]", "", f"Price: {self._fmt_price(fav_price)} (range: {min_price}-{max_price})", f"Deviation: {self._fmt_dev(fav_dev)} (need {min_dev}%–{max_dev}%)", f"Momentum: {self._fmt_momentum(fav_mom)}", f"Elapsed: {int(elapsed_sec)}s (need ≥{min_elapsed}s) [bin {time_bin}]", "", f"Up: {self._fmt_price(up.last_price)} | Dev: {self._fmt_dev(up_dev)} | Mom: {self._fmt_momentum(up_mom)}", f"Down: {self._fmt_price(down.last_price)} | Dev: {self._fmt_dev(down_dev)} | Mom: {self._fmt_momentum(down_mom)}", ] title = f"[bold]Strategy: P {min_price}-{max_price}, T≥{min_elapsed}s, Dev {min_dev}%-{max_dev}%[/bold]" border = "green" if signal_color == "bold green" else "magenta" return Panel("\n".join(lines), title=title, border_style=border) def create_trading_panel(self) -> Panel: s = self.stats bet = self.config.entry.bet_amount_usd wr_str = f"{s.win_rate:.1f}%" if s.trade_count > 0 else "N/A" stats_line = f"📊 Markets: {s.markets_seen} | Trades: {s.trade_count} | WR: {wr_str}" pnl_color = "green" if s.total_pnl >= 0 else "red" pnl_line = f"💰 PnL: [{pnl_color}]${s.total_pnl:+.2f}[/{pnl_color}]" if s.position: pos = s.position if pos.token_name == "UP" and self.state.up_token: current_price = self.state.up_token.best_bid or self.state.up_token.last_price elif pos.token_name == "DOWN" and self.state.down_token: current_price = self.state.down_token.best_bid or self.state.down_token.last_price else: current_price = pos.entry_price unrealized = (pos.contracts * current_price) - (pos.contracts * pos.entry_price) ur_color = "green" if unrealized >= 0 else "red" hedge_str = " [cyan]🛡️ HEDGED[/cyan]" if pos.hedged else "" flash = "🔔 " if self.entry_flash else "" self.entry_flash = False pos_line = f"{flash}🟢 LONG {pos.token_name} @ {pos.entry_price:.3f} ({pos.contracts} contracts){hedge_str}" ur_line = f" Unrealized: [{ur_color}]${unrealized:+.2f}[/{ur_color}] (price: {current_price:.3f})" # Live drawdown dd_price = max(0, pos.entry_price - pos.min_price_seen) dd_pct = (dd_price / pos.entry_price * 100) if pos.entry_price > 0 else 0 dd_usd = dd_price * pos.contracts if dd_price > 0: ur_line += f"\n Max DD: [red]-${dd_usd:.2f} (-{dd_pct:.1f}%)[/red] (low: {pos.min_price_seen:.3f})" else: pos_line = "⏳ No position (waiting for signal)" ur_line = "" last_trades_lines = [] for trade in s.trades[-3:][::-1]: icon = "✅" if trade.won else "❌" last_trades_lines.append(f" {icon} {trade.token_name} @ {trade.entry_price:.2f} → ${trade.pnl:+.2f}") lines = [stats_line, pnl_line, "", pos_line] if ur_line: lines.append(ur_line) if last_trades_lines: lines.append("") lines.append("Last trades:") lines.extend(last_trades_lines) border = "bold yellow" if self.entry_flash or self.hedge_flash else "cyan" self.hedge_flash = False return Panel("\n".join(lines), title=f"[bold]💰 REAL Trading (${bet:.0f}/trade)[/bold]", border_style=border) def create_btc_price_panel(self) -> Panel: """Panel showing Chainlink BTC/USD price and deviation from market start.""" s = self.state if s.btc_current_price <= 0: status = "[green]● LIVE[/green]" if s.btc_connected else "[red]○ OFF[/red]" return Panel( f"Chainlink {status}\nWaiting for price...", title="[bold]₿ BTC/USD (Chainlink)[/bold]", border_style="dim" ) # Connection status status = "[green]●[/green]" if s.btc_connected else "[red]○[/red]" # Freshness indicator age = time.time() - s.btc_last_update if s.btc_last_update > 0 else 999 if age < 5: fresh = "[green]LIVE[/green]" elif age < 30: fresh = f"[yellow]{int(age)}s ago[/yellow]" else: fresh = f"[red]{int(age)}s ago[/red]" lines = [ f"Price: [bold white]${s.btc_current_price:,.2f}[/bold white] {status} {fresh}", ] if s.btc_anchor_price > 0: dev_abs = s.btc_current_price - s.btc_anchor_price dev_pct = (dev_abs / s.btc_anchor_price) * 100 if s.btc_anchor_price else 0 # Color based on direction if dev_abs > 0: dev_abs_str = f"[green]+${dev_abs:,.2f}[/green]" dev_pct_str = f"[green]+{dev_pct:.3f}%[/green]" elif dev_abs < 0: dev_abs_str = f"[red]-${abs(dev_abs):,.2f}[/red]" dev_pct_str = f"[red]{dev_pct:.3f}%[/red]" else: dev_abs_str = "$0.00" dev_pct_str = "0.000%" lines.append(f"Anchor: [dim]${s.btc_anchor_price:,.2f}[/dim]") lines.append(f"Deviation: {dev_abs_str} ({dev_pct_str})") else: lines.append("[dim]Anchor: waiting for market start...[/dim]") return Panel( "\n".join(lines), title="[bold]₿ BTC/USD (Chainlink)[/bold]", border_style="yellow" ) def render(self) -> Layout: layout = Layout() layout.split_column( Layout(self.create_header(), name="header", size=3), Layout(name="body"), Layout(name="footer", size=16), Layout(self.create_btc_price_panel(), name="btc_price", size=6) ) layout["body"].split_row( Layout(name="left"), Layout(name="right") ) layout["left"].split_column( Layout(self.create_token_panel(self.state.up_token, "⬆️ UP"), name="up_book"), Layout(self.create_indicators_panel(self.state.up_token, "UP"), name="up_ind") ) layout["right"].split_column( Layout(self.create_token_panel(self.state.down_token, "⬇️ DOWN"), name="down_book"), Layout(self.create_indicators_panel(self.state.down_token, "DOWN"), name="down_ind") ) layout["footer"].split_row( Layout(name="strategy"), Layout(name="trading") ) layout["strategy"].update(self.create_strategy_panel()) layout["trading"].update(self.create_trading_panel()) return layout def build_web_snapshot(self) -> dict: """Plain dict for the HTTP dashboard (same numbers as terminal panels; no Rich markup).""" now = time.time() time_left = max(0.0, self.state.end_time - now) sim = bool(getattr(self.config, "simulation", None) and self.config.simulation.enabled) header = { "slug": self.state.slug or "—", "time_left_sec": time_left, "elapsed_sec": max(0.0, self.config.market.duration_sec - time_left), "ws_connected": bool(self.state.connected), "simulation": sim, "interval_minutes": self.config.market.interval_minutes, } def token_block(token: Optional[TokenData]) -> Optional[dict]: if not token: return None book = { "best_bid": token.best_bid, "best_bid_size": token.best_bid_size, "best_ask": token.best_ask, "best_ask_size": token.best_ask_size, "last_price": token.last_price, "trade_count": token.trade_count, "volume_total": token.volume_total, "volume_buy": token.volume_buy, "volume_sell": token.volume_sell, } ind = None if token.trades: vw = self.config.strategy.vwap_window_sec mw = self.config.strategy.momentum_window_sec vwap = self.calc.calc_vwap(self.calc.get_trades_in_window(token.trades, vw)) ind = { "vwap_window_sec": vw, "vwap": vwap, "deviation_pct": self.calc.calc_deviation(token.last_price, vwap), "zscore": self.calc.calc_zscore(token.trades, token.last_price, window=5), "momentum_window_sec": mw, "momentum_pct": self.calc.calc_momentum(token.trades, token.last_price, window=mw), } return {"book": book, "indicators": ind} strategy: dict = { "signal_text": "Waiting for data...", "favorite": None, "win_rate_str": None, "checks": {}, "up_line": "", "down_line": "", } if self.state.up_token and self.state.down_token: up = self.state.up_token down = self.state.down_token vwap_window = self.config.strategy.vwap_window_sec up_vwap = self.calc.calc_vwap(self.calc.get_trades_in_window(up.trades, vwap_window)) down_vwap = self.calc.calc_vwap(self.calc.get_trades_in_window(down.trades, vwap_window)) up_dev = self.calc.calc_deviation(up.last_price, up_vwap) down_dev = self.calc.calc_deviation(down.last_price, down_vwap) mom_window = self.config.strategy.momentum_window_sec up_mom = self.calc.calc_momentum(up.trades, up.last_price, window=mom_window) down_mom = self.calc.calc_momentum(down.trades, down.last_price, window=mom_window) time_minutes = time_left / 60.0 span = self.config.market.interval_minutes time_bin = int((span - 1) - time_minutes) time_bin = max(0, min(time_bin, span - 1)) if up.last_price > down.last_price: fav_name = "UP" fav_price = up.last_price fav_dev = up_dev fav_mom = up_mom else: fav_name = "DOWN" fav_price = down.last_price fav_dev = down_dev fav_mom = down_mom base_wr = self.winrate_table.get_winrate(fav_price, time_bin, span) wr_str = f"{base_wr:.1f}%" if base_wr else None min_price = self.config.strategy.min_price max_price = self.config.strategy.max_price min_elapsed = self.config.strategy.min_elapsed_sec min_dev = self.config.strategy.min_deviation_pct max_dev = self.config.strategy.max_deviation_pct no_entry_cutoff = self.config.strategy.no_entry_before_end_sec elapsed_sec = self.config.market.duration_sec - time_left price_ok = min_price <= fav_price <= max_price time_ok = elapsed_sec >= min_elapsed dev_ok = fav_dev > min_dev and fav_dev < max_dev mom_ok = fav_mom is not None and fav_mom > 5 time_cutoff_ok = time_left > no_entry_cutoff if not time_cutoff_ok: signal = f"🚫 NO ENTRY (< {no_entry_cutoff}s left)" elif price_ok and time_ok and dev_ok and mom_ok: signal = f"✅ BUY {fav_name}" elif fav_price >= 0.70 and time_ok: if not mom_ok: signal = "🟡 ALMOST (need Mom>0%)" elif fav_dev >= max_dev: signal = f"🟡 ALMOST (Dev≥{max_dev}%)" else: signal = "🟡 ALMOST (need dev)" elif not time_ok: signal = f"⏳ WAIT (elapsed<{min_elapsed}s)" elif not price_ok: signal = "⏳ WAIT (P not in range)" elif not dev_ok: signal = ( f"⏳ WAIT (Dev≥{max_dev}%)" if fav_dev >= max_dev else f"⏳ WAIT (Dev<{min_dev}%)" ) elif not mom_ok: signal = "⏳ WAIT (Mom≤0%)" else: signal = "⏳ WAIT" strategy = { "signal_text": signal, "favorite": f"{fav_name} ({fav_price:.3f})", "win_rate_str": wr_str, "time_bin": time_bin, "checks": { "price": price_ok, "time": time_ok, "dev": dev_ok, "mom": mom_ok, "time_cutoff": time_cutoff_ok, }, "up_line": f"{up.last_price:.3f} | Dev {up_dev:+.1f}% | Mom {up_mom if up_mom is not None else 0:.2f}%", "down_line": f"{down.last_price:.3f} | Dev {down_dev:+.1f}% | Mom {down_mom if down_mom is not None else 0:.2f}%", } s = self.state btc_age = time.time() - s.btc_last_update if s.btc_last_update > 0 else None btc_block: dict = { "btc_current_price": s.btc_current_price, "btc_anchor_price": s.btc_anchor_price, "btc_connected": s.btc_connected, "fresh_sec": btc_age, "deviation_line": "", } if s.btc_current_price > 0 and s.btc_anchor_price > 0: dev_abs = s.btc_current_price - s.btc_anchor_price dev_pct = (dev_abs / s.btc_anchor_price) * 100 if s.btc_anchor_price else 0.0 btc_block["deviation_line"] = f"${dev_abs:+,.2f} ({dev_pct:+.3f}%)" st = self.stats bet = self.config.entry.bet_amount_usd wr_str = f"{st.win_rate:.1f}%" if st.trade_count > 0 else None trading: dict = { "bet_usd": bet, "markets_seen": st.markets_seen, "trade_count": st.trade_count, "win_rate_str": wr_str, "total_pnl": st.total_pnl, "position": None, "recent_trades": [], } if st.position: pos = st.position if pos.token_name == "UP" and self.state.up_token: current_price = self.state.up_token.best_bid or self.state.up_token.last_price elif pos.token_name == "DOWN" and self.state.down_token: current_price = self.state.down_token.best_bid or self.state.down_token.last_price else: current_price = pos.entry_price unrealized = (pos.contracts * current_price) - (pos.contracts * pos.entry_price) dd_price = max(0.0, pos.entry_price - pos.min_price_seen) dd_pct = (dd_price / pos.entry_price * 100) if pos.entry_price > 0 else 0.0 dd_usd = dd_price * pos.contracts trading["position"] = { "token_name": pos.token_name, "entry_price": pos.entry_price, "contracts": pos.contracts, "hedged": pos.hedged, "current_price": current_price, "unrealized_pnl": unrealized, "max_dd_usd": dd_usd, "max_dd_pct": dd_pct, "min_price_seen": pos.min_price_seen, } for trade in st.trades[-5:][::-1]: icon = "✅" if trade.won else "❌" trading["recent_trades"].append({ "line": f"{icon} {trade.token_name} @ {trade.entry_price:.2f} → ${trade.pnl:+.2f}", }) return { "ts": now, "header": header, "strategy": strategy, "up": token_block(self.state.up_token), "down": token_block(self.state.down_token), "btc": btc_block, "trading": trading, "last_signal": self.last_signal, } # ============================================================================= # MAIN BOT # ============================================================================= class LiveTradingBot: def __init__(self): self.config = None self.state = MarketState() self.stats = TradingStats() self.dashboard: Dashboard = None # Trading components self.executor: OrderExecutor = None self.hedge_mgr: HedgeManager = None self.redeemer: Optional[AsyncAutoRedeemer] = None self.telegram: TelegramNotifier = None self.user_ws = None self._user_ws_task: Optional[asyncio.Task] = None # WebSocket self.ws_client: WebSocketClient = None # Chainlink BTC price self.chainlink_client: ChainlinkPriceClient = None self._chainlink_task: Optional[asyncio.Task] = None # Control self.running = False self.tasks = [] self._sim_history: Optional[SimulationHistoryLogger] = None self._web_snapshot_holder: Optional[WebSnapshotHolder] = None async def initialize(self) -> bool: # Load config self.config = load_config() errors = validate_config(self.config) if errors: for err in errors: console.print(f"[red]Config error: {err}[/red]") return False im = self.config.market.interval_minutes console.print(f"[bold cyan]🚀 BTC {im}-Min Live Trading Bot[/bold cyan]") if self.config.simulation.enabled: console.print("[bold yellow] SIMULATION MODE — no CLOB orders, no redeemer[/bold yellow]\n") else: console.print("[bold cyan] Real Trading + Dashboard[/bold cyan]\n") console.print(f"[green]✓ Market: BTC up/down {im}m (slug btc-updown-{im}m-*)[/green]") console.print(f"[green]✓ Config: P {self.config.strategy.min_price}-{self.config.strategy.max_price}, " f"T≥{self.config.strategy.min_elapsed_sec}s, " f"Dev {self.config.strategy.min_deviation_pct}%-{self.config.strategy.max_deviation_pct}%[/green]") console.print(f"[green]✓ Bet: ${self.config.entry.bet_amount_usd}, " f"Hedge: {'ON' if self.config.hedge.enabled else 'OFF'}[/green]") if self.config.simulation.enabled: if self.config.simulation.separate_trading_log: self.stats = TradingStats(self.config.simulation.trading_log_path) console.print( f"[yellow]✓ Simulation stats: {self.config.simulation.trading_log_path}[/yellow]" ) else: console.print("[yellow]✓ Simulation stats: same file as live (trading_log.json)[/yellow]") # Initialize trading components console.print("[yellow]Initializing trading components...[/yellow]") # Telegram self.telegram = TelegramNotifier( bot_token=self.config.telegram.bot_token, chat_id=self.config.telegram.chat_id, enabled=self.config.telegram.enabled ) sim = self.config.simulation.enabled if sim: self.user_ws = None self._user_ws_task = None # Dummy credentials — CLOB is never initialized in simulation pk = self.config.polymarket.private_key or "0x0000000000000000000000000000000000000000000000000000000000000001" ak = self.config.polymarket.api_key or "sim" sec = self.config.polymarket.api_secret or "sim" ph = self.config.polymarket.api_passphrase or "sim" self.executor = OrderExecutor( private_key=pk, api_key=ak, api_secret=sec, api_passphrase=ph, clob_host=self.config.polymarket.clob_host, chain_id=self.config.polymarket.chain_id, signature_type=self.config.polymarket.signature_type, funder_address=self.config.polymarket.funder_address or None, user_ws=None, simulation_mode=True, ) console.print("[green]✓ Order executor: simulation (no CLOB)[/green]") else: # Executor first — may replace stale env L2 keys with derived creds self._user_ws_task = None self.user_ws = None self.executor = OrderExecutor( private_key=self.config.polymarket.private_key, api_key=self.config.polymarket.api_key, api_secret=self.config.polymarket.api_secret, api_passphrase=self.config.polymarket.api_passphrase, clob_host=self.config.polymarket.clob_host, chain_id=self.config.polymarket.chain_id, signature_type=self.config.polymarket.signature_type, funder_address=self.config.polymarket.funder_address or None, user_ws=None, simulation_mode=False, ) if not await self.executor.initialize(): console.print("[red]Failed to initialize order executor[/red]") return False # User WS must use the same L2 creds as the CLOB client self.user_ws = UserWebSocket( api_key=self.executor.api_key, api_secret=self.executor.api_secret, api_passphrase=self.executor.api_passphrase, ) self.executor.user_ws = self.user_ws console.print("[yellow]Starting User WebSocket for order tracking...[/yellow]") self._user_ws_task = asyncio.create_task(self.user_ws.connect()) await asyncio.sleep(2) if self.user_ws.connected: console.print("[green]User WebSocket connected - order tracking active[/green]") logger.info("User WebSocket connected for order fill tracking") else: console.print("[yellow]User WebSocket connecting... (will retry in background)[/yellow]") logger.warning("User WebSocket not yet connected") # Hedge manager hedge_config = HedgeManagerConfig( enabled=self.config.hedge.enabled, hedge_price=self.config.hedge.hedge_price, order_type=self.config.hedge.order_type, max_retries=self.config.hedge.max_retries, retry_delay_ms=self.config.hedge.retry_delay_ms, simulation_mode=sim, ) self.hedge_mgr = HedgeManager(self.executor, hedge_config) # Auto redeemer (live only) if sim: self.redeemer = None console.print("[yellow]✓ Auto-redeemer: disabled in simulation[/yellow]") else: self.redeemer = AsyncAutoRedeemer( private_key=self.config.polymarket.private_key, rpc_url=self.config.polymarket.rpc_url, funder_address=self.config.polymarket.funder_address or None, signature_type=self.config.polymarket.signature_type, interval_seconds=self.config.redeem.interval_seconds, telegram_notifier=self.telegram ) if sim: jl = (self.config.simulation.history_jsonl_path or "").strip() self._sim_history = SimulationHistoryLogger( csv_path=self.config.simulation.history_csv_path, jsonl_path=jl if jl else None, summary_path=self.config.simulation.history_summary_path, ) if self.stats.trades: self._sim_history.write_summary( [t.__dict__ for t in self.stats.trades], self.stats.summary_dict(), ) csv_p = self.config.simulation.history_csv_path or "(disabled)" sum_p = self.config.simulation.history_summary_path or "(disabled)" jl_p = jl or "(disabled)" console.print( f"[green]✓ Simulation analytics: CSV={csv_p} | JSONL={jl_p} | summary={sum_p}[/green]" ) else: self._sim_history = None # Chainlink BTC price client self.chainlink_client = ChainlinkPriceClient( self.state, self.config.market.duration_sec ) self._chainlink_task = asyncio.create_task(self.chainlink_client.connect()) console.print("[green]✓ Chainlink BTC/USD price feed starting...[/green]") # Dashboard self.dashboard = Dashboard(self.state, self.stats, self.config) wd = self.config.web_dashboard if wd.enabled: self._web_snapshot_holder = WebSnapshotHolder() ok = start_web_dashboard(wd.host, wd.port, self._web_snapshot_holder) # 0.0.0.0 is not a valid host in a browser URL; use loopback for display. if wd.host in ("0.0.0.0", ""): open_url = f"http://127.0.0.1:{wd.port}/" elif wd.host in ("::", "[::]"): open_url = f"http://[::1]:{wd.port}/" else: open_url = f"http://{wd.host}:{wd.port}/" if ok: console.print(f"[green]✓ Web dashboard:[/green] [bold]{open_url}[/bold]") console.print( "[dim] Use http:// not https://. On Windows, if the page fails in your browser, " "open this exact URL (avoid typing only “localhost”, which may use IPv6).[/dim]" ) else: console.print( f"[yellow]⚠ Web dashboard did not start on port {wd.port} " f"(in use by another app, or bind failed). Check logs.[/yellow]" ) console.print("[green]✓ All components initialized[/green]\n") return True async def find_market(self) -> bool: d = self.config.market.duration_sec sfx = self.config.market.slug_infix console.print(f"[yellow]Searching for active BTC {self.config.market.interval_minutes}-min market...[/yellow]") async with _aiohttp_session() as session: now = int(time.time()) current_window = (now // d) * d for offset in [0, d, -d, 2 * d]: target_ts = current_window + offset expected_slug = f"btc-updown-{sfx}-{target_ts}" try: async with session.get( f"{GAMMA_API}/markets?slug={expected_slug}", proxy=_PROXY_URL or None, ) as resp: if resp.status == 200: markets = await resp.json() if markets: market = markets[0] returned_slug = market.get("slug", "") # CRITICAL: Verify API returned the market we asked for if returned_slug != expected_slug: logger.warning(f"API slug mismatch! Asked for {expected_slug}, got {returned_slug}") continue if not market.get("closed", True): return await self._setup_market(market) except Exception as e: logger.warning(f"Error finding/setup market {expected_slug}: {e}", exc_info=True) console.print(f"[red]Setup failed for {expected_slug}: {e}[/red]") continue return False async def _setup_market(self, market: dict) -> bool: console.print(f"[green]Found: {market.get('slug')}[/green]") outcomes = market.get("outcomes", []) tokens = market.get("clobTokenIds", []) if isinstance(outcomes, str): outcomes = json.loads(outcomes) if isinstance(tokens, str): tokens = json.loads(tokens) up_token_id = None down_token_id = None # Use exact index lookup like reference implementation try: up_index = outcomes.index("Up") if "Up" in outcomes else None down_index = outcomes.index("Down") if "Down" in outcomes else None if up_index is not None and up_index < len(tokens): up_token_id = tokens[up_index] if down_index is not None and down_index < len(tokens): down_token_id = tokens[down_index] except (ValueError, IndexError): pass # Fallback to contains-based matching if not up_token_id or not down_token_id: for i, outcome in enumerate(outcomes): if i < len(tokens): outcome_lower = str(outcome).lower() if not up_token_id and "up" in outcome_lower: up_token_id = tokens[i] elif not down_token_id and "down" in outcome_lower: down_token_id = tokens[i] # Last resort fallback if not up_token_id and len(tokens) >= 1: up_token_id = tokens[0] if not down_token_id and len(tokens) >= 2: down_token_id = tokens[1] if not up_token_id or not down_token_id: logger.warning("Market missing Up/Down token IDs: outcomes=%s tokens=%s", outcomes, tokens) return False up_token_id = str(up_token_id) down_token_id = str(down_token_id) end_str = market.get("end_date_iso") or market.get("endDate", "") try: end_time = datetime.fromisoformat(end_str.replace("Z", "+00:00")) end_timestamp = end_time.timestamp() except Exception: end_timestamp = time.time() + self.config.market.duration_sec slug = market.get("slug", "") self.state.market_id = market.get("id", "") self.state.condition_id = market.get("conditionId", "") self.state.slug = slug self.state.end_time = end_timestamp self.state.up_token = TokenData(token_id=up_token_id, name="Up") self.state.down_token = TokenData(token_id=down_token_id, name="Down") self.state.connected = False # Log token assignments for debugging logger.info(f"Market tokens assigned:") logger.info(f" Slug: {slug}") logger.info(f" End Time: {end_str} (timestamp: {end_timestamp})") logger.info(f" UP token: {up_token_id[:40]}...") logger.info(f" DOWN token: {down_token_id[:40]}...") self.stats.new_market(self.state.slug) if self.hedge_mgr: self.hedge_mgr.clear() # Reset hedge state for new market if self.user_ws: self.user_ws.clear_token_fills() # Reset WS fill buffer for new market # BTC anchor is now auto-managed by ChainlinkPriceClient # It detects interval boundaries from Chainlink timestamps independently return True def _simulation_log_entry( self, token_name: str, avg_price: float, contracts: int, total_cost: float, ) -> None: if not self._sim_history or not self.config.simulation.enabled: return pos = self.stats.position hedged = bool(pos and pos.hedged) self._sim_history.log_open( market_slug=self.state.slug, token_name=token_name, contracts=contracts, avg_price=avg_price, total_cost=total_cost, cumulative_realized_pnl=self.stats.total_pnl, hedged=hedged, trade_number=len(self.stats.trades) + 1, ) signal_logger.info( f" [SIM] History OPEN logged | realized PnL before exit: ${self.stats.total_pnl:+.4f}" ) def _simulation_log_close(self, record: TradeRecord, hedged_was: bool) -> None: if not self._sim_history or not self.config.simulation.enabled: return n = len(self.stats.trades) self._sim_history.log_close( record, cumulative_pnl=self.stats.total_pnl, total_closed=n, win_rate_pct=self.stats.win_rate, hedged=hedged_was, ) self._sim_history.write_summary( [t.__dict__ for t in self.stats.trades], self.stats.summary_dict(), ) s = self.stats.summary_dict() signal_logger.info( f" [SIM] History CLOSE logged | trade PnL ${record.pnl:+.4f} | " f"cumulative ${s['total_pnl_usd']:+.4f} | WR {s['win_rate_pct']:.2f}% ({n} closed)" ) async def execute_entry(self, side: str): """Execute entry order (live CLOB or simulation).""" if not self.stats.can_enter(): signal_logger.info(f"SIGNAL IGNORED: {side} - cannot enter (already in position)") return # Defensive time cutoff check (race condition guard) time_left = max(0, self.state.end_time - time.time()) no_entry_cutoff = self.config.strategy.no_entry_before_end_sec if time_left < no_entry_cutoff: signal_logger.info( f"SIGNAL BLOCKED: {side} - too close to market end " f"({time_left:.0f}s left < {no_entry_cutoff}s cutoff)" ) logger.warning(f"Entry blocked: {time_left:.0f}s left < {no_entry_cutoff}s cutoff") return if side == "BUY_UP": token = self.state.up_token token_name = "UP" opposite_token = self.state.down_token else: token = self.state.down_token token_name = "DOWN" opposite_token = self.state.up_token if not token or not opposite_token: signal_logger.warning(f"SIGNAL IGNORED: {side} - token data missing") return # Log full signal snapshot signal_logger.info("=" * 60) signal_logger.info( "TRADE SIGNAL TRIGGERED (SIMULATION)" if self.config.simulation.enabled else "TRADE SIGNAL TRIGGERED" ) signal_logger.info(f" Time: {datetime.now().isoformat()}") signal_logger.info(f" Market: {self.state.slug}") signal_logger.info(f" Signal: {side}") signal_logger.info(f" Token: {token_name}") time_left = max(0, self.state.end_time - time.time()) dur = self.config.market.duration_sec span = self.config.market.interval_minutes elapsed_sec = dur - time_left time_bin = int((span - 1) - time_left / 60) time_bin = max(0, min(time_bin, span - 1)) signal_logger.info(f" Elapsed: {elapsed_sec:.0f}s | Remaining: {time_left:.0f}s | Bin: {time_bin}") # Calculate all indicators for both tokens calc = self.dashboard.calc vwap_window = self.config.strategy.vwap_window_sec mom_window = self.config.strategy.momentum_window_sec for label, tk in [("UP", self.state.up_token), ("DOWN", self.state.down_token)]: if not tk: signal_logger.info(f" {label}: no data") continue vwap = calc.calc_vwap(calc.get_trades_in_window(tk.trades, vwap_window)) dev = calc.calc_deviation(tk.last_price, vwap) zscore = calc.calc_zscore(tk.trades, tk.last_price, window=5) mom = calc.calc_momentum(tk.trades, tk.last_price, window=mom_window) mom_str = f"{mom:+.2f}%" if mom is not None else "N/A" signal_logger.info(f" --- {label} ---") signal_logger.info(f" Price: LAST={tk.last_price:.4f} BID={tk.best_bid:.4f} ASK={tk.best_ask:.4f}") signal_logger.info(f" VWAP {vwap_window}s: {vwap:.4f} | Deviation: {dev:+.2f}%") signal_logger.info(f" Z-Score 5s: {zscore:+.2f} | Momentum {mom_window}s: {mom_str}") signal_logger.info(f" Trades: {tk.trade_count} | Volume: {tk.volume_total:.0f}") signal_logger.info(f" Buy Vol: {tk.volume_buy:.0f} | Sell Vol: {tk.volume_sell:.0f}") # Win rate up = self.state.up_token down = self.state.down_token if up and down: fav_price = up.last_price if up.last_price > down.last_price else down.last_price wr = self.dashboard.winrate_table.get_winrate( fav_price, time_bin, self.config.market.interval_minutes ) signal_logger.info(f" Win Rate: {wr:.1f}%" if wr else " Win Rate: N/A") # Strategy conditions snapshot signal_logger.info(f" Config: min_price={self.config.strategy.min_price}, " f"max_price={self.config.strategy.max_price}, " f"min_elapsed={self.config.strategy.min_elapsed_sec}s, " f"dev_range={self.config.strategy.min_deviation_pct}%-{self.config.strategy.max_deviation_pct}%, " f"no_entry_cutoff={self.config.strategy.no_entry_before_end_sec}s") # Chainlink BTC/USD s = self.state if s.btc_current_price > 0 and s.btc_anchor_price > 0: btc_dev_abs = s.btc_current_price - s.btc_anchor_price btc_dev_pct = (btc_dev_abs / s.btc_anchor_price) * 100 signal_logger.info(f" BTC Chainlink: ${s.btc_current_price:,.2f} (anchor: ${s.btc_anchor_price:,.2f})") signal_logger.info(f" BTC Deviation: ${btc_dev_abs:+,.2f} ({btc_dev_pct:+.4f}%)") else: signal_logger.info(f" BTC Chainlink: N/A") signal_logger.info("=" * 60) logger.info(f"Executing entry: {token_name}") exec_config = ExecutionConfig( bet_amount_usd=self.config.entry.bet_amount_usd, price_offset=self.config.entry.price_offset, max_retries=self.config.entry.max_retries, retry_delay_ms=self.config.entry.retry_delay_ms, fill_timeout_ms=self.config.entry.fill_timeout_ms, min_contracts=self.config.entry.min_contracts, min_order_usd=self.config.entry.min_order_usd, max_entry_price=self.config.entry.max_entry_price ) result = await self.executor.execute_entry( token_id=token.token_id, config=exec_config, websocket_price=token.best_ask # Для ПОКУПКИ нужен ASK! Мы платим продавцам. ) if result.success: self.stats.record_entry( token_name=token_name, token_id=token.token_id, opposite_token_id=opposite_token.token_id, price=result.avg_price, contracts=result.contracts_filled, market_slug=self.state.slug ) self._simulation_log_entry( token_name, result.avg_price, result.contracts_filled, result.total_cost ) self.dashboard.entry_flash = True # Log successful entry signal_logger.info( "ENTRY EXECUTED SUCCESSFULLY (SIMULATED)" if self.config.simulation.enabled else "ENTRY EXECUTED SUCCESSFULLY" ) signal_logger.info(f" Token: {token_name}") signal_logger.info(f" Contracts: {result.contracts_filled}") signal_logger.info(f" Avg Price: {result.avg_price:.4f}") signal_logger.info(f" Total Cost: ${result.total_cost:.2f}") signal_logger.info(f" Attempts: {result.attempts}") signal_logger.info("-" * 40) await self.telegram.notify_entry( side=token_name, price=result.avg_price, contracts=result.contracts_filled, cost=result.total_cost, retries=result.attempts, interval_minutes=self.config.market.interval_minutes, simulation=self.config.simulation.enabled, ) logger.info(f"Entry complete: {result.contracts_filled} @ {result.avg_price:.3f}") # === PLACE GTD HEDGE ORDER === if self.config.hedge.enabled: self.hedge_mgr.set_position( opposite_token_id=opposite_token.token_id, contracts=result.contracts_filled ) hedge_result = await self.hedge_mgr.place_gtd_hedge() if hedge_result.success: self.dashboard.hedge_flash = True hedge_cost = hedge_result.contracts * hedge_result.price hsim = "🎮 [模拟模式]\n" if self.config.simulation.enabled else "" await self.telegram.send_message( f"{hsim}" f"🛡️ 对冲单已挂 (GTD)\n" f"📦 {hedge_result.contracts}张 @ ${hedge_result.price:.4f}\n" f"💰 花费: ${hedge_cost:.2f}\n" f"🔖 订单ID: {hedge_result.order_id[:20]}...\n" f"📋 状态: 挂单中 (被动成交)\n" f"🔄 重试: {hedge_result.attempts}次" ) # Register WebSocket handler for hedge fills self._register_hedge_ws_handler() logger.info(f"GTD hedge placed: {hedge_result.contracts} @ ${hedge_result.price}") else: await self.telegram.send_message( f"⚠️ 对冲单失败\n" f"❌ {hedge_result.error}\n" f"🔄 重试: {hedge_result.attempts}次" ) logger.error(f"Hedge failed: {hedge_result.error}") else: signal_logger.error(f"ENTRY FAILED: {result.error}") signal_logger.info(f" Attempts: {result.attempts}") signal_logger.info("-" * 40) logger.error(f"Entry failed: {result.error}") # ============================================================ # КРИТИЧНО: Если был таймаут - НЕ делаем retry (двойная покупка!) # Вместо этого проверяем через WebSocket - может ордер исполнился # ============================================================ if result.was_timeout: signal_logger.error("🛑 TIMEOUT: Checking WebSocket for fills...") logger.warning("Timeout detected — starting WS recovery") recovered = False if self.user_ws and self.user_ws.connected: recovery_timeout = self.config.entry.ws_recovery_timeout_sec signal_logger.info(f" Checking WS for fills on {token.token_id[:30]}...") signal_logger.info(f" Recovery timeout: {recovery_timeout}s") fill_data = await self.user_ws.wait_for_fills_on_token( token_id=token.token_id, timeout=recovery_timeout ) if fill_data and fill_data["contracts"] > 0: # ============================== # RECOVERY: Order DID execute! # ============================== recovered = True rec_contracts = fill_data["contracts"] rec_price = fill_data["avg_price"] rec_cost = fill_data["total_cost"] signal_logger.info("=" * 60) signal_logger.info("✅ TIMEOUT RECOVERY: Position found via WebSocket!") signal_logger.info(f" Contracts: {rec_contracts}") signal_logger.info(f" Avg Price: {rec_price:.4f}") signal_logger.info(f" Total Cost: ${rec_cost:.2f}") signal_logger.info(f" Fills: {len(fill_data['fills'])}") signal_logger.info("=" * 60) logger.info(f"Timeout recovery: {rec_contracts} @ {rec_price:.4f}") # Record position as if entry succeeded self.stats.record_entry( token_name=token_name, token_id=token.token_id, opposite_token_id=opposite_token.token_id, price=rec_price, contracts=rec_contracts, market_slug=self.state.slug ) self._simulation_log_entry( token_name, rec_price, rec_contracts, rec_cost ) self.dashboard.entry_flash = True await self.telegram.send_message( f"🔄 超时恢复!\n" f"HTTP超时但订单已成交\n" f"📊 {token_name} {rec_contracts}张 @ ${rec_price:.4f}\n" f"💰 花费: ${rec_cost:.2f}\n" f"市场: {self.state.slug}" ) await self.telegram.notify_entry( side=token_name, price=rec_price, contracts=rec_contracts, cost=rec_cost, retries=result.attempts, interval_minutes=self.config.market.interval_minutes, simulation=self.config.simulation.enabled, ) # Place hedge (normal flow) if self.config.hedge.enabled: self.hedge_mgr.set_position( opposite_token_id=opposite_token.token_id, contracts=rec_contracts ) hedge_result = await self.hedge_mgr.place_gtd_hedge() if hedge_result.success: self.dashboard.hedge_flash = True hedge_cost = hedge_result.contracts * hedge_result.price hsim2 = "🎮 [模拟模式]\n" if self.config.simulation.enabled else "" await self.telegram.send_message( f"{hsim2}" f"🛡️ 对冲单已挂 (GTD)\n" f"📦 {hedge_result.contracts}张 @ ${hedge_result.price:.4f}\n" f"💰 花费: ${hedge_cost:.2f}\n" f"🔖 订单ID: {hedge_result.order_id[:20]}...\n" f"📋 状态: 挂单中 (被动成交)\n" f"🔄 重试: {hedge_result.attempts}次" ) self._register_hedge_ws_handler() logger.info(f"GTD hedge placed after recovery: {hedge_result.contracts} @ ${hedge_result.price}") else: await self.telegram.send_message( f"⚠️ 对冲失败 (恢复后)\n" f"❌ {hedge_result.error}" ) else: signal_logger.info(" WS recovery: no fills found") else: signal_logger.warning(" WS not connected — cannot recover") if not recovered: # No fill found — block entry (original behavior) self.stats.block_entry("Network timeout - no fill detected via WS. Blocking re-entry.") signal_logger.error("🛑 ENTRY BLOCKED: Timeout + no WS fill detected") await self.telegram.send_message( f"⚠️ 超时 — 未检测到成交\n" f"HTTP超时后订单状态未知\n" f"WebSocket恢复未找到成交\n" f"禁止重新入场\n" f"市场: {self.state.slug}" ) def _register_hedge_ws_handler(self): """Register WebSocket handler to track hedge order fills.""" if not self.user_ws: logger.warning("User WebSocket not available for hedge tracking") return hedge_order_id = self.hedge_mgr.hedge_order_id if not hedge_order_id: return original_on_trade = self.user_ws._on_trade async def _hedge_trade_handler(data: dict): """Handle trade events and check for hedge fills.""" # Call original handler first if original_on_trade: await original_on_trade(data) # Check if this trade is for our hedge order # GTD orders are maker orders, so check maker_order_id trade_order_id = data.get("maker_order_id", "") or data.get("taker_order_id", "") status = data.get("status", "") if trade_order_id == hedge_order_id and status == "MATCHED": size = int(float(data.get("size", 0))) price = float(data.get("price", 0)) self.hedge_mgr.on_hedge_fill(size, price) pos = self._position if hasattr(self, '_position') else None filled = self.hedge_mgr._position.hedge_contracts_filled if self.hedge_mgr._position else 0 total = self.hedge_mgr._position.contracts if self.hedge_mgr._position else 0 if self.hedge_mgr.is_hedged: # Fully filled self.stats.record_hedge(filled, price) self.dashboard.hedge_flash = True await self.telegram.send_message( f"✅ 对冲已全部成交!\n" f"📦 {filled}张 @ ${price:.4f}\n" f"🛡️ 仓位已完全保护" ) logger.info(f"Hedge fully filled: {filled} contracts") else: # Partial fill await self.telegram.send_message( f"🛡️ 对冲部分成交\n" f"📦 +{size}张 @ ${price:.4f}\n" f"📊 进度: {filled}/{total}" ) logger.info(f"Hedge partial fill: +{size}, total {filled}/{total}") self.user_ws._on_trade = _hedge_trade_handler logger.info(f"Registered hedge fill handler for order {hedge_order_id[:20]}...") async def check_market_end(self): """Close position at market end.""" pos = self.stats.position if not pos: return time_left = self.state.end_time - time.time() if time_left <= 10: # 10 seconds before end hedged_was = pos.hedged if pos.token_name == "UP" and self.state.up_token: final_price = self.state.up_token.last_price elif pos.token_name == "DOWN" and self.state.down_token: final_price = self.state.down_token.last_price else: final_price = 0.5 # Log market end details signal_logger.info("=" * 60) signal_logger.info("MARKET END - POSITION CLOSING") signal_logger.info(f" Time: {datetime.now().isoformat()}") signal_logger.info(f" Market: {self.state.slug}") signal_logger.info(f" Position: {pos.token_name}") signal_logger.info(f" Entry Price: {pos.entry_price:.4f}") signal_logger.info(f" Final Price: {final_price:.4f}") signal_logger.info(f" Contracts: {pos.contracts}") signal_logger.info(f" Hedged: {pos.hedged}") record = self.stats.close_position(final_price) if record: self._simulation_log_close(record, hedged_was) status = "✅ WIN" if record.won else "❌ LOSS" signal_logger.info(f" Result: {'WIN' if record.won else 'LOSS'}") signal_logger.info(f" P&L: ${record.pnl:+.2f}") signal_logger.info(f" Max Drawdown: -{record.max_drawdown_abs:.4f} (-{record.max_drawdown_pct:.2f}%)") dd_usd = record.max_drawdown_abs * record.contracts signal_logger.info(f" Max DD ($): -${dd_usd:.2f} (min price: {record.entry_price - record.max_drawdown_abs:.4f})") signal_logger.info(f" Total Trades: {len(self.stats.trades)}") signal_logger.info(f" Session Stats: W={sum(1 for r in self.stats.trades if r.won)} / L={sum(1 for r in self.stats.trades if not r.won)}") signal_logger.info(f" Total P&L: ${sum(r.pnl for r in self.stats.trades):+.2f}") signal_logger.info("=" * 60) logger.info(f"Position closed: {status}, PnL: ${record.pnl:+.2f}") # Send Telegram notification emoji = "🎯" if record.won else "❌" pnl_sign = "+" if record.pnl > 0 else "" await self.telegram.notify_market_end( winner=pos.token_name, pnl=record.pnl, total_pnl=sum(r.pnl for r in self.stats.trades), win_rate=self.stats.win_rate / 100 if self.stats.win_rate else 0, ) async def run_session(self): """Run single market session with dashboard.""" # Start WebSocket self.ws_client = WebSocketClient(self.state) ws_task = asyncio.create_task(self.ws_client.connect()) await asyncio.sleep(1) # Track running order task (для non-blocking execution) order_task: Optional[asyncio.Task] = None try: with Live(self.dashboard.render(), refresh_per_second=1, console=console) as live: while self.running: # Update dashboard (никогда не блокируется) live.update(self.dashboard.render()) if self._web_snapshot_holder: self._web_snapshot_holder.set(self.dashboard.build_web_snapshot()) # Check for entry signal - запускаем в отдельном task if self.stats.can_enter() and self.dashboard.last_signal: if order_task is None or order_task.done(): signal = self.dashboard.last_signal self.dashboard.last_signal = "" order_task = asyncio.create_task(self._safe_execute_entry(signal)) # Check if order completed if order_task and order_task.done(): try: order_task.result() # Получаем исключения если были except Exception as e: logger.error(f"Order task error: {e}") order_task = None # Track drawdown while in position if self.stats.position: pos = self.stats.position if pos.token_name == "UP" and self.state.up_token: self.stats.update_drawdown(self.state.up_token.last_price) elif pos.token_name == "DOWN" and self.state.down_token: self.stats.update_drawdown(self.state.down_token.last_price) # Check market end (быстрая операция - не выносим в task) await self.check_market_end() # Market ended? if time.time() > self.state.end_time: console.print("\n[yellow]Market ended![/yellow]") break await asyncio.sleep(1) finally: # Cancel any running order tasks for task in [order_task]: if task and not task.done(): task.cancel() try: await task except: pass # Graceful WebSocket shutdown await self.ws_client.stop_graceful() try: ws_task.cancel() await ws_task except: pass # Stop User WebSocket for order tracking if self.user_ws: await self.user_ws.disconnect() if self._user_ws_task: try: self._user_ws_task.cancel() await self._user_ws_task except: pass async def _safe_execute_entry(self, signal: str): """Execute entry in separate task with error handling.""" try: await self.execute_entry(signal) except Exception as e: logger.error(f"Entry execution error: {e}") signal_logger.error(f"ENTRY ERROR: {e}") # GTD hedge is placed immediately after entry (no polling needed) # Fills are tracked via WebSocket _register_hedge_ws_handler() async def run(self): """Main run loop.""" if not await self.initialize(): return self.running = True redeemer_task = None if self.redeemer is not None: redeemer_task = asyncio.create_task(self.redeemer.run_loop()) sim_note = "" if self.config.simulation.enabled: sim_note = "🎮 模拟模式 — 不下真实订单\n" await self.telegram.send_message( f"{sim_note}" f"🤖 机器人启动\n" f"策略: ${self.config.entry.bet_amount_usd}/笔\n" f"对冲: {'已开启' if self.config.hedge.enabled else '已关闭'}\n" f"周期: {self.config.market.interval_minutes}分钟\n" f"价格范围: {self.config.strategy.min_price}-{self.config.strategy.max_price}\n" f"偏离阈值: {self.config.strategy.min_deviation_pct}%-{self.config.strategy.max_deviation_pct}%" ) try: while self.running: # Find market if not await self.find_market(): console.print("[red]No market found. Waiting 30s...[/red]") await asyncio.sleep(30) continue console.print("\n[bold green]Starting session...[/bold green]\n") await self.run_session() console.print("[yellow]Waiting 5s for next market...[/yellow]") await asyncio.sleep(5) except KeyboardInterrupt: console.print("\n[yellow]Stopping...[/yellow]") finally: self.running = False if self.redeemer is not None: self.redeemer.stop() if redeemer_task is not None: try: redeemer_task.cancel() await redeemer_task except Exception: pass # Gracefully close Chainlink RTDS WebSocket if self.chainlink_client: await self.chainlink_client.disconnect() if self._chainlink_task: try: self._chainlink_task.cancel() await self._chainlink_task except: pass await self.telegram.send_message("🛑 机器人已停止") await self.telegram.close() console.print("[green]Bot stopped.[/green]") async def main(): bot = LiveTradingBot() loop = asyncio.get_event_loop() def shutdown(): bot.running = False if sys.platform != "win32": for sig in (signal.SIGTERM, signal.SIGINT): loop.add_signal_handler(sig, shutdown) await bot.run() if __name__ == "__main__": try: asyncio.run(main()) except KeyboardInterrupt: pass