From 0cef4d7f4c6e256e4e537d357a6340e1fc28a960 Mon Sep 17 00:00:00 2001 From: gavindiaz Date: Mon, 27 Jul 2026 19:22:46 +0800 Subject: [PATCH] =?UTF-8?q?=E5=85=A8=E9=9D=A2=E5=AE=8C=E6=88=90=E4=BB=A3?= =?UTF-8?q?=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- btc-binary-VWAP-Momentum-bot/_fix_indent.py | 20 + btc-binary-VWAP-Momentum-bot/analyze_data.py | 42 ++ btc-binary-VWAP-Momentum-bot/main.py | 131 +++--- btc-binary-VWAP-Momentum-bot/requirements.txt | 2 +- .../src/auto_redeemer.py | 20 +- .../src/hedge_manager.py | 7 +- .../src/order_executor.py | 108 ++--- .../src/telegram_notifier.py | 30 +- .../src/user_websocket.py | 389 +++++++++++++----- up-down-spread-bot/.env.example | 113 ++--- up-down-spread-bot/src/data_feed.py | 12 +- up-down-spread-bot/src/main.py | 63 +-- up-down-spread-bot/src/order_executor.py | 8 +- up-down-spread-bot/src/pnl_chart_generator.py | 13 +- up-down-spread-bot/src/polymarket_api.py | 4 +- up-down-spread-bot/src/proxy_util.py | 144 +++++++ .../src/simple_redeem_collector.py | 4 +- up-down-spread-bot/src/telegram_notifier.py | 22 +- up-down-spread-bot/src/trader.py | 8 +- 19 files changed, 800 insertions(+), 340 deletions(-) create mode 100644 btc-binary-VWAP-Momentum-bot/_fix_indent.py create mode 100644 btc-binary-VWAP-Momentum-bot/analyze_data.py create mode 100644 up-down-spread-bot/src/proxy_util.py diff --git a/btc-binary-VWAP-Momentum-bot/_fix_indent.py b/btc-binary-VWAP-Momentum-bot/_fix_indent.py new file mode 100644 index 0000000..57dd9ba --- /dev/null +++ b/btc-binary-VWAP-Momentum-bot/_fix_indent.py @@ -0,0 +1,20 @@ +with open('src/order_executor.py', 'r', encoding='utf-8') as f: + content = f.read() + +# Fix the logger line that's missing indentation (after my edit it lost its leading spaces) +target = "logger.info(f\"Attempt {attempt}: placing ${order_size * order_price:.2f} @ {order_price:.2f} cap\")" +correct = " " + target + +# Replace the unindented version with the indented one +if target in content and correct not in content: + # Find the unindented occurrence + content = content.replace(target, correct, 1) + with open('src/order_executor.py', 'w', encoding='utf-8') as f: + f.write(content) + print("Fixed indentation") +else: + print("Already correct or not found") + if target in content: + print(" target found, correct also found:", correct in content) + else: + print(" target not found") \ No newline at end of file diff --git a/btc-binary-VWAP-Momentum-bot/analyze_data.py b/btc-binary-VWAP-Momentum-bot/analyze_data.py new file mode 100644 index 0000000..d0b0a6e --- /dev/null +++ b/btc-binary-VWAP-Momentum-bot/analyze_data.py @@ -0,0 +1,42 @@ +import gzip, csv +from collections import defaultdict + +f = gzip.open('shadow_decisions.csv.gz', 'rt') +reader = csv.DictReader(f) + +# 只看 BTC, 5m, 实际入场了 (would_enter=1) +rows = [r for r in reader if r['symbol'] == 'BTCUSDT' and r['timeframe'] == '5m' and r['would_enter'] == '1'] +f.close() + +print(f"BTC 5m entries: {len(rows)}") + +# 各策略胜率 +print("\n===== 各策略胜率 (BTC 5m) =====") +stats = defaultdict(lambda: [0, 0, 0.0]) +for r in rows: + cfg = r['config_name'] + stats[cfg][0] += 1 + stats[cfg][1] += 1 if r['win'] == '1' else 0 + try: + stats[cfg][2] += float(r['realized_pnl']) if r['realized_pnl'] else 0 + except: + pass + +for cfg, (total, wins, pnl) in sorted(stats.items(), key=lambda x: -x[1][1] / max(x[1][0], 1)): + wr = wins / total * 100 + print(f" {cfg:40s} {wins:4d}/{total:<4d} WR={wr:5.1f}% PnL={pnl:+.2f}") + +# 总体 +total_wins = sum(s[1] for s in stats.values()) +total_entries = sum(s[0] for s in stats.values()) +total_pnl = sum(s[2] for s in stats.values()) +print(f"\n 总计: {total_wins}/{total_entries} WR={total_wins/total_entries*100:.1f}% PnL={total_pnl:+.2f}") + +# 按 side 统计 +print("\n===== 按方向统计 =====") +sides = defaultdict(lambda: [0, 0]) +for r in rows: + sides[r['side']][0] += 1 + sides[r['side']][1] += 1 if r['win'] == '1' else 0 +for side, (total, wins) in sides.items(): + print(f" {side}: {wins}/{total} WR={wins/total*100:.1f}%") \ No newline at end of file diff --git a/btc-binary-VWAP-Momentum-bot/main.py b/btc-binary-VWAP-Momentum-bot/main.py index f84e87a..0e2f914 100644 --- a/btc-binary-VWAP-Momentum-bot/main.py +++ b/btc-binary-VWAP-Momentum-bot/main.py @@ -1,4 +1,8 @@ #!/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 @@ -1615,13 +1619,9 @@ class LiveTradingBot: ) console.print("[green]✓ Order executor: simulation (no CLOB)[/green]") else: - # User WebSocket for order tracking (CRITICAL for fill confirmation!) - self.user_ws = UserWebSocket( - api_key=self.config.polymarket.api_key, - api_secret=self.config.polymarket.api_secret, - api_passphrase=self.config.polymarket.api_passphrase - ) + # 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, @@ -1632,7 +1632,7 @@ class LiveTradingBot: 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=self.user_ws, + user_ws=None, simulation_mode=False, ) @@ -1640,14 +1640,22 @@ class LiveTradingBot: 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(1) + 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)[/yellow]") + console.print("[yellow]User WebSocket connecting... (will retry in background)[/yellow]") logger.warning("User WebSocket not yet connected") # Hedge manager @@ -1764,7 +1772,8 @@ class LiveTradingBot: if not market.get("closed", True): return await self._setup_market(market) except Exception as e: - logger.debug(f"Error finding market {expected_slug}: {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 @@ -1812,13 +1821,17 @@ class LiveTradingBot: 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: + except Exception: end_timestamp = time.time() + self.config.market.duration_sec slug = market.get("slug", "") @@ -1839,7 +1852,8 @@ class LiveTradingBot: logger.info(f" DOWN token: {down_token_id[:40]}...") self.stats.new_market(self.state.slug) - self.hedge_mgr.clear() # Reset hedge state for new market + 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 @@ -2066,15 +2080,15 @@ class LiveTradingBot: self.dashboard.hedge_flash = True hedge_cost = hedge_result.contracts * hedge_result.price - hsim = "🎮 [SIMULATION]\n" if self.config.simulation.enabled else "" + hsim = "🎮 [模拟模式]\n" if self.config.simulation.enabled else "" await self.telegram.send_message( f"{hsim}" - f"🛡️ Hedge Order Placed (GTD)\n" - f"📦 {hedge_result.contracts} contracts @ ${hedge_result.price}\n" - f"💰 Cost: ${hedge_cost:.2f}\n" - f"🔖 Order ID: {hedge_result.order_id[:20]}...\n" - f"📋 Status: LIVE (passive)\n" - f"🔄 Attempts: {hedge_result.attempts}" + 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 @@ -2083,9 +2097,9 @@ class LiveTradingBot: logger.info(f"GTD hedge placed: {hedge_result.contracts} @ ${hedge_result.price}") else: await self.telegram.send_message( - f"⚠️ Hedge Failed\n" + f"⚠️ 对冲单失败\n" f"❌ {hedge_result.error}\n" - f"🔄 Attempts: {hedge_result.attempts}" + f"🔄 重试: {hedge_result.attempts}次" ) logger.error(f"Hedge failed: {hedge_result.error}") else: @@ -2150,11 +2164,11 @@ class LiveTradingBot: self.dashboard.entry_flash = True await self.telegram.send_message( - f"🔄 Timeout Recovery!\n" - f"Order filled despite HTTP timeout.\n" - f"📊 {token_name} {rec_contracts} @ ${rec_price:.4f}\n" - f"💰 Cost: ${rec_cost:.2f}\n" - f"Market: {self.state.slug}" + 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( @@ -2179,22 +2193,22 @@ class LiveTradingBot: if hedge_result.success: self.dashboard.hedge_flash = True hedge_cost = hedge_result.contracts * hedge_result.price - hsim2 = "🎮 [SIMULATION]\n" if self.config.simulation.enabled else "" + hsim2 = "🎮 [模拟模式]\n" if self.config.simulation.enabled else "" await self.telegram.send_message( f"{hsim2}" - f"🛡️ Hedge Order Placed (GTD)\n" - f"📦 {hedge_result.contracts} contracts @ ${hedge_result.price}\n" - f"💰 Cost: ${hedge_cost:.2f}\n" - f"🔖 Order ID: {hedge_result.order_id[:20]}...\n" - f"📋 Status: LIVE (passive)\n" - f"🔄 Attempts: {hedge_result.attempts}" + 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"⚠️ Hedge Failed (after recovery)\n" + f"⚠️ 对冲失败 (恢复后)\n" f"❌ {hedge_result.error}" ) else: @@ -2207,11 +2221,11 @@ class LiveTradingBot: 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"⚠️ TIMEOUT — No Fill Detected\n" - f"Order status unknown after timeout.\n" - f"WebSocket recovery found nothing.\n" - f"Re-entry blocked.\n" - f"Market: {self.state.slug}" + f"⚠️ 超时 — 未检测到成交\n" + f"HTTP超时后订单状态未知\n" + f"WebSocket恢复未找到成交\n" + f"禁止重新入场\n" + f"市场: {self.state.slug}" ) def _register_hedge_ws_handler(self): @@ -2253,17 +2267,17 @@ class LiveTradingBot: self.dashboard.hedge_flash = True await self.telegram.send_message( - f"✅ Hedge FULLY Filled!\n" - f"📦 {filled} contracts @ ${price}\n" - f"🛡️ Position fully protected" + 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"🛡️ Hedge Partial Fill\n" - f"📦 +{size} contracts @ ${price}\n" - f"📊 Progress: {filled}/{total}" + f"🛡️ 对冲部分成交\n" + f"📦 +{size}张 @ ${price:.4f}\n" + f"📊 进度: {filled}/{total}" ) logger.info(f"Hedge partial fill: +{size}, total {filled}/{total}") @@ -2313,6 +2327,16 @@ class LiveTradingBot: 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.""" @@ -2326,7 +2350,7 @@ class LiveTradingBot: order_task: Optional[asyncio.Task] = None try: - with Live(self.dashboard.render(), refresh_per_second=4, console=console) as live: + with Live(self.dashboard.render(), refresh_per_second=1, console=console) as live: while self.running: # Update dashboard (никогда не блокируется) live.update(self.dashboard.render()) @@ -2364,7 +2388,7 @@ class LiveTradingBot: console.print("\n[yellow]Market ended![/yellow]") break - await asyncio.sleep(0.25) + await asyncio.sleep(1) finally: # Cancel any running order tasks for task in [order_task]: @@ -2417,12 +2441,15 @@ class LiveTradingBot: sim_note = "" if self.config.simulation.enabled: - sim_note = "🎮 SIMULATION MODE — no real orders\n" + sim_note = "🎮 模拟模式 — 不下真实订单\n" await self.telegram.send_message( f"{sim_note}" - f"🤖 Bot Started\n" - f"Strategy: ${self.config.entry.bet_amount_usd} per trade\n" - f"Hedge: {'enabled' if self.config.hedge.enabled else 'disabled'}" + 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: @@ -2462,7 +2489,7 @@ class LiveTradingBot: except: pass - await self.telegram.send_message("🛑 Bot stopped") + await self.telegram.send_message("🛑 机器人已停止") await self.telegram.close() console.print("[green]Bot stopped.[/green]") diff --git a/btc-binary-VWAP-Momentum-bot/requirements.txt b/btc-binary-VWAP-Momentum-bot/requirements.txt index 3d83cea..472c1ad 100644 --- a/btc-binary-VWAP-Momentum-bot/requirements.txt +++ b/btc-binary-VWAP-Momentum-bot/requirements.txt @@ -1,7 +1,7 @@ # BTC 15-min Live Trading Bot - Dependencies # Polymarket SDK -py-clob-client>=0.16.0 +py-clob-client-v2>=1.0.0 # Web3 for blockchain interactions web3>=6.0.0 diff --git a/btc-binary-VWAP-Momentum-bot/src/auto_redeemer.py b/btc-binary-VWAP-Momentum-bot/src/auto_redeemer.py index 5e7f5e0..6dbb62e 100644 --- a/btc-binary-VWAP-Momentum-bot/src/auto_redeemer.py +++ b/btc-binary-VWAP-Momentum-bot/src/auto_redeemer.py @@ -312,9 +312,9 @@ class AsyncAutoRedeemer: def _redeem_position_sync(self, position: Dict) -> bool: """Redeem a single position (sync, runs in thread).""" - import fcntl import time - + import os + condition_id = position["condition_id"] up_balance = position["up_balance"] down_balance = position["down_balance"] @@ -327,11 +327,15 @@ class AsyncAutoRedeemer: logger.warning(f"Skipping {position['slug']} - oracle not resolved") return False - # File lock - lock_file = "/tmp/btc_live_redeem.lock" + # File lock (best-effort; fcntl is Unix-only, skip on Windows) + lock_file = os.path.join(os.path.dirname(__file__), "..", ".redeem.lock") + self._lock_fd = None try: + import fcntl self._lock_fd = open(lock_file, 'w') fcntl.flock(self._lock_fd, fcntl.LOCK_EX | fcntl.LOCK_NB) + except ImportError: + pass # Windows — skip file lock except (IOError, OSError): logger.warning("Another redeem in progress, skipping") return False @@ -354,7 +358,11 @@ class AsyncAutoRedeemer: finally: if self._lock_fd: try: + import fcntl fcntl.flock(self._lock_fd, fcntl.LOCK_UN) + except ImportError: + pass + try: self._lock_fd.close() except: pass @@ -553,8 +561,8 @@ class AsyncAutoRedeemer: if self.telegram: try: await self.telegram.send_message( - f"💰 Redeemed: {position['slug']}\n" - f"Value: ${value:.2f} USDC" + f"💰 已赎回: {position['slug']}\n" + f"价值: ${value:.2f} USDC" ) except: pass diff --git a/btc-binary-VWAP-Momentum-bot/src/hedge_manager.py b/btc-binary-VWAP-Momentum-bot/src/hedge_manager.py index 0dc2820..da9f64d 100644 --- a/btc-binary-VWAP-Momentum-bot/src/hedge_manager.py +++ b/btc-binary-VWAP-Momentum-bot/src/hedge_manager.py @@ -171,8 +171,7 @@ class HedgeManager: hedge_logger.info(f" Max Retries: {self.config.max_retries}") hedge_logger.info("-" * 40) - from py_clob_client.clob_types import OrderArgs, OrderType - from py_clob_client.order_builder.constants import BUY + from py_clob_client_v2.clob_types import OrderArgsV2, OrderType last_error = "" expiration = str(int(time.time()) + 3600) # 1 hour, market resolves before this @@ -184,10 +183,10 @@ class HedgeManager: # Create signed order signed_order = await asyncio.to_thread( self.executor._client.create_order, - OrderArgs( + OrderArgsV2( price=self.config.hedge_price, size=pos.contracts, - side=BUY, + side="BUY", token_id=pos.opposite_token_id, expiration=expiration ) diff --git a/btc-binary-VWAP-Momentum-bot/src/order_executor.py b/btc-binary-VWAP-Momentum-bot/src/order_executor.py index 68ac297..2fd45f2 100644 --- a/btc-binary-VWAP-Momentum-bot/src/order_executor.py +++ b/btc-binary-VWAP-Momentum-bot/src/order_executor.py @@ -23,9 +23,8 @@ from dataclasses import dataclass, field from datetime import datetime from typing import Optional, Dict, Any, Tuple, List -from py_clob_client.client import ClobClient -from py_clob_client.clob_types import OrderArgs, ApiCreds, OrderType -from py_clob_client.order_builder.constants import BUY +from py_clob_client_v2.client import ClobClient +from py_clob_client_v2.clob_types import OrderArgsV2, MarketOrderArgsV2, ApiCreds, OrderType logger = logging.getLogger("btc_live.executor") @@ -130,8 +129,38 @@ class OrderExecutor: signature_type=self.signature_type, funder=self.funder_address ) - - # Set API credentials + + # Prefer fresh L2 creds derived from PK — stale .env keys break User WS + orders + try: + derived = await asyncio.to_thread(self._client.create_or_derive_api_key) + + def _cred(obj, *names): + for n in names: + if isinstance(obj, dict) and obj.get(n): + return obj[n] + v = getattr(obj, n, None) + if v: + return v + return None + + d_key = _cred(derived, "api_key") + d_sec = _cred(derived, "api_secret") + d_ph = _cred(derived, "api_passphrase") + if d_key and d_sec and d_ph: + if str(d_key) != str(self.api_key): + logger.warning( + "POLY_API_KEY stale/mismatch — using derived creds from PRIVATE_KEY " + "(update .env POLY_API_KEY/SECRET/PASSPHRASE)" + ) + order_logger.warning( + f" Derived API key {str(d_key)[:8]}... (env was {str(self.api_key)[:8]}...)" + ) + self.api_key = str(d_key) + self.api_secret = str(d_sec) + self.api_passphrase = str(d_ph) + except Exception as e: + logger.warning(f"derive api creds failed, using env creds: {e}") + api_creds = ApiCreds( api_key=self.api_key, api_secret=self.api_secret, @@ -358,61 +387,48 @@ class OrderExecutor: self, token_id: str, price: float, - size: int + amount: int ) -> Tuple[bool, str, Dict]: """ - Place a FAK (Fill-And-Kill) order. - + Place a FAK market order (v2: market order, amount in USD cents). + Args: token_id: Token to buy - price: Order price - size: Number of contracts - + price: Price cap (max price willing to pay) + amount: USD amount to spend (max 2 decimals) + Returns: Tuple of (success, order_id, response) """ if not self._client: order_logger.error("PLACE_ORDER: Client not initialized") return False, "", {"error": "Client not initialized"} - - order_value = size * price + order_logger.info("-" * 50) order_logger.info(f"PLACING ORDER") order_logger.info(f" Token: {token_id[:30]}...") order_logger.info(f" Side: BUY") - order_logger.info(f" Price: {price:.4f}") - order_logger.info(f" Size: {size} contracts") - order_logger.info(f" Value: ${order_value:.2f}") - order_logger.info(f" Type: FAK (Fill-And-Kill)") - + order_logger.info(f" Price Cap: {price:.4f}") + order_logger.info(f" Amount: ${amount:.2f}") + order_logger.info(f" Type: FAK (market order)") + start_time = time.time() - + try: - # Create order - sign_start = time.time() - signed_order = await asyncio.to_thread( - self._client.create_order, - OrderArgs( - price=price, - size=size, - side=BUY, - token_id=token_id - ) - ) - sign_elapsed = (time.time() - sign_start) * 1000 - order_logger.debug(f" Order signed in {sign_elapsed:.0f}ms") - - # Post FAK order (Fill-And-Kill: fill what you can, cancel rest) post_start = time.time() response = await asyncio.to_thread( - self._client.post_order, - signed_order, - OrderType.FAK + self._client.create_and_post_market_order, + MarketOrderArgsV2( + token_id=token_id, + amount=float(amount), + side="BUY", + price=price, + ), + order_type=OrderType.FAK, ) post_elapsed = (time.time() - post_start) * 1000 total_elapsed = (time.time() - start_time) * 1000 - - # Handle response + if isinstance(response, dict): success = response.get("success", False) order_id = response.get("orderID", "") @@ -427,9 +443,9 @@ class OrderExecutor: error_msg = getattr(response, 'errorMsg', "") taking_amount = getattr(response, 'takingAmount', "") making_amount = getattr(response, 'makingAmount', "") - + self.orders_placed += 1 - + order_logger.info(f"ORDER RESPONSE:") order_logger.info(f" Success: {success}") order_logger.info(f" Order ID: {order_id[:40] if order_id else 'N/A'}...") @@ -440,7 +456,7 @@ class OrderExecutor: order_logger.info(f" Making Amount: {making_amount}") if error_msg: order_logger.warning(f" Error: {error_msg}") - order_logger.info(f" Latency: sign={sign_elapsed:.0f}ms, post={post_elapsed:.0f}ms, total={total_elapsed:.0f}ms") + order_logger.info(f" Latency: post={post_elapsed:.0f}ms, total={total_elapsed:.0f}ms") order_logger.info("-" * 50) logger.info(f"Order placed: {success}, ID: {order_id[:20] if order_id else 'N/A'}...") @@ -676,13 +692,13 @@ class OrderExecutor: order_logger.info(f" Remaining needed: {remaining}") order_logger.info(f" Order size: {order_size}") - logger.info(f"Attempt {attempt}: placing {order_size} contracts @ {order_price:.2f}") - - # Place order + logger.info(f"Attempt {attempt}: placing ${order_size * order_price:.2f} @ {order_price:.2f} cap") + + # Place FAK market order (v2: amount=USD, price=cap) success, order_id, response = await self.place_fak_order( token_id, order_price, - order_size + int(order_size * order_price * 100) / 100 # USD, max 2 decimals ) # Запоминаем order_id для отслеживания diff --git a/btc-binary-VWAP-Momentum-bot/src/telegram_notifier.py b/btc-binary-VWAP-Momentum-bot/src/telegram_notifier.py index 7549320..e3c23bf 100644 --- a/btc-binary-VWAP-Momentum-bot/src/telegram_notifier.py +++ b/btc-binary-VWAP-Momentum-bot/src/telegram_notifier.py @@ -184,14 +184,14 @@ class TelegramNotifier: simulation: bool = False, ): """Send entry notification.""" - mode = "🎮 [SIMULATION]\n" if simulation else "" + mode = "🎮 [模拟模式]\n" if simulation else "" text = ( f"{mode}" - f"🟢 ENTRY\n" - f"📊 BTC {interval_minutes}min - {side}\n" - f"💰 ${cost:.2f} @ {price:.2f}\n" - f"📦 {contracts} contracts\n" - f"🔄 {retries} retries" + f"🟢 开仓成功\n" + f"📊 BTC {interval_minutes}分钟 - {side}\n" + f"💰 金额: ${cost:.2f} @ {price:.4f}\n" + f"📦 合约数: {contracts}\n" + f"🔄 重试: {retries}次" ) await self.send_message(text) @@ -203,10 +203,10 @@ class TelegramNotifier: ): """Send hedge notification.""" text = ( - f"🛡 HEDGE\n" - f"📦 {contracts} contracts @ ${price:.3f}\n" - f"💰 Cost: ${cost:.2f}\n" - f"✅ Position protected" + f"🛡 对冲已开\n" + f"📦 {contracts}张 @ ${price:.4f}\n" + f"💰 花费: ${cost:.2f}\n" + f"✅ 仓位已保护" ) await self.send_message(text) @@ -222,11 +222,11 @@ class TelegramNotifier: pnl_sign = "+" if pnl > 0 else "" text = ( - f"🏁 MARKET RESOLVED\n" - f"{emoji} Winner: {winner}\n" - f"💵 P&L: {pnl_sign}${pnl:.2f}\n" - f"📈 Total: ${total_pnl:.2f}\n" - f"📊 Win rate: {win_rate:.1%}" + f"🏁 市场结算\n" + f"{emoji} 胜方: {winner}\n" + f"💵 本次盈亏: {pnl_sign}${pnl:.2f}\n" + f"📈 累计盈亏: ${total_pnl:.2f}\n" + f"📊 胜率: {win_rate:.1%}" ) await self.send_message(text) diff --git a/btc-binary-VWAP-Momentum-bot/src/user_websocket.py b/btc-binary-VWAP-Momentum-bot/src/user_websocket.py index 005a63a..e4a02c3 100644 --- a/btc-binary-VWAP-Momentum-bot/src/user_websocket.py +++ b/btc-binary-VWAP-Momentum-bot/src/user_websocket.py @@ -11,8 +11,9 @@ Docs: https://docs.polymarket.com/developers/CLOB/websocket/user-channel import asyncio import json import logging +import time import websockets -from typing import Optional, Dict, Callable, Any +from typing import Optional, Dict, Callable, Any, List from dataclasses import dataclass, field from datetime import datetime @@ -24,6 +25,9 @@ logger = logging.getLogger("btc_live.user_ws") WS_URL = "wss://ws-subscriptions-clob.polymarket.com/ws/user" +# Trade statuses that count as a real fill (lifecycle can emit multiple) +_FILL_STATUSES = frozenset({"MATCHED", "MINED", "CONFIRMED", ""}) + @dataclass class OrderStatus: @@ -42,10 +46,11 @@ class OrderStatus: class UserWebSocket: """ WebSocket client for User Channel. - + Tracks order placements and fills in real-time. + Never print() — Rich Live dashboard owns the terminal. """ - + def __init__(self, api_key: str, api_secret: str = "", api_passphrase: str = ""): self.api_key = api_key self.api_secret = api_secret @@ -55,123 +60,282 @@ class UserWebSocket: self._connected = False self._running = False self._orders: Dict[str, OrderStatus] = {} - + # asset_id -> list of fill dicts {size, price, trade_id, side, status} + self._token_fills: Dict[str, List[dict]] = {} + self._seen_trade_ids: set = set() + # Callbacks self._on_trade: Optional[Callable] = None self._on_order: Optional[Callable] = None - + + def set_credentials(self, api_key: str, api_secret: str, api_passphrase: str) -> None: + self.api_key = api_key + self.api_secret = api_secret + self.api_passphrase = api_passphrase + async def connect(self): - """Connect to User Channel WebSocket.""" - try: - self._running = True - - logger.info(f"Connecting to User WebSocket at {WS_URL}...") - print(f" Connecting to {WS_URL}...") - - async with ws_connect( - WS_URL, - **_ws_connect_kwargs(ping_interval=30, ping_timeout=10) - ) as ws: - self._ws = ws - self._connected = True - logger.info("User WebSocket connected") - print(" WebSocket connection established") - - # Subscribe to user channel with auth object - subscribe_msg = { - "type": "user", - "auth": { - "apiKey": self.api_key, - "secret": self.api_secret, - "passphrase": self.api_passphrase + """Connect to User Channel WebSocket with auto-reconnect.""" + self._running = True + delay = 2.0 + + while self._running: + session_ok = False + try: + logger.info("Connecting to User WebSocket at %s...", WS_URL) + + async with ws_connect( + WS_URL, + **_ws_connect_kwargs(ping_interval=None, ping_timeout=None), + ) as ws: + self._ws = ws + self._connected = True + logger.info("User WebSocket TCP connected") + + subscribe_msg = { + "auth": { + "apiKey": self.api_key, + "secret": self.api_secret, + "passphrase": self.api_passphrase, + }, + "type": "user", } - } - await ws.send(json.dumps(subscribe_msg)) - logger.info("Sent subscription message") - print(" Sent subscription message") - - # Wait for response - try: - first_msg = await asyncio.wait_for(ws.recv(), timeout=5) - logger.info(f"First message: {first_msg[:200]}") - print(f" First response: {first_msg[:100]}...") - await self._process_message(first_msg) - except asyncio.TimeoutError: - logger.warning("No initial response from WebSocket") - print(" No initial response (timeout)") - - # Listen for messages - while self._running: + await ws.send(json.dumps(subscribe_msg)) + logger.info("User WebSocket auth/subscribe sent") + + # Auth failure = immediate close with no frame try: - msg = await asyncio.wait_for(ws.recv(), timeout=60) - logger.debug(f"WS message: {msg[:100]}") - await self._process_message(msg) + first_msg = await asyncio.wait_for(ws.recv(), timeout=8) + logger.info("User WS first message: %s", str(first_msg)[:200]) + await self._process_message(first_msg) + session_ok = True + delay = 2.0 except asyncio.TimeoutError: - # Send ping to keep connection alive + # No welcome msg is OK if socket still open + session_ok = True + delay = 2.0 + logger.info("User WS open (no initial message)") + except websockets.exceptions.ConnectionClosed as e: + logger.error( + "User WS closed right after auth (bad/stale API creds?): %s", e + ) + session_ok = False + + last_ping = time.monotonic() + while self._running and session_ok: try: - await ws.ping() - except Exception: + timeout = max(1.0, 10.0 - (time.monotonic() - last_ping)) + msg = await asyncio.wait_for(ws.recv(), timeout=timeout) + logger.debug("WS message: %s", str(msg)[:100]) + await self._process_message(msg) + except asyncio.TimeoutError: + try: + await ws.send("PING") + last_ping = time.monotonic() + except Exception: + break + except websockets.exceptions.ConnectionClosed: + logger.warning("User WebSocket connection closed") break - except websockets.exceptions.ConnectionClosed: - logger.warning("User WebSocket connection closed") - break - except Exception as e: - logger.error(f"Error receiving message: {e}") - break - + except Exception as e: + logger.error("Error receiving message: %s", e) + break + + self._connected = False + self._ws = None + logger.info("User WebSocket disconnected") + + except asyncio.CancelledError: self._connected = False - logger.info("User WebSocket disconnected") - print(" WebSocket disconnected") - - except Exception as e: - logger.error(f"User WebSocket connection error: {e}") - print(f" Connection error: {e}") - self._connected = False - raise - + self._ws = None + raise + except Exception as e: + logger.error("User WebSocket connection error: %s", e) + self._connected = False + self._ws = None + session_ok = False + + if not self._running: + break + + # Auth/instant-fail: slow backoff so Rich Live isn't starved by reconnect thrash + if not session_ok: + delay = min(max(delay, 5.0) * 1.5, 120.0) + else: + delay = min(delay * 1.3, 30.0) + + logger.info("User WebSocket reconnecting in %.0fs...", delay) + await asyncio.sleep(delay) + async def _process_message(self, msg: str): """Process an incoming WebSocket message.""" + if msg == "PONG" or msg == "pong": + return try: data = json.loads(msg) except json.JSONDecodeError: - logger.warning(f"Invalid JSON: {msg[:100]}") + logger.warning("Invalid JSON: %s", str(msg)[:100]) return - + + if isinstance(data, list): + for item in data: + if isinstance(item, dict): + await self._process_one(item) + return + if isinstance(data, dict): + await self._process_one(data) + + async def _process_one(self, data: dict): msg_type = data.get("type", "") - + event_type = data.get("event_type", "") + # Order updates - if msg_type in ("PLACEMENT", "UPDATE", "CANCELLATION"): + if event_type == "order" or msg_type in ("PLACEMENT", "UPDATE", "CANCELLATION"): order_id = data.get("id", "") - if order_id and order_id in self._orders: + if not order_id: + return + + size_matched = int(float(data.get("size_matched", 0) or 0)) + if order_id in self._orders: order = self._orders[order_id] - order.status = msg_type - order.size_matched = int(float(data.get("size_matched", 0))) - - if msg_type == "MATCHED": - order.status = "MATCHED" - order.trades.append(data) - - if self._on_order: - if asyncio.iscoroutinefunction(self._on_order): - await self._on_order(order) - else: - self._on_order(order) - + order.status = msg_type or event_type or order.status + order.size_matched = max(order.size_matched, size_matched) + if msg_type == "UPDATE" or size_matched >= order.original_size > 0: + if size_matched >= order.original_size: + order.status = "MATCHED" + if msg_type == "CANCELLATION": + order.status = "CANCELLED" + else: + self._orders[order_id] = OrderStatus( + order_id=order_id, + asset_id=str(data.get("asset_id", "")), + side=str(data.get("side", "")), + price=float(data.get("price", 0) or 0), + original_size=int(float(data.get("original_size", 0) or 0)), + size_matched=size_matched, + status=msg_type or "PLACED", + ) + + order = self._orders[order_id] + if self._on_order: + if asyncio.iscoroutinefunction(self._on_order): + await self._on_order(order) + else: + self._on_order(order) + # Trade updates - elif msg_type == "TRADE": + elif event_type == "trade" or msg_type == "TRADE": + self._record_trade_fill(data) if self._on_trade: if asyncio.iscoroutinefunction(self._on_trade): await self._on_trade(data) else: self._on_trade(data) - - # Initial subscription response + elif msg_type in ("subscribed", "OK", "ok"): - logger.info(f"Subscription confirmed: {msg[:150]}") - + logger.info("Subscription confirmed: %s", str(data)[:150]) + else: - logger.debug(f"Unhandled msg type={msg_type}: {msg[:100]}") - + logger.debug( + "Unhandled msg type=%s event=%s: %s", + msg_type, + event_type, + str(data)[:100], + ) + + def _record_trade_fill(self, data: dict) -> None: + """Accumulate fills by asset_id; de-dupe by trade id across lifecycle statuses.""" + trade_id = str(data.get("id", "") or "") + asset_id = str(data.get("asset_id", "") or "") + status = str(data.get("status", "") or "") + try: + size = float(data.get("size", 0) or 0) + price = float(data.get("price", 0) or 0) + except (TypeError, ValueError): + return + + if not asset_id or size <= 0 or status not in _FILL_STATUSES: + return + + # Update matching registered order + taker_oid = str(data.get("taker_order_id", "") or "") + if taker_oid and taker_oid in self._orders: + order = self._orders[taker_oid] + order.size_matched = max(order.size_matched, int(size)) + if order.size_matched >= order.original_size > 0: + order.status = "MATCHED" + order.trades.append(data) + + if trade_id and trade_id in self._seen_trade_ids: + return + if trade_id: + self._seen_trade_ids.add(trade_id) + + self._token_fills.setdefault(asset_id, []).append({ + "trade_id": trade_id, + "size": size, + "price": price, + "side": str(data.get("side", "")), + "status": status, + "taker_order_id": taker_oid, + }) + + def clear_token_fills(self) -> None: + """Reset WS fill buffer for a new market.""" + self._token_fills.clear() + self._seen_trade_ids.clear() + + def _aggregate_token_fills(self, token_id: str) -> dict: + fills = self._token_fills.get(token_id, []) + contracts = sum(f["size"] for f in fills) + total_cost = sum(f["size"] * f["price"] for f in fills) + avg_price = (total_cost / contracts) if contracts > 0 else 0.0 + return { + "contracts": int(round(contracts)), + "avg_price": avg_price, + "total_cost": total_cost, + "fills": list(fills), + } + + async def wait_for_fills_on_token( + self, + token_id: str, + timeout: float = 10.0, + min_contracts: float = 0.0, + ) -> Optional[dict]: + """ + Wait until any fills arrive for token_id (timeout recovery path). + + Returns dict with contracts/avg_price/total_cost/fills, or None. + """ + deadline = time.monotonic() + max(0.0, float(timeout)) + while time.monotonic() < deadline: + agg = self._aggregate_token_fills(token_id) + if agg["contracts"] > min_contracts: + return agg + await asyncio.sleep(0.2) + agg = self._aggregate_token_fills(token_id) + return agg if agg["contracts"] > 0 else None + + async def wait_for_fill( + self, + order_id: str, + timeout: float = 10.0, + ) -> Optional[OrderStatus]: + """Wait until order has size_matched > 0; return OrderStatus or None.""" + deadline = time.monotonic() + max(0.0, float(timeout)) + while time.monotonic() < deadline: + status = self._orders.get(order_id) + if status and ( + status.size_matched > 0 + or status.status == "MATCHED" + ): + return status + await asyncio.sleep(0.15) + status = self._orders.get(order_id) + if status and status.size_matched > 0: + return status + return None + def register_order(self, order_id: str, asset_id: str, side: str, price: float, size: int): """Register an order we're tracking.""" self._orders[order_id] = OrderStatus( @@ -181,35 +345,42 @@ class UserWebSocket: price=price, original_size=size ) - + def get_order_status(self, order_id: str) -> Optional[OrderStatus]: """Get current status for an order.""" return self._orders.get(order_id) - + async def wait_for_order_match(self, order_id: str, timeout: float = 10.0) -> bool: """Wait until an order is matched or timeout.""" - start = asyncio.get_event_loop().time() - while asyncio.get_event_loop().time() - start < timeout: - status = self._orders.get(order_id) - if status and (status.status == "MATCHED" or status.size_matched >= status.original_size): - return True - await asyncio.sleep(0.2) - return False - + order = await self.wait_for_fill(order_id, timeout=timeout) + if not order: + return False + return order.status == "MATCHED" or ( + order.original_size > 0 and order.size_matched >= order.original_size + ) + async def wait_for_order_place(self, order_id: str, timeout: float = 5.0) -> bool: """Wait until an order placement is confirmed or timeout.""" - start = asyncio.get_event_loop().time() - while asyncio.get_event_loop().time() - start < timeout: + deadline = time.monotonic() + max(0.0, float(timeout)) + while time.monotonic() < deadline: status = self._orders.get(order_id) - if status and status.status in ("PLACED", "UPDATE", "MATCHED"): + if status and status.status in ("PLACED", "PLACEMENT", "UPDATE", "MATCHED"): return True await asyncio.sleep(0.15) return False - + + @property + def connected(self) -> bool: + return self._connected and self._ws is not None + @property def is_connected(self) -> bool: - return self._connected and self._ws is not None - + return self.connected + + async def disconnect(self): + """Alias used by main.py shutdown path.""" + await self.close() + async def close(self): """Close the WebSocket connection.""" self._running = False @@ -227,4 +398,4 @@ class UserWebSocket: await cm.__aexit__(None, None, None) except Exception: pass - self._connected = False \ No newline at end of file + self._connected = False diff --git a/up-down-spread-bot/.env.example b/up-down-spread-bot/.env.example index 0c3c22b..65422cf 100644 --- a/up-down-spread-bot/.env.example +++ b/up-down-spread-bot/.env.example @@ -1,81 +1,92 @@ # ============================================================================= -# Meridian — Polymarket 15m crypto desk (environment) +# Meridian — Polymarket 多币种交易机器人(环境变量) # ============================================================================= -# Copy this file to .env and fill in your values: -# cp .env.example .env +# 使用方法: +# 1. 将此文件复制为 .env +# cp .env.example .env +# 2. 填入你的真实值 +# 3. 启动机器人 # -# NEVER commit .env with real keys to git! +# ⚠️ 切勿将包含真实密钥的 .env 提交到 git! # ============================================================================= # ============================================================================= -# WALLET CREDENTIALS (REQUIRED) +# ① 钱包私钥(必须) # ============================================================================= - -# Your Polygon wallet private key -# Format: 64 hex characters with 0x prefix (66 characters total) -# -# How to get: -# - MetaMask: Settings -> Security & Privacy -> Export Private Key -# - Polymarket: polymarket.com/settings -> Export Private Key +# 你的 Polygon 钱包私钥,64 位十六进制字符 + 0x 前缀 = 66 字符 # -# WARNING: Never share this key or commit it to git! +# 获取方式: +# - MetaMask:设置 → 安全与隐私 → 导出私钥 +# - Polymarket:polymarket.com/settings → 导出私钥 +# +# ⚠️ 切勿泄露此密钥,切勿提交到 git! PRIVATE_KEY=0x0000000000000000000000000000000000000000000000000000000000000000 -# ============================================================================= -# POLYGON NETWORK (REQUIRED) -# ============================================================================= -# Polygon RPC endpoint -# Free options: -# - https://polygon-rpc.com (public, rate limited) -# - https://rpc.ankr.com/polygon (public, rate limited) +# ============================================================================= +# ② Polygon 网络(必须) +# ============================================================================= +# 免费 RPC 节点: +# - https://polygon-rpc.com(公共,有频率限制) +# - https://rpc.ankr.com/polygon(公共,有频率限制) # -# Recommended (private RPC): -# - Alchemy: https://polygon-mainnet.g.alchemy.com/v2/YOUR_API_KEY -# - Infura: https://polygon-mainnet.infura.io/v3/YOUR_PROJECT_ID -# - QuickNode: https://your-endpoint.quiknode.pro/YOUR_KEY/ +# 推荐使用私有 RPC(更稳定): +# - Alchemy:https://polygon-mainnet.g.alchemy.com/v2/YOUR_API_KEY +# - Infura:https://polygon-mainnet.infura.io/v3/YOUR_PROJECT_ID +# - QuickNode:https://your-endpoint.quiknode.pro/YOUR_KEY/ # -# Get free API key: -# - Alchemy: https://www.alchemy.com/ -# - Ankr: https://www.ankr.com/rpc/ +# 免费 API 密钥获取: +# - Alchemy:https://www.alchemy.com/ +# - Ankr:https://www.ankr.com/rpc/ RPC_URL=https://polygon-rpc.com -# Polygon chain ID (do not change) +# Polygon 链 ID(不要修改) CHAIN_ID=137 -# ============================================================================= -# POLYMARKET API (REQUIRED) -# ============================================================================= -# Polymarket CLOB API host (do not change) +# ============================================================================= +# ③ Polymarket API(必须) +# ============================================================================= +# Polymarket CLOB API 地址(不要修改) CLOB_HOST=https://clob.polymarket.com -# API Credentials -# How to get: -# 1. Go to polymarket.com/settings -# 2. Connect your wallet -# 3. Generate API credentials -# 4. Copy the values here +# API 凭证 +# 获取方式: +# 1. 访问 polymarket.com/settings +# 2. 连接钱包 +# 3. 生成 API 凭证(Generate API Credentials) +# 4. 将生成的 Key / Secret / Passphrase 填入下方 # -# Or generate programmatically using py-clob-client library +# 或者用 py-clob-client 库编程生成 POLYMARKET_API_KEY=your_api_key_here POLYMARKET_API_SECRET=your_api_secret_here POLYMARKET_API_PASSPHRASE=your_api_passphrase_here -# ============================================================================= -# TELEGRAM NOTIFICATIONS (OPTIONAL) -# ============================================================================= +# ============================================================================= +# ④ Telegram 通知(可选) +# ============================================================================= # Telegram Bot Token -# How to get: -# 1. Open Telegram and search for @BotFather -# 2. Send /newbot and follow instructions -# 3. Copy the token here +# 获取方式: +# 1. 打开 Telegram,搜索 @BotFather +# 2. 发送 /newbot 并按提示操作 +# 3. 复制 Bot Token 到此处 TELEGRAM_BOT_TOKEN= -# Telegram Chat ID -# How to get: -# 1. Open Telegram and search for @userinfobot -# 2. Send /start -# 3. Copy your numeric ID here +# Telegram Chat ID(你的用户 ID) +# 获取方式: +# 1. 打开 Telegram,搜索 @userinfobot +# 2. 发送 /start +# 3. 复制返回的数字 ID 到此处 TELEGRAM_CHAT_ID= + + +# ============================================================================= +# ⑤ 代理(国内用户必需) +# ============================================================================= +# 梯子的 HTTP 代理地址,看你梯子客户端里的「本地 HTTP 端口」 +# Clash / Clash Verge 默认 7890 +# V2RayN 默认 10809 +# 不用的话留空即可 +HTTP_PROXY=http://127.0.0.1:7890 +HTTPS_PROXY=http://127.0.0.1:7890 \ No newline at end of file diff --git a/up-down-spread-bot/src/data_feed.py b/up-down-spread-bot/src/data_feed.py index 62aa0cb..4d358db 100644 --- a/up-down-spread-bot/src/data_feed.py +++ b/up-down-spread-bot/src/data_feed.py @@ -14,6 +14,7 @@ import base64 from typing import Optional, Dict import trader as trader_module from position_tracker import PositionTracker +from proxy_util import get_session_proxies, patch_websocket_if_needed class DataFeed: @@ -102,9 +103,12 @@ class DataFeed: self.threads.append(pm_thread) print(f"[DATA] Started Polymarket feed for {coin.upper()}") - # ❌ USER CHANNEL DISABLED - WebSocket auth doesn't work - # Using REST API takingAmount/makingAmount instead! - print(f"[DATA] ℹ️ Position tracking via REST API responses") + # ✅ USER CHANNEL ENABLED - WebSocket for position tracking + patch_websocket_if_needed() + user_thread = threading.Thread(target=self._user_channel_worker, daemon=True) + user_thread.start() + self.threads.append(user_thread) + print(f"[DATA] ✅ User Channel WebSocket started") # Start local timer update (fixes timer freeze) timer_thread = threading.Thread(target=self._timer_worker, daemon=True) @@ -179,7 +183,7 @@ class DataFeed: # Use events API with specific slug url = f"{gamma_api}/events?slug={slug}" - resp = requests.get(url, timeout=10) + resp = requests.get(url, timeout=10, proxies=get_session_proxies()) resp.raise_for_status() events = resp.json() diff --git a/up-down-spread-bot/src/main.py b/up-down-spread-bot/src/main.py index e1585ef..2b93e1a 100644 --- a/up-down-spread-bot/src/main.py +++ b/up-down-spread-bot/src/main.py @@ -28,8 +28,12 @@ from safety_guard import SafetyGuard from order_executor import OrderExecutor from keyboard_listener import KeyboardListener from market_config import apply_market_window_settings +from proxy_util import get_session_proxies, get_log_dir, get_project_root, patch_websocket_if_needed import trader as trader_module +_PROJECT_ROOT = get_project_root() +_LOG_DIR = get_log_dir() + # Global configuration constants STRATEGY_BASES = ['late_v3'] @@ -77,18 +81,18 @@ def signal_handler(sig, frame): trade = { 'market_slug': market_slug, 'strategy': strategy_name, - 'up_contracts': pos['UP']['contracts'], - 'down_contracts': pos['DOWN']['contracts'], - 'up_invested': pos['UP']['invested'], - 'down_invested': pos['DOWN']['invested'], - 'total_invested': pos['UP']['invested'] + pos['DOWN']['invested'], - 'pnl': 0.0, # Unknown - will calculate on next run + 'up_contracts': pos['UP']['total_shares'], + 'down_contracts': pos['DOWN']['total_shares'], + 'up_invested': pos['UP']['total_invested'], + 'down_invested': pos['DOWN']['total_invested'], + 'total_invested': pos['UP']['total_invested'] + pos['DOWN']['total_invested'], + 'pnl': 0.0, 'winner': 'UNKNOWN', 'closed_at': int(time.time()), 'btc_start': pos.get('btc_start', 0), - 'btc_final': 0, # Unknown - 'entries_count': pos.get('entries_count', 0), - 'status': 'EMERGENCY_SAVE' # Mark as emergency + 'btc_final': 0, + 'entries_count': len(pos.get('all_entries', [])), + 'status': 'EMERGENCY_SAVE' } trader._log_trade(trade) saved_count += 1 @@ -221,13 +225,13 @@ def run_manual_redeem(): # Load environment from 4coins_live from dotenv import load_dotenv from pathlib import Path - env_path = Path("/root/4coins_live/.env") + env_path = _PROJECT_ROOT / ".env" load_dotenv(env_path, override=True) # Import and run redeemall with auto-confirm import redeemall print("[REDEEM] Starting automatic redemption...") - print("[REDEEM] Using wallet from: /root/4coins_live/.env") + print("[REDEEM] Using wallet from: .env") print() redeemall.main(auto_confirm=True) @@ -415,6 +419,9 @@ def main(args=None): # ═══════════════════════════════════════════════════════════ + # Apply proxy patch to websocket-client + patch_websocket_if_needed() + # Initialize data feed (shared across all strategies) print("[SYSTEM] Initializing multi-market data feed...") data_feed = DataFeed(config) @@ -499,12 +506,12 @@ def main(args=None): # Generate chart path (unique name to avoid conflicts) import uuid - chart_path = f"/root/4coins_live/logs/pnl_chart_on_demand_{uuid.uuid4().hex[:8]}.png" + chart_path = str(_LOG_DIR / f"pnl_chart_on_demand_{uuid.uuid4().hex[:8]}.png") print(f"[TELEGRAM CMD] 📊 Chart request received") print(f"[TELEGRAM CMD] Chart path: {chart_path}") print(f"[TELEGRAM CMD] COINS list: {COINS}") - print(f"[TELEGRAM CMD] Log dir: /root/4coins_live/logs") + print(f"[TELEGRAM CMD] Log dir: {_LOG_DIR}") # Import chart generator from pnl_chart_generator import generate_pnl_chart @@ -513,7 +520,7 @@ def main(args=None): # NOTE: We don't check total_completed_markets because it resets after restart # Instead, generate_pnl_chart will check actual files and return False if no data print(f"[TELEGRAM CMD] Calling generate_pnl_chart()...") - result = generate_pnl_chart('/root/4coins_live/logs', COINS, chart_path) + result = generate_pnl_chart(str(_LOG_DIR), COINS, chart_path) print(f"[TELEGRAM CMD] generate_pnl_chart() returned: {result}") if not result: @@ -535,7 +542,7 @@ def main(args=None): # This works correctly after bot restart actual_markets_count = 0 for coin in COINS: - trades_file = Path(f"/root/4coins_live/logs/late_v3_{coin}/trades.jsonl") + trades_file = _LOG_DIR / f"late_v3_{coin}" / "trades.jsonl" if trades_file.exists(): try: with open(trades_file, 'r') as f: @@ -598,7 +605,7 @@ def main(args=None): 'ids': 'polygon-ecosystem-token', 'vs_currencies': 'usd' } - response = requests.get(url, params=params, timeout=5) + response = requests.get(url, params=params, timeout=5, proxies=get_session_proxies()) if response.status_code == 200: data = response.json() @@ -640,7 +647,7 @@ def main(args=None): } print(f"[POSITIONS API] Fetching positions for {wallet[:6]}...{wallet[-4:]}") - response = requests.get(url, params=params, timeout=10) + response = requests.get(url, params=params, timeout=10, proxies=get_session_proxies()) if response.status_code == 200: positions = response.json() @@ -1305,9 +1312,9 @@ def main(args=None): if total_completed_markets - last_chart_at >= CHART_INTERVAL: print(f"[CHART] {total_completed_markets} markets completed, generating PnL chart...") - chart_path = f"/root/4coins_live/logs/pnl_chart_{total_completed_markets}.png" + chart_path = str(_LOG_DIR / f"pnl_chart_{total_completed_markets}.png") from pnl_chart_generator import generate_pnl_chart - if generate_pnl_chart('/root/4coins_live/logs', COINS, chart_path): + if generate_pnl_chart(str(_LOG_DIR), COINS, chart_path): caption = f"📊 PnL Chart - {total_completed_markets} Markets Completed" if notifier.send_photo(chart_path, caption): print(f"[CHART] ✓ Sent to Telegram successfully") @@ -1319,13 +1326,13 @@ def main(args=None): pnl_sign = "+" if result['pnl'] >= 0 else "" print(f"[{strategy_name:30s}] Closed {prev_market}: {pnl_sign}${result['pnl']:,.2f}") - elif redeem_amount > 0: + elif amount > 0: # ═══════════════════════════════════════════════════════════ # 🔥 FIX: If close_market() returned None (position empty after restart) # but redeem was successful, create minimal trade record from orders # This ensures ALL natural closes appear in dashboard! # ═══════════════════════════════════════════════════════════ - print(f"[{strategy_name}] Position was empty but redeem successful (${redeem_amount:.2f})") + print(f"[{strategy_name}] Position was empty but redeem successful (${amount:.2f})") print(f"[{strategy_name}] Creating trade record from orders for dashboard...") try: @@ -1354,7 +1361,7 @@ def main(args=None): if total_cost > 0: # Create minimal trade record - pnl = redeem_amount - total_cost + pnl = amount - total_cost roi_pct = (pnl / total_cost * 100) if total_cost > 0 else 0 minimal_trade = { @@ -1365,7 +1372,7 @@ def main(args=None): 'pnl': pnl, 'roi_pct': roi_pct, 'total_cost': total_cost, - 'payout': redeem_amount, + 'payout': amount, 'winner_ratio': 100.0, # Unknown 'total_entries': 0, # Unknown 'up_entries': 0, @@ -1391,8 +1398,6 @@ def main(args=None): pnl_sign = "+" if pnl >= 0 else "" print(f"[{strategy_name:30s}] Reconstructed {prev_market}: {pnl_sign}${pnl:,.2f} (from redeem)") - else: - print(f"[{strategy_name}] No buy orders found in logs, skipping reconstruction") except Exception as e: print(f"[{strategy_name}] Warning: Could not reconstruct trade: {e}") except Exception as e: @@ -1627,12 +1632,12 @@ def main(args=None): if total_completed_markets - last_chart_at >= CHART_INTERVAL: print(f"[CHART] {total_completed_markets} markets completed, generating PnL chart...") - chart_path = f"/root/4coins_live/logs/pnl_chart_{total_completed_markets}.png" + chart_path = str(_LOG_DIR / f"pnl_chart_{total_completed_markets}.png") # Import chart generator from pnl_chart_generator import generate_pnl_chart - if generate_pnl_chart('/root/4coins_live/logs', COINS, chart_path): + if generate_pnl_chart(str(_LOG_DIR), COINS, chart_path): # Send to Telegram caption = f"📊 PnL Chart - {total_completed_markets} Markets Completed" if notifier.send_photo(chart_path, caption): @@ -1723,12 +1728,12 @@ def main(args=None): if total_completed_markets - last_chart_at >= CHART_INTERVAL: print(f"[CHART] {total_completed_markets} markets completed, generating PnL chart...") - chart_path = f"/root/4coins_live/logs/pnl_chart_{total_completed_markets}.png" + chart_path = str(_LOG_DIR / f"pnl_chart_{total_completed_markets}.png") # Import chart generator from pnl_chart_generator import generate_pnl_chart - if generate_pnl_chart('/root/4coins_live/logs', COINS, chart_path): + if generate_pnl_chart(str(_LOG_DIR), COINS, chart_path): # Send to Telegram caption = f"📊 PnL Chart - {total_completed_markets} Markets Completed" if notifier.send_photo(chart_path, caption): diff --git a/up-down-spread-bot/src/order_executor.py b/up-down-spread-bot/src/order_executor.py index 5e5a001..cf6903a 100644 --- a/up-down-spread-bot/src/order_executor.py +++ b/up-down-spread-bot/src/order_executor.py @@ -23,6 +23,8 @@ import logging from trade_logger import log_buy_attempt, log_buy_result, log_sell_attempt, log_sell_result import threading +from proxy_util import get_session_proxies + # 🔥 GLOBAL: Blocked markets per-coin (race condition protection) # Markets in this dict CANNOT receive new buy orders (stop-loss/flip-stop active) # Structure: {'btc': set(), 'eth': set(), 'sol': set(), 'xrp': set()} @@ -1914,8 +1916,8 @@ class OrderExecutor: payload = { "chat_id": chat_id, "text": message - } - requests.post(url, json=payload, timeout=5) +} + requests.post(url, json=payload, timeout=5, proxies=get_session_proxies()) except Exception as e: print(f"[EXECUTOR] ⚠️ Telegram alert failed: {e}") @@ -2030,7 +2032,7 @@ class OrderExecutor: "parse_mode": "HTML" } - response = requests.post(url, json=payload, timeout=5) + response = requests.post(url, json=payload, timeout=5, proxies=get_session_proxies()) if response.status_code == 200: print(f"[EXECUTOR] 📱 Telegram alert sent") diff --git a/up-down-spread-bot/src/pnl_chart_generator.py b/up-down-spread-bot/src/pnl_chart_generator.py index c451475..9ab1d59 100644 --- a/up-down-spread-bot/src/pnl_chart_generator.py +++ b/up-down-spread-bot/src/pnl_chart_generator.py @@ -9,12 +9,15 @@ from pathlib import Path from typing import Dict, List from datetime import datetime +_LOG_DIR = Path(__file__).resolve().parent.parent / "logs" +_LOG_DIR.mkdir(parents=True, exist_ok=True) + def load_trades(log_dir: str, coins: List[str]) -> Dict[str, List[Dict]]: """Load all trades from JSONL files for each coin""" all_trades = {} # DEBUG: Write to file too - debug_file = "/root/4coins_live/logs/chart_debug.log" + debug_file = str(_LOG_DIR / "chart_debug.log") with open(debug_file, 'a') as f: f.write(f"\n{'='*80}\n") f.write(f"[CHART DEBUG] {datetime.now()} load_trades called\n") @@ -94,7 +97,7 @@ def generate_pnl_chart(log_dir: str, coins: List[str], output_path: str) -> bool # Take only final entries with real PnL! trade_map = {} # {coin_market_slug: trade_data} - debug_file = "/root/4coins_live/logs/chart_debug.log" + debug_file = str(_LOG_DIR / "chart_debug.log") with open(debug_file, 'a') as f: f.write(f"[CHART DEBUG] Starting deduplication...\n") @@ -134,7 +137,7 @@ def generate_pnl_chart(log_dir: str, coins: List[str], output_path: str) -> bool # Sort by close_time all_trades_timed.sort(key=lambda x: x['close_time']) - debug_file = "/root/4coins_live/logs/chart_debug.log" + debug_file = str(_LOG_DIR / "chart_debug.log") with open(debug_file, 'a') as f: f.write(f"[CHART DEBUG] Sorted {len(all_trades_timed)} trades\n") @@ -285,7 +288,7 @@ def generate_pnl_chart(log_dir: str, coins: List[str], output_path: str) -> bool # Tight layout and save plt.tight_layout() - debug_file = "/root/4coins_live/logs/chart_debug.log" + debug_file = str(_LOG_DIR / "chart_debug.log") with open(debug_file, 'a') as f: f.write(f"[CHART DEBUG] About to save chart to: {output_path}\n") @@ -299,7 +302,7 @@ def generate_pnl_chart(log_dir: str, coins: List[str], output_path: str) -> bool return True except Exception as e: - debug_file = "/root/4coins_live/logs/chart_debug.log" + debug_file = str(_LOG_DIR / "chart_debug.log") with open(debug_file, 'a') as f: f.write(f"[CHART ERROR] Exception: {str(e)}\n") import traceback diff --git a/up-down-spread-bot/src/polymarket_api.py b/up-down-spread-bot/src/polymarket_api.py index 1b63208..d38cc9d 100644 --- a/up-down-spread-bot/src/polymarket_api.py +++ b/up-down-spread-bot/src/polymarket_api.py @@ -6,6 +6,8 @@ import requests import json from typing import Optional, Dict +from proxy_util import get_session_proxies + GAMMA_API = "https://gamma-api.polymarket.com" def get_market_outcome(slug: str, timeout: int = 10) -> Dict: @@ -23,7 +25,7 @@ def get_market_outcome(slug: str, timeout: int = 10) -> Dict: """ try: url = f"{GAMMA_API}/events?slug={slug}" - resp = requests.get(url, timeout=timeout) + resp = requests.get(url, timeout=timeout, proxies=get_session_proxies()) resp.raise_for_status() events = resp.json() diff --git a/up-down-spread-bot/src/proxy_util.py b/up-down-spread-bot/src/proxy_util.py new file mode 100644 index 0000000..2dac8bf --- /dev/null +++ b/up-down-spread-bot/src/proxy_util.py @@ -0,0 +1,144 @@ +"""HTTP/HTTPS proxy helpers for websocket-client + requests. + +websocket-client's WebSocketApp has no native proxy support. +We monkey-patch the socket creation to route through HTTP CONNECT. +""" +import base64 +import os +import socket +from pathlib import Path +from typing import Optional +from urllib.parse import urlparse + +_ENV_KEYS = ("HTTPS_PROXY", "HTTP_PROXY", "https_proxy", "http_proxy") + + +def _normalize(raw: str) -> str: + raw = (raw or "").strip() + if not raw: + return "" + if raw.endswith("/"): + raw = raw[:-1] + if raw.startswith(("http://", "https://", "socks://", "socks4://", "socks5://")): + return raw + return "http://" + raw + + +def get_proxy_url() -> str: + for key in _ENV_KEYS: + val = os.getenv(key) + if val: + return _normalize(val) + return "" + + +def apply_proxy_env() -> str: + url = get_proxy_url() + if url: + for key in _ENV_KEYS: + os.environ.setdefault(key, url) + return url + + +PROXY_URL = apply_proxy_env() + + +def get_session_proxies() -> dict: + """Return {'http': ..., 'https': ...} for requests.get(proxies=...).""" + url = get_proxy_url() + if not url: + return {} + return {"http": url, "https": url} + + +def _connect_with_proxy(host: str, port: int, timeout: float = 15.0) -> socket.socket: + """HTTP CONNECT tunnel. Returns a non-blocking socket connected through proxy.""" + purl = get_proxy_url() + if not purl: + raise OSError("no proxy configured") + p = urlparse(purl) + ph, pp = p.hostname, p.port or 8080 + if not ph: + raise OSError(f"invalid proxy url: {purl}") + + raw = socket.create_connection((ph, pp), timeout=timeout) + try: + req = ( + f"CONNECT {host}:{port} HTTP/1.1\r\n" + f"Host: {host}:{port}\r\n" + f"Proxy-Connection: keep-alive\r\n" + ) + if p.username is not None: + token = base64.b64encode( + f"{p.username}:{p.password or ''}".encode() + ).decode() + req += f"Proxy-Authorization: Basic {token}\r\n" + req += "\r\n" + raw.sendall(req.encode()) + + raw.settimeout(timeout) + buf = b"" + while b"\r\n\r\n" not in buf: + chunk = raw.recv(4096) + if not chunk: + raise OSError("proxy closed during CONNECT") + buf += chunk + if len(buf) > 65536: + raise OSError("proxy CONNECT response too large") + + status = buf.split(b"\r\n", 1)[0].decode("latin1", "replace") + if "200" not in status: + raw.close() + raise OSError(f"proxy CONNECT failed: {status}") + + raw.settimeout(None) + raw.setblocking(False) + return raw + except Exception: + try: + raw.close() + except Exception: + pass + raise + + +def patch_websocket_connect() -> None: + """Monkey-patch websocket.WebSocketApp to use proxy CONNECT tunnel. + + Call once at startup if PROXY_URL is set. + """ + import websocket + + _orig_create_connection = websocket.create_connection + + def _patched_create_connection(url, timeout=socket.getdefaulttimeout(), **options): + if not get_proxy_url(): + return _orig_create_connection(url, timeout=timeout, **options) + parsed = urlparse(url) + host = parsed.hostname + port = parsed.port or (443 if parsed.scheme == "wss" else 80) + sock = _connect_with_proxy(host, port, timeout=timeout or 15.0) + # websocket.create_connection accepts sock= kwarg + options["sock"] = sock + return _orig_create_connection(url, timeout=timeout, **options) + + websocket.create_connection = _patched_create_connection + + +def patch_websocket_if_needed() -> None: + """Apply proxy patch to websocket-client if proxy is configured.""" + if get_proxy_url(): + patch_websocket_connect() + + +def get_project_root() -> Path: + """Return the project root (parent of src/).""" + return Path(__file__).resolve().parent.parent + + +def get_log_dir() -> Path: + """Return project logs/ directory.""" + root = get_project_root() + d = root / "logs" + d.mkdir(parents=True, exist_ok=True) + return d \ No newline at end of file diff --git a/up-down-spread-bot/src/simple_redeem_collector.py b/up-down-spread-bot/src/simple_redeem_collector.py index 90fe8c9..76a884b 100644 --- a/up-down-spread-bot/src/simple_redeem_collector.py +++ b/up-down-spread-bot/src/simple_redeem_collector.py @@ -8,6 +8,8 @@ import threading import requests from typing import Dict, List, Optional +from proxy_util import get_session_proxies + class SimpleRedeemCollector: """ @@ -212,7 +214,7 @@ class SimpleRedeemCollector: for attempt in range(1, self.api_max_retries + 1): try: - response = requests.get(url, params=params, timeout=self.api_timeout) + response = requests.get(url, params=params, timeout=self.api_timeout, proxies=get_session_proxies()) # ✅ SUCCESS if response.status_code == 200: diff --git a/up-down-spread-bot/src/telegram_notifier.py b/up-down-spread-bot/src/telegram_notifier.py index dfdcae0..593e658 100644 --- a/up-down-spread-bot/src/telegram_notifier.py +++ b/up-down-spread-bot/src/telegram_notifier.py @@ -6,13 +6,17 @@ import os import time import requests from datetime import timedelta +from pathlib import Path from threading import Thread, Lock from queue import Queue, Empty from typing import Dict from dotenv import load_dotenv -# Load environment variables from .env file -load_dotenv("/root/4coins_live/.env") +from proxy_util import get_session_proxies + +# Load project-level .env +_PROJ_ROOT = Path(__file__).resolve().parent.parent +load_dotenv(_PROJ_ROOT / ".env") class TelegramNotifier: @@ -115,7 +119,7 @@ class TelegramNotifier: "text": message, "parse_mode": "HTML", "disable_web_page_preview": True - }, timeout=5.0) + }, timeout=5.0, proxies=get_session_proxies()) return response.status_code == 200 @@ -255,7 +259,7 @@ Winner: {winner}""" 'parse_mode': 'HTML' } - response = requests.post(url, data=data, files=files, timeout=30) + response = requests.post(url, data=data, files=files, timeout=30, proxies=get_session_proxies()) if response.status_code == 200: self.sent_count += 1 @@ -342,7 +346,7 @@ Winner: {winner}""" 'allowed_updates': ['message', 'callback_query'] # Messages and button clicks } - response = requests.get(url, params=params, timeout=35) + response = requests.get(url, params=params, timeout=35, proxies=get_session_proxies()) # Reset error counter on successful connection consecutive_errors = 0 @@ -588,7 +592,7 @@ Winner: {winner}""" } } - response = requests.post(url, json=payload, timeout=10) + response = requests.post(url, json=payload, timeout=10, proxies=get_session_proxies()) if response.status_code == 200: data = response.json() @@ -630,7 +634,7 @@ Winner: {winner}""" if buttons: payload["reply_markup"] = {"inline_keyboard": buttons} - response = requests.post(url, json=payload, timeout=10) + response = requests.post(url, json=payload, timeout=10, proxies=get_session_proxies()) if response.status_code == 200: print(f"[TELEGRAM] ✅ Message edited (ID: {message_id})") @@ -666,7 +670,7 @@ Winner: {winner}""" "show_alert": show_alert } - response = requests.post(url, json=payload, timeout=10) + response = requests.post(url, json=payload, timeout=10, proxies=get_session_proxies()) return response.status_code == 200 except Exception as e: @@ -694,7 +698,7 @@ Winner: {winner}""" 'disable_web_page_preview': True } - response = requests.post(url, json=data, timeout=10) + response = requests.post(url, json=data, timeout=10, proxies=get_session_proxies()) if response.status_code == 200: self.sent_count += 1 diff --git a/up-down-spread-bot/src/trader.py b/up-down-spread-bot/src/trader.py index a54ba4d..cb83d1f 100644 --- a/up-down-spread-bot/src/trader.py +++ b/up-down-spread-bot/src/trader.py @@ -528,8 +528,8 @@ class Trader: # Now we can trade new market without limits! # ═══════════════════════════════════════════════════════════ try: - if order_executor and hasattr(order_executor, 'safety'): - order_executor.safety.reset_market(market_slug) + if _order_executor and hasattr(_order_executor, 'safety'): + _order_executor.safety.reset_market(market_slug) except Exception as reset_err: print(f"[TRADER] ⚠ Failed to reset market tracking: {reset_err}") @@ -816,8 +816,8 @@ class Trader: # Now we can trade new market without limits! # ═══════════════════════════════════════════════════════════ try: - if order_executor and hasattr(order_executor, 'safety'): - order_executor.safety.reset_market(market_slug) + if _order_executor and hasattr(_order_executor, 'safety'): + _order_executor.safety.reset_market(market_slug) except Exception as reset_err: print(f"[TRADER] ⚠ Failed to reset market tracking: {reset_err}")