Files
polymarket-5min-15min-1hour…/5min-15min-PTB-bot/polymarket_auto_trade.py
T

3175 lines
133 KiB
Python
Raw Normal View History

2026-07-26 22:56:35 +08:00
#!/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()