Skip to content

Commit 228a046

Browse files
committed
Extract LongBridge notification helpers
1 parent 2f80651 commit 228a046

3 files changed

Lines changed: 173 additions & 78 deletions

File tree

main.py

Lines changed: 10 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66
import os
77
import time
88
import traceback
9-
import requests
109
from datetime import datetime
1110

1211
from flask import Flask
@@ -22,6 +21,12 @@
2221
send_order_status_message as notifications_send_order_status_message,
2322
submit_order_with_alert as notifications_submit_order_with_alert,
2423
)
24+
from notifications.telegram import (
25+
build_issue_notifier,
26+
build_prefixer,
27+
build_sender,
28+
build_translator,
29+
)
2530
from quant_platform_kit.longbridge import (
2631
build_contexts,
2732
calculate_rotation_indicators,
@@ -90,90 +95,17 @@ def get_project_id():
9095

9196
SEPARATOR = "━━━━━━━━━━━━━━━━━━"
9297

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-
15298
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)
157100

158101
def with_prefix(message: str) -> str:
159-
return f"[{ACCOUNT_PREFIX}/{SERVICE_NAME}] {message}"
102+
return build_prefixer(ACCOUNT_PREFIX, SERVICE_NAME)(message)
160103

161104
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)
171106

172107
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)
177109

178110

179111
def is_filled_status(status):

notifications/telegram.py

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
"""Telegram notification helpers for LongBridgeQuant."""
2+
3+
from __future__ import annotations
4+
5+
import requests
6+
7+
8+
I18N = {
9+
"zh": {
10+
"rebalance_title": "🔔 【调仓指令】",
11+
"market_status": "📊 市场状态: {status}",
12+
"risk_position": "💼 交易层风险仓位: {ratio}",
13+
"income_target": "💰 收入层目标: {ratio}",
14+
"income_locked": "🏦 收入层锁定占比: {ratio}",
15+
"signal": "🎯 触发信号: {msg}",
16+
"heartbeat_title": "💓 【心跳检测】",
17+
"equity": "💰 净值: ${value}",
18+
"cash_label": "现金",
19+
"heartbeat_signal": "🎯 信号: {msg}",
20+
"no_trades": "✅ 无需调仓",
21+
"order_filled": "✅ 订单成交 | {symbol} {side} {qty}股 均价 ${price} (ID: {order_id})",
22+
"order_partial": "⚠️ 订单部分成交 | {symbol} {side} 已成交 {executed}/{qty}股 均价 ${price} (ID: {order_id})",
23+
"order_error": "❌ 订单异常 | {symbol} {side} {qty}股 已{status} (ID: {order_id}) 原因: {reason}",
24+
"error_title": "🚨 【策略异常】",
25+
"limit_buy": "📈 [限价买入] {symbol}: {qty}股 @ ${price}",
26+
"market_buy": "📈 [市价买入] {symbol}: {qty}股 @ ${price}",
27+
"limit_sell": "📉 [限价卖出] {symbol}: {qty}股 @ ${price}",
28+
"market_sell": "📉 [市价卖出] {symbol}: {qty}股 @ ${price}",
29+
"side_buy": "买入",
30+
"side_sell": "卖出",
31+
"status_rejected": "拒绝",
32+
"status_canceled": "取消",
33+
"status_expired": "过期",
34+
"signal_risk_on": "SOXL 站上 {window} 日均线,持有 SOXL,交易层风险仓位 {ratio}",
35+
"signal_delever": "SOXL 跌破 {window} 日均线,切换至 SOXX,交易层风险仓位 {ratio}",
36+
},
37+
"en": {
38+
"rebalance_title": "🔔 【Trade Execution Report】",
39+
"market_status": "📊 Market: {status}",
40+
"risk_position": "💼 Risk Position: {ratio}",
41+
"income_target": "💰 Income Target: {ratio}",
42+
"income_locked": "🏦 Income Locked: {ratio}",
43+
"signal": "🎯 Signal: {msg}",
44+
"heartbeat_title": "💓 【Heartbeat】",
45+
"equity": "💰 Equity: ${value}",
46+
"cash_label": "Cash",
47+
"heartbeat_signal": "🎯 Signal: {msg}",
48+
"no_trades": "✅ No trades needed",
49+
"order_filled": "✅ Order Filled | {symbol} {side} {qty} shares avg ${price} (ID: {order_id})",
50+
"order_partial": "⚠️ Partial Fill | {symbol} {side} filled {executed}/{qty} shares avg ${price} (ID: {order_id})",
51+
"order_error": "❌ Order Error | {symbol} {side} {qty} shares {status} (ID: {order_id}) reason: {reason}",
52+
"error_title": "🚨 【Strategy Error】",
53+
"limit_buy": "📈 [Limit buy] {symbol}: {qty} shares @ ${price}",
54+
"market_buy": "📈 [Market buy] {symbol}: {qty} shares @ ${price}",
55+
"limit_sell": "📉 [Limit sell] {symbol}: {qty} shares @ ${price}",
56+
"market_sell": "📉 [Market sell] {symbol}: {qty} shares @ ${price}",
57+
"side_buy": "Buy",
58+
"side_sell": "Sell",
59+
"status_rejected": "Rejected",
60+
"status_canceled": "Canceled",
61+
"status_expired": "Expired",
62+
"signal_risk_on": "SOXL above {window}d MA, hold SOXL, risk {ratio}",
63+
"signal_delever": "SOXL below {window}d MA, switch to SOXX, risk {ratio}",
64+
},
65+
}
66+
67+
68+
def build_translator(lang):
69+
def translate(key, **kwargs):
70+
active_lang = lang if lang in I18N else "en"
71+
template = I18N[active_lang].get(key, key)
72+
return template.format(**kwargs) if kwargs else template
73+
74+
return translate
75+
76+
77+
def build_prefixer(account_prefix: str, service_name: str):
78+
def with_prefix(message: str) -> str:
79+
return f"[{account_prefix}/{service_name}] {message}"
80+
81+
return with_prefix
82+
83+
84+
def build_sender(token, chat_id, *, with_prefix_fn, requests_module=requests):
85+
def send_tg_message(message):
86+
if not token or not chat_id:
87+
return
88+
url = f"https://api.telegram.org/bot{token}/sendMessage"
89+
try:
90+
prefixed = with_prefix_fn(message)
91+
print(f"TG:\n{prefixed}", flush=True)
92+
requests_module.post(url, json={"chat_id": chat_id, "text": prefixed}, timeout=10)
93+
except Exception as exc:
94+
print(f"Telegram send failed: {exc}", flush=True)
95+
96+
return send_tg_message
97+
98+
99+
def build_issue_notifier(*, with_prefix_fn, send_tg_message_fn):
100+
def notify_issue(title, detail):
101+
message = f"{title}\n{detail}"
102+
print(with_prefix_fn(message), flush=True)
103+
send_tg_message_fn(message)
104+
105+
return notify_issue

