- OKX交易自动化 (okx-auto-position, okx-crypto, okx-exchange) - 交易信号处理 (signal-confirmation-templates, trading-signal-aggregator) - 量化因子挖掘 (quant-factor-mining) - 长桥集成 (longbridge-cli, longbridge-python-sdk) - 六合彩分析 (lottery-hk) - 股息投资 (dividend-investing, dividend-scanner) - 日内交易 (intraday-trading) - 同花顺 (tonghuashun)
412 lines
14 KiB
Python
412 lines
14 KiB
Python
#!/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()
|