okx-auto-position: 新增脚本 (whitelist.json 驱动白名单/manual_order/代理测试/执行验证)
This commit is contained in:
@@ -0,0 +1,134 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
manual_order.py - 用户手动 raw REST 下单 OKX 永续合约
|
||||||
|
|
||||||
|
用法:
|
||||||
|
python3 manual_order.py <INST_ID> <SIDE> <SZ> [leverage]
|
||||||
|
|
||||||
|
示例:
|
||||||
|
python3 manual_order.py MSTR-USDT-SWAP sell 0.01 5
|
||||||
|
python3 manual_order.py BTC-USDT-SWAP buy 0.1 5
|
||||||
|
python3 manual_order.py ETH-USDT-SWAP sell 1 5
|
||||||
|
|
||||||
|
参数:
|
||||||
|
INST_ID : OKX 合约 instId, 如 MSTR-USDT-SWAP
|
||||||
|
SIDE : buy / sell
|
||||||
|
SZ : 张数 (OKX 最小变动单位一般是 0.01 张)
|
||||||
|
leverage : 可选, 默认 5, 自动调杠杆
|
||||||
|
|
||||||
|
⚠ 部署位置: ~/.hermes/skills/trading/okx-auto-position/scripts/manual_order.py
|
||||||
|
调用方: process_signal 或用户手动 raw execute
|
||||||
|
反查: 调用后必须跑 check_account.py 验实际 contracts
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
import hmac
|
||||||
|
import hashlib
|
||||||
|
import base64
|
||||||
|
import requests
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
def okx_get(path, t=15):
|
||||||
|
"""GET 请求 (用于查持仓/余额/调杠杆)"""
|
||||||
|
creds = open(Path.home() / '.bashrc').read()
|
||||||
|
k = s = pw = None
|
||||||
|
for l in creds.splitlines():
|
||||||
|
if l.startswith('export OKX_API_KEY='):
|
||||||
|
k = l.split('=', 1)[1].strip().strip('"')
|
||||||
|
elif l.startswith('export OKX_SECRET='):
|
||||||
|
s = l.split('=', 1)[1].strip().strip('"')
|
||||||
|
elif l.startswith('export OKX_PASSPHRASE='):
|
||||||
|
pw = l.split('=', 1)[1].strip().strip('"')
|
||||||
|
ts = time.strftime('%Y-%m-%dT%H:%M:%S.000Z', time.gmtime())
|
||||||
|
method = 'GET'
|
||||||
|
pre = ts + method + path
|
||||||
|
sig = base64.b64encode(hmac.new(s.encode(), pre.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'}
|
||||||
|
r = requests.get('https://www.okx.com' + path, headers=h, timeout=t,
|
||||||
|
proxies={'http': 'http://127.0.0.1:7890', 'https': 'http://127.0.0.1:7890'})
|
||||||
|
return r.json()
|
||||||
|
|
||||||
|
|
||||||
|
def okx_post(path, body, t=15):
|
||||||
|
"""POST 请求 (用于下单/撤单/调杠杆)"""
|
||||||
|
creds = open(Path.home() / '.bashrc').read()
|
||||||
|
k = s = pw = None
|
||||||
|
for l in creds.splitlines():
|
||||||
|
if l.startswith('export OKX_API_KEY='):
|
||||||
|
k = l.split('=', 1)[1].strip().strip('"')
|
||||||
|
elif l.startswith('export OKX_SECRET='):
|
||||||
|
s = l.split('=', 1)[1].strip().strip('"')
|
||||||
|
elif l.startswith('export OKX_PASSPHRASE='):
|
||||||
|
pw = l.split('=', 1)[1].strip().strip('"')
|
||||||
|
ts = time.strftime('%Y-%m-%dT%H:%M:%S.000Z', time.gmtime())
|
||||||
|
method = 'POST' # ⚠ 必须是字符串, 别写 method=POST
|
||||||
|
body_str = json.dumps(body)
|
||||||
|
pre = ts + method + path + body_str
|
||||||
|
sig = base64.b64encode(hmac.new(s.encode(), pre.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'}
|
||||||
|
r = requests.post('https://www.okx.com' + path, headers=h, data=body_str,
|
||||||
|
timeout=t, proxies={'http': 'http://127.0.0.1:7890',
|
||||||
|
'https': 'http://127.0.0.1:7890'})
|
||||||
|
return r.json()
|
||||||
|
|
||||||
|
|
||||||
|
def set_leverage(inst_id, lever, side='net'):
|
||||||
|
"""调杠杆 (调杠杆后下单才能用对应张数)"""
|
||||||
|
body = {'instId': inst_id, 'lever': str(lever), 'mgnMode': 'cross', 'posSide': side}
|
||||||
|
return okx_post('/api/v5/account/set-leverage', body)
|
||||||
|
|
||||||
|
|
||||||
|
def market_order(inst_id, side, sz, td_mode='cross', reduce_only=False):
|
||||||
|
"""市价单"""
|
||||||
|
body = {
|
||||||
|
'instId': inst_id,
|
||||||
|
'tdMode': td_mode,
|
||||||
|
'side': side,
|
||||||
|
'posSide': 'net',
|
||||||
|
'ordType': 'market',
|
||||||
|
'sz': str(sz),
|
||||||
|
}
|
||||||
|
if reduce_only:
|
||||||
|
body['reduceOnly'] = True
|
||||||
|
return okx_post('/api/v5/trade/order', body)
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
if len(sys.argv) < 4:
|
||||||
|
print(__doc__)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
inst_id = sys.argv[1]
|
||||||
|
side = sys.argv[2] # buy / sell
|
||||||
|
sz = sys.argv[3]
|
||||||
|
leverage = int(sys.argv[4]) if len(sys.argv) > 4 else 5
|
||||||
|
|
||||||
|
print(f'📊 准备下单:')
|
||||||
|
print(f' 币种: {inst_id}')
|
||||||
|
print(f' 方向: {side}')
|
||||||
|
print(f' 张数: {sz}')
|
||||||
|
print(f' 杠杆: {leverage}x')
|
||||||
|
|
||||||
|
# 1. 调杠杆
|
||||||
|
print(f'\n1️⃣ 调杠杆 {leverage}x...')
|
||||||
|
r = set_leverage(inst_id, leverage)
|
||||||
|
print(json.dumps(r, ensure_ascii=False, indent=2))
|
||||||
|
|
||||||
|
# 2. 市价单
|
||||||
|
print(f'\n2️⃣ 市价单 {side} {sz} 张...')
|
||||||
|
r = market_order(inst_id, side, sz)
|
||||||
|
print(json.dumps(r, ensure_ascii=False, indent=2))
|
||||||
|
|
||||||
|
# 3. 提示反查
|
||||||
|
print(f'\n⚠ execute 后必查:')
|
||||||
|
print(f' timeout 30 python3 ~/.hermes/skills/trading/okx-auto-position/scripts/check_account.py {inst_id}')
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# okx_proxy_test.sh - 完整测试 OKX 在国内 VPS 的可达性
|
||||||
|
# 区分: 直连 vs 走代理 vs 不带 proxies 字段的 ccxt
|
||||||
|
# 2026-08-31 实测: 国内 VPS 直连 OKX 不通 (No route to host) -> GFW 屏蔽 (DNS 劫持到 169.254.0.2)
|
||||||
|
# OKX 没有"国内节点", 必须依赖代理 (mihomo/clash 127.0.0.1:7890)
|
||||||
|
|
||||||
|
set -e
|
||||||
|
PYTHON="/home/openclaw/.hermes/hermes-agent/venv/bin/python"
|
||||||
|
|
||||||
|
echo "=== OKX 国内 VPS 可达性测试 ==="
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
echo "--- 1. 直连 (无 proxy env, 无 curl -x) ---"
|
||||||
|
unset HTTPS_PROXY HTTP_PROXY https_proxy http_proxy
|
||||||
|
RESP=$(curl -sS --max-time 10 -o /tmp/okx_resp.txt -w "%{http_code}" "https://www.okx.com/api/v5/public/time" 2>&1 || echo "FAIL")
|
||||||
|
echo "HTTP: $RESP"
|
||||||
|
echo "body: $(cat /tmp/okx_resp.txt 2>/dev/null | head -1)"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
echo "--- 2. 走 127.0.0.1:7890 代理 ---"
|
||||||
|
RESP=$(curl -sS --max-time 10 -x http://127.0.0.1:7890 -o /tmp/okx_resp.txt -w "%{http_code}" "https://www.okx.com/api/v5/public/time" 2>&1 || echo "FAIL")
|
||||||
|
echo "HTTP: $RESP"
|
||||||
|
echo "body: $(cat /tmp/okx_resp.txt 2>/dev/null | head -1)"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
echo "--- 3. ccxt 不带 proxies (Python 默认不捡 shell env, trust_env=False) ---"
|
||||||
|
$PYTHON << 'PYEOF'
|
||||||
|
import ccxt, os
|
||||||
|
bashrc = open('/home/openclaw/.bashrc').read()
|
||||||
|
key = bashrc.split('OKX_API_KEY=')[1].split('\n')[0].strip()
|
||||||
|
sec = bashrc.split('OKX_SECRET=')[1].split('\n')[0].strip()
|
||||||
|
pwd = bashrc.split('OKX_PASSPHRASE=')[1].split('\n')[0].strip()
|
||||||
|
ex = ccxt.okx({'apiKey':key,'secret':sec,'password':pwd, 'options':{'defaultType':'swap'}})
|
||||||
|
try:
|
||||||
|
bal = ex.fetch_balance()
|
||||||
|
print(f'✅ Free: ${bal["USDT"]["free"]:.2f}')
|
||||||
|
except Exception as e:
|
||||||
|
print(f'❌ {type(e).__name__}')
|
||||||
|
PYEOF
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
echo "--- 4. ccxt 带 proxies 127.0.0.1:7890 (推荐方式) ---"
|
||||||
|
proxychains4 -f /home/openclaw/.proxychains/proxychains.conf $PYTHON << 'PYEOF'
|
||||||
|
import ccxt
|
||||||
|
bashrc = open('/home/openclaw/.bashrc').read()
|
||||||
|
key = bashrc.split('OKX_API_KEY=')[1].split('\n')[0].strip()
|
||||||
|
sec = bashrc.split('OKX_SECRET=')[1].split('\n')[0].strip()
|
||||||
|
pwd = bashrc.split('OKX_PASSPHRASE=')[1].split('\n')[0].strip()
|
||||||
|
ex = ccxt.okx({'apiKey':key,'secret':sec,'password':pwd,
|
||||||
|
'proxies':{'http':'http://127.0.0.1:7890','https':'http://127.0.0.1:7890'},
|
||||||
|
'options':{'defaultType':'swap'}})
|
||||||
|
try:
|
||||||
|
bal = ex.fetch_balance()
|
||||||
|
print(f'✅ Free: ${bal["USDT"]["free"]:.2f}')
|
||||||
|
except Exception as e:
|
||||||
|
print(f'❌ {type(e).__name__}')
|
||||||
|
PYEOF
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
echo "--- 5. DNS 验证 (理解为什么直连失败) ---"
|
||||||
|
echo "国内 DNS (114):"
|
||||||
|
dig @114.114.114.114 www.okx.com +short 2>&1 | head -3
|
||||||
|
echo "Google DNS (8.8.8.8):"
|
||||||
|
dig @8.8.8.8 www.okx.com +short 2>&1 | head -3
|
||||||
|
echo ""
|
||||||
|
echo "都解析到 169.254.0.2 (APIPA, 不可路由) = GFW 屏蔽"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
echo "=== 结论 ==="
|
||||||
|
echo "1. 国内 VPS 必须走 127.0.0.1:7890 代理"
|
||||||
|
echo "2. 不是 OKX 限制, 是 GFW 屏蔽 (DNS 劫持 169.254.0.2)"
|
||||||
|
echo "3. ccxt 必须显式写 proxies 字段 (不自动捡 shell env)"
|
||||||
|
echo "4. okx_position_advisor.py 的 proxies 字段必须保留"
|
||||||
|
echo "5. VPS 搬海外 (东京/新加坡/香港) 才能砍掉代理"
|
||||||
@@ -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()
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"_comment": "跟单白名单配置 - 不要在代码里硬编码,改这里。trader=跟单交易员名字,设置在 trader_whitelist 数组。",
|
||||||
|
"trader_whitelist": ["熬鹰资本", "熬鹰币安实盘", "熬鹰FOMO合约实盘", "熬鹰交易信号"]
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user