全面完成代理

This commit is contained in:
2026-07-27 19:22:46 +08:00
parent 4cc94bf316
commit 0cef4d7f4c
19 changed files with 800 additions and 340 deletions
@@ -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")
@@ -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}%")
+79 -52
View File
@@ -1,4 +1,8 @@
#!/usr/bin/env python3 #!/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 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]") console.print("[green]✓ Order executor: simulation (no CLOB)[/green]")
else: else:
# User WebSocket for order tracking (CRITICAL for fill confirmation!) # Executor first — may replace stale env L2 keys with derived creds
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
)
self._user_ws_task = None self._user_ws_task = None
self.user_ws = None
self.executor = OrderExecutor( self.executor = OrderExecutor(
private_key=self.config.polymarket.private_key, private_key=self.config.polymarket.private_key,
@@ -1632,7 +1632,7 @@ class LiveTradingBot:
chain_id=self.config.polymarket.chain_id, chain_id=self.config.polymarket.chain_id,
signature_type=self.config.polymarket.signature_type, signature_type=self.config.polymarket.signature_type,
funder_address=self.config.polymarket.funder_address or None, funder_address=self.config.polymarket.funder_address or None,
user_ws=self.user_ws, user_ws=None,
simulation_mode=False, simulation_mode=False,
) )
@@ -1640,14 +1640,22 @@ class LiveTradingBot:
console.print("[red]Failed to initialize order executor[/red]") console.print("[red]Failed to initialize order executor[/red]")
return False 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]") console.print("[yellow]Starting User WebSocket for order tracking...[/yellow]")
self._user_ws_task = asyncio.create_task(self.user_ws.connect()) self._user_ws_task = asyncio.create_task(self.user_ws.connect())
await asyncio.sleep(1) await asyncio.sleep(2)
if self.user_ws.connected: if self.user_ws.connected:
console.print("[green]User WebSocket connected - order tracking active[/green]") console.print("[green]User WebSocket connected - order tracking active[/green]")
logger.info("User WebSocket connected for order fill tracking") logger.info("User WebSocket connected for order fill tracking")
else: 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") logger.warning("User WebSocket not yet connected")
# Hedge manager # Hedge manager
@@ -1764,7 +1772,8 @@ class LiveTradingBot:
if not market.get("closed", True): if not market.get("closed", True):
return await self._setup_market(market) return await self._setup_market(market)
except Exception as e: 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 continue
return False return False
@@ -1812,13 +1821,17 @@ class LiveTradingBot:
down_token_id = tokens[1] down_token_id = tokens[1]
if not up_token_id or not down_token_id: 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 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", "") end_str = market.get("end_date_iso") or market.get("endDate", "")
try: try:
end_time = datetime.fromisoformat(end_str.replace("Z", "+00:00")) end_time = datetime.fromisoformat(end_str.replace("Z", "+00:00"))
end_timestamp = end_time.timestamp() end_timestamp = end_time.timestamp()
except: except Exception:
end_timestamp = time.time() + self.config.market.duration_sec end_timestamp = time.time() + self.config.market.duration_sec
slug = market.get("slug", "") slug = market.get("slug", "")
@@ -1839,7 +1852,8 @@ class LiveTradingBot:
logger.info(f" DOWN token: {down_token_id[:40]}...") logger.info(f" DOWN token: {down_token_id[:40]}...")
self.stats.new_market(self.state.slug) 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: if self.user_ws:
self.user_ws.clear_token_fills() # Reset WS fill buffer for new market self.user_ws.clear_token_fills() # Reset WS fill buffer for new market
@@ -2066,15 +2080,15 @@ class LiveTradingBot:
self.dashboard.hedge_flash = True self.dashboard.hedge_flash = True
hedge_cost = hedge_result.contracts * hedge_result.price hedge_cost = hedge_result.contracts * hedge_result.price
hsim = "🎮 <b>[SIMULATION]</b>\n" if self.config.simulation.enabled else "" hsim = "🎮 <b>[模拟模式]</b>\n" if self.config.simulation.enabled else ""
await self.telegram.send_message( await self.telegram.send_message(
f"{hsim}" f"{hsim}"
f"🛡️ <b>Hedge Order Placed (GTD)</b>\n" f"🛡️ <b>对冲单已挂 (GTD)</b>\n"
f"📦 {hedge_result.contracts} contracts @ ${hedge_result.price}\n" f"📦 {hedge_result.contracts} @ ${hedge_result.price:.4f}\n"
f"💰 Cost: ${hedge_cost:.2f}\n" f"💰 花费: ${hedge_cost:.2f}\n"
f"🔖 Order ID: {hedge_result.order_id[:20]}...\n" f"🔖 订单ID: {hedge_result.order_id[:20]}...\n"
f"📋 Status: LIVE (passive)\n" f"📋 状态: 挂单中 (被动成交)\n"
f"🔄 Attempts: {hedge_result.attempts}" f"🔄 重试: {hedge_result.attempts}"
) )
# Register WebSocket handler for hedge fills # Register WebSocket handler for hedge fills
@@ -2083,9 +2097,9 @@ class LiveTradingBot:
logger.info(f"GTD hedge placed: {hedge_result.contracts} @ ${hedge_result.price}") logger.info(f"GTD hedge placed: {hedge_result.contracts} @ ${hedge_result.price}")
else: else:
await self.telegram.send_message( await self.telegram.send_message(
f"⚠️ <b>Hedge Failed</b>\n" f"⚠️ <b>对冲单失败</b>\n"
f"{hedge_result.error}\n" f"{hedge_result.error}\n"
f"🔄 Attempts: {hedge_result.attempts}" f"🔄 重试: {hedge_result.attempts}"
) )
logger.error(f"Hedge failed: {hedge_result.error}") logger.error(f"Hedge failed: {hedge_result.error}")
else: else:
@@ -2150,11 +2164,11 @@ class LiveTradingBot:
self.dashboard.entry_flash = True self.dashboard.entry_flash = True
await self.telegram.send_message( await self.telegram.send_message(
f"🔄 <b>Timeout Recovery!</b>\n" f"🔄 <b>超时恢复!</b>\n"
f"Order filled despite HTTP timeout.\n" f"HTTP超时但订单已成交\n"
f"📊 {token_name} {rec_contracts} @ ${rec_price:.4f}\n" f"📊 {token_name} {rec_contracts} @ ${rec_price:.4f}\n"
f"💰 Cost: ${rec_cost:.2f}\n" f"💰 花费: ${rec_cost:.2f}\n"
f"Market: {self.state.slug}" f"市场: {self.state.slug}"
) )
await self.telegram.notify_entry( await self.telegram.notify_entry(
@@ -2179,22 +2193,22 @@ class LiveTradingBot:
if hedge_result.success: if hedge_result.success:
self.dashboard.hedge_flash = True self.dashboard.hedge_flash = True
hedge_cost = hedge_result.contracts * hedge_result.price hedge_cost = hedge_result.contracts * hedge_result.price
hsim2 = "🎮 <b>[SIMULATION]</b>\n" if self.config.simulation.enabled else "" hsim2 = "🎮 <b>[模拟模式]</b>\n" if self.config.simulation.enabled else ""
await self.telegram.send_message( await self.telegram.send_message(
f"{hsim2}" f"{hsim2}"
f"🛡️ <b>Hedge Order Placed (GTD)</b>\n" f"🛡️ <b>对冲单已挂 (GTD)</b>\n"
f"📦 {hedge_result.contracts} contracts @ ${hedge_result.price}\n" f"📦 {hedge_result.contracts} @ ${hedge_result.price:.4f}\n"
f"💰 Cost: ${hedge_cost:.2f}\n" f"💰 花费: ${hedge_cost:.2f}\n"
f"🔖 Order ID: {hedge_result.order_id[:20]}...\n" f"🔖 订单ID: {hedge_result.order_id[:20]}...\n"
f"📋 Status: LIVE (passive)\n" f"📋 状态: 挂单中 (被动成交)\n"
f"🔄 Attempts: {hedge_result.attempts}" f"🔄 重试: {hedge_result.attempts}"
) )
self._register_hedge_ws_handler() self._register_hedge_ws_handler()
logger.info(f"GTD hedge placed after recovery: {hedge_result.contracts} @ ${hedge_result.price}") logger.info(f"GTD hedge placed after recovery: {hedge_result.contracts} @ ${hedge_result.price}")
else: else:
await self.telegram.send_message( await self.telegram.send_message(
f"⚠️ <b>Hedge Failed (after recovery)</b>\n" f"⚠️ <b>对冲失败 (恢复后)</b>\n"
f"{hedge_result.error}" f"{hedge_result.error}"
) )
else: else:
@@ -2207,11 +2221,11 @@ class LiveTradingBot:
self.stats.block_entry("Network timeout - no fill detected via WS. Blocking re-entry.") self.stats.block_entry("Network timeout - no fill detected via WS. Blocking re-entry.")
signal_logger.error("🛑 ENTRY BLOCKED: Timeout + no WS fill detected") signal_logger.error("🛑 ENTRY BLOCKED: Timeout + no WS fill detected")
await self.telegram.send_message( await self.telegram.send_message(
f"⚠️ <b>TIMEOUT — No Fill Detected</b>\n" f"⚠️ <b>超时 — 未检测到成交</b>\n"
f"Order status unknown after timeout.\n" f"HTTP超时后订单状态未知\n"
f"WebSocket recovery found nothing.\n" f"WebSocket恢复未找到成交\n"
f"Re-entry blocked.\n" f"禁止重新入场\n"
f"Market: {self.state.slug}" f"市场: {self.state.slug}"
) )
def _register_hedge_ws_handler(self): def _register_hedge_ws_handler(self):
@@ -2253,17 +2267,17 @@ class LiveTradingBot:
self.dashboard.hedge_flash = True self.dashboard.hedge_flash = True
await self.telegram.send_message( await self.telegram.send_message(
f"✅ <b>Hedge FULLY Filled!</b>\n" f"✅ <b>对冲已全部成交!</b>\n"
f"📦 {filled} contracts @ ${price}\n" f"📦 {filled} @ ${price:.4f}\n"
f"🛡️ Position fully protected" f"🛡️ 仓位已完全保护"
) )
logger.info(f"Hedge fully filled: {filled} contracts") logger.info(f"Hedge fully filled: {filled} contracts")
else: else:
# Partial fill # Partial fill
await self.telegram.send_message( await self.telegram.send_message(
f"🛡️ <b>Hedge Partial Fill</b>\n" f"🛡️ <b>对冲部分成交</b>\n"
f"📦 +{size} contracts @ ${price}\n" f"📦 +{size} @ ${price:.4f}\n"
f"📊 Progress: {filled}/{total}" f"📊 进度: {filled}/{total}"
) )
logger.info(f"Hedge partial fill: +{size}, total {filled}/{total}") logger.info(f"Hedge partial fill: +{size}, total {filled}/{total}")
@@ -2313,6 +2327,16 @@ class LiveTradingBot:
signal_logger.info("=" * 60) signal_logger.info("=" * 60)
logger.info(f"Position closed: {status}, PnL: ${record.pnl:+.2f}") 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): async def run_session(self):
"""Run single market session with dashboard.""" """Run single market session with dashboard."""
@@ -2326,7 +2350,7 @@ class LiveTradingBot:
order_task: Optional[asyncio.Task] = None order_task: Optional[asyncio.Task] = None
try: 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: while self.running:
# Update dashboard (никогда не блокируется) # Update dashboard (никогда не блокируется)
live.update(self.dashboard.render()) live.update(self.dashboard.render())
@@ -2364,7 +2388,7 @@ class LiveTradingBot:
console.print("\n[yellow]Market ended![/yellow]") console.print("\n[yellow]Market ended![/yellow]")
break break
await asyncio.sleep(0.25) await asyncio.sleep(1)
finally: finally:
# Cancel any running order tasks # Cancel any running order tasks
for task in [order_task]: for task in [order_task]:
@@ -2417,12 +2441,15 @@ class LiveTradingBot:
sim_note = "" sim_note = ""
if self.config.simulation.enabled: if self.config.simulation.enabled:
sim_note = "🎮 <b>SIMULATION MODE</b> — no real orders\n" sim_note = "🎮 <b>模拟模式 — 不下真实订单</b>\n"
await self.telegram.send_message( await self.telegram.send_message(
f"{sim_note}" f"{sim_note}"
f"🤖 <b>Bot Started</b>\n" f"🤖 <b>机器人启动</b>\n"
f"Strategy: ${self.config.entry.bet_amount_usd} per trade\n" f"策略: ${self.config.entry.bet_amount_usd}/笔\n"
f"Hedge: {'enabled' if self.config.hedge.enabled else 'disabled'}" 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: try:
@@ -2462,7 +2489,7 @@ class LiveTradingBot:
except: except:
pass pass
await self.telegram.send_message("🛑 Bot stopped") await self.telegram.send_message("🛑 机器人已停止")
await self.telegram.close() await self.telegram.close()
console.print("[green]Bot stopped.[/green]") console.print("[green]Bot stopped.[/green]")
@@ -1,7 +1,7 @@
# BTC 15-min Live Trading Bot - Dependencies # BTC 15-min Live Trading Bot - Dependencies
# Polymarket SDK # Polymarket SDK
py-clob-client>=0.16.0 py-clob-client-v2>=1.0.0
# Web3 for blockchain interactions # Web3 for blockchain interactions
web3>=6.0.0 web3>=6.0.0
@@ -312,9 +312,9 @@ class AsyncAutoRedeemer:
def _redeem_position_sync(self, position: Dict) -> bool: def _redeem_position_sync(self, position: Dict) -> bool:
"""Redeem a single position (sync, runs in thread).""" """Redeem a single position (sync, runs in thread)."""
import fcntl
import time import time
import os
condition_id = position["condition_id"] condition_id = position["condition_id"]
up_balance = position["up_balance"] up_balance = position["up_balance"]
down_balance = position["down_balance"] down_balance = position["down_balance"]
@@ -327,11 +327,15 @@ class AsyncAutoRedeemer:
logger.warning(f"Skipping {position['slug']} - oracle not resolved") logger.warning(f"Skipping {position['slug']} - oracle not resolved")
return False return False
# File lock # File lock (best-effort; fcntl is Unix-only, skip on Windows)
lock_file = "/tmp/btc_live_redeem.lock" lock_file = os.path.join(os.path.dirname(__file__), "..", ".redeem.lock")
self._lock_fd = None
try: try:
import fcntl
self._lock_fd = open(lock_file, 'w') self._lock_fd = open(lock_file, 'w')
fcntl.flock(self._lock_fd, fcntl.LOCK_EX | fcntl.LOCK_NB) fcntl.flock(self._lock_fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
except ImportError:
pass # Windows — skip file lock
except (IOError, OSError): except (IOError, OSError):
logger.warning("Another redeem in progress, skipping") logger.warning("Another redeem in progress, skipping")
return False return False
@@ -354,7 +358,11 @@ class AsyncAutoRedeemer:
finally: finally:
if self._lock_fd: if self._lock_fd:
try: try:
import fcntl
fcntl.flock(self._lock_fd, fcntl.LOCK_UN) fcntl.flock(self._lock_fd, fcntl.LOCK_UN)
except ImportError:
pass
try:
self._lock_fd.close() self._lock_fd.close()
except: except:
pass pass
@@ -553,8 +561,8 @@ class AsyncAutoRedeemer:
if self.telegram: if self.telegram:
try: try:
await self.telegram.send_message( await self.telegram.send_message(
f"💰 Redeemed: {position['slug']}\n" f"💰 已赎回: {position['slug']}\n"
f"Value: ${value:.2f} USDC" f"价值: ${value:.2f} USDC"
) )
except: except:
pass pass
@@ -171,8 +171,7 @@ class HedgeManager:
hedge_logger.info(f" Max Retries: {self.config.max_retries}") hedge_logger.info(f" Max Retries: {self.config.max_retries}")
hedge_logger.info("-" * 40) hedge_logger.info("-" * 40)
from py_clob_client.clob_types import OrderArgs, OrderType from py_clob_client_v2.clob_types import OrderArgsV2, OrderType
from py_clob_client.order_builder.constants import BUY
last_error = "" last_error = ""
expiration = str(int(time.time()) + 3600) # 1 hour, market resolves before this expiration = str(int(time.time()) + 3600) # 1 hour, market resolves before this
@@ -184,10 +183,10 @@ class HedgeManager:
# Create signed order # Create signed order
signed_order = await asyncio.to_thread( signed_order = await asyncio.to_thread(
self.executor._client.create_order, self.executor._client.create_order,
OrderArgs( OrderArgsV2(
price=self.config.hedge_price, price=self.config.hedge_price,
size=pos.contracts, size=pos.contracts,
side=BUY, side="BUY",
token_id=pos.opposite_token_id, token_id=pos.opposite_token_id,
expiration=expiration expiration=expiration
) )
@@ -23,9 +23,8 @@ from dataclasses import dataclass, field
from datetime import datetime from datetime import datetime
from typing import Optional, Dict, Any, Tuple, List from typing import Optional, Dict, Any, Tuple, List
from py_clob_client.client import ClobClient from py_clob_client_v2.client import ClobClient
from py_clob_client.clob_types import OrderArgs, ApiCreds, OrderType from py_clob_client_v2.clob_types import OrderArgsV2, MarketOrderArgsV2, ApiCreds, OrderType
from py_clob_client.order_builder.constants import BUY
logger = logging.getLogger("btc_live.executor") logger = logging.getLogger("btc_live.executor")
@@ -130,8 +129,38 @@ class OrderExecutor:
signature_type=self.signature_type, signature_type=self.signature_type,
funder=self.funder_address 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_creds = ApiCreds(
api_key=self.api_key, api_key=self.api_key,
api_secret=self.api_secret, api_secret=self.api_secret,
@@ -358,61 +387,48 @@ class OrderExecutor:
self, self,
token_id: str, token_id: str,
price: float, price: float,
size: int amount: int
) -> Tuple[bool, str, Dict]: ) -> Tuple[bool, str, Dict]:
""" """
Place a FAK (Fill-And-Kill) order. Place a FAK market order (v2: market order, amount in USD cents).
Args: Args:
token_id: Token to buy token_id: Token to buy
price: Order price price: Price cap (max price willing to pay)
size: Number of contracts amount: USD amount to spend (max 2 decimals)
Returns: Returns:
Tuple of (success, order_id, response) Tuple of (success, order_id, response)
""" """
if not self._client: if not self._client:
order_logger.error("PLACE_ORDER: Client not initialized") order_logger.error("PLACE_ORDER: Client not initialized")
return False, "", {"error": "Client not initialized"} return False, "", {"error": "Client not initialized"}
order_value = size * price
order_logger.info("-" * 50) order_logger.info("-" * 50)
order_logger.info(f"PLACING ORDER") order_logger.info(f"PLACING ORDER")
order_logger.info(f" Token: {token_id[:30]}...") order_logger.info(f" Token: {token_id[:30]}...")
order_logger.info(f" Side: BUY") order_logger.info(f" Side: BUY")
order_logger.info(f" Price: {price:.4f}") order_logger.info(f" Price Cap: {price:.4f}")
order_logger.info(f" Size: {size} contracts") order_logger.info(f" Amount: ${amount:.2f}")
order_logger.info(f" Value: ${order_value:.2f}") order_logger.info(f" Type: FAK (market order)")
order_logger.info(f" Type: FAK (Fill-And-Kill)")
start_time = time.time() start_time = time.time()
try: 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() post_start = time.time()
response = await asyncio.to_thread( response = await asyncio.to_thread(
self._client.post_order, self._client.create_and_post_market_order,
signed_order, MarketOrderArgsV2(
OrderType.FAK token_id=token_id,
amount=float(amount),
side="BUY",
price=price,
),
order_type=OrderType.FAK,
) )
post_elapsed = (time.time() - post_start) * 1000 post_elapsed = (time.time() - post_start) * 1000
total_elapsed = (time.time() - start_time) * 1000 total_elapsed = (time.time() - start_time) * 1000
# Handle response
if isinstance(response, dict): if isinstance(response, dict):
success = response.get("success", False) success = response.get("success", False)
order_id = response.get("orderID", "") order_id = response.get("orderID", "")
@@ -427,9 +443,9 @@ class OrderExecutor:
error_msg = getattr(response, 'errorMsg', "") error_msg = getattr(response, 'errorMsg', "")
taking_amount = getattr(response, 'takingAmount', "") taking_amount = getattr(response, 'takingAmount', "")
making_amount = getattr(response, 'makingAmount', "") making_amount = getattr(response, 'makingAmount', "")
self.orders_placed += 1 self.orders_placed += 1
order_logger.info(f"ORDER RESPONSE:") order_logger.info(f"ORDER RESPONSE:")
order_logger.info(f" Success: {success}") order_logger.info(f" Success: {success}")
order_logger.info(f" Order ID: {order_id[:40] if order_id else 'N/A'}...") 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}") order_logger.info(f" Making Amount: {making_amount}")
if error_msg: if error_msg:
order_logger.warning(f" Error: {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) order_logger.info("-" * 50)
logger.info(f"Order placed: {success}, ID: {order_id[:20] if order_id else 'N/A'}...") 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" Remaining needed: {remaining}")
order_logger.info(f" Order size: {order_size}") order_logger.info(f" Order size: {order_size}")
logger.info(f"Attempt {attempt}: placing {order_size} contracts @ {order_price:.2f}") logger.info(f"Attempt {attempt}: placing ${order_size * order_price:.2f} @ {order_price:.2f} cap")
# Place order # Place FAK market order (v2: amount=USD, price=cap)
success, order_id, response = await self.place_fak_order( success, order_id, response = await self.place_fak_order(
token_id, token_id,
order_price, order_price,
order_size int(order_size * order_price * 100) / 100 # USD, max 2 decimals
) )
# Запоминаем order_id для отслеживания # Запоминаем order_id для отслеживания
@@ -184,14 +184,14 @@ class TelegramNotifier:
simulation: bool = False, simulation: bool = False,
): ):
"""Send entry notification.""" """Send entry notification."""
mode = "🎮 <b>[SIMULATION]</b>\n" if simulation else "" mode = "🎮 <b>[模拟模式]</b>\n" if simulation else ""
text = ( text = (
f"{mode}" f"{mode}"
f"🟢 <b>ENTRY</b>\n" f"🟢 <b>开仓成功</b>\n"
f"📊 BTC {interval_minutes}min - {side}\n" f"📊 BTC {interval_minutes}分钟 - {side}\n"
f"💰 ${cost:.2f} @ {price:.2f}\n" f"💰 金额: ${cost:.2f} @ {price:.4f}\n"
f"📦 {contracts} contracts\n" f"📦 合约数: {contracts}\n"
f"🔄 {retries} retries" f"🔄 重试: {retries}"
) )
await self.send_message(text) await self.send_message(text)
@@ -203,10 +203,10 @@ class TelegramNotifier:
): ):
"""Send hedge notification.""" """Send hedge notification."""
text = ( text = (
f"🛡 <b>HEDGE</b>\n" f"🛡 <b>对冲已开</b>\n"
f"📦 {contracts} contracts @ ${price:.3f}\n" f"📦 {contracts} @ ${price:.4f}\n"
f"💰 Cost: ${cost:.2f}\n" f"💰 花费: ${cost:.2f}\n"
f"Position protected" f"仓位已保护"
) )
await self.send_message(text) await self.send_message(text)
@@ -222,11 +222,11 @@ class TelegramNotifier:
pnl_sign = "+" if pnl > 0 else "" pnl_sign = "+" if pnl > 0 else ""
text = ( text = (
f"🏁 <b>MARKET RESOLVED</b>\n" f"🏁 <b>市场结算</b>\n"
f"{emoji} Winner: <b>{winner}</b>\n" f"{emoji} 胜方: <b>{winner}</b>\n"
f"💵 P&L: {pnl_sign}${pnl:.2f}\n" f"💵 本次盈亏: {pnl_sign}${pnl:.2f}\n"
f"📈 Total: ${total_pnl:.2f}\n" f"📈 累计盈亏: ${total_pnl:.2f}\n"
f"📊 Win rate: {win_rate:.1%}" f"📊 胜率: {win_rate:.1%}"
) )
await self.send_message(text) await self.send_message(text)
+280 -109
View File
@@ -11,8 +11,9 @@ Docs: https://docs.polymarket.com/developers/CLOB/websocket/user-channel
import asyncio import asyncio
import json import json
import logging import logging
import time
import websockets import websockets
from typing import Optional, Dict, Callable, Any from typing import Optional, Dict, Callable, Any, List
from dataclasses import dataclass, field from dataclasses import dataclass, field
from datetime import datetime 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" 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 @dataclass
class OrderStatus: class OrderStatus:
@@ -42,10 +46,11 @@ class OrderStatus:
class UserWebSocket: class UserWebSocket:
""" """
WebSocket client for User Channel. WebSocket client for User Channel.
Tracks order placements and fills in real-time. 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 = ""): def __init__(self, api_key: str, api_secret: str = "", api_passphrase: str = ""):
self.api_key = api_key self.api_key = api_key
self.api_secret = api_secret self.api_secret = api_secret
@@ -55,123 +60,282 @@ class UserWebSocket:
self._connected = False self._connected = False
self._running = False self._running = False
self._orders: Dict[str, OrderStatus] = {} 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 # Callbacks
self._on_trade: Optional[Callable] = None self._on_trade: Optional[Callable] = None
self._on_order: 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): async def connect(self):
"""Connect to User Channel WebSocket.""" """Connect to User Channel WebSocket with auto-reconnect."""
try: self._running = True
self._running = True delay = 2.0
logger.info(f"Connecting to User WebSocket at {WS_URL}...") while self._running:
print(f" Connecting to {WS_URL}...") session_ok = False
try:
async with ws_connect( logger.info("Connecting to User WebSocket at %s...", WS_URL)
WS_URL,
**_ws_connect_kwargs(ping_interval=30, ping_timeout=10) async with ws_connect(
) as ws: WS_URL,
self._ws = ws **_ws_connect_kwargs(ping_interval=None, ping_timeout=None),
self._connected = True ) as ws:
logger.info("User WebSocket connected") self._ws = ws
print(" WebSocket connection established") self._connected = True
logger.info("User WebSocket TCP connected")
# Subscribe to user channel with auth object
subscribe_msg = { subscribe_msg = {
"type": "user", "auth": {
"auth": { "apiKey": self.api_key,
"apiKey": self.api_key, "secret": self.api_secret,
"secret": self.api_secret, "passphrase": self.api_passphrase,
"passphrase": self.api_passphrase },
"type": "user",
} }
} await ws.send(json.dumps(subscribe_msg))
await ws.send(json.dumps(subscribe_msg)) logger.info("User WebSocket auth/subscribe sent")
logger.info("Sent subscription message")
print(" Sent subscription message") # Auth failure = immediate close with no frame
# 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:
try: try:
msg = await asyncio.wait_for(ws.recv(), timeout=60) first_msg = await asyncio.wait_for(ws.recv(), timeout=8)
logger.debug(f"WS message: {msg[:100]}") logger.info("User WS first message: %s", str(first_msg)[:200])
await self._process_message(msg) await self._process_message(first_msg)
session_ok = True
delay = 2.0
except asyncio.TimeoutError: 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: try:
await ws.ping() timeout = max(1.0, 10.0 - (time.monotonic() - last_ping))
except Exception: 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 break
except websockets.exceptions.ConnectionClosed: except Exception as e:
logger.warning("User WebSocket connection closed") logger.error("Error receiving message: %s", e)
break break
except Exception as e:
logger.error(f"Error receiving message: {e}") self._connected = False
break self._ws = None
logger.info("User WebSocket disconnected")
except asyncio.CancelledError:
self._connected = False self._connected = False
logger.info("User WebSocket disconnected") self._ws = None
print(" WebSocket disconnected") raise
except Exception as e:
except Exception as e: logger.error("User WebSocket connection error: %s", e)
logger.error(f"User WebSocket connection error: {e}") self._connected = False
print(f" Connection error: {e}") self._ws = None
self._connected = False session_ok = False
raise
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): async def _process_message(self, msg: str):
"""Process an incoming WebSocket message.""" """Process an incoming WebSocket message."""
if msg == "PONG" or msg == "pong":
return
try: try:
data = json.loads(msg) data = json.loads(msg)
except json.JSONDecodeError: except json.JSONDecodeError:
logger.warning(f"Invalid JSON: {msg[:100]}") logger.warning("Invalid JSON: %s", str(msg)[:100])
return 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", "") msg_type = data.get("type", "")
event_type = data.get("event_type", "")
# Order updates # 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", "") 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 = self._orders[order_id]
order.status = msg_type order.status = msg_type or event_type or order.status
order.size_matched = int(float(data.get("size_matched", 0))) order.size_matched = max(order.size_matched, size_matched)
if msg_type == "UPDATE" or size_matched >= order.original_size > 0:
if msg_type == "MATCHED": if size_matched >= order.original_size:
order.status = "MATCHED" order.status = "MATCHED"
order.trades.append(data) if msg_type == "CANCELLATION":
order.status = "CANCELLED"
if self._on_order: else:
if asyncio.iscoroutinefunction(self._on_order): self._orders[order_id] = OrderStatus(
await self._on_order(order) order_id=order_id,
else: asset_id=str(data.get("asset_id", "")),
self._on_order(order) 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 # Trade updates
elif msg_type == "TRADE": elif event_type == "trade" or msg_type == "TRADE":
self._record_trade_fill(data)
if self._on_trade: if self._on_trade:
if asyncio.iscoroutinefunction(self._on_trade): if asyncio.iscoroutinefunction(self._on_trade):
await self._on_trade(data) await self._on_trade(data)
else: else:
self._on_trade(data) self._on_trade(data)
# Initial subscription response
elif msg_type in ("subscribed", "OK", "ok"): elif msg_type in ("subscribed", "OK", "ok"):
logger.info(f"Subscription confirmed: {msg[:150]}") logger.info("Subscription confirmed: %s", str(data)[:150])
else: 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): def register_order(self, order_id: str, asset_id: str, side: str, price: float, size: int):
"""Register an order we're tracking.""" """Register an order we're tracking."""
self._orders[order_id] = OrderStatus( self._orders[order_id] = OrderStatus(
@@ -181,35 +345,42 @@ class UserWebSocket:
price=price, price=price,
original_size=size original_size=size
) )
def get_order_status(self, order_id: str) -> Optional[OrderStatus]: def get_order_status(self, order_id: str) -> Optional[OrderStatus]:
"""Get current status for an order.""" """Get current status for an order."""
return self._orders.get(order_id) return self._orders.get(order_id)
async def wait_for_order_match(self, order_id: str, timeout: float = 10.0) -> bool: async def wait_for_order_match(self, order_id: str, timeout: float = 10.0) -> bool:
"""Wait until an order is matched or timeout.""" """Wait until an order is matched or timeout."""
start = asyncio.get_event_loop().time() order = await self.wait_for_fill(order_id, timeout=timeout)
while asyncio.get_event_loop().time() - start < timeout: if not order:
status = self._orders.get(order_id) return False
if status and (status.status == "MATCHED" or status.size_matched >= status.original_size): return order.status == "MATCHED" or (
return True order.original_size > 0 and order.size_matched >= order.original_size
await asyncio.sleep(0.2) )
return False
async def wait_for_order_place(self, order_id: str, timeout: float = 5.0) -> bool: async def wait_for_order_place(self, order_id: str, timeout: float = 5.0) -> bool:
"""Wait until an order placement is confirmed or timeout.""" """Wait until an order placement is confirmed or timeout."""
start = asyncio.get_event_loop().time() deadline = time.monotonic() + max(0.0, float(timeout))
while asyncio.get_event_loop().time() - start < timeout: while time.monotonic() < deadline:
status = self._orders.get(order_id) 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 return True
await asyncio.sleep(0.15) await asyncio.sleep(0.15)
return False return False
@property
def connected(self) -> bool:
return self._connected and self._ws is not None
@property @property
def is_connected(self) -> bool: 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): async def close(self):
"""Close the WebSocket connection.""" """Close the WebSocket connection."""
self._running = False self._running = False
@@ -227,4 +398,4 @@ class UserWebSocket:
await cm.__aexit__(None, None, None) await cm.__aexit__(None, None, None)
except Exception: except Exception:
pass pass
self._connected = False self._connected = False
+62 -51
View File
@@ -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) # ① 钱包私钥(必须)
# ============================================================================= # =============================================================================
# 你的 Polygon 钱包私钥,64 位十六进制字符 + 0x 前缀 = 66 字符
# 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
# #
# WARNING: Never share this key or commit it to git! # 获取方式:
# - MetaMask:设置 → 安全与隐私 → 导出私钥
# - Polymarketpolymarket.com/settings → 导出私钥
#
# ⚠️ 切勿泄露此密钥,切勿提交到 git!
PRIVATE_KEY=0x0000000000000000000000000000000000000000000000000000000000000000 PRIVATE_KEY=0x0000000000000000000000000000000000000000000000000000000000000000
# =============================================================================
# POLYGON NETWORK (REQUIRED)
# =============================================================================
# Polygon RPC endpoint # =============================================================================
# Free options: # ② Polygon 网络(必须)
# - https://polygon-rpc.com (public, rate limited) # =============================================================================
# - https://rpc.ankr.com/polygon (public, rate limited) # 免费 RPC 节点:
# - https://polygon-rpc.com(公共,有频率限制)
# - https://rpc.ankr.com/polygon(公共,有频率限制)
# #
# Recommended (private RPC): # 推荐使用私有 RPC(更稳定):
# - Alchemy: https://polygon-mainnet.g.alchemy.com/v2/YOUR_API_KEY # - Alchemyhttps://polygon-mainnet.g.alchemy.com/v2/YOUR_API_KEY
# - Infura: https://polygon-mainnet.infura.io/v3/YOUR_PROJECT_ID # - Infurahttps://polygon-mainnet.infura.io/v3/YOUR_PROJECT_ID
# - QuickNode: https://your-endpoint.quiknode.pro/YOUR_KEY/ # - QuickNodehttps://your-endpoint.quiknode.pro/YOUR_KEY/
# #
# Get free API key: # 免费 API 密钥获取:
# - Alchemy: https://www.alchemy.com/ # - Alchemyhttps://www.alchemy.com/
# - Ankr: https://www.ankr.com/rpc/ # - Ankrhttps://www.ankr.com/rpc/
RPC_URL=https://polygon-rpc.com RPC_URL=https://polygon-rpc.com
# Polygon chain ID (do not change) # Polygon 链 ID(不要修改)
CHAIN_ID=137 CHAIN_ID=137
# =============================================================================
# POLYMARKET API (REQUIRED)
# =============================================================================
# Polymarket CLOB API host (do not change) # =============================================================================
# ③ Polymarket API(必须)
# =============================================================================
# Polymarket CLOB API 地址(不要修改)
CLOB_HOST=https://clob.polymarket.com CLOB_HOST=https://clob.polymarket.com
# API Credentials # API 凭证
# How to get: # 获取方式:
# 1. Go to polymarket.com/settings # 1. 访问 polymarket.com/settings
# 2. Connect your wallet # 2. 连接钱包
# 3. Generate API credentials # 3. 生成 API 凭证(Generate API Credentials
# 4. Copy the values here # 4. 将生成的 Key / Secret / Passphrase 填入下方
# #
# Or generate programmatically using py-clob-client library # 或者用 py-clob-client 库编程生成
POLYMARKET_API_KEY=your_api_key_here POLYMARKET_API_KEY=your_api_key_here
POLYMARKET_API_SECRET=your_api_secret_here POLYMARKET_API_SECRET=your_api_secret_here
POLYMARKET_API_PASSPHRASE=your_api_passphrase_here POLYMARKET_API_PASSPHRASE=your_api_passphrase_here
# =============================================================================
# TELEGRAM NOTIFICATIONS (OPTIONAL)
# =============================================================================
# =============================================================================
# ④ Telegram 通知(可选)
# =============================================================================
# Telegram Bot Token # Telegram Bot Token
# How to get: # 获取方式:
# 1. Open Telegram and search for @BotFather # 1. 打开 Telegram,搜索 @BotFather
# 2. Send /newbot and follow instructions # 2. 发送 /newbot 并按提示操作
# 3. Copy the token here # 3. 复制 Bot Token 到此处
TELEGRAM_BOT_TOKEN= TELEGRAM_BOT_TOKEN=
# Telegram Chat ID # Telegram Chat ID(你的用户 ID
# How to get: # 获取方式:
# 1. Open Telegram and search for @userinfobot # 1. 打开 Telegram,搜索 @userinfobot
# 2. Send /start # 2. 发送 /start
# 3. Copy your numeric ID here # 3. 复制返回的数字 ID 到此处
TELEGRAM_CHAT_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
+8 -4
View File
@@ -14,6 +14,7 @@ import base64
from typing import Optional, Dict from typing import Optional, Dict
import trader as trader_module import trader as trader_module
from position_tracker import PositionTracker from position_tracker import PositionTracker
from proxy_util import get_session_proxies, patch_websocket_if_needed
class DataFeed: class DataFeed:
@@ -102,9 +103,12 @@ class DataFeed:
self.threads.append(pm_thread) self.threads.append(pm_thread)
print(f"[DATA] Started Polymarket feed for {coin.upper()}") print(f"[DATA] Started Polymarket feed for {coin.upper()}")
# USER CHANNEL DISABLED - WebSocket auth doesn't work # USER CHANNEL ENABLED - WebSocket for position tracking
# Using REST API takingAmount/makingAmount instead! patch_websocket_if_needed()
print(f"[DATA] ️ Position tracking via REST API responses") 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) # Start local timer update (fixes timer freeze)
timer_thread = threading.Thread(target=self._timer_worker, daemon=True) timer_thread = threading.Thread(target=self._timer_worker, daemon=True)
@@ -179,7 +183,7 @@ class DataFeed:
# Use events API with specific slug # Use events API with specific slug
url = f"{gamma_api}/events?slug={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() resp.raise_for_status()
events = resp.json() events = resp.json()
+34 -29
View File
@@ -28,8 +28,12 @@ from safety_guard import SafetyGuard
from order_executor import OrderExecutor from order_executor import OrderExecutor
from keyboard_listener import KeyboardListener from keyboard_listener import KeyboardListener
from market_config import apply_market_window_settings 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 import trader as trader_module
_PROJECT_ROOT = get_project_root()
_LOG_DIR = get_log_dir()
# Global configuration constants # Global configuration constants
STRATEGY_BASES = ['late_v3'] STRATEGY_BASES = ['late_v3']
@@ -77,18 +81,18 @@ def signal_handler(sig, frame):
trade = { trade = {
'market_slug': market_slug, 'market_slug': market_slug,
'strategy': strategy_name, 'strategy': strategy_name,
'up_contracts': pos['UP']['contracts'], 'up_contracts': pos['UP']['total_shares'],
'down_contracts': pos['DOWN']['contracts'], 'down_contracts': pos['DOWN']['total_shares'],
'up_invested': pos['UP']['invested'], 'up_invested': pos['UP']['total_invested'],
'down_invested': pos['DOWN']['invested'], 'down_invested': pos['DOWN']['total_invested'],
'total_invested': pos['UP']['invested'] + pos['DOWN']['invested'], 'total_invested': pos['UP']['total_invested'] + pos['DOWN']['total_invested'],
'pnl': 0.0, # Unknown - will calculate on next run 'pnl': 0.0,
'winner': 'UNKNOWN', 'winner': 'UNKNOWN',
'closed_at': int(time.time()), 'closed_at': int(time.time()),
'btc_start': pos.get('btc_start', 0), 'btc_start': pos.get('btc_start', 0),
'btc_final': 0, # Unknown 'btc_final': 0,
'entries_count': pos.get('entries_count', 0), 'entries_count': len(pos.get('all_entries', [])),
'status': 'EMERGENCY_SAVE' # Mark as emergency 'status': 'EMERGENCY_SAVE'
} }
trader._log_trade(trade) trader._log_trade(trade)
saved_count += 1 saved_count += 1
@@ -221,13 +225,13 @@ def run_manual_redeem():
# Load environment from 4coins_live # Load environment from 4coins_live
from dotenv import load_dotenv from dotenv import load_dotenv
from pathlib import Path from pathlib import Path
env_path = Path("/root/4coins_live/.env") env_path = _PROJECT_ROOT / ".env"
load_dotenv(env_path, override=True) load_dotenv(env_path, override=True)
# Import and run redeemall with auto-confirm # Import and run redeemall with auto-confirm
import redeemall import redeemall
print("[REDEEM] Starting automatic redemption...") print("[REDEEM] Starting automatic redemption...")
print("[REDEEM] Using wallet from: /root/4coins_live/.env") print("[REDEEM] Using wallet from: .env")
print() print()
redeemall.main(auto_confirm=True) 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) # Initialize data feed (shared across all strategies)
print("[SYSTEM] Initializing multi-market data feed...") print("[SYSTEM] Initializing multi-market data feed...")
data_feed = DataFeed(config) data_feed = DataFeed(config)
@@ -499,12 +506,12 @@ def main(args=None):
# Generate chart path (unique name to avoid conflicts) # Generate chart path (unique name to avoid conflicts)
import uuid 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 request received")
print(f"[TELEGRAM CMD] Chart path: {chart_path}") print(f"[TELEGRAM CMD] Chart path: {chart_path}")
print(f"[TELEGRAM CMD] COINS list: {COINS}") 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 # Import chart generator
from pnl_chart_generator import generate_pnl_chart 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 # 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 # Instead, generate_pnl_chart will check actual files and return False if no data
print(f"[TELEGRAM CMD] Calling generate_pnl_chart()...") 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}") print(f"[TELEGRAM CMD] generate_pnl_chart() returned: {result}")
if not result: if not result:
@@ -535,7 +542,7 @@ def main(args=None):
# This works correctly after bot restart # This works correctly after bot restart
actual_markets_count = 0 actual_markets_count = 0
for coin in COINS: 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(): if trades_file.exists():
try: try:
with open(trades_file, 'r') as f: with open(trades_file, 'r') as f:
@@ -598,7 +605,7 @@ def main(args=None):
'ids': 'polygon-ecosystem-token', 'ids': 'polygon-ecosystem-token',
'vs_currencies': 'usd' '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: if response.status_code == 200:
data = response.json() data = response.json()
@@ -640,7 +647,7 @@ def main(args=None):
} }
print(f"[POSITIONS API] Fetching positions for {wallet[:6]}...{wallet[-4:]}") 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: if response.status_code == 200:
positions = response.json() positions = response.json()
@@ -1305,9 +1312,9 @@ def main(args=None):
if total_completed_markets - last_chart_at >= CHART_INTERVAL: if total_completed_markets - last_chart_at >= CHART_INTERVAL:
print(f"[CHART] {total_completed_markets} markets completed, generating PnL chart...") 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 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"<b>📊 PnL Chart - {total_completed_markets} Markets Completed</b>" caption = f"<b>📊 PnL Chart - {total_completed_markets} Markets Completed</b>"
if notifier.send_photo(chart_path, caption): if notifier.send_photo(chart_path, caption):
print(f"[CHART] ✓ Sent to Telegram successfully") print(f"[CHART] ✓ Sent to Telegram successfully")
@@ -1319,13 +1326,13 @@ def main(args=None):
pnl_sign = "+" if result['pnl'] >= 0 else "" pnl_sign = "+" if result['pnl'] >= 0 else ""
print(f"[{strategy_name:30s}] Closed {prev_market}: {pnl_sign}${result['pnl']:,.2f}") 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) # 🔥 FIX: If close_market() returned None (position empty after restart)
# but redeem was successful, create minimal trade record from orders # but redeem was successful, create minimal trade record from orders
# This ensures ALL natural closes appear in dashboard! # 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...") print(f"[{strategy_name}] Creating trade record from orders for dashboard...")
try: try:
@@ -1354,7 +1361,7 @@ def main(args=None):
if total_cost > 0: if total_cost > 0:
# Create minimal trade record # 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 roi_pct = (pnl / total_cost * 100) if total_cost > 0 else 0
minimal_trade = { minimal_trade = {
@@ -1365,7 +1372,7 @@ def main(args=None):
'pnl': pnl, 'pnl': pnl,
'roi_pct': roi_pct, 'roi_pct': roi_pct,
'total_cost': total_cost, 'total_cost': total_cost,
'payout': redeem_amount, 'payout': amount,
'winner_ratio': 100.0, # Unknown 'winner_ratio': 100.0, # Unknown
'total_entries': 0, # Unknown 'total_entries': 0, # Unknown
'up_entries': 0, 'up_entries': 0,
@@ -1391,8 +1398,6 @@ def main(args=None):
pnl_sign = "+" if pnl >= 0 else "" pnl_sign = "+" if pnl >= 0 else ""
print(f"[{strategy_name:30s}] Reconstructed {prev_market}: {pnl_sign}${pnl:,.2f} (from redeem)") 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: except Exception as e:
print(f"[{strategy_name}] Warning: Could not reconstruct trade: {e}") print(f"[{strategy_name}] Warning: Could not reconstruct trade: {e}")
except Exception as e: except Exception as e:
@@ -1627,12 +1632,12 @@ def main(args=None):
if total_completed_markets - last_chart_at >= CHART_INTERVAL: if total_completed_markets - last_chart_at >= CHART_INTERVAL:
print(f"[CHART] {total_completed_markets} markets completed, generating PnL chart...") 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 # Import chart generator
from pnl_chart_generator import generate_pnl_chart 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 # Send to Telegram
caption = f"<b>📊 PnL Chart - {total_completed_markets} Markets Completed</b>" caption = f"<b>📊 PnL Chart - {total_completed_markets} Markets Completed</b>"
if notifier.send_photo(chart_path, caption): if notifier.send_photo(chart_path, caption):
@@ -1723,12 +1728,12 @@ def main(args=None):
if total_completed_markets - last_chart_at >= CHART_INTERVAL: if total_completed_markets - last_chart_at >= CHART_INTERVAL:
print(f"[CHART] {total_completed_markets} markets completed, generating PnL chart...") 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 # Import chart generator
from pnl_chart_generator import generate_pnl_chart 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 # Send to Telegram
caption = f"<b>📊 PnL Chart - {total_completed_markets} Markets Completed</b>" caption = f"<b>📊 PnL Chart - {total_completed_markets} Markets Completed</b>"
if notifier.send_photo(chart_path, caption): if notifier.send_photo(chart_path, caption):
+5 -3
View File
@@ -23,6 +23,8 @@ import logging
from trade_logger import log_buy_attempt, log_buy_result, log_sell_attempt, log_sell_result from trade_logger import log_buy_attempt, log_buy_result, log_sell_attempt, log_sell_result
import threading import threading
from proxy_util import get_session_proxies
# 🔥 GLOBAL: Blocked markets per-coin (race condition protection) # 🔥 GLOBAL: Blocked markets per-coin (race condition protection)
# Markets in this dict CANNOT receive new buy orders (stop-loss/flip-stop active) # Markets in this dict CANNOT receive new buy orders (stop-loss/flip-stop active)
# Structure: {'btc': set(), 'eth': set(), 'sol': set(), 'xrp': set()} # Structure: {'btc': set(), 'eth': set(), 'sol': set(), 'xrp': set()}
@@ -1914,8 +1916,8 @@ class OrderExecutor:
payload = { payload = {
"chat_id": chat_id, "chat_id": chat_id,
"text": message "text": message
} }
requests.post(url, json=payload, timeout=5) requests.post(url, json=payload, timeout=5, proxies=get_session_proxies())
except Exception as e: except Exception as e:
print(f"[EXECUTOR] ⚠️ Telegram alert failed: {e}") print(f"[EXECUTOR] ⚠️ Telegram alert failed: {e}")
@@ -2030,7 +2032,7 @@ class OrderExecutor:
"parse_mode": "HTML" "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: if response.status_code == 200:
print(f"[EXECUTOR] 📱 Telegram alert sent") print(f"[EXECUTOR] 📱 Telegram alert sent")
@@ -9,12 +9,15 @@ from pathlib import Path
from typing import Dict, List from typing import Dict, List
from datetime import datetime 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]]: def load_trades(log_dir: str, coins: List[str]) -> Dict[str, List[Dict]]:
"""Load all trades from JSONL files for each coin""" """Load all trades from JSONL files for each coin"""
all_trades = {} all_trades = {}
# DEBUG: Write to file too # 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: with open(debug_file, 'a') as f:
f.write(f"\n{'='*80}\n") f.write(f"\n{'='*80}\n")
f.write(f"[CHART DEBUG] {datetime.now()} load_trades called\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! # Take only final entries with real PnL!
trade_map = {} # {coin_market_slug: trade_data} 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: with open(debug_file, 'a') as f:
f.write(f"[CHART DEBUG] Starting deduplication...\n") 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 # Sort by close_time
all_trades_timed.sort(key=lambda x: x['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: with open(debug_file, 'a') as f:
f.write(f"[CHART DEBUG] Sorted {len(all_trades_timed)} trades\n") 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 # Tight layout and save
plt.tight_layout() 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: with open(debug_file, 'a') as f:
f.write(f"[CHART DEBUG] About to save chart to: {output_path}\n") 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 return True
except Exception as e: 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: with open(debug_file, 'a') as f:
f.write(f"[CHART ERROR] Exception: {str(e)}\n") f.write(f"[CHART ERROR] Exception: {str(e)}\n")
import traceback import traceback
+3 -1
View File
@@ -6,6 +6,8 @@ import requests
import json import json
from typing import Optional, Dict from typing import Optional, Dict
from proxy_util import get_session_proxies
GAMMA_API = "https://gamma-api.polymarket.com" GAMMA_API = "https://gamma-api.polymarket.com"
def get_market_outcome(slug: str, timeout: int = 10) -> Dict: 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: try:
url = f"{GAMMA_API}/events?slug={slug}" 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() resp.raise_for_status()
events = resp.json() events = resp.json()
+144
View File
@@ -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
@@ -8,6 +8,8 @@ import threading
import requests import requests
from typing import Dict, List, Optional from typing import Dict, List, Optional
from proxy_util import get_session_proxies
class SimpleRedeemCollector: class SimpleRedeemCollector:
""" """
@@ -212,7 +214,7 @@ class SimpleRedeemCollector:
for attempt in range(1, self.api_max_retries + 1): for attempt in range(1, self.api_max_retries + 1):
try: 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 # ✅ SUCCESS
if response.status_code == 200: if response.status_code == 200:
+13 -9
View File
@@ -6,13 +6,17 @@ import os
import time import time
import requests import requests
from datetime import timedelta from datetime import timedelta
from pathlib import Path
from threading import Thread, Lock from threading import Thread, Lock
from queue import Queue, Empty from queue import Queue, Empty
from typing import Dict from typing import Dict
from dotenv import load_dotenv from dotenv import load_dotenv
# Load environment variables from .env file from proxy_util import get_session_proxies
load_dotenv("/root/4coins_live/.env")
# Load project-level .env
_PROJ_ROOT = Path(__file__).resolve().parent.parent
load_dotenv(_PROJ_ROOT / ".env")
class TelegramNotifier: class TelegramNotifier:
@@ -115,7 +119,7 @@ class TelegramNotifier:
"text": message, "text": message,
"parse_mode": "HTML", "parse_mode": "HTML",
"disable_web_page_preview": True "disable_web_page_preview": True
}, timeout=5.0) }, timeout=5.0, proxies=get_session_proxies())
return response.status_code == 200 return response.status_code == 200
@@ -255,7 +259,7 @@ Winner: {winner}"""
'parse_mode': 'HTML' '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: if response.status_code == 200:
self.sent_count += 1 self.sent_count += 1
@@ -342,7 +346,7 @@ Winner: {winner}"""
'allowed_updates': ['message', 'callback_query'] # Messages and button clicks '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 # Reset error counter on successful connection
consecutive_errors = 0 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: if response.status_code == 200:
data = response.json() data = response.json()
@@ -630,7 +634,7 @@ Winner: {winner}"""
if buttons: if buttons:
payload["reply_markup"] = {"inline_keyboard": 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: if response.status_code == 200:
print(f"[TELEGRAM] ✅ Message edited (ID: {message_id})") print(f"[TELEGRAM] ✅ Message edited (ID: {message_id})")
@@ -666,7 +670,7 @@ Winner: {winner}"""
"show_alert": show_alert "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 return response.status_code == 200
except Exception as e: except Exception as e:
@@ -694,7 +698,7 @@ Winner: {winner}"""
'disable_web_page_preview': True '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: if response.status_code == 200:
self.sent_count += 1 self.sent_count += 1
+4 -4
View File
@@ -528,8 +528,8 @@ class Trader:
# Now we can trade new market without limits! # Now we can trade new market without limits!
# ═══════════════════════════════════════════════════════════ # ═══════════════════════════════════════════════════════════
try: try:
if order_executor and hasattr(order_executor, 'safety'): if _order_executor and hasattr(_order_executor, 'safety'):
order_executor.safety.reset_market(market_slug) _order_executor.safety.reset_market(market_slug)
except Exception as reset_err: except Exception as reset_err:
print(f"[TRADER] ⚠ Failed to reset market tracking: {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! # Now we can trade new market without limits!
# ═══════════════════════════════════════════════════════════ # ═══════════════════════════════════════════════════════════
try: try:
if order_executor and hasattr(order_executor, 'safety'): if _order_executor and hasattr(_order_executor, 'safety'):
order_executor.safety.reset_market(market_slug) _order_executor.safety.reset_market(market_slug)
except Exception as reset_err: except Exception as reset_err:
print(f"[TRADER] ⚠ Failed to reset market tracking: {reset_err}") print(f"[TRADER] ⚠ Failed to reset market tracking: {reset_err}")