Skip to content

Commit 3c5ced4

Browse files
committed
Extract IBKR notification helpers
1 parent 5efd82f commit 3c5ced4

4 files changed

Lines changed: 122 additions & 69 deletions

File tree

main.py

Lines changed: 8 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
except ImportError:
1717
compute_v1 = None
1818

19+
from notifications.telegram import build_translator, send_telegram_message
1920
from quant_platform_kit.common.models import OrderIntent
2021
from quant_platform_kit.ibkr import (
2122
connect_ib as ibkr_connect_ib,
@@ -164,79 +165,17 @@ def get_ib_port():
164165

165166
SEPARATOR = "━━━━━━━━━━━━━━━━━━"
166167

167-
# ---------------------------------------------------------------------------
168-
# i18n
169-
# ---------------------------------------------------------------------------
170-
I18N = {
171-
"zh": {
172-
"rebalance_title": "🔔 【调仓指令】",
173-
"heartbeat_title": "💓 【心跳检测】",
174-
"error_title": "🚨 【策略异常】",
175-
"canary_title": "🐤 【金丝雀检查】",
176-
"equity": "净值",
177-
"buying_power": "购买力",
178-
"signal_label": "信号",
179-
"no_trades": "✅ 无需调仓",
180-
"emergency": "🛡️ 金丝雀应急: {n_bad}/4 坏, 全部转入 {safe}",
181-
"quarterly": "📊 季度调仓: Top {n} 轮动",
182-
"daily_check": "📋 每日检查: 金丝雀正常, 持仓不变",
183-
"hold": "💎 持仓不变",
184-
"market_sell": "📉 [市价卖出] {symbol}: {qty}股",
185-
"limit_buy": "📈 [限价买入] {symbol}: {qty}股 @ ${price}",
186-
"submitted": "已下发 (ID: {order_id})",
187-
"failed": "失败: {reason}",
188-
"order_filled": "✅ 订单成交 | {symbol} {side} {qty}股 均价 ${price} (ID: {order_id})",
189-
"order_partial": "⚠️ 部分成交 | {symbol} {side} {executed}/{qty}股 均价 ${price} (ID: {order_id})",
190-
"order_rejected": "❌ 订单异常 | {symbol} {side} {qty}股 状态: {status} (ID: {order_id})",
191-
},
192-
"en": {
193-
"rebalance_title": "🔔 【Trade Execution Report】",
194-
"heartbeat_title": "💓 【Heartbeat】",
195-
"error_title": "🚨 【Strategy Error】",
196-
"canary_title": "🐤 【Canary Check】",
197-
"equity": "Equity",
198-
"buying_power": "Buying Power",
199-
"signal_label": "Signal",
200-
"no_trades": "✅ No rebalance needed",
201-
"emergency": "🛡️ Canary Emergency: {n_bad}/4 bad, rotating to {safe}",
202-
"quarterly": "📊 Quarterly Rebalance: Top {n} rotation",
203-
"daily_check": "📋 Daily Check: canary OK, holding",
204-
"hold": "💎 Hold positions",
205-
"market_sell": "📉 [Market sell] {symbol}: {qty} shares",
206-
"limit_buy": "📈 [Limit buy] {symbol}: {qty} shares @ ${price}",
207-
"submitted": "submitted (ID: {order_id})",
208-
"failed": "failed: {reason}",
209-
"order_filled": "✅ Filled | {symbol} {side} {qty} shares avg ${price} (ID: {order_id})",
210-
"order_partial": "⚠️ Partial | {symbol} {side} {executed}/{qty} shares avg ${price} (ID: {order_id})",
211-
"order_rejected": "❌ Rejected | {symbol} {side} {qty} shares status: {status} (ID: {order_id})",
212-
},
213-
}
214-
215-
216168
def t(key, **kwargs):
217-
lang = NOTIFY_LANG if NOTIFY_LANG in I18N else "en"
218-
template = I18N[lang].get(key, key)
219-
return template.format(**kwargs) if kwargs else template
169+
return build_translator(NOTIFY_LANG)(key, **kwargs)
220170

221171

222172
def send_tg_message(message):
223-
if not TG_TOKEN or not TG_CHAT_ID:
224-
return
225-
url = f"https://api.telegram.org/bot{TG_TOKEN}/sendMessage"
226-
try:
227-
print(f"TG:\n{message}", flush=True)
228-
response = requests.post(
229-
url,
230-
json={"chat_id": TG_CHAT_ID, "text": message},
231-
timeout=10,
232-
)
233-
if not 200 <= response.status_code < 300:
234-
print(
235-
f"Telegram send failed with status {response.status_code}: {response.text}",
236-
flush=True,
237-
)
238-
except Exception as e:
239-
print(f"Telegram send failed: {e}", flush=True)
173+
return send_telegram_message(
174+
message,
175+
token=TG_TOKEN,
176+
chat_id=TG_CHAT_ID,
177+
requests_module=requests,
178+
)
240179

241180

242181
def connect_ib():

notifications/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
"""Notification helpers for IBKRQuant."""

notifications/telegram.py

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
"""Telegram notification and i18n helpers for IBKRQuant."""
2+
3+
from __future__ import annotations
4+
5+
6+
I18N = {
7+
"zh": {
8+
"rebalance_title": "🔔 【调仓指令】",
9+
"heartbeat_title": "💓 【心跳检测】",
10+
"error_title": "🚨 【策略异常】",
11+
"canary_title": "🐤 【金丝雀检查】",
12+
"equity": "净值",
13+
"buying_power": "购买力",
14+
"signal_label": "信号",
15+
"no_trades": "✅ 无需调仓",
16+
"emergency": "🛡️ 金丝雀应急: {n_bad}/4 坏, 全部转入 {safe}",
17+
"quarterly": "📊 季度调仓: Top {n} 轮动",
18+
"daily_check": "📋 每日检查: 金丝雀正常, 持仓不变",
19+
"hold": "💎 持仓不变",
20+
"market_sell": "📉 [市价卖出] {symbol}: {qty}股",
21+
"limit_buy": "📈 [限价买入] {symbol}: {qty}股 @ ${price}",
22+
"submitted": "已下发 (ID: {order_id})",
23+
"failed": "失败: {reason}",
24+
"order_filled": "✅ 订单成交 | {symbol} {side} {qty}股 均价 ${price} (ID: {order_id})",
25+
"order_partial": "⚠️ 部分成交 | {symbol} {side} {executed}/{qty}股 均价 ${price} (ID: {order_id})",
26+
"order_rejected": "❌ 订单异常 | {symbol} {side} {qty}股 状态: {status} (ID: {order_id})",
27+
},
28+
"en": {
29+
"rebalance_title": "🔔 【Trade Execution Report】",
30+
"heartbeat_title": "💓 【Heartbeat】",
31+
"error_title": "🚨 【Strategy Error】",
32+
"canary_title": "🐤 【Canary Check】",
33+
"equity": "Equity",
34+
"buying_power": "Buying Power",
35+
"signal_label": "Signal",
36+
"no_trades": "✅ No rebalance needed",
37+
"emergency": "🛡️ Canary Emergency: {n_bad}/4 bad, rotating to {safe}",
38+
"quarterly": "📊 Quarterly Rebalance: Top {n} rotation",
39+
"daily_check": "📋 Daily Check: canary OK, holding",
40+
"hold": "💎 Hold positions",
41+
"market_sell": "📉 [Market sell] {symbol}: {qty} shares",
42+
"limit_buy": "📈 [Limit buy] {symbol}: {qty} shares @ ${price}",
43+
"submitted": "submitted (ID: {order_id})",
44+
"failed": "failed: {reason}",
45+
"order_filled": "✅ Filled | {symbol} {side} {qty} shares avg ${price} (ID: {order_id})",
46+
"order_partial": "⚠️ Partial | {symbol} {side} {executed}/{qty} shares avg ${price} (ID: {order_id})",
47+
"order_rejected": "❌ Rejected | {symbol} {side} {qty} shares status: {status} (ID: {order_id})",
48+
},
49+
}
50+
51+
52+
def build_translator(lang):
53+
def translate(key, **kwargs):
54+
active_lang = lang if lang in I18N else "en"
55+
template = I18N[active_lang].get(key, key)
56+
return template.format(**kwargs) if kwargs else template
57+
58+
return translate
59+
60+
61+
def send_telegram_message(
62+
message,
63+
*,
64+
token,
65+
chat_id,
66+
requests_module,
67+
printer=print,
68+
):
69+
if not token or not chat_id:
70+
return
71+
72+
url = f"https://api.telegram.org/bot{token}/sendMessage"
73+
try:
74+
printer(f"TG:\n{message}", flush=True)
75+
response = requests_module.post(
76+
url,
77+
json={"chat_id": chat_id, "text": message},
78+
timeout=10,
79+
)
80+
if not 200 <= response.status_code < 300:
81+
printer(
82+
f"Telegram send failed with status {response.status_code}: {response.text}",
83+
flush=True,
84+
)
85+
except Exception as exc:
86+
printer(f"Telegram send failed: {exc}", flush=True)

tests/test_notifications.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
from notifications.telegram import build_translator, send_telegram_message
2+
3+
4+
def test_build_translator_supports_chinese():
5+
translate = build_translator("zh")
6+
assert translate("equity") == "净值"
7+
8+
9+
def test_send_telegram_message_logs_non_200_response(capsys):
10+
class FakeResponse:
11+
status_code = 401
12+
text = "unauthorized"
13+
14+
class FakeRequests:
15+
@staticmethod
16+
def post(*args, **kwargs):
17+
return FakeResponse()
18+
19+
send_telegram_message(
20+
"hello",
21+
token="token",
22+
chat_id="chat-id",
23+
requests_module=FakeRequests,
24+
)
25+
26+
captured = capsys.readouterr()
27+
assert "Telegram send failed with status 401: unauthorized" in captured.out

0 commit comments

Comments
 (0)