tests/test_notifications.py

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import sys
2+
import unittest
3+
from pathlib import Path
4+
5+
6+
ROOT = Path(__file__).resolve().parents[1]
7+
if str(ROOT) not in sys.path:
8+
sys.path.insert(0, str(ROOT))
9+
10+
from notifications.telegram import build_issue_notifier, build_prefixer, build_sender, build_translator
11+
12+
13+
class FakeRequests:
14+
def __init__(self):
15+
self.calls = []
16+
17+
def post(self, url, json, timeout):
18+
self.calls.append((url, json, timeout))
19+
return object()
20+
21+
22+
class NotificationTests(unittest.TestCase):
23+
def test_build_translator_supports_chinese(self):
24+
translate = build_translator("zh")
25+
self.assertEqual(translate("equity", value="123.45"), "💰 净值: $123.45")
26+
27+
def test_build_prefixer_formats_account_and_service(self):
28+
with_prefix = build_prefixer("HK", "LongBridgeQuant")
29+
self.assertEqual(with_prefix("hello"), "[HK/LongBridgeQuant] hello")
30+
31+
def test_build_sender_posts_prefixed_message(self):
32+
fake_requests = FakeRequests()
33+
sender = build_sender(
34+
"token-1",
35+
"chat-1",
36+
with_prefix_fn=build_prefixer("HK", "LongBridgeQuant"),
37+
requests_module=fake_requests,
38+
)
39+
sender("hello")
40+
self.assertEqual(len(fake_requests.calls), 1)
41+
url, payload, timeout = fake_requests.calls[0]
42+
self.assertIn("token-1", url)
43+
self.assertEqual(payload["chat_id"], "chat-1")
44+
self.assertEqual(payload["text"], "[HK/LongBridgeQuant] hello")
45+
self.assertEqual(timeout, 10)
46+
47+
def test_build_issue_notifier_logs_and_sends(self):
48+
sent = []
49+
notifier = build_issue_notifier(
50+
with_prefix_fn=build_prefixer("SG", "LongBridgeQuant"),
51+
send_tg_message_fn=sent.append,
52+
)
53+
notifier("Problem", "details")
54+
self.assertEqual(sent, ["Problem\ndetails"])
55+
56+
57+
if __name__ == "__main__":
58+
unittest.main()

0 commit comments

Comments
 (0)