- 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)
253 lines
9.2 KiB
Python
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()
|