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