全面完成代理

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
# -*- 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)
+280 -109
View File
@@ -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
+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)
# ① 钱包私钥(必须)
# =============================================================================
# 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:设置 → 安全与隐私 → 导出私钥
# - Polymarketpolymarket.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(更稳定):
# - Alchemyhttps://polygon-mainnet.g.alchemy.com/v2/YOUR_API_KEY
# - Infurahttps://polygon-mainnet.infura.io/v3/YOUR_PROJECT_ID
# - QuickNodehttps://your-endpoint.quiknode.pro/YOUR_KEY/
#
# Get free API key:
# - Alchemy: https://www.alchemy.com/
# - Ankr: https://www.ankr.com/rpc/
# 免费 API 密钥获取:
# - Alchemyhttps://www.alchemy.com/
# - Ankrhttps://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
+8 -4
View File
@@ -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()
+34 -29
View File
@@ -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):
+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
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
+3 -1
View File
@@ -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()
+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
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:
+13 -9
View File
@@ -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
+4 -4
View File
@@ -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}")