okx-auto-position: 新增脚本 (whitelist.json 驱动白名单/manual_order/代理测试/执行验证)
This commit is contained in:
@@ -0,0 +1,165 @@
|
||||
#!/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 <SYMBOL-USDT-SWAP> [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()
|
||||
Reference in New Issue
Block a user