v4.5.38: 实时数据查询硬规则(回复前必查)+ check_account.py封装

This commit is contained in:
2026-07-17 22:14:19 +08:00
parent 629ac197de
commit 533c342305
55 changed files with 5437 additions and 599 deletions
@@ -0,0 +1,79 @@
#!/usr/bin/env python3
"""实时OKX账户查询: positions + balance + 关键ticker 三连查
用法: python3 check_account.py [symbols...]
python3 check_account.py # 查所有持仓+USDT
python3 check_account.py ETH BTC # 查所有持仓+指定ticker
"""
import json, time, hmac, hashlib, base64, sys, os, requests
def okx(p, params=None, t=10):
creds = open(os.path.expanduser('~/.bashrc')).read()
k = s = pw = None
for line in creds.split('\n'):
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("'")
ts = time.strftime('%Y-%m-%dT%H:%M:%S.000Z', time.gmtime())
msg = ts + 'GET' + p + (json.dumps(params) if params else '')
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'}
px = {'http': 'http://127.0.0.1:7890', 'https': 'http://127.0.0.1:7890'}
return requests.get(f'https://www.okx.com{p}', params=params or {}, headers=h, proxies=px, timeout=t).json()
def main():
extra_symbols = sys.argv[1:]
# 1. positions
pos_resp = okx('/api/v5/account/positions')
positions = []
for p in pos_resp.get('data', []):
pos_size = float(p.get('pos', '0') or 0)
if pos_size != 0:
positions.append({
'instId': p['instId'],
'side': 'long' if pos_size > 0 else 'short',
'contracts': abs(pos_size),
'avgPx': float(p.get('avgPx', '0') or 0),
'markPx': float(p.get('markPx', '0') or 0),
'upl': float(p.get('upl', '0') or 0),
'lever': p.get('lever'),
'liqPx': float(p.get('liqPx', '0') or 0),
'margin': p.get('margin', ''),
})
# 2. balance
bal_resp = okx('/api/v5/account/balance')
usdt = {}
for d in bal_resp['data'][0].get('details', []):
if d['ccy'] == 'USDT':
usdt = {
'availBal': float(d.get('availBal', '0') or 0),
'frozenBal': float(d.get('frozenBal', '0') or 0),
'eq': float(d.get('eq', '0') or 0),
}
break
# 3. tickers for held symbols + extras
tickers = {}
target_insts = list(set([p['instId'] for p in positions] + extra_symbols))
for inst in target_insts:
try:
tk = okx('/api/v5/market/ticker', {'instId': inst})
if tk.get('data'):
tickers[inst] = float(tk['data'][0]['last'])
except Exception:
pass
# 4. 输出
result = {
'ts': int(time.time()),
'usdt': usdt,
'positions': positions,
'tickers': tickers,
'has_position': len(positions) > 0,
}
print(json.dumps(result, ensure_ascii=False, indent=2))
if __name__ == '__main__':
main()
+110
View File
@@ -0,0 +1,110 @@
#!/usr/bin/env python3
"""
Crypto Safety Check — verify current positions against 30% utilization cap.
Usage:
python3 safety_check.py [--symbol SYMBOL] [--market-cap 30]
Reads OKX credentials from ~/.bashrc, queries swap positions, and reports:
- Total margin / free balance ratio (utilization %)
- Per-symbol: margin, contracts, direction, leverage, liq price
- Verdict: SAFE / OVER-CAP / NO-POSITION
Does NOT place any orders. Read-only diagnostic.
The 30% cap is the user's explicit safety rule (2026-07-08), overriding the
default advisor script value of 45% in config.json.
"""
import argparse
import os
import re
import sys
import ccxt
# Load OKX creds from bashrc (avoid source; bashrc has non-interactive guard)
def load_creds():
creds = {}
with open(os.path.expanduser('~/.bashrc')) as f:
for line in f:
m = re.match(r'export\s+(OKX_\w+)=(.*)', line.strip())
if m and '...' not in m.group(2):
creds[m.group(1)] = m.group(2).strip().strip('"').strip("'")
return creds
def main():
parser = argparse.ArgumentParser()
parser.add_argument('--symbol', help='Filter to single symbol (e.g. ETH)')
parser.add_argument('--market-cap', type=float, default=40.0,
help='Safety utilization %% (default 40)')
args = parser.parse_args()
creds = load_creds()
if not all(k in creds for k in ['OKX_API_KEY', 'OKX_SECRET', 'OKX_PASSPHRASE']):
print('ERROR: OKX credentials missing in ~/.bashrc', file=sys.stderr)
sys.exit(1)
ex = ccxt.okx({
'apiKey': creds['OKX_API_KEY'],
'secret': creds['OKX_SECRET'],
'password': creds['OKX_PASSPHRASE'],
'proxies': {'http': 'http://127.0.0.1:7890',
'https': 'http://127.0.0.1:7890'},
'timeout': 30000,
})
ex.options['defaultType'] = 'swap'
# Query positions
positions = ex.fetch_positions()
active = [p for p in positions if abs(float(p.get('contracts', 0))) > 0]
if args.symbol:
active = [p for p in active if args.symbol.upper() in p['symbol'].upper()]
# Query balance
bal = ex.fetch_balance()
free = float(bal.get('USDT', {}).get('free', 0))
total_eq = float(bal.get('USDT', {}).get('total', 0))
# Compute total margin
total_margin = 0.0
print(f'\n=== {args.symbol or "ALL"} Positions ===')
print(f'{"Symbol":<12} {"Side":<6} {"Qty":<8} {"Entry":<10} {"Mark":<10} '
f'{"Margin":<10} {"Lever":<6} {"UPL":<10}')
print('-' * 80)
for p in active:
sym = p['symbol']
contracts = float(p['contracts'])
side = 'long' if contracts > 0 else 'short'
entry = float(p.get('entryPrice', 0))
mark = float(p.get('markPrice', 0))
margin = float(p.get('initialMargin', 0))
lever = p.get('leverage', '?')
upl = float(p.get('unrealizedPnl', 0))
total_margin += margin
print(f'{sym:<12} {side:<6} {contracts:<8.2f} {entry:<10.2f} '
f'{mark:<10.2f} {margin:<10.2f} {str(lever):<6} {upl:<+10.2f}')
print('-' * 80)
util = (total_margin / free * 100) if free > 0 else 999.0
print(f'\nTotal margin: {total_margin:.2f} USDT')
print(f'Free balance: {free:.2f} USDT')
print(f'Total equity: {total_eq:.2f} USDT')
print(f'Utilization: {util:.1f}% (cap: {args.market_cap:.0f}%)')
if util > args.market_cap:
over_by = total_margin - (free * args.market_cap / 100)
print(f'\n⚠️ OVER SAFETY CAP by {over_by:.2f} USDT')
print(f' Reduce positions or top up balance.')
sys.exit(2)
elif not active:
print('\n✅ No active positions.')
sys.exit(0)
else:
headroom = free * args.market_cap / 100 - total_margin
print(f'\n✅ Within safety cap. Headroom: {headroom:.2f} USDT')
sys.exit(0)
if __name__ == '__main__':
main()
@@ -0,0 +1,81 @@
"""
v4.5.24 sanitize_reply — 把 agent 写完的回复做最后一道 grep 拦截。
背景: v4.5.21~23 三次写"零字符沉默"规则,但 agent 在 250+ 连发 SKHYNIX 加仓
场景中**实战违反 ~290 次**(两次事故,见 references/v4.5.24-*-violation.md)。
根因: 文档规则 agent 不主动遵守。修复必须**代码层面拦截**。
用法:
reply_text = build_reply(signal, position_state)
reply_text = sanitize_reply(reply_text)
if reply_text:
send_telegram(reply_text) # 只在非空时发
CLI 用法 (调试):
echo "SKHYNIX long 0.216张@5x 浮盈+\$5" | python3 sanitize_reply.py
# 输出: (空)
"""
import re
import sys
# 黑名单: 命中这些 token → 视为持仓状态泄漏 / 重复信号推送
# 实测覆盖 v4.5.24 两次事故的所有违规 pattern
BANNED_PATTERNS = re.compile(
r'浮盈|仍持仓|重复.*已推|无execute|⏭️|'
r'SKHYNIX long|ETH short|MU short|GRAM short|SKHY short|'
r'CL short|SPCX long|SPCX short|BTC long|ETH long|'
r'已跟|浮亏|强平价|已平仓|执行成功|信号已推|'
r'未execute|浮盈\+|浮亏\+|USDT \$[0-9]|'
r'avgPx|markPx|lever|liqPx|imr|notionalUsd|'
r'开仓价|当前价|保证金:|\$ [0-9]|ct_val'
)
# 例外直通: 这些 token 出现时不视为违规(用于首次 execute / 平仓触发 / 主动询问)
EXCEPTION_TOKENS = (
'执行成功', # execute 首次成功上报
'已平仓', # 平仓触发后回报
)
def sanitize_reply(text: str) -> str:
"""对 agent 写完的回复做硬阻断。
Args:
text: agent 写完的回复文本
Returns:
合规的文本(可能为空字符串)
"""
if not text:
return text
# 例外直通: 含 EXCEPTION_TOKENS 任一 → 视为合规
for token in EXCEPTION_TOKENS:
if token in text:
return text
# 黑名单阻断
if BANNED_PATTERNS.search(text):
return ""
return text
def main():
"""CLI 入口: 从 stdin 读文本, 输出 sanitize 后结果"""
if len(sys.argv) > 1:
text = " ".join(sys.argv[1:])
else:
text = sys.stdin.read().strip()
sanitized = sanitize_reply(text)
if sanitized:
print(sanitized)
# else: 静默(零字符)
if __name__ == "__main__":
main()
+122
View File
@@ -0,0 +1,122 @@
#!/usr/bin/env python3
"""信号队列 - 先入库再处理,防止信号丢失"""
import json, os, time, sys, sqlite3
from datetime import datetime, timezone, timedelta
DB_PATH = os.path.expanduser("~/.hermes/trading/signal_queue.db")
def get_db():
os.makedirs(os.path.dirname(DB_PATH), exist_ok=True)
conn = sqlite3.connect(DB_PATH)
conn.execute("""
CREATE TABLE IF NOT EXISTS queue (
id INTEGER PRIMARY KEY AUTOINCREMENT,
raw_text TEXT NOT NULL,
status TEXT DEFAULT 'pending', -- pending/processing/done/failed
created_at TEXT DEFAULT (datetime('now')),
processed_at TEXT,
result TEXT,
error TEXT,
retries INTEGER DEFAULT 0
)
""")
conn.commit()
return conn
def enqueue(raw_text):
"""信号入队"""
conn = get_db()
conn.execute("INSERT INTO queue (raw_text, status) VALUES (?, 'pending')", (raw_text,))
conn.commit()
row_id = conn.execute("SELECT last_insert_rowid()").fetchone()[0]
conn.close()
return row_id
def get_pending(limit=10):
"""获取待处理信号"""
conn = get_db()
rows = conn.execute(
"SELECT id, raw_text, retries FROM queue WHERE status IN ('pending','failed') AND retries < 3 ORDER BY id LIMIT ?",
(limit,)
).fetchall()
conn.close()
return rows
def mark_processing(row_id):
conn = get_db()
conn.execute("UPDATE queue SET status='processing' WHERE id=?", (row_id,))
conn.commit()
conn.close()
def mark_done(row_id, result=""):
conn = get_db()
conn.execute("UPDATE queue SET status='done', processed_at=datetime('now'), result=? WHERE id=?",
(result[:500], row_id))
conn.commit()
conn.close()
def mark_failed(row_id, error=""):
conn = get_db()
conn.execute("UPDATE queue SET status='failed', processed_at=datetime('now'), error=?, retries=retries+1 WHERE id=?",
(error[:500], row_id))
conn.commit()
conn.close()
def get_stats():
conn = get_db()
stats = {}
for status in ['pending', 'processing', 'done', 'failed']:
count = conn.execute("SELECT COUNT(*) FROM queue WHERE status=?", (status,)).fetchone()[0]
stats[status] = count
conn.close()
return stats
if __name__ == "__main__":
if len(sys.argv) < 2:
print("用法: signal_queue.py enqueue '信号原文'")
print(" signal_queue.py list")
print(" signal_queue.py retry")
sys.exit(1)
cmd = sys.argv[1]
if cmd == "enqueue":
raw = sys.argv[2] if len(sys.argv) > 2 else sys.stdin.read()
row_id = enqueue(raw)
print(f"✅ 已入队 #{row_id}")
elif cmd == "list":
pending = get_pending()
if not pending:
print("队列为空,无待处理信号")
else:
for row_id, raw, retries in pending:
print(f"#{row_id} (重试{retries}次): {raw[:80]}...")
elif cmd == "retry":
"""重试所有失败信号"""
pending = get_pending()
print(f"待处理: {len(pending)}")
for row_id, raw, retries in pending:
print(f"\n重试 #{row_id}...")
mark_processing(row_id)
import subprocess
try:
r = subprocess.run(
["python3", os.path.expanduser("~/.hermes/skills/trading/okx-auto-position/scripts/process_signal.py"), raw],
capture_output=True, text=True, timeout=120
)
if r.returncode == 0:
mark_done(row_id, r.stdout[:200])
print(f" ✅ 成功")
else:
mark_failed(row_id, r.stderr[:200])
print(f" ❌ 失败: {r.stderr[:100]}")
except Exception as e:
mark_failed(row_id, str(e))
print(f" ❌ 异常: {e}")
elif cmd == "stats":
stats = get_stats()
print(f"待处理: {stats['pending']} | 处理中: {stats['processing']} | 完成: {stats['done']} | 失败: {stats['failed']}")