Files
polymarket-5min-15min-1hour…/5min-15min-PTB-bot/polymarket_auto_trade.py
T
2026-07-26 22:56:35 +08:00

3175 lines
133 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
"""
Polymarket BTC 5m / 15m up-down auto-trading (WebSocket)
Monitors markets, checks rules, places orders, manages stops.
Uses WebSockets for lower-latency prices.
"""
import os
import sys
import time
import json
import threading
import requests
from datetime import datetime, timezone
from urllib.parse import urlencode
from dotenv import load_dotenv
from flask import Flask, Response, jsonify, request, send_from_directory, stream_with_context
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
STATIC_DIR = os.path.join(BASE_DIR, "static")
# Load environment
load_dotenv(os.path.join(BASE_DIR, "config.env"))
try:
from py_clob_client.client import ClobClient
from py_clob_client.clob_types import OrderArgs
from py_clob_client.order_builder.constants import BUY, SELL
HAS_CLOB = True
except:
HAS_CLOB = False
print("Please install: pip install py-clob-client")
sys.exit(1)
try:
import websocket
HAS_WS = True
except:
HAS_WS = False
print("Please install: pip install websocket-client")
sys.exit(1)
try:
from web3 import Web3
HAS_WEB3 = True
except:
HAS_WEB3 = False
# ============== Settings ==============
GAMMA_API = "https://gamma-api.polymarket.com"
CRYPTO_PRICE_API = "https://polymarket.com/api/crypto/crypto-price"
# PTB must match Polymarket UI: use "fifteen" for both 5m and 15m BTC up/down when passing
# the event's eventStartTime + endDate from Gamma. variant "five" returns a different anchor.
CRYPTO_PRICE_PTB_VARIANT = "fifteen"
BINANCE_WSS = "wss://stream.binance.com:9443/ws/btcusdt@trade"
POLYMARKET_WSS = "wss://ws-subscriptions-clob.polymarket.com/ws/market"
CLOB_API = "https://clob.polymarket.com"
RTDS_WS = "wss://ws-live-data.polymarket.com" # Chainlink price WebSocket
DATA_API = "https://data-api.polymarket.com"
CTF_CONTRACT = "0x4d97dcd97ec945f40cf65f87097ace5ea0476045"
USDC_E_CONTRACT = "0x2791bca1f2de4661ed88a30c99a7a9449aa84174"
# Proxy (optional)
HTTP_PROXY = os.getenv("HTTP_PROXY", "") # e.g. http://127.0.0.1:7890
HTTPS_PROXY = os.getenv("HTTPS_PROXY", "")
# Build proxy dict
PROXIES = {}
if HTTP_PROXY:
PROXIES["http"] = HTTP_PROXY
# log(f"Using HTTP proxy: {HTTP_PROXY}", "INFO") # log function not yet defined here
if HTTPS_PROXY:
PROXIES["https"] = HTTPS_PROXY
# log(f"Using HTTPS proxy: {HTTPS_PROXY}", "INFO") # log function not yet defined here
# Trading
AUTO_TRADE = os.getenv("AUTO_TRADE", "false").lower() == "true"
TRADE_AMOUNT = float(os.getenv("TRADE_AMOUNT", "5"))
# Paper trading: run strategy with instant fills; no CLOB orders. Implies no on-chain redeem in main().
SIMULATION_MODE = os.getenv("SIMULATION_MODE", "false").lower() == "true"
# One JSON object per line: fills, PnL, BTC/PTB context for offline analysis
# Relative paths are resolved against BASE_DIR (script folder), not the shell cwd.
_tal_raw = (os.getenv("TRADING_ANALYSIS_LOG", "") or "").strip()
if not _tal_raw:
TRADING_ANALYSIS_LOG = os.path.join(BASE_DIR, "trading_analysis.jsonl")
elif os.path.isabs(_tal_raw):
TRADING_ANALYSIS_LOG = os.path.normpath(_tal_raw)
else:
TRADING_ANALYSIS_LOG = os.path.normpath(os.path.join(BASE_DIR, _tal_raw))
TRADING_ANALYSIS_LOG = os.path.abspath(TRADING_ANALYSIS_LOG)
# Trigger rules (examples in comments)
# C1: within 120s left, diff ≥ 30, UP prob ~8092%
C1_TIME = int(os.getenv("CONDITION_1_TIME", "120"))
C1_DIFF = float(os.getenv("CONDITION_1_DIFF", "30"))
C1_MIN_PROB = float(os.getenv("CONDITION_1_MIN_PROB", "0.80"))
C1_MAX_PROB = float(os.getenv("CONDITION_1_MAX_PROB", "0.92"))
# C2: within 120s, diff ≥ 30, DOWN prob ~8092%
C2_TIME = int(os.getenv("CONDITION_2_TIME", "120"))
C2_DIFF = float(os.getenv("CONDITION_2_DIFF", "30"))
C2_MIN_PROB = float(os.getenv("CONDITION_2_MIN_PROB", "0.80"))
C2_MAX_PROB = float(os.getenv("CONDITION_2_MAX_PROB", "0.92"))
# C3: within 60s, diff ≥ 50, UP prob ~8092%
C3_TIME = int(os.getenv("CONDITION_3_TIME", "60"))
C3_DIFF = float(os.getenv("CONDITION_3_DIFF", "50"))
C3_MIN_PROB = float(os.getenv("CONDITION_3_MIN_PROB", "0.80"))
C3_MAX_PROB = float(os.getenv("CONDITION_3_MAX_PROB", "0.92"))
# C4: within 60s, diff ≥ 50, DOWN prob ~8092%
C4_TIME = int(os.getenv("CONDITION_4_TIME", "60"))
C4_DIFF = float(os.getenv("CONDITION_4_DIFF", "50"))
C4_MIN_PROB = float(os.getenv("CONDITION_4_MIN_PROB", "0.80"))
C4_MAX_PROB = float(os.getenv("CONDITION_4_MAX_PROB", "0.92"))
ORDER_TIMEOUT_SEC = int(os.getenv("ORDER_TIMEOUT_SEC", "8")) # cancel if unfilled after N seconds
SLIPPAGE_THRESHOLD = float(os.getenv("SLIPPAGE_THRESHOLD", "0.05")) # 5% slippage cap
MAX_RETRY_PER_MARKET = int(os.getenv("MAX_RETRY_PER_MARKET", "2")) # max retries per market
BUY_RETRY_STEP = max(0.001, float(os.getenv("BUY_RETRY_STEP", "0.01")))
STOP_LOSS_PROB_PCT = float(os.getenv("STOP_LOSS_PROB_PCT", "0.15"))
TAKE_PROFIT_RR = max(0.2, float(os.getenv("TAKE_PROFIT_RR", "1.0")))
TAKE_PROFIT_CAP = min(0.995, max(0.55, float(os.getenv("TAKE_PROFIT_CAP", "0.99"))))
TAKE_PROFIT_RETRY_STEP = max(0.001, float(os.getenv("TAKE_PROFIT_RETRY_STEP", "0.005")))
TAKE_PROFIT_RETRY_MAX = max(1, int(os.getenv("TAKE_PROFIT_RETRY_MAX", "3")))
MARKET_DATA_MAX_LAG_SEC = max(0.2, float(os.getenv("MARKET_DATA_MAX_LAG_SEC", "1.2")))
LOOP_INTERVAL_SEC = max(0.1, float(os.getenv("LOOP_INTERVAL_SEC", "0.25")))
# Risk / ops
CHECK_INTERVAL = int(os.getenv("CHECK_INTERVAL", "2"))
AUTO_REDEEM = os.getenv("AUTO_REDEEM", "true").lower() == "true"
POLYGON_RPC_URL = os.getenv("POLYGON_RPC_URL", "")
REDEEM_SCAN_INTERVAL = max(3, int(os.getenv("REDEEM_SCAN_INTERVAL", "15")))
REDEEM_RETRY_INTERVAL = max(10, int(os.getenv("REDEEM_RETRY_INTERVAL", "120")))
REDEEM_MAX_PER_SCAN = max(1, int(os.getenv("REDEEM_MAX_PER_SCAN", "2")))
REDEEM_PENDING_LOG_INTERVAL = max(10, int(os.getenv("REDEEM_PENDING_LOG_INTERVAL", "30")))
POLY_BUILDER_API_KEY = os.getenv("POLY_BUILDER_API_KEY", "")
POLY_BUILDER_SECRET = os.getenv("POLY_BUILDER_SECRET", "")
POLY_BUILDER_PASSPHRASE = os.getenv("POLY_BUILDER_PASSPHRASE", "")
RELAYER_URL = os.getenv("RELAYER_URL", "https://relayer-v2.polymarket.com")
RELAYER_TX_TYPE = os.getenv("RELAYER_TX_TYPE", "SAFE").upper()
DASHBOARD_ACCOUNT_SYNC_SEC = max(10, int(os.getenv("DASHBOARD_ACCOUNT_SYNC_SEC", "20")))
MARKET_FOUND_LOG_INTERVAL = max(10, int(os.getenv("MARKET_FOUND_LOG_INTERVAL", "30")))
MARKET_META_REFRESH_SEC = max(2, int(os.getenv("MARKET_META_REFRESH_SEC", "5")))
WEB_ENABLED = os.getenv("WEB_ENABLED", "true").lower() == "true"
WEB_HOST = os.getenv("WEB_HOST", "0.0.0.0")
WEB_PORT = int(os.getenv("WEB_PORT", "5080"))
def _normalize_btc_market_minutes(m):
"""Polymarket supports 5m and 15m BTC up/down events."""
try:
n = int(float(str(m).strip()))
except (TypeError, ValueError):
return 15
return 5 if n == 5 else 15
_btc_market_minutes = _normalize_btc_market_minutes(os.getenv("BTC_MARKET_MINUTES", "15"))
_market_interval_sec = _btc_market_minutes * 60
# Persisted state file
STATE_FILE = os.path.join(BASE_DIR, "state.json")
# Global price snapshot
price_data = {
"btc": None, # Chainlink BTC (trading reference)
"binance": None, # Binance BTC (secondary)
"ptb": None, # Price to Beat
"up_price": None, # UP token mid
"down_price": None, # DOWN token mid
"up_bid": None,
"up_ask": None,
"down_bid": None,
"down_ask": None,
"btc_update_ts": 0.0,
"up_update_ts": 0.0,
"down_update_ts": 0.0,
"last_update": None,
}
dashboard_lock = threading.Lock()
dashboard_cond = threading.Condition(dashboard_lock)
dashboard_version = 0
dashboard_state = {
"updated_at": None,
"market": {},
"wallet_balance": None,
"prices": {},
"position": {},
"pending_order": {},
"last_order": {},
"trade_history": [],
"wallet_positions": [],
"wallet_history": [],
"live_trades": [],
"live_positions_count": 0,
"live_realized_pnl": 0.0,
"live_unrealized_pnl": 0.0,
"live_total_pnl": 0.0,
"auto_redeem": {},
"activity": [],
"btc_market_minutes": _btc_market_minutes,
"cumulative_realized_pnl": 0.0,
"simulation_mode": SIMULATION_MODE,
}
app = Flask(__name__, static_folder=STATIC_DIR)
_market_found_log_state = {"slug": "", "kind": "", "last_ts": 0.0}
_price_refresh_lock = threading.Lock()
_price_refresh_running = False
_market_cache_lock = threading.Lock()
_market_cache = None
_market_refresh_running = False
_account_sync_lock = threading.Lock()
_account_sync_running = False
_trading_analysis_log_lock = threading.Lock()
def _log_market_found_throttled(kind, slug, remaining):
same_market = (_market_found_log_state.get("slug") == slug and _market_found_log_state.get("kind") == kind)
if same_market:
return
_market_found_log_state["slug"] = slug
_market_found_log_state["kind"] = kind
_market_found_log_state["last_ts"] = time.time()
log(f"Found {kind} market: {slug[:40]}... ({remaining//60}m {remaining%60}s left)", "OK")
def _trigger_price_refresh():
global _price_refresh_running
with _price_refresh_lock:
if _price_refresh_running:
return
_price_refresh_running = True
def worker():
global _price_refresh_running
try:
chainlink_price = get_chainlink_btc_price()
if chainlink_price:
price_data["btc"] = chainlink_price
ts = time.time()
price_data["btc_update_ts"] = ts
price_data["last_update"] = ts
binance_price = get_binance_btc_price()
if binance_price:
price_data["binance"] = binance_price
finally:
with _price_refresh_lock:
_price_refresh_running = False
threading.Thread(target=worker, daemon=True).start()
def _trigger_market_refresh():
global _market_refresh_running, _market_cache
with _market_cache_lock:
if _market_refresh_running:
return
_market_refresh_running = True
def worker():
global _market_refresh_running, _market_cache
try:
market = get_active_market()
with _market_cache_lock:
_market_cache = dict(market) if isinstance(market, dict) else None
finally:
with _market_cache_lock:
_market_refresh_running = False
threading.Thread(target=worker, daemon=True).start()
def _get_market_cache():
with _market_cache_lock:
return dict(_market_cache) if isinstance(_market_cache, dict) else None
def _clear_market_cache():
global _market_cache
with _market_cache_lock:
_market_cache = None
def _trigger_account_sync(user):
global _account_sync_running
u = str(user or "").strip().lower()
if not u:
return
with _account_sync_lock:
if _account_sync_running:
return
_account_sync_running = True
def worker():
global _account_sync_running
try:
_sync_dashboard_account_snapshot(u)
except Exception:
pass
finally:
with _account_sync_lock:
_account_sync_running = False
threading.Thread(target=worker, daemon=True).start()
def _dashboard_set(**kwargs):
global dashboard_version
with dashboard_cond:
for k, v in kwargs.items():
dashboard_state[k] = v
dashboard_state["updated_at"] = datetime.now().isoformat()
dashboard_version += 1
dashboard_cond.notify_all()
@app.route("/")
def dashboard_index():
return send_from_directory(STATIC_DIR, "dashboard.html")
@app.route("/api/status")
def dashboard_status():
with dashboard_lock:
return jsonify(dict(dashboard_state))
@app.route("/api/logs")
def dashboard_logs():
with dashboard_lock:
return jsonify({"items": list(dashboard_state.get("activity") or [])[-300:]})
@app.route("/api/stream")
def dashboard_stream():
def _event(name, payload):
return f"event: {name}\ndata: {json.dumps(payload, ensure_ascii=False)}\n\n"
def generate():
last_seen = -1
last_log_sig = ""
while True:
with dashboard_cond:
if dashboard_version == last_seen:
dashboard_cond.wait(timeout=15)
version_now = dashboard_version
state_now = dict(dashboard_state)
if version_now != last_seen:
logs = list(state_now.get("activity") or [])[-300:]
state_now.pop("activity", None)
yield _event("status", {"data": state_now})
if logs:
tail = logs[-1]
sig = f"{len(logs)}|{tail.get('time','')}|{tail.get('message','')}"
else:
sig = "0"
if sig != last_log_sig:
yield _event("logs", {"items": logs})
last_log_sig = sig
last_seen = version_now
else:
yield ": ping\n\n"
return Response(
stream_with_context(generate()),
mimetype="text/event-stream",
headers={
"Cache-Control": "no-cache",
"Connection": "keep-alive",
"X-Accel-Buffering": "no",
},
)
@app.route("/api/history")
def dashboard_history():
with dashboard_lock:
live_items = list(dashboard_state.get("live_trades") or [])
if live_items:
return jsonify({"items": live_items[-300:]})
local_items = list(dashboard_state.get("trade_history") or [])
wallet_items = list(dashboard_state.get("wallet_history") or [])
return jsonify({"items": (local_items + wallet_items)[-300:]})
@app.route("/api/btc-market-minutes", methods=["POST"])
def api_btc_market_minutes():
"""Switch between 5m and 15m BTC markets (clears cached market metadata)."""
if not WEB_ENABLED:
return jsonify({"ok": False, "error": "web disabled"}), 404
try:
body = request.get_json(silent=True) or {}
m = body.get("minutes", body.get("interval", 15))
m = int(m)
except (TypeError, ValueError):
return jsonify({"ok": False, "error": "invalid body"}), 400
if m not in (5, 15):
return jsonify({"ok": False, "error": "minutes must be 5 or 15"}), 400
set_btc_market_minutes(m)
return jsonify({"ok": True, "minutes": get_btc_market_minutes()})
def start_web_server():
if not WEB_ENABLED:
return
def run():
app.run(host=WEB_HOST, port=WEB_PORT, threaded=True, use_reloader=False)
t = threading.Thread(target=run, daemon=True)
t.start()
# ============== Utilities ==============
def log(msg, level="INFO", force=False):
"""Console + dashboard log."""
if force or level in ["OK", "ERR", "WARN", "TRADE"]:
icons = {"INFO": "️", "OK": "✅", "ERR": "❌", "WARN": "⚠️", "TRADE": "💰"}
icon = icons.get(level, "️")
ts = datetime.now().strftime("%H:%M:%S")
log_msg = f"[{ts}] {icon} {msg}"
print(log_msg)
global dashboard_version
with dashboard_cond:
arr = dashboard_state.get("activity") or []
arr.append({
"time": ts,
"level": level,
"message": str(msg),
})
if len(arr) > 400:
arr = arr[-400:]
dashboard_state["activity"] = arr
dashboard_state["updated_at"] = datetime.now().isoformat()
dashboard_version += 1
dashboard_cond.notify_all()
# Persist TRADE and ERR to trade.log
if level in ["TRADE", "ERR"]:
try:
with open("trade.log", "a", encoding="utf-8") as f:
f.write(log_msg + "\n")
except:
pass
def get_btc_market_minutes():
return _btc_market_minutes
def set_btc_market_minutes(m):
global _btc_market_minutes, _market_interval_sec
_btc_market_minutes = _normalize_btc_market_minutes(m)
_market_interval_sec = _btc_market_minutes * 60
price_data["ptb"] = None
_clear_market_cache()
_trigger_market_refresh()
_dashboard_set(btc_market_minutes=_btc_market_minutes)
log(
f"BTC market interval set to {_btc_market_minutes}m "
f"(slug btc-updown-{_btc_market_minutes}m-*, PTB via {CRYPTO_PRICE_PTB_VARIANT!r} + event window)",
"OK",
force=True,
)
def get_binance_btc_price():
"""Fetch BTC/USDT from Binance REST."""
try:
r = requests.get("https://api.binance.com/api/v3/ticker/price",
params={"symbol": "BTCUSDT"},
proxies=PROXIES if PROXIES else None,
timeout=5)
if r.status_code == 200:
return float(r.json().get("price"))
except:
pass
return None
def get_chainlink_btc_price():
"""Chainlink BTC via Polymarket RTDS WebSocket (fallback)."""
result = {"price": None}
def on_message(ws, message):
try:
data = json.loads(message)
if data.get("topic") == "crypto_prices" and data.get("payload"):
payload = data["payload"]
if "data" in payload and payload.get("symbol") == "btc/usd":
prices = payload["data"]
if prices:
result["price"] = prices[-1]["value"]
elif "value" in payload:
result["price"] = payload["value"]
ws.close()
except:
pass
def on_open(ws):
sub_msg = {
"action": "subscribe",
"subscriptions": [{
"topic": "crypto_prices_chainlink",
"type": "*",
"filters": "{\"symbol\":\"btc/usd\"}"
}]
}
ws.send(json.dumps(sub_msg))
def on_error(ws, error):
pass
try:
ws = websocket.WebSocketApp(RTDS_WS,
on_open=on_open,
on_message=on_message,
on_error=on_error)
def close_after():
time.sleep(3)
try:
ws.close()
except:
pass
threading.Thread(target=close_after, daemon=True).start()
ws.run_forever()
return result["price"]
except:
return None
def get_crypto_price_api(start_time, end_time):
"""
PTB from Polymarket crypto-price API.
Returns: {"openPrice": PTB, "closePrice": current or None, "completed": bool}
"""
try:
# Accept str or datetime for window bounds
if isinstance(start_time, str):
start_str = start_time.replace("Z", "+00:00")
if "+" in start_str:
start_str = start_str.split("+")[0] + "Z"
else:
start_str = start_time
else:
start_str = start_time.strftime("%Y-%m-%dT%H:%M:%SZ")
if isinstance(end_time, str):
end_str = end_time.replace("Z", "+00:00")
if "+" in end_str:
end_str = end_str.split("+")[0] + "Z"
else:
end_str = end_time
else:
end_str = end_time.strftime("%Y-%m-%dT%H:%M:%SZ")
params = {
"symbol": "BTC",
"eventStartTime": start_str,
"variant": CRYPTO_PRICE_PTB_VARIANT,
"endDate": end_str
}
# Browser-like headers
headers = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
"Accept": "application/json",
"Referer": "https://polymarket.com/"
}
log(f"PTB request: {CRYPTO_PRICE_API}?{urlencode(params)}", "INFO")
r = requests.get(CRYPTO_PRICE_API, params=params, headers=headers,
proxies=PROXIES if PROXIES else None, timeout=10)
log(f"PTB HTTP status: {r.status_code}", "INFO")
if r.status_code == 200:
data = r.json()
log(f"PTB payload: {data}", "INFO")
return data
else:
log(f"PTB request failed: HTTP {r.status_code} - {r.text[:200]}", "ERR")
except Exception as e:
log(f"crypto-price error: {type(e).__name__}: {str(e)}", "ERR")
return {}
def get_current_slug():
"""Current window slug for configured interval (5m or 15m)."""
ts = int(time.time())
step = _market_interval_sec
window_start = (ts // step) * step
return f"btc-updown-{_btc_market_minutes}m-{window_start}"
def get_next_slug():
"""Next window slug for configured interval."""
ts = int(time.time())
step = _market_interval_sec
window_start = ((ts // step) + 1) * step
return f"btc-updown-{_btc_market_minutes}m-{window_start}"
def get_active_market():
"""Active BTC up/down market for the configured interval."""
try:
# Try current window first
current_slug = get_current_slug()
market = fetch_market_by_slug(current_slug)
if market and market["remaining"] > 0:
_log_market_found_throttled("current", current_slug, market["remaining"])
return market
# Then next window
next_slug = get_next_slug()
market = fetch_market_by_slug(next_slug)
if market and market["remaining"] > 0:
_log_market_found_throttled("next", next_slug, market["remaining"])
return market
log("No active market in current or next window", "WARN")
except Exception as e:
log(f"Market fetch failed: {e}", "ERR")
import traceback
traceback.print_exc()
return None
def fetch_market_by_slug(slug):
"""Gamma API market row for slug."""
try:
r = requests.get(f"{GAMMA_API}/events", params={"slug": slug},
proxies=PROXIES if PROXIES else None, timeout=10)
data = r.json()
if not data:
return None
event = data[0]
# Skip closed events
if event.get("closed", False):
return None
end_str = event.get("endDate", "")
start_str = event.get("startTime", "")
if not end_str or not start_str:
return None
# Seconds until end
now = datetime.now(timezone.utc).timestamp()
end_ts = datetime.fromisoformat(end_str.replace("Z", "+00:00")).timestamp()
remaining_time = int(end_ts - now)
if remaining_time <= 0:
return None
# Parse first market
markets = event.get("markets", [])
if not markets:
return None
m = markets[0]
outcomes = json.loads(m.get("outcomes", "[]")) if isinstance(m.get("outcomes"), str) else m.get("outcomes", [])
prices = json.loads(m.get("outcomePrices", "[]")) if isinstance(m.get("outcomePrices"), str) else m.get("outcomePrices", [])
tokens = json.loads(m.get("clobTokenIds", "[]")) if isinstance(m.get("clobTokenIds"), str) else m.get("clobTokenIds", [])
# Assume outcomes[0]=UP, [1]=DOWN
up_price = float(prices[0]) if len(prices) > 0 else None
down_price = float(prices[1]) if len(prices) > 1 else None
up_token = tokens[0] if len(tokens) > 0 else None
down_token = tokens[1] if len(tokens) > 1 else None
return {
"slug": slug,
"start": start_str,
"end": end_str,
"remaining": remaining_time,
"up_price": up_price,
"down_price": down_price,
"up_token": up_token,
"down_token": down_token
}
except Exception as e:
# Missing market is normal
return None
def get_ptb(start_time, end_time):
"""Fetch Price to Beat (open) for window."""
try:
params = {
"symbol": "BTC",
"eventStartTime": start_time,
"variant": CRYPTO_PRICE_PTB_VARIANT,
"endDate": end_time
}
r = requests.get(CRYPTO_PRICE_API, params=params, timeout=10)
if r.status_code == 200:
data = r.json()
return float(data.get("openPrice")) if data.get("openPrice") else None
except:
pass
return None
def _normalize_state(state):
if not isinstance(state, dict):
state = {}
if not isinstance(state.get("position"), dict):
state["position"] = {}
if not isinstance(state.get("pending_order"), dict):
state["pending_order"] = {}
if not isinstance(state.get("last_order"), dict):
state["last_order"] = {}
if not isinstance(state.get("take_profit_order"), dict):
state["take_profit_order"] = {}
if not isinstance(state.get("trade_history"), list):
state["trade_history"] = []
if state.get("cumulative_realized_pnl") is None or not isinstance(
state.get("cumulative_realized_pnl"), (int, float)
):
try:
state["cumulative_realized_pnl"] = float(state.get("cumulative_realized_pnl") or 0.0)
except (TypeError, ValueError):
state["cumulative_realized_pnl"] = 0.0
return state
def _dashboard_pending_order_from_state(state):
state = _normalize_state(state)
pending = dict(state.get("pending_order") or {})
if pending:
return pending
tp = dict(state.get("take_profit_order") or {})
if tp:
tp.setdefault("action", "SELL")
tp.setdefault("reason", "take_profit")
return tp
def _append_trade_history(state, item):
state = _normalize_state(state)
hist = list(state.get("trade_history") or [])
hist.append(item)
if len(hist) > 300:
hist = hist[-300:]
state["trade_history"] = hist
_dashboard_set(trade_history=list(hist))
return state
def _planned_take_profit_stop_loss(entry_prob):
"""
Same TP/SL probability levels as the main loop (for logging planned targets after a buy).
Returns (take_profit_prob, stop_loss_prob) or (None, None).
"""
if entry_prob is None or entry_prob <= 0:
return None, None
try:
ep = float(entry_prob)
except (TypeError, ValueError):
return None, None
stop_prob = max(0.0, ep * (1.0 - STOP_LOSS_PROB_PCT))
risk_abs = max(0.0, ep - stop_prob)
tp_trigger = min(TAKE_PROFIT_CAP, ep + risk_abs * TAKE_PROFIT_RR)
if tp_trigger <= ep:
return None, stop_prob
balanced_risk = (tp_trigger - ep) / TAKE_PROFIT_RR
balanced_stop = max(0.0, ep - balanced_risk)
if balanced_stop > stop_prob:
stop_prob = balanced_stop
return tp_trigger, stop_prob
def _emit_trading_analysis(event, **fields):
"""Append one JSON line with a stable schema for analysis (see schema_version)."""
ts = fields.get("time") or datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%S.%fZ")
btc = fields.get("btc_price")
if btc is None:
btc = fields.get("chainlink_btc")
ptb = fields.get("ptb")
diff = fields.get("difference")
if diff is None:
diff = fields.get("diff_rule", fields.get("diff"))
if diff is None and btc is not None and ptb is not None:
try:
diff = float(btc) - float(ptb)
except (TypeError, ValueError):
diff = None
st = fields.get("status")
if not st:
act = str(fields.get("action") or "").upper()
if act == "BUY":
st = "buy"
elif act == "SELL":
st = "sell"
shares_type = fields.get("shares_type") or fields.get("side")
share_price = fields.get("share_price")
if share_price is None:
share_price = fields.get("price")
if share_price is None:
share_price = fields.get("exit_share_price")
share_amount = fields.get("share_amount")
if share_amount is None:
share_amount = fields.get("shares")
pnl_trade = fields.get("pnl_trade_usd")
if pnl_trade is None:
pnl_trade = fields.get("realized_pnl_usd")
pnl_total = fields.get("pnl_total_usd")
if pnl_total is None:
pnl_total = fields.get("cumulative_realized_pnl_usd")
tp = fields.get("take_profit")
sl = fields.get("stop_loss")
if tp is None and sl is None:
entry_plan = fields.get("entry_share_price")
if entry_plan is None:
entry_plan = share_price
_no_auto_plan = (
"SELL_CLOSE",
"SELL_SUBMIT",
"SELL_FAILED",
"SELL_ALERT",
"BUY_CANCEL_TIMEOUT",
)
if entry_plan is not None and event not in _no_auto_plan:
tp, sl = _planned_take_profit_stop_loss(entry_plan)
def _nf(x):
if x is None:
return None
try:
return float(x)
except (TypeError, ValueError):
return None
row = {
"schema_version": 2,
"event": event,
"slug": fields.get("slug"),
"shares_type": shares_type,
"share_price": _nf(share_price),
"share_amount": _nf(share_amount),
"ptb": _nf(ptb),
"btc_price": _nf(btc),
"difference": _nf(diff),
"difference_note": "Chainlink BTC minus PTB (USD); same as diff in bot logic.",
"status": st,
"take_profit": _nf(tp),
"stop_loss": _nf(sl),
"time": ts,
"pnl_trade_usd": _nf(pnl_trade),
"pnl_total_usd": _nf(pnl_total),
"simulation": SIMULATION_MODE,
"btc_market_minutes": _btc_market_minutes,
}
passthrough = (
"reason",
"order_id",
"order_size_usdc",
"remaining_sec",
"entry_share_price",
"exit_share_price",
"notional_exit_usd",
"action",
"chainlink_btc",
"btc_minus_ptb",
"diff_rule",
)
for k in passthrough:
if k in fields and fields[k] is not None:
row[k] = fields[k]
try:
log_dir = os.path.dirname(TRADING_ANALYSIS_LOG)
if log_dir:
os.makedirs(log_dir, exist_ok=True)
with _trading_analysis_log_lock:
with open(TRADING_ANALYSIS_LOG, "a", encoding="utf-8") as f:
f.write(json.dumps(row, default=str, ensure_ascii=False) + "\n")
except Exception as e:
try:
log(f"Trading analysis log write failed ({TRADING_ANALYSIS_LOG}): {e}", "ERR", force=True)
except Exception:
print(f"Trading analysis log write failed ({TRADING_ANALYSIS_LOG}): {e}", file=sys.stderr)
def _init_trading_analysis_session():
"""Create log file and write SESSION_START so path is visible even before any trade."""
row = {
"schema_version": 2,
"event": "SESSION_START",
"log_path": TRADING_ANALYSIS_LOG,
"slug": None,
"shares_type": None,
"share_price": None,
"share_amount": None,
"ptb": None,
"btc_price": None,
"difference": None,
"difference_note": "Chainlink BTC minus PTB (USD).",
"status": None,
"take_profit": None,
"stop_loss": None,
"time": None,
"pnl_trade_usd": None,
"pnl_total_usd": None,
"simulation": SIMULATION_MODE,
"auto_trade": AUTO_TRADE,
"btc_market_minutes": _btc_market_minutes,
"trade_amount_usdc": TRADE_AMOUNT,
"note": "Trade rows use the same keys as SESSION_START; pnl_total_usd is cumulative realized.",
}
row["logged_at"] = row["time"] = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%S.%fZ")
try:
log_dir = os.path.dirname(TRADING_ANALYSIS_LOG)
if log_dir:
os.makedirs(log_dir, exist_ok=True)
with _trading_analysis_log_lock:
with open(TRADING_ANALYSIS_LOG, "a", encoding="utf-8") as f:
f.write(json.dumps(row, default=str, ensure_ascii=False) + "\n")
except Exception as e:
print(f"FATAL: cannot write trading log at {TRADING_ANALYSIS_LOG}: {e}", file=sys.stderr)
try:
log(f"Cannot init trading analysis log: {e}", "ERR", force=True)
except Exception:
pass
def _shares_from_usdc_buy(usdc, share_price):
if share_price and share_price > 0 and usdc and usdc > 0:
return float(usdc) / float(share_price)
return 0.0
def _btc_ptb_snapshot(btc, ptb):
if btc is None or ptb is None:
return None
try:
return float(btc) - float(ptb)
except (TypeError, ValueError):
return None
def _to_float(value, default=0.0):
try:
return float(value)
except Exception:
return float(default)
def _maybe_float(value):
try:
return float(value)
except Exception:
return None
def _to_bool(value):
if isinstance(value, bool):
return value
if value is None:
return False
s = str(value).strip().lower()
return s in ("1", "true", "yes", "y", "on")
def _data_api_get(path, params=None):
try:
r = requests.get(
f"{DATA_API}{path}",
params=params or {},
proxies=PROXIES if PROXIES else None,
timeout=12,
)
if r.status_code == 200:
return r.json()
except Exception:
return None
return None
def _text_scalar(v):
if isinstance(v, (str, int, float, bool)):
return str(v).strip()
return ""
def _normalize_outcome_label(v):
s = str(v or "").upper()
if "UP" in s or s == "YES":
return "UP"
if "DOWN" in s or s == "NO":
return "DOWN"
return s or "-"
def _trade_pick_field(tr, *keys):
if not isinstance(tr, dict):
return ""
sources = [tr]
market = tr.get("market")
if isinstance(market, dict):
sources.append(market)
event = tr.get("event")
if isinstance(event, dict):
sources.append(event)
for src in sources:
for k in keys:
if k not in src:
continue
s = _text_scalar(src.get(k))
if s:
return s
return ""
def _trade_event_kind(tr):
typ = str((tr or {}).get("type") or "").upper().strip()
side = str((tr or {}).get("side") or "").upper().strip()
if typ == "REDEEM":
return "REDEEM"
if typ in ["DEPOSIT", "WITHDRAW", "WITHDRAWAL", "TRANSFER"]:
return "IGNORE"
if side in ["BUY", "SELL"]:
return side
return "IGNORE"
def _trade_ts_ms(tr):
v = (tr or {}).get("matchtime") or (tr or {}).get("match_time") or (tr or {}).get("timestamp") or (tr or {}).get("created_at") or (tr or {}).get("time")
if isinstance(v, (int, float)):
n = float(v)
return int(n if n > 1e12 else n * 1000)
s = str(v or "").strip()
if not s:
return 0
if s.isdigit():
n = int(s)
return n if n > 1e12 else n * 1000
try:
dt = datetime.fromisoformat(s.replace("Z", "+00:00"))
return int(dt.timestamp() * 1000)
except Exception:
return 0
def _trade_usdc_size(tr):
usdc = _maybe_float((tr or {}).get("usdcSize") or (tr or {}).get("usdc_size"))
if usdc is not None:
return abs(usdc)
price = _maybe_float((tr or {}).get("price"))
size = _maybe_float((tr or {}).get("size_matched") or (tr or {}).get("size") or (tr or {}).get("original_size"))
if price is not None and size is not None:
return abs(price * size)
return 0.0
def _trade_market_key(tr):
cond = _trade_pick_field(tr, "conditionId", "condition_id", "market", "market_id")
slug = _trade_pick_field(tr, "eventSlug", "slug")
if cond:
return cond
if slug:
return slug
asset = _trade_pick_field(tr, "asset_id", "asset", "token_id")
return asset or "market"
def _resolve_trade_reason(tr):
title = _trade_pick_field(tr, "title", "eventTitle", "name", "question")
if title:
return title
slug = _trade_pick_field(tr, "eventSlug", "slug")
if slug:
return slug
return "market"
def _fetch_trade_activity(user, limit=500):
if not user:
return []
lim = min(max(int(limit), 50), 1000)
param_sets = [
{"user": user, "limit": lim, "offset": 0},
{"user": user},
{"address": user, "limit": lim, "offset": 0},
{"wallet": user, "limit": lim, "offset": 0},
]
rows = []
seen = set()
for params in param_sets:
data = _data_api_get("/activity", params)
if not isinstance(data, list):
continue
for item in data:
if not isinstance(item, dict):
continue
kind = _trade_event_kind(item)
if kind == "IGNORE":
continue
tid = _text_scalar(item.get("id") or item.get("tradeID") or item.get("transaction_hash") or item.get("transactionHash"))
if not tid:
tid = f"act-{kind}-{_trade_ts_ms(item)}-{_trade_usdc_size(item):.6f}-{_trade_market_key(item)}"
if tid in seen:
continue
seen.add(tid)
norm = dict(item)
if norm.get("type") is not None:
norm["type"] = str(norm.get("type")).upper()
if norm.get("side") is not None:
norm["side"] = str(norm.get("side")).upper()
norm["id"] = tid
rows.append(norm)
if rows:
break
rows.sort(key=_trade_ts_ms)
return rows
def _build_market_aggregated_trades(raw_trades):
groups = {}
for tr in sorted((raw_trades or []), key=_trade_ts_ms):
if not isinstance(tr, dict):
continue
kind = _trade_event_kind(tr)
if kind == "IGNORE":
continue
price = _maybe_float(tr.get("price"))
size = _maybe_float(tr.get("size_matched") or tr.get("size") or tr.get("original_size"))
usdc_size = _trade_usdc_size(tr)
if kind in ["BUY", "SELL"] and (price is None or size is None or size <= 0):
continue
if kind == "REDEEM" and usdc_size <= 0:
continue
key = _trade_market_key(tr)
ts = tr.get("matchtime") or tr.get("match_time") or tr.get("timestamp") or tr.get("created_at") or tr.get("time")
ts_ms = _trade_ts_ms(tr)
g = groups.get(key)
if g is None:
g = {
"id": f"agg-{key}",
"direction": _normalize_outcome_label(tr.get("outcome") or tr.get("direction")),
"outcomes": set(),
"reason": _resolve_trade_reason(tr),
"buy_count": 0,
"sell_count": 0,
"redeem_count": 0,
"buy_size": 0.0,
"sell_size": 0.0,
"buy_notional": 0.0,
"sell_notional": 0.0,
"redeem_notional": 0.0,
"first_ts": ts,
"last_ts": ts,
"first_ts_ms": ts_ms,
"last_ts_ms": ts_ms,
}
groups[key] = g
if ts_ms and ts_ms < g["first_ts_ms"]:
g["first_ts_ms"] = ts_ms
g["first_ts"] = ts
if ts_ms and ts_ms >= g["last_ts_ms"]:
g["last_ts_ms"] = ts_ms
g["last_ts"] = ts
outcome = _normalize_outcome_label(tr.get("outcome") or tr.get("direction"))
if outcome and outcome != "-":
g["outcomes"].add(outcome)
if kind == "BUY":
g["buy_count"] += 1
g["buy_size"] += float(size)
g["buy_notional"] += float(usdc_size)
elif kind == "SELL":
g["sell_count"] += 1
g["sell_size"] += float(size)
g["sell_notional"] += float(usdc_size)
elif kind == "REDEEM":
g["redeem_count"] += 1
g["redeem_notional"] += float(usdc_size)
rows = []
for g in groups.values():
if (g["buy_count"] + g["sell_count"] + g["redeem_count"]) <= 0:
continue
buy_avg = (g["buy_notional"] / g["buy_size"]) if g["buy_size"] > 1e-9 else None
sell_avg = (g["sell_notional"] / g["sell_size"]) if g["sell_size"] > 1e-9 else None
matched_size = min(g["buy_size"], g["sell_size"])
pnl = g["sell_notional"] + g["redeem_notional"] - g["buy_notional"]
if len(g["outcomes"]) == 1:
g["direction"] = list(g["outcomes"])[0]
elif len(g["outcomes"]) > 1:
g["direction"] = "MIX"
result = "CLOSED" if (g["sell_count"] > 0 or g["redeem_count"] > 0) else "OPEN"
rows.append({
"id": g["id"],
"pair_id": g["id"],
"direction": g["direction"],
"reason": g["reason"],
"buy_count": g["buy_count"],
"sell_count": g["sell_count"],
"redeem_count": g["redeem_count"],
"buy_usdc": g["buy_notional"],
"sell_usdc": g["sell_notional"],
"redeem_usdc": g["redeem_notional"],
"size": matched_size if matched_size > 1e-9 else max(g["buy_size"], g["sell_size"]),
"entry_price_quote": buy_avg,
"exit_price_quote": sell_avg,
"order_time": g["first_ts"],
"settle_time": g["last_ts"],
"profit": pnl,
"result": result,
"status": "AGG",
})
rows.sort(key=lambda x: _trade_ts_ms({"timestamp": x.get("settle_time")}) if isinstance(x, dict) else 0)
return rows
def _compute_wallet_realized_pnl(rows):
realized = 0.0
for row in rows or []:
if not isinstance(row, dict):
continue
rp = _maybe_float(row.get("realizedPnl") if row.get("realizedPnl") is not None else row.get("realized_pnl"))
if rp is not None:
realized += rp
return float(realized)
def _compute_wallet_unrealized_pnl(rows):
unrealized = 0.0
for row in rows or []:
if not isinstance(row, dict):
continue
mark = _maybe_float(row.get("curPrice") if row.get("curPrice") is not None else row.get("cur_price"))
avg = _maybe_float(row.get("avgPrice") if row.get("avgPrice") is not None else row.get("avg_price"))
size = _maybe_float(row.get("size"))
if mark is None or avg is None or size is None:
continue
unrealized += (mark - avg) * size
return float(unrealized)
def _fetch_wallet_usdc_balance(user):
if not HAS_WEB3:
return None
rpc_url = (POLYGON_RPC_URL or "").strip()
if not rpc_url or not user:
return None
try:
w3 = Web3(Web3.HTTPProvider(rpc_url, request_kwargs={"timeout": 8}))
if not w3.is_connected():
return None
usdc_addr = Web3.to_checksum_address(USDC_E_CONTRACT)
user_addr = Web3.to_checksum_address(user)
contract = w3.eth.contract(
address=usdc_addr,
abi=[
{
"name": "balanceOf",
"type": "function",
"stateMutability": "view",
"inputs": [{"name": "account", "type": "address"}],
"outputs": [{"name": "", "type": "uint256"}],
},
{
"name": "decimals",
"type": "function",
"stateMutability": "view",
"inputs": [],
"outputs": [{"name": "", "type": "uint8"}],
},
],
)
raw = contract.functions.balanceOf(user_addr).call()
decimals = contract.functions.decimals().call()
return float(raw) / (10 ** int(decimals))
except Exception:
return None
def _sync_dashboard_account_snapshot(user):
u = str(user or "").strip().lower()
if not u:
return False
wallet_positions = _fetch_wallet_positions(u)
wallet_closed = _fetch_wallet_closed_positions(u)
wallet_history = _build_wallet_history_items(wallet_closed)
raw_activity = _fetch_trade_activity(u, limit=500)
agg_trades = _build_market_aggregated_trades(raw_activity)
realized_pnl = _compute_wallet_realized_pnl(wallet_closed)
unrealized_pnl = _compute_wallet_unrealized_pnl(wallet_positions)
wallet_balance = _fetch_wallet_usdc_balance(u)
_dashboard_set(
wallet_balance=wallet_balance,
wallet_positions=list(wallet_positions)[:120],
wallet_history=list(wallet_history)[:200],
live_trades=list(agg_trades)[-300:],
live_positions_count=len(wallet_positions),
live_realized_pnl=float(realized_pnl),
live_unrealized_pnl=float(unrealized_pnl),
live_total_pnl=float(realized_pnl + unrealized_pnl),
)
return True
def _fetch_wallet_positions(user):
if not user:
return []
try:
r = requests.get(
f"{DATA_API}/positions",
params={"user": user, "sizeThreshold": 0},
proxies=PROXIES if PROXIES else None,
timeout=12,
)
if r.status_code == 200:
rows = r.json()
if isinstance(rows, list):
out = []
for row in rows:
if not isinstance(row, dict):
continue
size = _to_float(row.get("size"), 0)
if size <= 0:
continue
if _to_bool(row.get("redeemable")) or _to_bool(row.get("mergeable")):
continue
out.append(row)
return out
except Exception:
pass
return []
def _fetch_wallet_closed_positions(user):
if not user:
return []
try:
r = requests.get(
f"{DATA_API}/closed-positions",
params={
"user": user,
"limit": 200,
"offset": 0,
"sortBy": "TIMESTAMP",
"sortDirection": "DESC",
},
proxies=PROXIES if PROXIES else None,
timeout=12,
)
if r.status_code == 200:
rows = r.json()
if isinstance(rows, list):
return rows
except Exception:
pass
return []
def _build_wallet_history_items(rows):
items = []
for row in rows or []:
if not isinstance(row, dict):
continue
side = row.get("outcome") or row.get("side") or row.get("positionSide") or "-"
item = {
"time": row.get("endDate") or row.get("timestamp") or row.get("updatedAt") or "-",
"slug": row.get("slug") or row.get("marketSlug") or row.get("question") or "-",
"action": "CLOSE",
"side": side,
"price": row.get("avgPrice") if row.get("avgPrice") is not None else row.get("avg_price"),
"amount": row.get("size"),
"order_id": row.get("transactionHash") or row.get("id") or "",
"status": "closed",
"reason": "wallet_sync",
"pnl": row.get("realizedPnl") if row.get("realizedPnl") is not None else row.get("realized_pnl"),
}
items.append(item)
return items[:200]
def load_state():
"""Load persisted bot state."""
if not os.path.exists(STATE_FILE):
return _normalize_state({})
try:
with open(STATE_FILE, "r", encoding="utf-8") as f:
return _normalize_state(json.load(f))
except:
return _normalize_state({})
def save_state(state):
"""Persist bot state + latest prices."""
try:
state = _normalize_state(state)
# Snapshot prices into state file
state["ptb"] = price_data.get("ptb")
state["chainlink"] = price_data.get("btc")
state["binance"] = price_data.get("binance")
state["up_price"] = price_data.get("up_price")
state["down_price"] = price_data.get("down_price")
state["last_update"] = datetime.now().isoformat()
with open(STATE_FILE, "w", encoding="utf-8") as f:
json.dump(state, f, indent=2)
except Exception as e:
log(f"save_state failed: {e}", "ERR")
# ============== WebSocket feeds ==============
class BTCPriceListener:
"""Binance BTC trades WebSocket."""
def __init__(self):
self.ws = None
self.running = False
def on_message(self, ws, message):
try:
data = json.loads(message)
if "p" in data: # trade price field
price_data["btc"] = float(data["p"])
ts = time.time()
price_data["btc_update_ts"] = ts
price_data["last_update"] = ts
except:
pass
def on_error(self, ws, error):
pass
def on_close(self, ws, *args):
if self.running:
log("BTC feed disconnected, reconnecting in 5s...", "WARN")
time.sleep(5)
self.start()
def on_open(self, ws):
log("BTC WebSocket connected", "OK")
def start(self):
self.running = True
self.ws = websocket.WebSocketApp(
BINANCE_WSS,
on_open=self.on_open,
on_message=self.on_message,
on_error=self.on_error,
on_close=self.on_close
)
threading.Thread(target=self.ws.run_forever, daemon=True).start()
def stop(self):
self.running = False
if self.ws:
self.ws.close()
class MarketPriceListener:
"""CLOB market book / price_change for UP/DOWN."""
def __init__(self, up_token, down_token):
self.up_token = up_token
self.down_token = down_token
self.ws = None
self.running = False
def on_message(self, ws, message):
try:
data = json.loads(message)
# Message may be list or dict
items = data if isinstance(data, list) else [data]
for item in items:
if not isinstance(item, dict):
continue
event_type = item.get("event_type")
asset_id = item.get("asset_id")
# Order book snapshot
if event_type == "book":
bids = item.get("bids") or []
asks = item.get("asks") or []
if bids and asks:
best_bid = max([float(b["price"]) for b in bids], default=0)
best_ask = min([float(a["price"]) for a in asks], default=0)
mid_price = (best_bid + best_ask) / 2
ts = time.time()
if asset_id == self.up_token:
price_data["up_bid"] = best_bid
price_data["up_ask"] = best_ask
price_data["up_price"] = mid_price
price_data["up_update_ts"] = ts
price_data["last_update"] = ts
elif asset_id == self.down_token:
price_data["down_bid"] = best_bid
price_data["down_ask"] = best_ask
price_data["down_price"] = mid_price
price_data["down_update_ts"] = ts
price_data["last_update"] = ts
# Incremental price_change
elif event_type == "price_change":
price_changes = item.get("price_changes", [])
if price_changes:
pc = price_changes[0]
best_bid = float(pc.get("best_bid", 0))
best_ask = float(pc.get("best_ask", 0))
if best_bid > 0 and best_ask > 0:
mid_price = (best_bid + best_ask) / 2
ts = time.time()
if asset_id == self.up_token:
price_data["up_bid"] = best_bid
price_data["up_ask"] = best_ask
price_data["up_price"] = mid_price
price_data["up_update_ts"] = ts
price_data["last_update"] = ts
elif asset_id == self.down_token:
price_data["down_bid"] = best_bid
price_data["down_ask"] = best_ask
price_data["down_price"] = mid_price
price_data["down_update_ts"] = ts
price_data["last_update"] = ts
except:
pass
def on_error(self, ws, error):
pass
def on_close(self, ws, *args):
if self.running:
log("Market feed disconnected, reconnecting in 5s...", "WARN")
time.sleep(5)
self.start()
def on_open(self, ws):
# Subscribe both outcome tokens
ws.send(json.dumps({
"assets_ids": [self.up_token, self.down_token],
"type": "market"
}))
log("Market WebSocket connected", "OK")
def start(self):
self.running = True
self.ws = websocket.WebSocketApp(
POLYMARKET_WSS,
on_open=self.on_open,
on_message=self.on_message,
on_error=self.on_error,
on_close=self.on_close
)
threading.Thread(target=self.ws.run_forever, daemon=True).start()
def stop(self):
self.running = False
if self.ws:
self.ws.close()
# ============== CLOB client ==============
class Trader:
def __init__(self):
self.client = None
self.connected = False
self.address = None
def connect(self):
"""Connect py-clob client."""
pk = os.getenv("PRIVATE_KEY")
if not pk:
log("PRIVATE_KEY not set", "ERR")
return False
try:
if not pk.startswith("0x"):
pk = "0x" + pk
log("Connecting CLOB client...")
temp = ClobClient(host="https://clob.polymarket.com", chain_id=137, key=pk)
self.address = temp.get_address()
log(f"Wallet: {self.address}")
creds = temp.create_or_derive_api_creds()
funder = os.getenv("FUNDER_ADDRESS") or self.address
sig_type = int(os.getenv("SIGNATURE_TYPE", "2"))
self.client = ClobClient(
host="https://clob.polymarket.com",
chain_id=137,
key=pk,
creds=creds,
signature_type=sig_type,
funder=funder
)
self.connected = True
log("CLOB client connected", "OK")
return True
except Exception as e:
log(f"Connect failed: {e}", "ERR")
return False
def place_order(self, token_id, side, price, size):
"""Place limit order."""
if not self.connected:
log("CLOB client not connected", "ERR")
return None
try:
log(f"Order: {side} ${size} @ {price:.3f}", "TRADE")
order_args = OrderArgs(
token_id=token_id,
price=price,
size=size,
side=BUY if side == "BUY" else SELL
)
signed_order = self.client.create_order(order_args)
resp = self.client.post_order(signed_order)
if resp and resp.get("orderID"):
order_id = resp.get("orderID")
log(f"Order placed, id: {order_id}", "OK")
return order_id
else:
log("Order rejected", "ERR")
return None
except Exception as e:
log(f"Order error: {e}", "ERR")
return None
def get_order_status(self, order_id):
"""Poll order status."""
if not self.connected or not order_id:
return None
try:
order = self.client.get_order(order_id)
if order:
status = order.get("status", "").upper()
original_size = float(order.get("original_size", 0) or 0)
size_matched = float(order.get("size_matched", 0) or 0)
return {
"status": status,
"original_size": original_size,
"size_matched": size_matched,
"filled": size_matched >= original_size if original_size > 0 else False
}
except Exception as e:
log(f"get_order failed: {e}", "WARN")
return None
def cancel_order(self, order_id):
"""Cancel open order."""
if not self.connected or not order_id:
return False
try:
log(f"Cancel order: {order_id}", "WARN")
resp = self.client.cancel(order_id)
if resp:
log("Order canceled", "OK")
return True
else:
log("Cancel failed", "ERR")
return False
except Exception as e:
log(f"Cancel error: {e}", "ERR")
return False
class AutoRedeemer:
def __init__(self, private_key, funder_address):
self.enabled = bool(AUTO_REDEEM)
self.private_key = (private_key or "").strip()
if self.private_key and not self.private_key.startswith("0x"):
self.private_key = "0x" + self.private_key
self.funder_address = (funder_address or "").strip()
self.scan_addresses = []
self.last_try_by_condition = {}
self.last_pending_signature = ""
self.last_pending_log_ts = 0.0
self.running = False
self.thread = None
self.relayer_client = None
self.relayer_error = ""
self.last_pending_count = 0
self.last_claimable_count = 0
self.last_result = {}
self.last_error = ""
if not self.enabled:
_dashboard_set(auto_redeem={"enabled": False, "pending_count": 0, "claimable_count": 0, "last_result": {}, "last_error": ""})
return
if not HAS_WEB3:
log("Auto-redeem disabled: web3 not installed", "WARN", force=True)
self.enabled = False
_dashboard_set(auto_redeem={"enabled": False, "pending_count": 0, "claimable_count": 0, "last_result": {}, "last_error": "web3 missing"})
return
if not self.private_key:
log("Auto-redeem disabled: PRIVATE_KEY missing", "WARN", force=True)
self.enabled = False
_dashboard_set(auto_redeem={"enabled": False, "pending_count": 0, "claimable_count": 0, "last_result": {}, "last_error": "PRIVATE_KEY missing"})
return
if not self.funder_address:
log("Auto-redeem disabled: FUNDER_ADDRESS missing (proxy wallet)", "WARN", force=True)
self.enabled = False
_dashboard_set(auto_redeem={"enabled": False, "pending_count": 0, "claimable_count": 0, "last_result": {}, "last_error": "FUNDER_ADDRESS missing"})
return
if not (POLY_BUILDER_API_KEY and POLY_BUILDER_SECRET and POLY_BUILDER_PASSPHRASE):
log("Auto-redeem disabled: POLY_BUILDER_API_KEY/SECRET/PASSPHRASE missing", "WARN", force=True)
self.enabled = False
_dashboard_set(auto_redeem={"enabled": False, "pending_count": 0, "claimable_count": 0, "last_result": {}, "last_error": "Builder API creds missing"})
return
self.scan_addresses = [self.funder_address]
client, err = self._create_relayer_client()
if client is None:
log(f"Auto-redeem disabled: relayer init failed {err}", "ERR", force=True)
self.enabled = False
_dashboard_set(auto_redeem={"enabled": False, "pending_count": 0, "claimable_count": 0, "last_result": {}, "last_error": str(err)})
return
self.relayer_client = client
def _normalize_condition_id(self, value):
s = str(value or "").strip().lower()
if not s:
return ""
if s.startswith("0x"):
s = s[2:]
if len(s) != 64:
return ""
try:
int(s, 16)
except Exception:
return ""
return "0x" + s
def _fetch_positions(self, user):
try:
r = requests.get(
f"{DATA_API}/positions",
params={"user": user, "sizeThreshold": 0},
proxies=PROXIES if PROXIES else None,
timeout=12,
)
if r.status_code == 200:
data = r.json()
if isinstance(data, list):
return data
except Exception:
pass
return []
def _create_relayer_client(self):
try:
import inspect
import py_builder_relayer_client.client as rel_mod
from py_builder_relayer_client.client import RelayClient
try:
from py_builder_signing_sdk import BuilderConfig, BuilderApiKeyCreds
except Exception:
from py_builder_signing_sdk.config import BuilderConfig, BuilderApiKeyCreds
cfg = BuilderConfig(
local_builder_creds=BuilderApiKeyCreds(
key=POLY_BUILDER_API_KEY,
secret=POLY_BUILDER_SECRET,
passphrase=POLY_BUILDER_PASSPHRASE,
)
)
args = [RELAYER_URL, 137, self.private_key, cfg]
init_params = inspect.signature(RelayClient.__init__).parameters
if len(init_params) >= 6:
tx_enum = getattr(rel_mod, "RelayerTxType", None) or getattr(rel_mod, "TransactionType", None)
tx_value = None
if tx_enum is not None:
if RELAYER_TX_TYPE == "PROXY" and hasattr(tx_enum, "PROXY"):
tx_value = getattr(tx_enum, "PROXY")
elif hasattr(tx_enum, "SAFE"):
tx_value = getattr(tx_enum, "SAFE")
elif hasattr(tx_enum, "SAFE_CREATE"):
tx_value = getattr(tx_enum, "SAFE_CREATE")
if tx_value is not None:
args.append(tx_value)
return RelayClient(*args), ""
except Exception as e:
return None, str(e)
def _collect_redeemable(self):
pending = []
seen = set()
claimable = []
for owner in self.scan_addresses:
rows = self._fetch_positions(owner)
owner_l = owner.lower()
for row in rows:
if not isinstance(row, dict):
continue
size = row.get("size")
try:
size_f = float(size or 0)
except Exception:
size_f = 0.0
if size_f <= 0:
continue
redeemable = bool(row.get("redeemable") or row.get("mergeable"))
if not redeemable:
continue
cid = self._normalize_condition_id(
row.get("conditionId") or row.get("condition_id")
)
if not cid:
continue
key = owner_l + "|" + cid
if key in seen:
continue
seen.add(key)
pending.append({"owner": owner, "condition_id": cid})
if owner_l == self.funder_address.lower() and cid not in claimable:
claimable.append(cid)
return pending, claimable
def _redeem_condition(self, condition_id):
try:
from py_builder_relayer_client.models import SafeTransaction, OperationType
ctf_addr = Web3.to_checksum_address(CTF_CONTRACT)
usdc_addr = Web3.to_checksum_address(USDC_E_CONTRACT)
contract = Web3().eth.contract(
address=ctf_addr,
abi=[{
"name": "redeemPositions",
"type": "function",
"stateMutability": "nonpayable",
"inputs": [
{"name": "collateralToken", "type": "address"},
{"name": "parentCollectionId", "type": "bytes32"},
{"name": "conditionId", "type": "bytes32"},
{"name": "indexSets", "type": "uint256[]"},
],
"outputs": [],
}],
)
cond_bytes = bytes.fromhex(condition_id[2:])
data = contract.encode_abi(
abi_element_identifier="redeemPositions",
args=[usdc_addr, b"\x00" * 32, cond_bytes, [1, 2]],
)
op_call = getattr(OperationType, "Call", None)
if op_call is None:
op_call = list(OperationType)[0]
tx = SafeTransaction(to=str(ctf_addr), operation=op_call, data=str(data), value="0")
def execute_once():
resp = self.relayer_client.execute([tx], f"Redeem {condition_id}")
result = resp.wait()
txh = str(getattr(resp, "transaction_hash", "") or "")
state = ""
if isinstance(result, dict):
txh = str(result.get("transaction_hash") or result.get("transactionHash") or txh)
state = str(result.get("state") or "")
else:
txh = str(getattr(result, "transaction_hash", "") or getattr(result, "transactionHash", "") or txh)
state = str(getattr(result, "state", "") or "")
if result is None:
return False, txh, "relayer_not_confirmed"
if state and state not in ["STATE_CONFIRMED", "STATE_MINED", "STATE_EXECUTED"]:
return False, txh, f"state={state}"
return True, txh, ""
try:
return execute_once()
except Exception as e:
msg = str(e)
low = msg.lower()
if "expected safe" in low and "not deployed" in low:
dep = self.relayer_client.deploy()
dep.wait()
return execute_once()
return False, "", msg
except Exception as e:
return False, "", str(e)
def scan_once(self):
if not self.enabled:
return
pending, claimable = self._collect_redeemable()
now = time.time()
self.last_pending_count = len(pending)
self.last_claimable_count = len(claimable)
_dashboard_set(auto_redeem={
"enabled": self.enabled,
"pending_count": self.last_pending_count,
"claimable_count": self.last_claimable_count,
"last_result": dict(self.last_result or {}),
"last_error": self.last_error,
"scan_interval": REDEEM_SCAN_INTERVAL,
})
if pending:
signature = "|".join([f"{x['owner']}:{x['condition_id']}" for x in pending])
if signature != self.last_pending_signature or (now - self.last_pending_log_ts) >= REDEEM_PENDING_LOG_INTERVAL:
self.last_pending_signature = signature
self.last_pending_log_ts = now
owners = sorted(list({x["owner"] for x in pending}))
owner_text = ", ".join(owners[:3])
if len(owners) > 3:
owner_text += f" +{len(owners) - 3} more"
log(f"Redeemable pending: {len(pending)}, relayer-claimable: {len(claimable)}, owners: {owner_text}", "WARN", force=True)
if not claimable:
return
processed = 0
for cid in claimable:
t0 = self.last_try_by_condition.get(cid, 0)
if now - t0 < REDEEM_RETRY_INTERVAL:
continue
self.last_try_by_condition[cid] = now
ok, tx_hash, err = self._redeem_condition(cid)
if ok:
log(f"Relayer redeem ok: {cid} | tx {tx_hash}", "TRADE", force=True)
self.last_error = ""
self.last_result = {
"time": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
"ok": True,
"condition_id": cid,
"tx": tx_hash,
"message": "ok",
}
else:
log(f"Relayer redeem failed: {cid} | {err}", "ERR", force=True)
self.last_error = str(err)
self.last_result = {
"time": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
"ok": False,
"condition_id": cid,
"tx": tx_hash,
"message": str(err),
}
_dashboard_set(auto_redeem={
"enabled": self.enabled,
"pending_count": self.last_pending_count,
"claimable_count": self.last_claimable_count,
"last_result": dict(self.last_result or {}),
"last_error": self.last_error,
"scan_interval": REDEEM_SCAN_INTERVAL,
})
_sync_dashboard_account_snapshot(self.funder_address)
processed += 1
if processed >= REDEEM_MAX_PER_SCAN:
break
def _loop(self):
while self.running:
try:
self.scan_once()
except Exception as e:
log(f"Auto-redeem scan error: {e}", "ERR", force=True)
for _ in range(REDEEM_SCAN_INTERVAL):
if not self.running:
break
time.sleep(1)
def start(self):
if not self.enabled:
return
if self.running:
return
self.running = True
self.thread = threading.Thread(target=self._loop, daemon=True)
self.thread.start()
log(f"Auto-redeem on: scan every {REDEEM_SCAN_INTERVAL}s", "OK", force=True)
_dashboard_set(auto_redeem={
"enabled": self.enabled,
"pending_count": self.last_pending_count,
"claimable_count": self.last_claimable_count,
"last_result": dict(self.last_result or {}),
"last_error": self.last_error,
"scan_interval": REDEEM_SCAN_INTERVAL,
})
def stop(self):
self.running = False
# ============== Main loop ==============
def main():
start_web_server()
if WEB_ENABLED:
log(f"Dashboard: http://{WEB_HOST}:{WEB_PORT}", "OK", force=True)
print("\n" + "="*60)
print(f" Polymarket BTC {_btc_market_minutes}m auto-trader")
print("="*60)
print(f" Simulation mode: {'on (paper, no CLOB)' if SIMULATION_MODE else 'off'}")
print(f" Auto trade: {'on' if AUTO_TRADE else 'off'}")
print(f" BTC market window: {_btc_market_minutes}m (config BTC_MARKET_MINUTES or dashboard)")
print(f" Trading analysis log: {TRADING_ANALYSIS_LOG}")
print(f" Auto redeem: {'on' if AUTO_REDEEM and not SIMULATION_MODE else 'off'}")
_init_trading_analysis_session()
log(f"Trading analysis log ready (append-only): {TRADING_ANALYSIS_LOG}", "OK", force=True)
print(f" Order size: ${TRADE_AMOUNT}")
print(f" Rule 1: time≤{C1_TIME}s and diff≥${C1_DIFF} (UP prob {C1_MIN_PROB*100:.0f}-{C1_MAX_PROB*100:.0f}%)")
print(f" Rule 2: time≤{C2_TIME}s and diff≥${C2_DIFF} (DOWN prob {C2_MIN_PROB*100:.0f}-{C2_MAX_PROB*100:.0f}%)")
print(f" Rule 3: time≤{C3_TIME}s and diff≥${C3_DIFF} (UP prob {C3_MIN_PROB*100:.0f}-{C3_MAX_PROB*100:.0f}%)")
print(f" Rule 4: time≤{C4_TIME}s and diff≥${C4_DIFF} (DOWN prob {C4_MIN_PROB*100:.0f}-{C4_MAX_PROB*100:.0f}%)")
print(f" TP/SL: prob-based (SL {STOP_LOSS_PROB_PCT*100:.0f}%, RR≈{TAKE_PROFIT_RR:.2f}, TP cap {TAKE_PROFIT_CAP*100:.1f}%)")
print(f" Cancel after: {ORDER_TIMEOUT_SEC}s unfilled")
print(f" Slippage cap: {SLIPPAGE_THRESHOLD*100:.0f}%")
print(f" Max retries / market: {MAX_RETRY_PER_MARKET}")
print(f" Chase step: +{BUY_RETRY_STEP*100:.1f}% per retry")
print(f" TP retries: up to {TAKE_PROFIT_RETRY_MAX}, step +{TAKE_PROFIT_RETRY_STEP*100:.1f}%")
print(f" Stop: entry prob down {STOP_LOSS_PROB_PCT*100:.0f}%")
print(f" Stale data skip: >{MARKET_DATA_MAX_LAG_SEC:.1f}s")
print(f" Loop interval: {LOOP_INTERVAL_SEC:.2f}s")
print("="*60 + "\n")
trader = Trader()
redeemer = AutoRedeemer(os.getenv("PRIVATE_KEY"), os.getenv("FUNDER_ADDRESS"))
if SIMULATION_MODE:
log("SIMULATION_MODE: paper trading — instant fills, no CLOB orders, no redeem", "OK", force=True)
elif AUTO_TRADE:
if not trader.connect():
log("Cannot connect CLOB client, exiting", "ERR", force=True)
return
if not SIMULATION_MODE:
redeemer.start()
else:
_dashboard_set(auto_redeem={"enabled": False, "pending_count": 0, "claimable_count": 0, "last_result": {}, "last_error": "simulation mode"})
init_state = load_state()
_dashboard_set(
position=dict(init_state.get("position") or {}),
pending_order=_dashboard_pending_order_from_state(init_state),
last_order=dict(init_state.get("last_order") or {}),
trade_history=list(init_state.get("trade_history") or []),
wallet_balance=None,
wallet_positions=[],
wallet_history=[],
live_trades=[],
live_positions_count=0,
live_realized_pnl=0.0,
live_unrealized_pnl=0.0,
live_total_pnl=0.0,
cumulative_realized_pnl=float(init_state.get("cumulative_realized_pnl") or 0.0),
simulation_mode=SIMULATION_MODE,
)
log("Starting price feeds...", "INFO", force=True)
last_slug = None
market_listener = None
first_display = True
last_chainlink_update = 0
last_account_sync = 0.0
last_market_fetch = 0.0
last_stale_log_ts = 0.0
dashboard_user = (os.getenv("FUNDER_ADDRESS", "") or "").strip().lower()
if not dashboard_user:
dashboard_user = (os.getenv("PRIVATE_KEY_ADDRESS", "") or "").strip().lower()
if not SIMULATION_MODE and AUTO_TRADE and trader.address:
dashboard_user = ((os.getenv("FUNDER_ADDRESS", "") or trader.address) or "").strip().lower()
_trigger_market_refresh()
_trigger_account_sync(dashboard_user)
try:
while True:
now = time.time()
# Refresh reference prices async so the loop stays responsive
if now - last_chainlink_update > 5:
_trigger_price_refresh()
last_chainlink_update = now
# Market metadata refresh async
if now - last_market_fetch >= MARKET_META_REFRESH_SEC:
_trigger_market_refresh()
last_market_fetch = now
market = None
market_data_cache = _get_market_cache()
if market_data_cache:
try:
end_ts = datetime.fromisoformat(str(market_data_cache.get("end", "")).replace("Z", "+00:00")).timestamp()
remaining_live = int(end_ts - now)
except Exception:
remaining_live = 0
if remaining_live <= 0:
_clear_market_cache()
last_market_fetch = 0.0
else:
market = dict(market_data_cache)
market["remaining"] = remaining_live
if now - last_account_sync >= DASHBOARD_ACCOUNT_SYNC_SEC:
_trigger_account_sync(dashboard_user)
last_account_sync = now
if not market:
state_snapshot = load_state()
_dashboard_set(
market={"slug": "", "remaining": 0, "status": "waiting"},
prices={
"ptb": price_data.get("ptb"),
"chainlink_btc": price_data.get("btc"),
"binance_btc": price_data.get("binance"),
"up_price": price_data.get("up_price"),
"down_price": price_data.get("down_price"),
"diff": None,
"diff_abs": None,
},
position=dict(state_snapshot.get("position") or {}),
pending_order=_dashboard_pending_order_from_state(state_snapshot),
last_order=dict(state_snapshot.get("last_order") or {}),
trade_history=list(state_snapshot.get("trade_history") or []),
btc_market_minutes=_btc_market_minutes,
cumulative_realized_pnl=float(state_snapshot.get("cumulative_realized_pnl") or 0.0),
simulation_mode=SIMULATION_MODE,
)
if first_display:
print("\nWaiting for active market...")
if price_data["btc"]:
print(f"BTC (Chainlink): ${price_data['btc']:,.2f}")
time.sleep(LOOP_INTERVAL_SEC)
continue
slug = market["slug"]
remaining = market["remaining"]
# Market rollover
if last_slug and slug != last_slug:
if market_listener:
market_listener.stop()
state = load_state()
state.pop("position", None)
state.pop("last_order", None)
state.pop("take_profit_order", None)
save_state(state)
market_listener = MarketPriceListener(market["up_token"], market["down_token"])
market_listener.start()
price_data["ptb"] = None
first_display = True
time.sleep(2)
elif not last_slug:
market_listener = MarketPriceListener(market["up_token"], market["down_token"])
market_listener.start()
time.sleep(2)
last_slug = slug
# PTB from crypto-price API
if not price_data["ptb"]:
crypto_data = get_crypto_price_api(market["start"], market["end"])
if crypto_data.get("openPrice"):
price_data["ptb"] = crypto_data["openPrice"]
elif crypto_data.get("closePrice"):
price_data["ptb"] = crypto_data["closePrice"]
log(f"Using prior window closePrice as PTB: {price_data['ptb']}", "INFO")
# Live book mid from WebSocket
btc = _to_float(price_data.get("btc"), 0.0)
ptb = _to_float(price_data.get("ptb"), 0.0)
up_price = _to_float(price_data.get("up_price") if price_data.get("up_price") is not None else market.get("up_price"), 0.0)
down_price = _to_float(price_data.get("down_price") if price_data.get("down_price") is not None else market.get("down_price"), 0.0)
up_bid = _maybe_float(price_data.get("up_bid"))
up_ask = _maybe_float(price_data.get("up_ask"))
down_bid = _maybe_float(price_data.get("down_bid"))
down_ask = _maybe_float(price_data.get("down_ask"))
# Use ask side for buys (executable)
up_entry_price = up_ask if (up_ask is not None and up_ask > 0) else up_price
down_entry_price = down_ask if (down_ask is not None and down_ask > 0) else down_price
# Diff vs PTB
diff = btc - ptb if (btc > 0 and ptb > 0) else 0
diff_abs = abs(diff)
_dashboard_set(
market={
"slug": slug,
"remaining": remaining,
"remaining_text": f"{remaining//60}m {remaining%60}s",
"start": market.get("start"),
"end": market.get("end"),
"status": "active",
},
prices={
"ptb": ptb if ptb > 0 else None,
"chainlink_btc": btc if btc > 0 else None,
"binance_btc": (price_data.get("binance") or None),
"up_price": up_price,
"down_price": down_price,
"up_bid": up_bid,
"up_ask": up_ask,
"down_bid": down_bid,
"down_ask": down_ask,
"diff": diff if (btc > 0 and ptb > 0) else None,
"diff_abs": diff_abs if (btc > 0 and ptb > 0) else None,
"updated_ts": time.time(),
},
btc_market_minutes=_btc_market_minutes,
)
state_snapshot = load_state()
_dashboard_set(
position=dict(state_snapshot.get("position") or {}),
pending_order=_dashboard_pending_order_from_state(state_snapshot),
last_order=dict(state_snapshot.get("last_order") or {}),
trade_history=list(state_snapshot.get("trade_history") or []),
cumulative_realized_pnl=float(state_snapshot.get("cumulative_realized_pnl") or 0.0),
simulation_mode=SIMULATION_MODE,
)
if first_display:
print("\n" + "="*90)
print(f"Market: {slug}")
print(f"Time left: {remaining//60}m {remaining%60}s")
print()
print("┌────────────────────────┬────────────────────────┬────────────────────────┐")
print("│ PTB │ Chainlink (ref) │ Binance (ref) │")
ptb_display = f"${ptb:,.2f}" if ptb > 0 else "fetching..."
btc_display = f"${btc:,.2f}" if btc > 0 else "fetching..."
binance = price_data.get("binance") or 0
binance_display = f"${binance:,.2f}" if binance > 0 else "fetching..."
print(f"│ {ptb_display:22s}{btc_display:22s}{binance_display:22s} │")
print("├────────────────────────┴────────────────────────┴────────────────────────┤")
print("│ Market mid │")
print(f"│ UP: {up_price*100:.2f}% DOWN: {down_price*100:.2f}% │")
print("├──────────────────────────────────────────────────────────────────────────┤")
print("│ Live diff (Chainlink - PTB) │")
if btc > 0 and ptb > 0:
diff_display = f"{diff:+.0f} USD"
else:
diff_display = "waiting for prices..."
print(f"│ {diff_display:72s} │")
print("└──────────────────────────────────────────────────────────────────────────┘")
print()
print("="*90)
print("Live log:")
print("="*90)
first_display = False
ptb_str = f"${ptb:,.0f}" if ptb > 0 else "..."
btc_str = f"${btc:,.0f}" if btc > 0 else "..."
binance = price_data.get("binance") or 0
binance_str = f"${binance:,.0f}" if binance > 0 else "N/A"
diff_str = f"{diff:+.0f}" if (btc > 0 and ptb > 0) else "N/A"
status = f"[{datetime.now().strftime('%H:%M:%S')}] left {remaining//60:02d}m{remaining%60:02d}s | CL:{btc_str} | BN:{binance_str} | PTB:{ptb_str} | diff:{diff_str} | UP:{up_price*100:.1f}% DOWN:{down_price*100:.1f}%"
print(f"\r{status}" + " "*10, end="", flush=True)
# Evaluate trigger rules
triggered = False
condition = None
side = None
desired_side = None
price = None
token = None
if remaining <= C1_TIME and diff >= C1_DIFF:
prob = up_entry_price
if C1_MIN_PROB <= prob <= C1_MAX_PROB:
triggered = True
desired_side = "UP"
condition = f"R1: time≤{C1_TIME}s & diff≥${C1_DIFF} (UP {prob*100:.0f}%)"
else:
log(f"R1 skip: UP prob {prob*100:.1f}% not in {C1_MIN_PROB*100:.0f}{C1_MAX_PROB*100:.0f}%", "INFO")
elif remaining <= C2_TIME and diff <= -C2_DIFF:
prob = down_entry_price
if C2_MIN_PROB <= prob <= C2_MAX_PROB:
triggered = True
desired_side = "DOWN"
condition = f"R2: time≤{C2_TIME}s & diff≤-${C2_DIFF} (DOWN {prob*100:.0f}%)"
else:
log(f"R2 skip: DOWN prob {prob*100:.1f}% not in {C2_MIN_PROB*100:.0f}{C2_MAX_PROB*100:.0f}%", "INFO")
elif remaining <= C3_TIME and diff >= C3_DIFF:
prob = up_entry_price
if C3_MIN_PROB <= prob <= C3_MAX_PROB:
triggered = True
desired_side = "UP"
condition = f"R3: time≤{C3_TIME}s & diff≥${C3_DIFF} (UP {prob*100:.0f}%)"
else:
log(f"R3 skip: UP prob {prob*100:.1f}% not in {C3_MIN_PROB*100:.0f}{C3_MAX_PROB*100:.0f}%", "INFO")
elif remaining <= C4_TIME and diff <= -C4_DIFF:
prob = down_entry_price
if C4_MIN_PROB <= prob <= C4_MAX_PROB:
triggered = True
desired_side = "DOWN"
condition = f"R4: time≤{C4_TIME}s & diff≤-${C4_DIFF} (DOWN {prob*100:.0f}%)"
else:
log(f"R4 skip: DOWN prob {prob*100:.1f}% not in {C4_MIN_PROB*100:.0f}{C4_MAX_PROB*100:.0f}%", "INFO")
if triggered:
side = desired_side or ("UP" if diff > 0 else "DOWN")
price = up_entry_price if side == "UP" else down_entry_price
token = market["up_token"] if side == "UP" else market["down_token"]
# Skip stale book / BTC to avoid chasing on lag
side_ts = _to_float(price_data.get("up_update_ts" if side == "UP" else "down_update_ts"), 0.0)
btc_ts = _to_float(price_data.get("btc_update_ts"), 0.0)
side_age = now - side_ts if side_ts > 0 else 999.0
btc_age = now - btc_ts if btc_ts > 0 else 999.0
if price <= 0:
triggered = False
condition = None
elif side_age > MARKET_DATA_MAX_LAG_SEC or btc_age > MARKET_DATA_MAX_LAG_SEC:
if now - last_stale_log_ts >= 2:
log(
f"Stale data skip: {side} book age {side_age:.2f}s, BTC age {btc_age:.2f}s (max {MARKET_DATA_MAX_LAG_SEC:.1f}s)",
"WARN",
)
last_stale_log_ts = now
triggered = False
condition = None
# Order / position state
state = load_state()
last_order = state.get("last_order", {})
order_key = f"{slug}|{side}"
# Track working orders
pending_order = state.get("pending_order")
_dashboard_set(
position=dict(state.get("position") or {}),
pending_order=_dashboard_pending_order_from_state(state),
last_order=dict(last_order or {}),
)
if pending_order and (not SIMULATION_MODE) and AUTO_TRADE and trader.connected:
order_id = pending_order.get("order_id")
order_time = pending_order.get("time")
if order_time:
elapsed = (datetime.now() - datetime.fromisoformat(order_time)).total_seconds()
if elapsed > ORDER_TIMEOUT_SEC:
order_status = trader.get_order_status(order_id)
if order_status and not order_status.get("filled"):
log(f"Order timeout, cancel & retry (id {order_id})", "TRADE")
trader.cancel_order(order_id)
state.pop("pending_order", None)
save_state(state)
_emit_trading_analysis(
"BUY_CANCEL_TIMEOUT",
slug=slug,
order_id=order_id,
status="buy",
shares_type=pending_order.get("side"),
share_price=float(pending_order.get("price") or 0) or None,
btc_price=btc if btc > 0 else None,
chainlink_btc=btc if btc > 0 else None,
ptb=ptb if ptb > 0 else None,
btc_minus_ptb=_btc_ptb_snapshot(btc if btc > 0 else None, ptb if ptb > 0 else None),
remaining_sec=remaining,
pnl_trade_usd=None,
pnl_total_usd=_to_float(state.get("cumulative_realized_pnl"), 0.0),
)
_dashboard_set(
position=dict(state.get("position") or {}),
pending_order=_dashboard_pending_order_from_state(state),
last_order=dict(state.get("last_order") or {}),
)
elif order_status and order_status.get("filled"):
filled_side = pending_order.get("side") or side
filled_price = float(pending_order.get("price") or price or 0)
filled_slug = pending_order.get("slug") or slug
log(f"Filled {filled_side} @ {filled_price*100:.2f}% ({filled_slug})", "TRADE")
state.pop("pending_order", None)
filled_size = float(order_status.get("size_matched") or order_status.get("original_size") or TRADE_AMOUNT)
state["position"] = {
"slug": filled_slug,
"side": filled_side,
"entry_price": filled_price,
"entry_diff": diff_abs,
"size": filled_size,
}
cum = _to_float(state.get("cumulative_realized_pnl"), 0.0)
state = _append_trade_history(state, {
"time": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
"slug": filled_slug,
"action": "BUY",
"side": filled_side,
"price": filled_price,
"amount": TRADE_AMOUNT,
"shares": filled_size,
"order_size_usdc": TRADE_AMOUNT,
"order_id": order_id,
"status": "filled",
"reason": "pending_filled",
"diff": diff,
"btc": btc if btc > 0 else None,
"ptb": ptb if ptb > 0 else None,
"btc_minus_ptb": _btc_ptb_snapshot(btc if btc > 0 else None, ptb if ptb > 0 else None),
"remaining_sec": remaining,
"cumulative_realized_pnl_usd": cum,
})
_emit_trading_analysis(
"BUY_FILL",
action="BUY",
slug=filled_slug,
status="buy",
shares_type=filled_side,
share_price=filled_price,
share_amount=filled_size,
order_size_usdc=TRADE_AMOUNT,
order_id=order_id,
reason="pending_filled",
btc_price=btc if btc > 0 else None,
chainlink_btc=btc if btc > 0 else None,
ptb=ptb if ptb > 0 else None,
btc_minus_ptb=_btc_ptb_snapshot(btc if btc > 0 else None, ptb if ptb > 0 else None),
diff_rule=diff,
remaining_sec=remaining,
pnl_trade_usd=0.0,
pnl_total_usd=cum,
)
save_state(state)
_dashboard_set(
position=dict(state.get("position") or {}),
pending_order=_dashboard_pending_order_from_state(state),
last_order=dict(state.get("last_order") or {}),
trade_history=list(state.get("trade_history") or []),
cumulative_realized_pnl=cum,
)
_sync_dashboard_account_snapshot(dashboard_user)
# New entry if flat and no working order
has_position = bool(state.get("position"))
retry_count = int(last_order.get("retry_count", 0) or 0)
same_key_retry = (last_order.get("key") == order_key)
can_place = (not pending_order) and (not has_position) and ((not same_key_retry) or (retry_count < MAX_RETRY_PER_MARKET))
if can_place:
if same_key_retry and retry_count > 0:
last_price = _to_float(last_order.get("last_price"), price)
retry_cap_price = min(0.995, last_price + BUY_RETRY_STEP)
if price > retry_cap_price:
log(
f"Chase cap: {price*100:.2f}% > last {last_price*100:.2f}%+{BUY_RETRY_STEP*100:.2f}%, use {retry_cap_price*100:.2f}%",
"INFO",
)
price = min(price, retry_cap_price)
current_price = up_entry_price if side == "UP" else down_entry_price
if price > 0:
slippage = abs(current_price - price) / price
if slippage > SLIPPAGE_THRESHOLD:
log(f"Slippage too high: {slippage*100:.1f}% > {SLIPPAGE_THRESHOLD*100:.0f}%, skip", "WARN")
triggered = False
condition = None
if triggered:
if same_key_retry and retry_count >= MAX_RETRY_PER_MARKET:
log(f"Max retries ({MAX_RETRY_PER_MARKET}) for {order_key}", "WARN")
triggered = False
condition = None
if triggered:
log(f"Trigger: {condition} -> {side} @ {price*100:.1f}%", "TRADE")
if SIMULATION_MODE:
sim_shares = _shares_from_usdc_buy(TRADE_AMOUNT, price)
if sim_shares <= 0:
log(f"[SIM] Buy skipped: invalid price {price}", "WARN")
else:
sim_oid = f"SIM-{int(time.time() * 1000)}"
state.pop("pending_order", None)
current_retry = retry_count if same_key_retry else 0
state["last_order"] = {
"key": order_key,
"time": datetime.now().isoformat(),
"retry_count": current_retry + 1,
"last_price": price,
}
state["position"] = {
"slug": slug,
"side": side,
"entry_price": price,
"entry_diff": diff_abs,
"size": sim_shares,
}
cum = _to_float(state.get("cumulative_realized_pnl"), 0.0)
state = _append_trade_history(state, {
"time": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
"slug": slug,
"action": "BUY",
"side": side,
"price": price,
"amount": TRADE_AMOUNT,
"shares": sim_shares,
"order_size_usdc": TRADE_AMOUNT,
"order_id": sim_oid,
"status": "filled",
"reason": condition,
"diff": diff,
"btc": btc if btc > 0 else None,
"ptb": ptb if ptb > 0 else None,
"btc_minus_ptb": _btc_ptb_snapshot(btc if btc > 0 else None, ptb if ptb > 0 else None),
"remaining_sec": remaining,
"cumulative_realized_pnl_usd": cum,
})
_emit_trading_analysis(
"BUY_FILL",
action="BUY",
slug=slug,
status="buy",
shares_type=side,
share_price=price,
share_amount=sim_shares,
order_size_usdc=TRADE_AMOUNT,
order_id=sim_oid,
reason=condition,
btc_price=btc if btc > 0 else None,
chainlink_btc=btc if btc > 0 else None,
ptb=ptb if ptb > 0 else None,
btc_minus_ptb=_btc_ptb_snapshot(btc if btc > 0 else None, ptb if ptb > 0 else None),
diff_rule=diff,
remaining_sec=remaining,
pnl_trade_usd=0.0,
pnl_total_usd=cum,
)
save_state(state)
_dashboard_set(
position=dict(state.get("position") or {}),
pending_order=_dashboard_pending_order_from_state(state),
last_order=dict(state.get("last_order") or {}),
trade_history=list(state.get("trade_history") or []),
cumulative_realized_pnl=cum,
)
log(
f"[SIM] BUY {side} @ {price*100:.2f}% | USDC {TRADE_AMOUNT} | shares≈{sim_shares:.4f} | id {sim_oid}",
"TRADE",
)
elif AUTO_TRADE and trader.connected:
order_id = trader.place_order(token, "BUY", price, TRADE_AMOUNT)
if order_id:
state["pending_order"] = {
"order_id": order_id,
"time": datetime.now().isoformat(),
"slug": slug,
"side": side,
"price": price
}
current_retry = retry_count if same_key_retry else 0
state["last_order"] = {
"key": order_key,
"time": datetime.now().isoformat(),
"retry_count": current_retry + 1,
"last_price": price,
}
cum = _to_float(state.get("cumulative_realized_pnl"), 0.0)
state = _append_trade_history(state, {
"time": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
"slug": slug,
"action": "BUY",
"side": side,
"price": price,
"amount": TRADE_AMOUNT,
"order_size_usdc": TRADE_AMOUNT,
"order_id": order_id,
"status": "submitted",
"reason": condition,
"diff": diff,
"btc": btc if btc > 0 else None,
"ptb": ptb if ptb > 0 else None,
"btc_minus_ptb": _btc_ptb_snapshot(btc if btc > 0 else None, ptb if ptb > 0 else None),
"remaining_sec": remaining,
"cumulative_realized_pnl_usd": cum,
})
_emit_trading_analysis(
"BUY_SUBMIT",
action="BUY",
slug=slug,
status="buy",
shares_type=side,
share_price=price,
order_size_usdc=TRADE_AMOUNT,
order_id=order_id,
reason=condition,
btc_price=btc if btc > 0 else None,
chainlink_btc=btc if btc > 0 else None,
ptb=ptb if ptb > 0 else None,
btc_minus_ptb=_btc_ptb_snapshot(btc if btc > 0 else None, ptb if ptb > 0 else None),
diff_rule=diff,
remaining_sec=remaining,
pnl_trade_usd=0.0,
pnl_total_usd=cum,
)
save_state(state)
_dashboard_set(
pending_order=_dashboard_pending_order_from_state(state),
last_order=dict(state.get("last_order") or {}),
trade_history=list(state.get("trade_history") or []),
cumulative_realized_pnl=cum,
)
_sync_dashboard_account_snapshot(dashboard_user)
log(f"Order submitted, watching id {order_id}", "TRADE")
else:
log(f"Order failed: {side} @ {price*100:.1f}%", "ERR")
current_retry = retry_count if same_key_retry else 0
state["last_order"] = {
"key": order_key,
"time": datetime.now().isoformat(),
"retry_count": current_retry + 1,
"last_price": price,
}
cum = _to_float(state.get("cumulative_realized_pnl"), 0.0)
state = _append_trade_history(state, {
"time": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
"slug": slug,
"action": "BUY",
"side": side,
"price": price,
"amount": TRADE_AMOUNT,
"order_id": "",
"status": "failed",
"reason": condition,
"diff": diff,
"btc": btc if btc > 0 else None,
"ptb": ptb if ptb > 0 else None,
"btc_minus_ptb": _btc_ptb_snapshot(btc if btc > 0 else None, ptb if ptb > 0 else None),
"remaining_sec": remaining,
"cumulative_realized_pnl_usd": cum,
})
_emit_trading_analysis(
"BUY_FAILED",
action="BUY",
slug=slug,
status="buy",
shares_type=side,
share_price=price,
order_size_usdc=TRADE_AMOUNT,
reason=condition,
btc_price=btc if btc > 0 else None,
chainlink_btc=btc if btc > 0 else None,
ptb=ptb if ptb > 0 else None,
btc_minus_ptb=_btc_ptb_snapshot(btc if btc > 0 else None, ptb if ptb > 0 else None),
diff_rule=diff,
remaining_sec=remaining,
pnl_trade_usd=None,
pnl_total_usd=cum,
)
save_state(state)
_dashboard_set(
last_order=dict(state.get("last_order") or {}),
trade_history=list(state.get("trade_history") or []),
cumulative_realized_pnl=cum,
)
_sync_dashboard_account_snapshot(dashboard_user)
elif not SIMULATION_MODE:
log(f"Alert: consider BUY {side} @ {price*100:.1f}%", "TRADE")
current_retry = retry_count if same_key_retry else 0
state["last_order"] = {
"key": order_key,
"time": datetime.now().isoformat(),
"retry_count": current_retry + 1,
"last_price": price,
}
_emit_trading_analysis(
"BUY_ALERT",
action="BUY",
slug=slug,
status="buy",
shares_type=side,
share_price=price,
order_size_usdc=TRADE_AMOUNT,
reason=condition,
btc_price=btc if btc > 0 else None,
chainlink_btc=btc if btc > 0 else None,
ptb=ptb if ptb > 0 else None,
btc_minus_ptb=_btc_ptb_snapshot(btc if btc > 0 else None, ptb if ptb > 0 else None),
diff_rule=diff,
remaining_sec=remaining,
pnl_trade_usd=None,
pnl_total_usd=_to_float(state.get("cumulative_realized_pnl"), 0.0),
)
save_state(state)
_dashboard_set(last_order=dict(state.get("last_order") or {}))
# Position TP / SL
state = load_state()
pos = state.get("position")
tp_order = state.get("take_profit_order") or {}
if pos and pos.get("slug") == slug:
pos_side = pos.get("side")
current_prob = up_price if pos_side == "UP" else down_price
position_size = max(0.001, _to_float(pos.get("size"), TRADE_AMOUNT))
entry_prob = _maybe_float(pos.get("entry_price"))
stop_loss_triggered = False
stop_prob = None
tp_trigger_prob = None
tp_sell_price = None
if entry_prob is not None and entry_prob > 0:
stop_prob = max(0.0, entry_prob * (1.0 - STOP_LOSS_PROB_PCT))
risk_abs = max(0.0, entry_prob - stop_prob)
tp_trigger_prob = min(TAKE_PROFIT_CAP, entry_prob + risk_abs * TAKE_PROFIT_RR)
if tp_trigger_prob <= entry_prob:
tp_trigger_prob = None
else:
# If TP capped, tighten stop to preserve RR
balanced_risk = (tp_trigger_prob - entry_prob) / TAKE_PROFIT_RR
balanced_stop_prob = max(0.0, entry_prob - balanced_risk)
if balanced_stop_prob > stop_prob:
stop_prob = balanced_stop_prob
tp_sell_price = tp_trigger_prob
stop_loss_triggered = (current_prob > 0) and (current_prob <= stop_prob)
if SIMULATION_MODE and pos and pos.get("slug") == slug and entry_prob is not None and entry_prob > 0:
sim_exit = False
if stop_loss_triggered:
sell_x = (up_bid if pos_side == "UP" else down_bid) or (up_price if pos_side == "UP" else down_price)
ep = float(entry_prob)
xp = float(sell_x)
realized = position_size * (xp - ep)
cum = _to_float(state.get("cumulative_realized_pnl"), 0.0) + realized
state["cumulative_realized_pnl"] = cum
state = _append_trade_history(state, {
"time": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
"slug": slug,
"action": "SELL",
"side": pos_side,
"price": xp,
"entry_price": ep,
"shares": position_size,
"amount": position_size * xp,
"order_id": f"SIM-SL-{int(time.time() * 1000)}",
"status": "filled",
"reason": "stop_loss",
"diff": diff,
"btc": btc if btc > 0 else None,
"ptb": ptb if ptb > 0 else None,
"btc_minus_ptb": _btc_ptb_snapshot(btc if btc > 0 else None, ptb if ptb > 0 else None),
"remaining_sec": remaining,
"realized_pnl_usd": realized,
"cumulative_realized_pnl_usd": cum,
})
_emit_trading_analysis(
"SELL_CLOSE",
reason="stop_loss",
slug=slug,
action="SELL",
status="sell",
shares_type=pos_side,
share_price=xp,
share_amount=position_size,
entry_share_price=ep,
exit_share_price=xp,
notional_exit_usd=position_size * xp,
take_profit=tp_trigger_prob,
stop_loss=stop_prob,
btc_price=btc if btc > 0 else None,
chainlink_btc=btc if btc > 0 else None,
ptb=ptb if ptb > 0 else None,
btc_minus_ptb=_btc_ptb_snapshot(btc if btc > 0 else None, ptb if ptb > 0 else None),
diff_rule=diff,
remaining_sec=remaining,
order_id=f"SIM-SL-{int(time.time() * 1000)}",
pnl_trade_usd=realized,
pnl_total_usd=cum,
)
state.pop("position", None)
state.pop("take_profit_order", None)
save_state(state)
_dashboard_set(
position={},
pending_order=_dashboard_pending_order_from_state(state),
trade_history=list(state.get("trade_history") or []),
cumulative_realized_pnl=cum,
)
log(
f"[SIM] Stop-loss {pos_side} @ {xp*100:.2f}% | PnL ${realized:+.4f} | cumulative ${cum:+.4f}",
"TRADE",
)
sim_exit = True
elif (
tp_trigger_prob is not None
and tp_sell_price
and current_prob > 0
and current_prob >= tp_trigger_prob
):
ep = float(entry_prob)
xp = float(tp_sell_price)
realized = position_size * (xp - ep)
cum = _to_float(state.get("cumulative_realized_pnl"), 0.0) + realized
state["cumulative_realized_pnl"] = cum
state = _append_trade_history(state, {
"time": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
"slug": slug,
"action": "SELL",
"side": pos_side,
"price": xp,
"entry_price": ep,
"shares": position_size,
"amount": position_size * xp,
"order_id": f"SIM-TP-{int(time.time() * 1000)}",
"status": "filled",
"reason": "take_profit",
"diff": diff,
"btc": btc if btc > 0 else None,
"ptb": ptb if ptb > 0 else None,
"btc_minus_ptb": _btc_ptb_snapshot(btc if btc > 0 else None, ptb if ptb > 0 else None),
"remaining_sec": remaining,
"realized_pnl_usd": realized,
"cumulative_realized_pnl_usd": cum,
})
_emit_trading_analysis(
"SELL_CLOSE",
reason="take_profit",
slug=slug,
action="SELL",
status="sell",
shares_type=pos_side,
share_price=xp,
share_amount=position_size,
entry_share_price=ep,
exit_share_price=xp,
notional_exit_usd=position_size * xp,
take_profit=tp_trigger_prob,
stop_loss=stop_prob,
btc_price=btc if btc > 0 else None,
chainlink_btc=btc if btc > 0 else None,
ptb=ptb if ptb > 0 else None,
btc_minus_ptb=_btc_ptb_snapshot(btc if btc > 0 else None, ptb if ptb > 0 else None),
diff_rule=diff,
remaining_sec=remaining,
order_id=f"SIM-TP-{int(time.time() * 1000)}",
pnl_trade_usd=realized,
pnl_total_usd=cum,
)
state.pop("position", None)
state.pop("take_profit_order", None)
save_state(state)
_dashboard_set(
position={},
pending_order=_dashboard_pending_order_from_state(state),
trade_history=list(state.get("trade_history") or []),
cumulative_realized_pnl=cum,
)
log(
f"[SIM] Take-profit {pos_side} @ {xp*100:.2f}% | PnL ${realized:+.4f} | cumulative ${cum:+.4f}",
"TRADE",
)
sim_exit = True
if sim_exit:
state = load_state()
pos = state.get("position")
tp_order = state.get("take_profit_order") or {}
# Working TP order lifecycle (live only)
if (
(not SIMULATION_MODE)
and tp_order
and tp_order.get("slug") == slug
and tp_order.get("side") == pos_side
and AUTO_TRADE
and trader.connected
):
tp_order_id = tp_order.get("order_id")
tp_status = trader.get_order_status(tp_order_id)
if tp_status and tp_status.get("filled"):
tp_price = float(tp_order.get("price") or tp_sell_price or 0.0)
tp_amount = max(0.001, _to_float(tp_order.get("amount"), position_size))
ep = _maybe_float(pos.get("entry_price")) or 0.0
realized = tp_amount * (tp_price - ep) if ep else 0.0
cum = _to_float(state.get("cumulative_realized_pnl"), 0.0) + realized
state["cumulative_realized_pnl"] = cum
state = _append_trade_history(state, {
"time": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
"slug": slug,
"action": "SELL",
"side": pos_side,
"price": tp_price,
"entry_price": ep,
"shares": tp_amount,
"amount": tp_amount * tp_price,
"order_id": tp_order_id or "",
"status": "filled",
"reason": "take_profit",
"diff": diff,
"btc": btc if btc > 0 else None,
"ptb": ptb if ptb > 0 else None,
"btc_minus_ptb": _btc_ptb_snapshot(btc if btc > 0 else None, ptb if ptb > 0 else None),
"remaining_sec": remaining,
"realized_pnl_usd": realized,
"cumulative_realized_pnl_usd": cum,
})
_emit_trading_analysis(
"SELL_CLOSE",
reason="take_profit",
slug=slug,
action="SELL",
status="sell",
shares_type=pos_side,
share_price=tp_price,
share_amount=tp_amount,
entry_share_price=ep,
exit_share_price=tp_price,
notional_exit_usd=tp_amount * tp_price,
take_profit=tp_trigger_prob,
stop_loss=stop_prob,
btc_price=btc if btc > 0 else None,
chainlink_btc=btc if btc > 0 else None,
ptb=ptb if ptb > 0 else None,
btc_minus_ptb=_btc_ptb_snapshot(btc if btc > 0 else None, ptb if ptb > 0 else None),
diff_rule=diff,
remaining_sec=remaining,
order_id=tp_order_id or "",
pnl_trade_usd=realized,
pnl_total_usd=cum,
)
state.pop("take_profit_order", None)
state.pop("position", None)
save_state(state)
_dashboard_set(
position={},
pending_order=_dashboard_pending_order_from_state(state),
trade_history=list(state.get("trade_history") or []),
cumulative_realized_pnl=cum,
)
_sync_dashboard_account_snapshot(dashboard_user)
log(f"TP order filled: {pos_side} @ {tp_price*100:.2f}%", "TRADE")
pos = None
tp_order = {}
elif tp_status and tp_status.get("status") in ["CANCELED", "CANCELLED", "REJECTED", "EXPIRED"]:
log("TP order dead; will re-arm if price moves", "WARN")
state.pop("take_profit_order", None)
save_state(state)
_dashboard_set(pending_order=_dashboard_pending_order_from_state(state))
tp_order = {}
if (
(not SIMULATION_MODE)
and pos
and (not tp_order)
and tp_trigger_prob is not None
and current_prob > 0
and current_prob >= tp_trigger_prob
):
log(
f"TP arm: entry {entry_prob*100:.1f}%, now {current_prob*100:.1f}%, target {tp_trigger_prob*100:.1f}% (RR≈{TAKE_PROFIT_RR:.2f})",
"TRADE",
)
if AUTO_TRADE and trader.connected:
sell_token = market["up_token"] if pos_side == "UP" else market["down_token"]
tp_order_id = None
tp_submit_price = tp_sell_price
attempt_price = tp_sell_price
for attempt_idx in range(TAKE_PROFIT_RETRY_MAX):
tp_order_id = trader.place_order(sell_token, "SELL", attempt_price, position_size)
if tp_order_id:
tp_submit_price = attempt_price
break
if attempt_idx + 1 >= TAKE_PROFIT_RETRY_MAX:
break
next_price = min(TAKE_PROFIT_CAP, attempt_price + TAKE_PROFIT_RETRY_STEP)
if next_price <= attempt_price + 1e-9:
break
log(
f"TP retry {attempt_idx+2}/{TAKE_PROFIT_RETRY_MAX}: bump to {next_price*100:.1f}%",
"WARN",
)
attempt_price = next_price
if tp_order_id:
state["take_profit_order"] = {
"order_id": tp_order_id,
"time": datetime.now().isoformat(),
"slug": slug,
"side": pos_side,
"price": tp_submit_price,
"amount": position_size,
"action": "SELL",
"reason": "take_profit",
}
cum = _to_float(state.get("cumulative_realized_pnl"), 0.0)
state = _append_trade_history(state, {
"time": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
"slug": slug,
"action": "SELL",
"side": pos_side,
"price": tp_submit_price,
"amount": position_size,
"order_id": tp_order_id,
"status": "submitted",
"reason": "take_profit",
"diff": diff,
"btc": btc if btc > 0 else None,
"ptb": ptb if ptb > 0 else None,
"btc_minus_ptb": _btc_ptb_snapshot(btc if btc > 0 else None, ptb if ptb > 0 else None),
"remaining_sec": remaining,
"cumulative_realized_pnl_usd": cum,
})
_emit_trading_analysis(
"SELL_SUBMIT",
reason="take_profit",
slug=slug,
action="SELL",
status="sell",
shares_type=pos_side,
share_price=tp_submit_price,
share_amount=position_size,
take_profit=tp_trigger_prob,
stop_loss=stop_prob,
order_id=tp_order_id,
btc_price=btc if btc > 0 else None,
chainlink_btc=btc if btc > 0 else None,
ptb=ptb if ptb > 0 else None,
btc_minus_ptb=_btc_ptb_snapshot(btc if btc > 0 else None, ptb if ptb > 0 else None),
diff_rule=diff,
remaining_sec=remaining,
pnl_trade_usd=None,
pnl_total_usd=cum,
)
save_state(state)
_dashboard_set(
pending_order=_dashboard_pending_order_from_state(state),
trade_history=list(state.get("trade_history") or []),
cumulative_realized_pnl=cum,
)
_sync_dashboard_account_snapshot(dashboard_user)
tp_order = dict(state.get("take_profit_order") or {})
log(f"TP order live id {tp_order_id}", "TRADE")
else:
log(f"TP order failed: {pos_side} @ {attempt_price*100:.1f}%", "ERR")
elif not SIMULATION_MODE:
log(f"Alert: consider SELL {pos_side} @ {tp_sell_price*100:.1f}% (size {position_size:.4f})", "TRADE")
_emit_trading_analysis(
"SELL_ALERT",
reason="take_profit",
slug=slug,
action="SELL",
status="sell",
shares_type=pos_side,
share_price=tp_sell_price,
share_amount=position_size,
take_profit=tp_trigger_prob,
stop_loss=stop_prob,
btc_price=btc if btc > 0 else None,
chainlink_btc=btc if btc > 0 else None,
ptb=ptb if ptb > 0 else None,
btc_minus_ptb=_btc_ptb_snapshot(btc if btc > 0 else None, ptb if ptb > 0 else None),
remaining_sec=remaining,
pnl_trade_usd=None,
pnl_total_usd=_to_float(state.get("cumulative_realized_pnl"), 0.0),
)
if (not SIMULATION_MODE) and pos and stop_loss_triggered:
log(
f"Stop hit: {pos_side} prob {current_prob*100:.1f}% <= stop {stop_prob*100:.1f}% (entry {entry_prob*100:.1f}%)",
"TRADE",
)
if AUTO_TRADE and trader.connected:
if tp_order and tp_order.get("order_id"):
trader.cancel_order(tp_order.get("order_id"))
state.pop("take_profit_order", None)
sell_price = (up_bid if pos_side == "UP" else down_bid) or (up_price if pos_side == "UP" else down_price)
sell_token = market["up_token"] if pos_side == "UP" else market["down_token"]
sell_order_id = trader.place_order(sell_token, "SELL", sell_price, position_size)
ep = _maybe_float(pos.get("entry_price")) or 0.0
xp = float(sell_price)
realized = position_size * (xp - ep) if sell_order_id and ep else 0.0
cum = _to_float(state.get("cumulative_realized_pnl"), 0.0) + (realized if sell_order_id else 0.0)
if sell_order_id:
state["cumulative_realized_pnl"] = cum
state = _append_trade_history(state, {
"time": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
"slug": slug,
"action": "SELL",
"side": pos_side,
"price": sell_price,
"entry_price": ep,
"shares": position_size,
"amount": position_size * xp,
"order_id": sell_order_id or "",
"status": "submitted" if sell_order_id else "failed",
"reason": "stop_loss",
"diff": diff,
"btc": btc if btc > 0 else None,
"ptb": ptb if ptb > 0 else None,
"btc_minus_ptb": _btc_ptb_snapshot(btc if btc > 0 else None, ptb if ptb > 0 else None),
"remaining_sec": remaining,
"realized_pnl_usd": realized if sell_order_id else None,
"cumulative_realized_pnl_usd": cum if sell_order_id else _to_float(state.get("cumulative_realized_pnl"), 0.0),
})
_emit_trading_analysis(
"SELL_SUBMIT" if sell_order_id else "SELL_FAILED",
reason="stop_loss",
slug=slug,
action="SELL",
status="sell",
shares_type=pos_side,
share_price=xp,
share_amount=position_size,
entry_share_price=ep,
exit_share_price=xp,
take_profit=tp_trigger_prob,
stop_loss=stop_prob,
order_id=sell_order_id or "",
btc_price=btc if btc > 0 else None,
chainlink_btc=btc if btc > 0 else None,
ptb=ptb if ptb > 0 else None,
btc_minus_ptb=_btc_ptb_snapshot(btc if btc > 0 else None, ptb if ptb > 0 else None),
diff_rule=diff,
remaining_sec=remaining,
pnl_trade_usd=realized if sell_order_id else None,
pnl_total_usd=cum if sell_order_id else _to_float(state.get("cumulative_realized_pnl"), 0.0),
)
state.pop("position", None)
state.pop("take_profit_order", None)
save_state(state)
_dashboard_set(
position={},
pending_order=_dashboard_pending_order_from_state(state),
trade_history=list(state.get("trade_history") or []),
cumulative_realized_pnl=cum if sell_order_id else _to_float(state.get("cumulative_realized_pnl"), 0.0),
)
_sync_dashboard_account_snapshot(dashboard_user)
log(f"Stop-loss sell done: {pos_side} @ {sell_price*100:.2f}%", "TRADE")
time.sleep(LOOP_INTERVAL_SEC)
except KeyboardInterrupt:
print("\n\nStopped.")
if market_listener:
market_listener.stop()
redeemer.stop()
if __name__ == "__main__":
main()