#!/usr/bin/env python3 """ verify_execution_after_push_fail.py 当 process_signal.py 返回 "❌ 推送失败" / "⚠️ advisor错误" 时, 自动用 OKX REST API 验证实际是否 execute 成功 (而不是凭推送结果下结论)。 来源: v4.5.115 (2026-09-26 ONDO 加仓实战) - 推送失败不等于 execute 失败 (QQ mihomo proxy SSL 抽风 vs OKX execute 成功是独立事件) - 唯一金标准: 跑 check_account.py 看实际持仓变化 + /api/v5/trade/fills-history 看最近成交 用法: python3 verify_execution_after_push_fail.py [min_minutes_ago] 示例: python3 verify_execution_after_push_fail.py ONDO-USDT-SWAP 30 输出: - 当前实际持仓 (symbol/side/contracts/avgPx/markPx/upl) - 最近 N 分钟内成交记录 (sid/sz/px/fillPnl) - 结论: 是否真的 execute 了 依赖: ~/.bashrc OKX_API_KEY / OKX_SECRET / OKX_PASSPHRASE """ import sys import json import time import hmac import hashlib import base64 import datetime import requests PROXY = {"http": "http://127.0.0.1:7890", "https": "http://127.0.0.1:7890"} BASE = "https://www.okx.com" def load_creds(): creds = open("/home/openclaw/.bashrc").read() k = s = pw = None for line in creds.splitlines(): if line.startswith("export OKX_API_KEY="): k = line.split("=", 1)[1].strip().strip('"').strip("'") elif line.startswith("export OKX_SECRET="): s = line.split("=", 1)[1].strip().strip('"').strip("'") elif line.startswith("export OKX_PASSPHRASE="): pw = line.split("=", 1)[1].strip().strip('"').strip("'") if not (k and s and pw): raise SystemExit("❌ ~/.bashrc 缺 OKX_API_KEY/SECRET/PASSPHRASE") return k, s, pw def okx_request(method, path, params=None, body=None, timeout=10): k, s, pw = load_creds() ts = time.strftime("%Y-%m-%dT%H:%M:%S.000Z", time.gmtime()) if method == "GET": query = "?" + "&".join(f"{k_}={v}" for k_, v in (params or {}).items()) msg = ts + method + path + query else: body_str = json.dumps(body or {}) msg = ts + method + path + body_str query = "" sig = base64.b64encode(hmac.new(s.encode(), msg.encode(), hashlib.sha256).digest()).decode() h = { "OK-ACCESS-KEY": k, "OK-ACCESS-SIGN": sig, "OK-ACCESS-TIMESTAMP": ts, "OK-ACCESS-PASSPHRASE": pw, "Content-Type": "application/json", } url = BASE + path + query if method == "GET": r = requests.get(url, headers=h, proxies=PROXY, timeout=timeout) else: r = requests.post(url, headers=h, data=body_str, proxies=PROXY, timeout=timeout) return r.json() def get_current_position(symbol): """实时持仓 (类似 check_account.py)""" return okx_request("GET", "/api/v5/account/positions", {"instId": symbol}) def get_recent_fills(symbol, minutes_ago): """最近 N 分钟的 SWAP 成交""" end = datetime.datetime.utcnow() start = end - datetime.timedelta(minutes=minutes_ago) return okx_request( "GET", "/api/v5/trade/fills-history", { "instType": "SWAP", "instId": symbol, "begin": str(int(start.timestamp() * 1000)), "end": str(int(end.timestamp() * 1000)), "limit": "20", }, ) def main(): if len(sys.argv) < 2: print(__doc__) sys.exit(1) symbol = sys.argv[1].upper() minutes = int(sys.argv[2]) if len(sys.argv) > 2 else 30 # 不允许包含 / (ccxt 格式) - 强制 OKX REST 格式 if "/" in symbol: symbol = symbol.replace("/", "-").replace(":USDT", "-SWAP").replace("/USDT:USDT", "-USDT-SWAP") print(f"⚠️ 转换符号 → {symbol}") print(f"🔍 验证 {symbol} 最近 {minutes} 分钟内是否 execute\n") # 1. 当前持仓 pos_resp = get_current_position(symbol) print(f"=== 当前持仓 (实时) ===") if pos_resp.get("code") != "0": print(f"❌ 查询失败: {pos_resp}") else: positions = [p for p in pos_resp.get("data", []) if int(p.get("contracts", "0") or "0") > 0] if not positions: print(f" 无持仓 (但平仓后仍可能有最近成交)") for p in positions: print( f" {p['instId']:20s} {p['side']:6s} {p['contracts']:>10s} 张 " f"@ avgPx={float(p['avgPx']):.6f} markPx={float(p['markPx']):.6f} " f"upl=${float(p['upl']):.2f} liq={p.get('liqPx')}" ) # 2. 最近成交 fills_resp = get_recent_fills(symbol, minutes) print(f"\n=== 最近 {minutes} 分钟成交 ({len(fills_resp.get('data', []))} 笔) ===") total_pnl = 0.0 has_recent = False for f in fills_resp.get("data", []): ts = int(f["ts"]) / 1000 age_min = (time.time() - ts) / 60 if age_min > minutes: continue has_recent = True pnl = float(f["fillPnl"]) fee = float(f["fee"]) total_pnl += pnl - abs(fee) print( f" {age_min:>5.1f}min前 {f['side']:5s} {f['fillSz']:>10s} 张 " f"@ {f['fillPx']:>12s} pnl=${pnl:>+8.2f} fee=${fee:.4f}" ) # 3. 结论 print(f"\n=== 结论 ===") if not positions and not has_recent: print(f" ❌ {symbol} 无持仓 + 无最近成交 = 信号**未** execute") print(f" → 建议手动 raw REST 救火 (templates/manual_order_template.py)") elif has_recent: recent_total = sum( float(f["fillPnl"]) - abs(float(f["fee"])) for f in fills_resp.get("data", []) if (time.time() - int(f["ts"]) / 1000) / 60 <= minutes ) print(f" ✅ {symbol} 在最近 {minutes} 分钟内有 execute") print(f" 净 pnl: ${recent_total:.2f}") print(f" → 推送失败 ≠ execute 失败, 信号处理**成功**, 只需修 QQ 推送") if __name__ == "__main__": main()