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
This commit is contained in:
@@ -0,0 +1,411 @@
|
||||
#!/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()
|
||||
Reference in New Issue
Block a user