全面完成代理
This commit is contained in:
@@ -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}%")
|
||||
@@ -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 = "🎮 <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(
|
||||
f"{hsim}"
|
||||
f"🛡️ <b>Hedge Order Placed (GTD)</b>\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"🛡️ <b>对冲单已挂 (GTD)</b>\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"⚠️ <b>Hedge Failed</b>\n"
|
||||
f"⚠️ <b>对冲单失败</b>\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"🔄 <b>Timeout Recovery!</b>\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"🔄 <b>超时恢复!</b>\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 = "🎮 <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(
|
||||
f"{hsim2}"
|
||||
f"🛡️ <b>Hedge Order Placed (GTD)</b>\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"🛡️ <b>对冲单已挂 (GTD)</b>\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"⚠️ <b>Hedge Failed (after recovery)</b>\n"
|
||||
f"⚠️ <b>对冲失败 (恢复后)</b>\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"⚠️ <b>TIMEOUT — No Fill Detected</b>\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"⚠️ <b>超时 — 未检测到成交</b>\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"✅ <b>Hedge FULLY Filled!</b>\n"
|
||||
f"📦 {filled} contracts @ ${price}\n"
|
||||
f"🛡️ Position fully protected"
|
||||
f"✅ <b>对冲已全部成交!</b>\n"
|
||||
f"📦 {filled}张 @ ${price:.4f}\n"
|
||||
f"🛡️ 仓位已完全保护"
|
||||
)
|
||||
logger.info(f"Hedge fully filled: {filled} contracts")
|
||||
else:
|
||||
# Partial fill
|
||||
await self.telegram.send_message(
|
||||
f"🛡️ <b>Hedge Partial Fill</b>\n"
|
||||
f"📦 +{size} contracts @ ${price}\n"
|
||||
f"📊 Progress: {filled}/{total}"
|
||||
f"🛡️ <b>对冲部分成交</b>\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 = "🎮 <b>SIMULATION MODE</b> — no real orders\n"
|
||||
sim_note = "🎮 <b>模拟模式 — 不下真实订单</b>\n"
|
||||
await self.telegram.send_message(
|
||||
f"{sim_note}"
|
||||
f"🤖 <b>Bot Started</b>\n"
|
||||
f"Strategy: ${self.config.entry.bet_amount_usd} per trade\n"
|
||||
f"Hedge: {'enabled' if self.config.hedge.enabled else 'disabled'}"
|
||||
f"🤖 <b>机器人启动</b>\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]")
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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 для отслеживания
|
||||
|
||||
@@ -184,14 +184,14 @@ class TelegramNotifier:
|
||||
simulation: bool = False,
|
||||
):
|
||||
"""Send entry notification."""
|
||||
mode = "🎮 <b>[SIMULATION]</b>\n" if simulation else ""
|
||||
mode = "🎮 <b>[模拟模式]</b>\n" if simulation else ""
|
||||
text = (
|
||||
f"{mode}"
|
||||
f"🟢 <b>ENTRY</b>\n"
|
||||
f"📊 BTC {interval_minutes}min - {side}\n"
|
||||
f"💰 ${cost:.2f} @ {price:.2f}\n"
|
||||
f"📦 {contracts} contracts\n"
|
||||
f"🔄 {retries} retries"
|
||||
f"🟢 <b>开仓成功</b>\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"🛡 <b>HEDGE</b>\n"
|
||||
f"📦 {contracts} contracts @ ${price:.3f}\n"
|
||||
f"💰 Cost: ${cost:.2f}\n"
|
||||
f"✅ Position protected"
|
||||
f"🛡 <b>对冲已开</b>\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"🏁 <b>MARKET RESOLVED</b>\n"
|
||||
f"{emoji} Winner: <b>{winner}</b>\n"
|
||||
f"💵 P&L: {pnl_sign}${pnl:.2f}\n"
|
||||
f"📈 Total: ${total_pnl:.2f}\n"
|
||||
f"📊 Win rate: {win_rate:.1%}"
|
||||
f"🏁 <b>市场结算</b>\n"
|
||||
f"{emoji} 胜方: <b>{winner}</b>\n"
|
||||
f"💵 本次盈亏: {pnl_sign}${pnl:.2f}\n"
|
||||
f"📈 累计盈亏: ${total_pnl:.2f}\n"
|
||||
f"📊 胜率: {win_rate:.1%}"
|
||||
)
|
||||
await self.send_message(text)
|
||||
|
||||
|
||||
@@ -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
|
||||
self._connected = False
|
||||
|
||||
@@ -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
|
||||
@@ -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()
|
||||
|
||||
@@ -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"<b>📊 PnL Chart - {total_completed_markets} Markets Completed</b>"
|
||||
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"<b>📊 PnL Chart - {total_completed_markets} Markets Completed</b>"
|
||||
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"<b>📊 PnL Chart - {total_completed_markets} Markets Completed</b>"
|
||||
if notifier.send_photo(chart_path, caption):
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}")
|
||||
|
||||
|
||||
Reference in New Issue
Block a user