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}")