Files
Hermes-Skills/okx-auto-position/scripts/trade_notifier.py
T
mike 657dc41c46 Initial commit: Trading skills collection
- 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)
2026-07-05 02:39:41 -04:00

253 lines
9.2 KiB
Python

#!/usr/bin/env python3
"""
交易通知器 - 带Inline Keyboard按钮的推送
用法:
python3 trade_notifier.py notify '{"symbol":"BTC","side":"long","leverage":10,...}'
python3 trade_notifier.py callback <callback_data> # 处理按钮点击
"""
import os
import re
import sys
import json
import time
import requests
def _load_env():
"""Load TELEGRAM_BOT_TOKEN from ~/.hermes/.env"""
env_path = os.path.expanduser("~/.hermes/.env")
with open(env_path) as f:
for line in f:
m = re.match(r'TELEGRAM_BOT_TOKEN=(.*)', line.strip())
if m:
return m.group(1).strip()
return ''
BOT_TOKEN = _load_env()
PROXY = 'http://127.0.0.1:7890'
PENDING_DIR = os.path.expanduser("~/.hermes/trading/pending")
CALLBACK_LOG = os.path.expanduser("~/.hermes/trading/callbacks.jsonl")
os.makedirs(PENDING_DIR, exist_ok=True)
def send_message_with_buttons(chat_id, text, buttons=None):
"""Send message, optionally with inline keyboard buttons"""
url = f"https://api.telegram.org/bot{BOT_TOKEN}/sendMessage"
payload = {
"chat_id": chat_id,
"text": text,
"parse_mode": "Markdown",
}
if buttons:
payload["reply_markup"] = json.dumps({"inline_keyboard": buttons})
resp = requests.post(url, data=payload, proxies={"https": PROXY, "http": PROXY}, timeout=15)
return resp.json()
def edit_message_buttons(chat_id, message_id, text, buttons=None):
"""Edit message text and optionally update buttons"""
url = f"https://api.telegram.org/bot{BOT_TOKEN}/editMessageText"
payload = {
"chat_id": chat_id,
"message_id": message_id,
"text": text,
"parse_mode": "Markdown",
}
if buttons:
payload["reply_markup"] = json.dumps({"inline_keyboard": buttons})
resp = requests.post(url, data=payload, proxies={"https": PROXY, "http": PROXY}, timeout=15)
return resp.json()
def answer_callback(callback_query_id, text=""):
"""Answer callback query to remove loading state"""
url = f"https://api.telegram.org/bot{BOT_TOKEN}/answerCallbackQuery"
payload = {"callback_query_id": callback_query_id}
if text:
payload["text"] = text
resp = requests.post(url, data=payload, proxies={"https": PROXY, "http": PROXY}, timeout=10)
return resp.json()
def format_recommendation(rec):
"""Format recommendation for display"""
symbol = rec.get("symbol", "?")
side_cn = rec.get("side_cn", "做多" if rec.get("side") in ("long", "buy") else "做空")
leverage = rec.get("leverage", 10)
contracts = rec.get("contracts", 0)
entry = rec.get("price", 0)
tp = rec.get("tp_price", 0)
sl = rec.get("sl_price", 0)
margin = rec.get("margin", 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)
rr = rec.get("rr", 0)
liq_price = rec.get("liq_price", 0)
liq_pct = rec.get("liq_pct", 0)
balance = rec.get("acct_free", 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
lines = [
f"📊 *{symbol} {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}:1*",
]
if liq_price:
lines.append(f"• ⚠️ 清算价: {liq_price} (距离 {liq_pct}%)")
return "\n".join(lines)
def notify(chat_id, rec_json):
"""Send trade recommendation (text only, no buttons to avoid polling conflict)"""
rec = json.loads(rec_json) if isinstance(rec_json, str) else rec_json
symbol = rec.get("symbol", "").split("/")[0].replace("USDT", "")
side = rec.get("side", "long")
# Save pending
pending_path = os.path.join(PENDING_DIR, f"{symbol}.json")
with open(pending_path, "w") as f:
json.dump({"symbol": symbol, "side": side, "rec": rec, "timestamp": time.time()}, f)
text = format_recommendation(rec)
# No buttons - use text Y/N reply instead (avoids getUpdates conflict with gateway)
result = send_message_with_buttons(chat_id, text, None)
return result
def handle_callback(callback_data, chat_id, message_id, callback_query_id):
"""Handle button click"""
action, symbol = callback_data.split(":", 1)
# Log callback
with open(CALLBACK_LOG, "a") as f:
f.write(json.dumps({"action": action, "symbol": symbol, "time": time.time(), "chat_id": chat_id}) + "\n")
if action == "trade_confirm":
# Load pending
pending_path = os.path.join(PENDING_DIR, f"{symbol}.json")
if not os.path.exists(pending_path):
answer_callback(callback_query_id, "❌ 未找到待确认交易")
return {"error": "no pending"}
with open(pending_path) as f:
pending = json.load(f)
rec = pending["rec"]
# Execute trade
import subprocess
script_dir = os.path.dirname(os.path.abspath(__file__))
advisor = os.path.join(script_dir, "okx_position_advisor.py")
rec_str = json.dumps(rec, ensure_ascii=False)
cmd = ["bash", "-c", f"source ~/.bashrc && python3 {advisor} --symbol {symbol} --side {rec.get('side','long')} --execute --json --rec-json '{rec_str}'"]
result = subprocess.run(cmd, capture_output=True, text=True, timeout=60)
# Remove pending
os.remove(pending_path)
if result.returncode != 0:
answer_callback(callback_query_id, "❌ 下单失败")
edit_message_buttons(chat_id, message_id, f"❌ *{symbol} 下单失败*\n\n{result.stderr[:200]}")
return {"error": result.stderr}
try:
exec_result = json.loads(result.stdout)
except:
exec_result = {"raw": result.stdout}
# Format result
side_cn = "做多" if rec.get("side") in ("long", "buy") else "做空"
result_text = f"✅ *{symbol} {side_cn} 开仓成功*\n\n"
for step in exec_result.get("steps", []):
if step.get("status") == "ok":
if step["step"] == "leverage":
result_text += "✅ 杠杆设置成功\n"
elif step["step"] == "order":
result_text += f"✅ 下单成功 (ID: {step.get('order_id', '?')})\n"
elif step["step"] == "tp_sl":
result_text += f"✅ 止盈止损设置成功\n"
pos = exec_result.get("position")
if pos:
pnl_emoji = "🟢" if pos.get("pnl", 0) >= 0 else "🔴"
result_text += f"\n📊 *持仓确认:*\n"
result_text += f"• 数量: {pos.get('contracts', '?')}\n"
result_text += f"• 入场价: *{pos.get('entry', '?')}*\n"
result_text += f"• {pnl_emoji} 浮盈: {pos.get('pnl', 0):.2f} USDT\n"
algo = exec_result.get("algo")
if algo:
result_text += f"\n🎯 止盈: *{algo.get('tp', '?')}*\n"
result_text += f"🛑 止损: *{algo.get('sl', '?')}*\n"
answer_callback(callback_query_id, "✅ 已下单")
edit_message_buttons(chat_id, message_id, result_text)
return exec_result
elif action == "trade_cancel":
# Remove pending
pending_path = os.path.join(PENDING_DIR, f"{symbol}.json")
if os.path.exists(pending_path):
os.remove(pending_path)
answer_callback(callback_query_id, "❌ 已取消")
edit_message_buttons(chat_id, message_id, f"❌ *{symbol} 交易已取消*")
return {"cancelled": True}
def main():
if len(sys.argv) < 2:
print("用法: trade_notifier.py notify|callback [args]")
sys.exit(1)
action = sys.argv[1]
if action == "notify":
if len(sys.argv) < 4:
print("用法: trade_notifier.py notify <chat_id> <rec_json>")
sys.exit(1)
chat_id = sys.argv[2]
rec_json = sys.argv[3]
result = notify(chat_id, rec_json)
print(json.dumps(result, ensure_ascii=False))
elif action == "callback":
if len(sys.argv) < 6:
print("用法: trade_notifier.py callback <callback_data> <chat_id> <message_id> <callback_query_id>")
sys.exit(1)
callback_data = sys.argv[2]
chat_id = sys.argv[3]
message_id = sys.argv[4]
callback_query_id = sys.argv[5]
result = handle_callback(callback_data, chat_id, message_id, callback_query_id)
print(json.dumps(result, ensure_ascii=False, default=str))
else:
print(f"Unknown action: {action}")
sys.exit(1)
if __name__ == "__main__":
main()