User 2026-07-30 反馈: - 脚本名 lottery_te_ma.py → lottery_特码.py (中文, 更直观) - 同步 SKILL.md 引用 (description + scripts 段) - version 1.2.5 → 1.2.6
114 lines
4.1 KiB
Markdown
114 lines
4.1 KiB
Markdown
---
|
|
name: okx-raw-rest-signing-pitfall
|
|
description: "OKX v5 raw REST HMAC 签名: GET 必须 '? + sorted query', POST 必须 body 直接拼; 任何 'Invalid Sign' 几乎都是这里错"
|
|
version: 1.0.0
|
|
type: reference
|
|
---
|
|
|
|
# OKX v5 Raw REST HMAC 签名坑 (2026-07-13 v4.5.3 实测)
|
|
|
|
## 签名规范 (按 ccxt/okx.py sign() 实现)
|
|
|
|
```python
|
|
auth = timestamp + method.upper() + request_path # request_path = '/api/v5/...'
|
|
|
|
if method == 'GET':
|
|
if query:
|
|
urlencoded_query = '?' + self.urlencode(query) # ⚠️ 必须带 ? 前缀
|
|
auth += urlencoded_query
|
|
elif method == 'POST':
|
|
if isArray or query:
|
|
body = self.json(query)
|
|
auth += body
|
|
```
|
|
|
|
**3 个常见错**:
|
|
1. ❌ GET 签名 = `ts + 'GET' + path + json.dumps(params)` (没有 `?` 前缀, 用 json 而非 urlencode)
|
|
2. ❌ query 没按字典序排序 (ccxt 默认按字典序, requests 的 urlencode 保持插入顺序)
|
|
3. ❌ POST body 用了 `urllib.parse.urlencode({...})` 而不是 `json.dumps(...)`
|
|
|
|
**症状**: `{"code":"50111","msg":"Invalid Sign"}` 或 `code=51000 "Parameter posSide error"`
|
|
|
|
## 正确实现 (从 process_signal.py v4.5.3 直接抠)
|
|
|
|
```python
|
|
import hmac, hashlib, base64, urllib.parse, re, os, requests, time
|
|
from datetime import datetime
|
|
|
|
# 1. 读 bashrc 凭证 (绕过 shell 展开)
|
|
creds = {}
|
|
with open(os.path.expanduser('~/.bashrc')) as f:
|
|
for line in f:
|
|
line = line.strip()
|
|
if line.startswith('export OKX_'):
|
|
k, v = line.replace('export ', '').split('=', 1)
|
|
creds[k] = v.strip().strip('"').strip("'")
|
|
for k, v in creds.items():
|
|
if '${' not in v: os.environ[k] = v
|
|
for k, v in creds.items():
|
|
if '${' in v:
|
|
os.environ[k] = re.sub(r'\$\{(\w+)\}', lambda m: os.environ.get(m.group(1), ''), v)
|
|
|
|
# 2. 签名
|
|
ts = datetime.utcnow().strftime('%Y-%m-%dT%H:%M:%S.') + f"{datetime.utcnow().microsecond // 1000:03d}Z"
|
|
body_str = json.dumps(body) if body else ''
|
|
auth = ts + method.upper() + path
|
|
|
|
if method.upper() == 'GET':
|
|
if params:
|
|
# 关键: 按字典序排序 + ? 前缀
|
|
sorted_q = '&'.join(f"{k}={urllib.parse.quote_plus(str(v), safe='')}" for k, v in sorted(params.items()))
|
|
auth += '?' + sorted_q
|
|
query = '?' + sorted_q
|
|
else:
|
|
query = ''
|
|
else: # POST
|
|
auth += body_str
|
|
query = ''
|
|
|
|
sig = base64.b64encode(hmac.new(os.environ['OKX_SECRET'].encode(), auth.encode(), hashlib.sha256).digest()).decode()
|
|
|
|
# 3. 请求
|
|
headers = {
|
|
'OK-ACCESS-KEY': os.environ['OKX_API_KEY'],
|
|
'OK-ACCESS-SIGN': sig,
|
|
'OK-ACCESS-TIMESTAMP': ts,
|
|
'OK-ACCESS-PASSPHRASE': os.environ['OKX_PASSPHRASE'],
|
|
'Content-Type': 'application/json',
|
|
}
|
|
proxies = {'http': 'http://127.0.0.1:7890', 'https': 'http://127.0.0.1:7890'}
|
|
url = f'https://www.okx.com{path}{query}'
|
|
|
|
r = requests.get(url, headers=headers, proxies=proxies, timeout=15) if method.upper() == 'GET' \
|
|
else requests.post(url, data=body_str, headers=headers, proxies=proxies, timeout=15)
|
|
return r.json()
|
|
```
|
|
|
|
## 实战验证 (2026-07-13)
|
|
|
|
| 步骤 | 错版 | 正版 |
|
|
|------|------|------|
|
|
| GET `/api/v5/account/positions?instId=SKHY-USDT-SWAP` | `Invalid Sign` | `code=0, data=[{pos: -2.89, side: short, ...}]` |
|
|
| POST `/api/v5/trade/order` 平仓 | `51000 Parameter posSide error` (posSide 没设) | `code=0, data=[{ordId: MOCK-12345}]` |
|
|
|
|
## 应用位置 (2026-07-30)
|
|
|
|
- `process_signal.py:close_position_raw()` (v4.5.3 平仓信号自动跟平)
|
|
- `signal_inbox.py` 不直接调 raw REST, 但通过 `process_signal.py` 触发
|
|
|
|
## 老的错版 (在 references/okx-rest-fallback.md)
|
|
|
|
```python
|
|
# ❌ 这种写法会一直 Invalid Sign
|
|
ts = time.strftime('%Y-%m-%dT%H:%M:%S.000Z', time.gmtime())
|
|
msg = ts + 'GET' + path + (json.dumps(params) if params else '')
|
|
```
|
|
|
|
**下次 session 看老 reference 复制会再次踩坑**。如果改 okx-rest-fallback.md, 用上面"正确实现"替换那个函数。
|
|
|
|
## 给下次 session 的指令
|
|
|
|
任何 raw REST 调用 OKX 时:
|
|
1. **复制上面的"正确实现"**, 不要复制 `references/okx-rest-fallback.md` 里的版本
|
|
2. 跑通后再封装成函数, 别一边写一边 debug 签名
|
|
3. 看到 `Invalid Sign` 第一反应 = 检查 `?` 前缀和字典序 |