Files
Hermes-Skills/trading/okx-auto-position/scripts/trade_signal_handler.py
T
Hermes Skills Manager 6770bc9b9d Initial commit: Hermes Agent skills collection
- Trading skills (OKX, dividend, lottery, quantitative)
- Creative skills (ASCII art, diagrams, video)
- Development skills (GitHub, debugging, TDD)
- Research skills (arXiv, blog monitoring)
- Productivity skills (email, documents, notes)
- MCP integration skills
- Custom user skills
2026-07-05 02:31:15 -04:00

412 lines
14 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
"""
交易信号处理器 - 一体化脚本
用法:
python3 trade_signal_handler.py signal "【币种】BTCUSDT|永续|10x\n【方向】做多\n【仓位】0.5 BTC"
python3 trade_signal_handler.py confirm BTC
python3 trade_signal_handler.py cancel BTC
python3 trade_signal_handler.py status
"""
import re
import os
import sys
import json
import time
import glob
import subprocess
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
ADVISOR_SCRIPT = os.path.join(SCRIPT_DIR, "okx_position_advisor.py")
SIGNAL_DB_SCRIPT = os.path.join(SCRIPT_DIR, "signal_db.py")
PENDING_DIR = os.path.expanduser("~/.hermes/trading/pending")
os.makedirs(PENDING_DIR, exist_ok=True)
def log_signal_to_db(signal_text):
"""Log signal to history database, return signal_id or None"""
try:
result = subprocess.run(
[sys.executable, SIGNAL_DB_SCRIPT, "log", signal_text],
capture_output=True, text=True, timeout=10,
)
if result.returncode == 0:
return json.loads(result.stdout).get("id")
except Exception:
pass
return None
def update_signal_outcome(signal_id, outcome, detail=""):
"""Update signal outcome in database"""
if not signal_id:
return
try:
subprocess.run(
[sys.executable, SIGNAL_DB_SCRIPT, "update", str(signal_id), outcome, detail],
capture_output=True, text=True, timeout=10,
)
except Exception:
pass
def parse_signal(text):
"""Parse trading signal text, extract symbol/direction/leverage/size"""
result = {}
# 币种: BTCUSDT|永续|10x or 【币种】BTCUSDT
symbol_match = re.search(r'(?:【币种】|币种[:]\s*)(\w+)', text)
if not symbol_match:
symbol_match = re.search(r'([A-Z]{2,10})USDT', text)
if symbol_match:
raw = symbol_match.group(1).upper()
raw = raw.replace("USDT", "").replace("/USDT", "").replace(":USDT", "")
result["symbol"] = raw
else:
return None
# 方向
if re.search(r'(做空|卖出|short|sell|空单|开空)', text, re.IGNORECASE):
result["side"] = "short"
elif re.search(r'(做多|买入|long|buy|多单|开多)', text, re.IGNORECASE):
result["side"] = "long"
else:
return None
# 杠杆
lev_match = re.search(r'(\d+)\s*[xX倍]', text)
result["leverage"] = int(lev_match.group(1)) if lev_match else 10
# 仓位数量
size_match = re.search(r'(?:【仓位】|仓位[:]\s*)([\d,.]+)\s*(\w+)', text)
if size_match:
result["raw_size"] = float(size_match.group(1).replace(",", ""))
result["raw_unit"] = size_match.group(2)
# 是否加仓/平仓
result["is_add"] = bool(re.search(r'(加仓|追仓)', text))
result["is_close"] = bool(re.search(r'(平仓|止盈|止损|close|全平)', text, re.IGNORECASE))
return result
def save_pending(symbol, rec_json, signal_text, signal_id=None):
"""Save pending recommendation to file"""
path = os.path.join(PENDING_DIR, f"{symbol.upper()}.json")
data = {
"symbol": symbol.upper(),
"rec": rec_json,
"signal": signal_text,
"signal_id": signal_id,
"timestamp": time.time(),
"time_str": time.strftime("%Y-%m-%d %H:%M:%S"),
}
with open(path, "w") as f:
json.dump(data, f, ensure_ascii=False, indent=2)
return path
def load_pending(symbol):
"""Load pending recommendation"""
path = os.path.join(PENDING_DIR, f"{symbol.upper()}.json")
if not os.path.exists(path):
return None
with open(path) as f:
return json.load(f)
def remove_pending(symbol):
"""Remove pending recommendation"""
path = os.path.join(PENDING_DIR, f"{symbol.upper()}.json")
if os.path.exists(path):
os.remove(path)
def run_advisor(symbol, side, leverage):
"""Run the advisor script and return JSON result"""
cmd = [
sys.executable, ADVISOR_SCRIPT,
"--symbol", symbol,
"--side", side,
"--leverage", str(leverage),
"--json",
]
env = os.environ.copy()
# Source bashrc to get OKX credentials
result = subprocess.run(
["bash", "-c", f"source ~/.bashrc && {' '.join(cmd)}"],
capture_output=True, text=True, timeout=30,
)
if result.returncode != 0:
return {"error": result.stderr.strip() or "Advisor script failed"}
try:
return json.loads(result.stdout)
except json.JSONDecodeError:
return {"error": f"Invalid JSON output: {result.stdout[:200]}"}
def execute_trade(rec_json):
"""Execute the trade using the advisor script"""
import shlex
rec_str = json.dumps(rec_json, ensure_ascii=False)
symbol = rec_json.get("symbol", "").split("/")[0]
side = rec_json.get("side", "")
cmd = f"source ~/.bashrc && python3 {ADVISOR_SCRIPT} --symbol {symbol} --side {side} --execute --json --rec-json {shlex.quote(rec_str)}"
result = subprocess.run(
["bash", "-c", cmd],
capture_output=True, text=True, timeout=60,
)
if result.returncode != 0:
return {"error": result.stderr.strip() or "Execution failed"}
try:
return json.loads(result.stdout)
except json.JSONDecodeError:
return {"raw": result.stdout.strip()}
def format_recommendation(rec, signal_text=""):
"""Format recommendation for user display"""
symbol = rec.get("symbol", "?")
side = rec.get("side", "?")
side_cn = "做多" if side in ("long", "buy") else "做空"
leverage = rec.get("leverage", 10)
contracts = rec.get("contracts", 0)
entry = rec.get("entry_price", 0)
tp = rec.get("tp_price", 0)
sl = rec.get("sl_price", 0)
margin = rec.get("margin_used", 0)
balance = rec.get("balance", 0)
margin_pct = rec.get("margin_pct", 0)
tp_pct = rec.get("tp_pct", 0) # 标的价格变动%
sl_pct = rec.get("sl_pct", 0)
tp_pnl = rec.get("tp_pnl", 0)
sl_pnl = rec.get("sl_pnl", 0)
# 保证金收益率
tp_margin_pct = (tp_pnl / margin * 100) if margin > 0 else 0
sl_margin_pct = (sl_pnl / margin * 100) if margin > 0 else 0
liq_price = rec.get("liq_price", 0)
liq_pct = rec.get("liq_pct", 0)
rr = rec.get("rr_ratio", 0)
lines = [
f"📊 **{symbol}USDT {side_cn}** - 仓位推荐",
"",
f"💰 可用余额: {balance:.2f} USDT",
f"📈 当前价: **{entry}**",
"",
"**开仓方案:**",
f"• 方向: {side_cn}",
f"• 杠杆: **{leverage}x**",
f"• 张数: **{contracts}张**",
f"• 保证金: {margin:.2f} USDT ({margin_pct:.0f}%)",
"",
"**止盈止损:**",
f"• 🎯 止盈: **{tp}** (保证金+{tp_margin_pct:.0f}%) → +{tp_pnl:.2f} USDT",
f"• 🛑 止损: **{sl}** (保证金-{sl_margin_pct:.0f}%) → -{sl_pnl:.2f} USDT",
f"• 📐 盈亏比: **{rr:.1f}:1**",
]
if liq_price:
lines.append(f"• ⚠️ 清算价: {liq_price} (距离 {liq_pct:.1f}%)")
lines.extend([
"",
"回复 **Y** 确认下单",
"回复 **N** 取消",
])
return "\n".join(lines)
def format_execution_result(result, symbol, side):
"""Format execution result for user display"""
if "error" in result:
return f"❌ **{symbol}USDT 下单失败**\n\n{result['error']}"
side_cn = "做多" if side in ("long", "buy") else "做空"
lines = [f"✅ **{symbol}USDT {side_cn} 开仓成功**"]
# Parse steps
for step in result.get('steps', []):
if step['step'] == 'leverage':
if step['status'] == 'ok':
lines.append("✅ 杠杆设置成功")
else:
lines.append(f"⚠️ 杠杆: {step.get('msg', '')}")
elif step['step'] == 'order':
if step['status'] == 'ok':
lines.append(f"✅ 下单成功 (ID: {step['order_id']})")
else:
lines.append(f"❌ 下单失败: {step.get('msg', '')}")
return '\n'.join(lines)
elif step['step'] == 'tp_sl':
if step['status'] == 'ok':
lines.append(f"✅ 止盈止损设置成功 (ID: {step['algo_id']})")
else:
lines.append(f"⚠️ 止盈止损: {step.get('msg', '')}")
# Position info
pos = result.get('position')
if pos:
pnl_emoji = "🟢" if pos.get('pnl', 0) >= 0 else "🔴"
lines.extend([
"",
"📊 **持仓确认:**",
f"• 方向: {side_cn}",
f"• 数量: {pos.get('contracts', '?')}张",
f"• 入场价: **{pos.get('entry', '?')}**",
f"• {pnl_emoji} 浮盈: {pos.get('pnl', 0):.2f} USDT",
])
# TP/SL info
algo = result.get('algo')
if algo:
lines.extend([
"",
"🎯 **止盈止损:**",
f"• 止盈: **{algo.get('tp', '?')}**",
f"• 止损: **{algo.get('sl', '?')}**",
])
return "\n".join(lines)
def main():
if len(sys.argv) < 2:
print("用法: trade_signal_handler.py <signal|confirm|cancel|status> [args]")
sys.exit(1)
action = sys.argv[1]
if action == "signal":
if len(sys.argv) < 3:
print("用法: trade_signal_handler.py signal '<signal_text>'")
sys.exit(1)
signal_text = sys.argv[2]
parsed = parse_signal(signal_text)
if not parsed:
print(json.dumps({"error": "无法解析信号", "raw": signal_text}))
sys.exit(1)
if parsed.get("is_close"):
# 平仓信号
print(json.dumps({"action": "close", "symbol": parsed["symbol"]}))
sys.exit(0)
# 记录信号到数据库
signal_id = log_signal_to_db(signal_text)
# 计算仓位
rec = run_advisor(parsed["symbol"], parsed["side"], parsed["leverage"])
if "error" in rec:
if signal_id:
update_signal_outcome(signal_id, "error", rec["error"])
print(json.dumps(rec))
sys.exit(1)
# 保存待确认
save_pending(parsed["symbol"], rec, signal_text, signal_id)
# 输出推荐
output = {
"action": "recommend",
"symbol": parsed["symbol"],
"side": parsed["side"],
"recommendation": rec,
"display": format_recommendation(rec, signal_text),
}
print(json.dumps(output, ensure_ascii=False))
elif action == "confirm":
if len(sys.argv) < 3:
print("用法: trade_signal_handler.py confirm <SYMBOL>")
sys.exit(1)
symbol = sys.argv[2].upper().replace("USDT", "")
pending = load_pending(symbol)
if not pending:
print(json.dumps({"error": f"没有待确认的 {symbol} 交易"}))
sys.exit(1)
rec = pending["rec"]
signal_id = pending.get("signal_id")
result = execute_trade(rec)
# Only remove pending if execution succeeded
if not result.get("error"):
remove_pending(symbol)
if signal_id:
update_signal_outcome(signal_id, "confirmed", json.dumps(result, ensure_ascii=False)[:500])
else:
if signal_id:
update_signal_outcome(signal_id, "error", result.get("error", "")[:200])
output = {
"action": "executed",
"symbol": symbol,
"side": rec.get("side"),
"result": result,
"display": format_execution_result(result, symbol, rec.get("side")),
}
print(json.dumps(output, ensure_ascii=False))
elif action == "cancel":
if len(sys.argv) < 3:
print("用法: trade_signal_handler.py cancel <SYMBOL>")
sys.exit(1)
symbol = sys.argv[2].upper().replace("USDT", "")
pending = load_pending(symbol)
signal_id = pending.get("signal_id") if pending else None
remove_pending(symbol)
if signal_id:
update_signal_outcome(signal_id, "cancelled")
print(json.dumps({"action": "cancelled", "symbol": symbol}))
elif action == "status":
pending_files = glob.glob(os.path.join(PENDING_DIR, "*.json"))
if not pending_files:
print(json.dumps({"pending": []}))
else:
pending = []
for f in pending_files:
with open(f) as fh:
d = json.load(fh)
pending.append({
"symbol": d["symbol"],
"side": d["rec"].get("side"),
"time": d.get("time_str", d.get("timestamp", "unknown")),
})
print(json.dumps({"pending": pending}, ensure_ascii=False))
elif action == "history":
# Forward to signal_db.py
result = subprocess.run(
[sys.executable, SIGNAL_DB_SCRIPT, "history"] + sys.argv[2:],
capture_output=True, text=True, timeout=10,
)
print(result.stdout)
if result.returncode != 0 and result.stderr:
print(result.stderr, file=sys.stderr)
elif action == "stats":
result = subprocess.run(
[sys.executable, SIGNAL_DB_SCRIPT, "stats"],
capture_output=True, text=True, timeout=10,
)
print(result.stdout)
elif action == "traders":
result = subprocess.run(
[sys.executable, SIGNAL_DB_SCRIPT, "traders"],
capture_output=True, text=True, timeout=10,
)
print(result.stdout)
else:
print(f"Unknown action: {action}")
sys.exit(1)
if __name__ == "__main__":
main()