|
6 | 6 | import os |
7 | 7 | import time |
8 | 8 | import traceback |
9 | | -import requests |
10 | 9 | from datetime import datetime |
11 | 10 |
|
12 | 11 | from flask import Flask |
|
22 | 21 | send_order_status_message as notifications_send_order_status_message, |
23 | 22 | submit_order_with_alert as notifications_submit_order_with_alert, |
24 | 23 | ) |
| 24 | +from notifications.telegram import ( |
| 25 | + build_issue_notifier, |
| 26 | + build_prefixer, |
| 27 | + build_sender, |
| 28 | + build_translator, |
| 29 | +) |
25 | 30 | from quant_platform_kit.longbridge import ( |
26 | 31 | build_contexts, |
27 | 32 | calculate_rotation_indicators, |
@@ -90,90 +95,17 @@ def get_project_id(): |
90 | 95 |
|
91 | 96 | SEPARATOR = "━━━━━━━━━━━━━━━━━━" |
92 | 97 |
|
93 | | -I18N = { |
94 | | - "zh": { |
95 | | - "rebalance_title": "🔔 【调仓指令】", |
96 | | - "market_status": "📊 市场状态: {status}", |
97 | | - "risk_position": "💼 交易层风险仓位: {ratio}", |
98 | | - "income_target": "💰 收入层目标: {ratio}", |
99 | | - "income_locked": "🏦 收入层锁定占比: {ratio}", |
100 | | - "signal": "🎯 触发信号: {msg}", |
101 | | - "heartbeat_title": "💓 【心跳检测】", |
102 | | - "equity": "💰 净值: ${value}", |
103 | | - "cash_label": "现金", |
104 | | - "heartbeat_signal": "🎯 信号: {msg}", |
105 | | - "no_trades": "✅ 无需调仓", |
106 | | - "order_filled": "✅ 订单成交 | {symbol} {side} {qty}股 均价 ${price} (ID: {order_id})", |
107 | | - "order_partial": "⚠️ 订单部分成交 | {symbol} {side} 已成交 {executed}/{qty}股 均价 ${price} (ID: {order_id})", |
108 | | - "order_error": "❌ 订单异常 | {symbol} {side} {qty}股 已{status} (ID: {order_id}) 原因: {reason}", |
109 | | - "error_title": "🚨 【策略异常】", |
110 | | - "limit_buy": "📈 [限价买入] {symbol}: {qty}股 @ ${price}", |
111 | | - "market_buy": "📈 [市价买入] {symbol}: {qty}股 @ ${price}", |
112 | | - "limit_sell": "📉 [限价卖出] {symbol}: {qty}股 @ ${price}", |
113 | | - "market_sell": "📉 [市价卖出] {symbol}: {qty}股 @ ${price}", |
114 | | - "side_buy": "买入", |
115 | | - "side_sell": "卖出", |
116 | | - "status_rejected": "拒绝", |
117 | | - "status_canceled": "取消", |
118 | | - "status_expired": "过期", |
119 | | - "signal_risk_on": "SOXL 站上 {window} 日均线,持有 SOXL,交易层风险仓位 {ratio}", |
120 | | - "signal_delever": "SOXL 跌破 {window} 日均线,切换至 SOXX,交易层风险仓位 {ratio}", |
121 | | - }, |
122 | | - "en": { |
123 | | - "rebalance_title": "🔔 【Trade Execution Report】", |
124 | | - "market_status": "📊 Market: {status}", |
125 | | - "risk_position": "💼 Risk Position: {ratio}", |
126 | | - "income_target": "💰 Income Target: {ratio}", |
127 | | - "income_locked": "🏦 Income Locked: {ratio}", |
128 | | - "signal": "🎯 Signal: {msg}", |
129 | | - "heartbeat_title": "💓 【Heartbeat】", |
130 | | - "equity": "💰 Equity: ${value}", |
131 | | - "cash_label": "Cash", |
132 | | - "heartbeat_signal": "🎯 Signal: {msg}", |
133 | | - "no_trades": "✅ No trades needed", |
134 | | - "order_filled": "✅ Order Filled | {symbol} {side} {qty} shares avg ${price} (ID: {order_id})", |
135 | | - "order_partial": "⚠️ Partial Fill | {symbol} {side} filled {executed}/{qty} shares avg ${price} (ID: {order_id})", |
136 | | - "order_error": "❌ Order Error | {symbol} {side} {qty} shares {status} (ID: {order_id}) reason: {reason}", |
137 | | - "error_title": "🚨 【Strategy Error】", |
138 | | - "limit_buy": "📈 [Limit buy] {symbol}: {qty} shares @ ${price}", |
139 | | - "market_buy": "📈 [Market buy] {symbol}: {qty} shares @ ${price}", |
140 | | - "limit_sell": "📉 [Limit sell] {symbol}: {qty} shares @ ${price}", |
141 | | - "market_sell": "📉 [Market sell] {symbol}: {qty} shares @ ${price}", |
142 | | - "side_buy": "Buy", |
143 | | - "side_sell": "Sell", |
144 | | - "status_rejected": "Rejected", |
145 | | - "status_canceled": "Canceled", |
146 | | - "status_expired": "Expired", |
147 | | - "signal_risk_on": "SOXL above {window}d MA, hold SOXL, risk {ratio}", |
148 | | - "signal_delever": "SOXL below {window}d MA, switch to SOXX, risk {ratio}", |
149 | | - }, |
150 | | -} |
151 | | - |
152 | 98 | def t(key, **kwargs): |
153 | | - """Get translated string for current LANG.""" |
154 | | - lang = NOTIFY_LANG if NOTIFY_LANG in I18N else "en" |
155 | | - template = I18N[lang].get(key, key) |
156 | | - return template.format(**kwargs) if kwargs else template |
| 99 | + return build_translator(NOTIFY_LANG)(key, **kwargs) |
157 | 100 |
|
158 | 101 | def with_prefix(message: str) -> str: |
159 | | - return f"[{ACCOUNT_PREFIX}/{SERVICE_NAME}] {message}" |
| 102 | + return build_prefixer(ACCOUNT_PREFIX, SERVICE_NAME)(message) |
160 | 103 |
|
161 | 104 | def send_tg_message(message): |
162 | | - """Send text to Telegram; no-op if token or chat_id missing.""" |
163 | | - if not TG_TOKEN or not TG_CHAT_ID: return |
164 | | - url = f"https://api.telegram.org/bot{TG_TOKEN}/sendMessage" |
165 | | - try: |
166 | | - prefixed = with_prefix(message) |
167 | | - print(f"TG:\n{prefixed}", flush=True) |
168 | | - requests.post(url, json={"chat_id": TG_CHAT_ID, "text": prefixed}, timeout=10) |
169 | | - except Exception as e: |
170 | | - print(f"Telegram send failed: {e}", flush=True) |
| 105 | + return build_sender(TG_TOKEN, TG_CHAT_ID, with_prefix_fn=with_prefix)(message) |
171 | 106 |
|
172 | 107 | def notify_issue(title, detail): |
173 | | - """Log and send to Telegram (alerts for order/API failures).""" |
174 | | - message = f"{title}\n{detail}" |
175 | | - print(with_prefix(message), flush=True) |
176 | | - send_tg_message(message) |
| 108 | + return build_issue_notifier(with_prefix_fn=with_prefix, send_tg_message_fn=send_tg_message)(title, detail) |
177 | 109 |
|
178 | 110 |
|
179 | 111 | def is_filled_status(status): |
|
0 commit comments