v4.5.38: 实时数据查询硬规则(回复前必查)+ check_account.py封装
This commit is contained in:
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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']}")
|
||||
Reference in New Issue
Block a user