Skip to content

Commit 73c87fc

Browse files
authored
Show TQQQ delever allocation in notifications (#167)
1 parent ebf57e6 commit 73c87fc

6 files changed

Lines changed: 83 additions & 13 deletions

File tree

application/signal_snapshot.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,11 @@
8282
"dual_drive_volatility_delever_retention_context_found",
8383
"dual_drive_volatility_delever_retention_reason_codes",
8484
"dual_drive_volatility_delever_redirect_symbol",
85+
"dual_drive_volatility_delever_source_value",
86+
"dual_drive_volatility_delever_retained_value",
8587
"dual_drive_volatility_delever_removed_value",
88+
"dual_drive_volatility_delever_retained_ratio",
89+
"dual_drive_volatility_delever_redirected_ratio",
8690
"dual_drive_macro_risk_governor_enabled",
8791
"dual_drive_macro_risk_governor_found",
8892
"dual_drive_macro_risk_governor_route",

decision_mapper.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,11 @@
6363
"dual_drive_volatility_delever_retention_context_found",
6464
"dual_drive_volatility_delever_retention_reason_codes",
6565
"dual_drive_volatility_delever_redirect_symbol",
66+
"dual_drive_volatility_delever_source_value",
67+
"dual_drive_volatility_delever_retained_value",
6668
"dual_drive_volatility_delever_removed_value",
69+
"dual_drive_volatility_delever_retained_ratio",
70+
"dual_drive_volatility_delever_redirected_ratio",
6771
"dual_drive_macro_risk_governor_enabled",
6872
"dual_drive_macro_risk_governor_found",
6973
"dual_drive_macro_risk_governor_route",

notifications/renderers.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,13 @@ def _format_percent(value) -> str:
200200
return "n/a"
201201

202202

203+
def _as_float_or_none(value):
204+
try:
205+
return float(value)
206+
except (TypeError, ValueError):
207+
return None
208+
209+
203210
def _format_percentile(value) -> str:
204211
try:
205212
percentile = float(value) * 100
@@ -255,6 +262,27 @@ def _format_volatility_delever_threshold_detail(execution, *, prefix: str, trans
255262
)
256263

257264

265+
def _format_tqqq_volatility_delever_allocation_detail(
266+
execution,
267+
*,
268+
prefix: str,
269+
redirect_symbol: str,
270+
translator,
271+
) -> str:
272+
retained_ratio = _as_float_or_none(execution.get(f"{prefix}_retained_ratio"))
273+
redirected_ratio = _as_float_or_none(execution.get(f"{prefix}_redirected_ratio"))
274+
if retained_ratio is None:
275+
retained_ratio = _as_float_or_none(execution.get(f"{prefix}_retention_ratio"))
276+
if redirected_ratio is None and retained_ratio is not None:
277+
redirected_ratio = max(0.0, min(1.0, 1.0 - retained_ratio))
278+
return translator(
279+
"tqqq_volatility_delever_allocation_detail",
280+
retained_ratio=_format_percent(retained_ratio),
281+
redirected_ratio=_format_percent(redirected_ratio),
282+
redirect_symbol=redirect_symbol or "QQQ",
283+
)
284+
285+
258286
def _build_risk_control_lines(execution, *, translator):
259287
if _is_truthy(execution.get("dual_drive_volatility_delever_applied")):
260288
redirect_symbol = str(execution.get("dual_drive_volatility_delever_redirect_symbol") or "QQQ").strip().upper()
@@ -268,6 +296,12 @@ def _build_risk_control_lines(execution, *, translator):
268296
prefix="dual_drive_volatility_delever",
269297
translator=translator,
270298
)
299+
allocation_detail = _format_tqqq_volatility_delever_allocation_detail(
300+
execution,
301+
prefix="dual_drive_volatility_delever",
302+
redirect_symbol=redirect_symbol or "QQQ",
303+
translator=translator,
304+
)
271305
if str(execution.get("dual_drive_volatility_delever_trigger_reason") or "").strip() == "hysteresis_hold":
272306
return [
273307
translator(
@@ -279,6 +313,7 @@ def _build_risk_control_lines(execution, *, translator):
279313
threshold_detail=threshold_detail,
280314
source_symbol="TQQQ",
281315
redirect_symbol=redirect_symbol or "QQQ",
316+
allocation_detail=allocation_detail,
282317
)
283318
]
284319
return [
@@ -290,6 +325,7 @@ def _build_risk_control_lines(execution, *, translator):
290325
threshold_detail=threshold_detail,
291326
source_symbol="TQQQ",
292327
redirect_symbol=redirect_symbol or "QQQ",
328+
allocation_detail=allocation_detail,
293329
)
294330
]
295331
return []

notifications/telegram.py

Lines changed: 26 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -71,10 +71,19 @@
7171
"buy_deferred_non_usd_cash": "检测到非 USD 现金({currencies}),但美股策略可用 USD 现金为 ${available}、可投资现金为 ${investable};请先换汇或入金 USD 后再买入",
7272
"buy_deferred_small_target_gap": "{symbol} 目标差额 ${diff} 未超过 1 股价格 ${price};为避免超过目标仓位,本轮不买入",
7373
"buy_deferred_small_account_cash_substitution": "{symbol} 目标金额 ${diff} 低于 1 股价格 ${price};为避免超过目标仓位,本轮保留现金(现金替代:{cash_symbols})",
74-
"risk_control_tqqq_volatility_delever_applied": "🛡️ 风控: QQQ {window} 日年化波动率 {volatility} 高于 {threshold},{source_symbol} 转向 {redirect_symbol}",
75-
"risk_control_tqqq_volatility_delever_applied_dynamic": "🛡️ 风控: QQQ {window} 日年化波动率 {volatility} 高于实际阈值 {threshold}({threshold_detail}),{source_symbol} 转向 {redirect_symbol}",
76-
"risk_control_tqqq_volatility_delever_hysteresis": "🛡️ 风控: QQQ {window} 日年化波动率 {volatility} 仍高于退出阈值 {exit_threshold},维持 {source_symbol} 转向 {redirect_symbol}",
77-
"risk_control_tqqq_volatility_delever_hysteresis_dynamic": "🛡️ 风控: QQQ {window} 日年化波动率 {volatility} 仍高于退出阈值 {exit_threshold};入场实际阈值 {threshold}({threshold_detail}),维持 {source_symbol} 转向 {redirect_symbol}",
74+
"risk_control_tqqq_volatility_delever_applied": "🛡️ 风控: QQQ {window} 日年化波动率 {volatility} 高于 {threshold},{source_symbol} 转向 {redirect_symbol}({allocation_detail})",
75+
"risk_control_tqqq_volatility_delever_applied_dynamic": "🛡️ 风控: QQQ {window} 日年化波动率 {volatility} 高于实际阈值 {threshold}({threshold_detail}),{source_symbol} 转向 {redirect_symbol}({allocation_detail})",
76+
"risk_control_tqqq_volatility_delever_hysteresis": "🛡️ 风控: QQQ {window} 日年化波动率 {volatility} 仍高于退出阈值 {exit_threshold},维持 {source_symbol} 转向 {redirect_symbol}({allocation_detail})",
77+
"risk_control_tqqq_volatility_delever_hysteresis_dynamic": "🛡️ 风控: QQQ {window} 日年化波动率 {volatility} 仍高于退出阈值 {exit_threshold};入场实际阈值 {threshold}({threshold_detail}),维持 {source_symbol} 转向 {redirect_symbol}({allocation_detail})",
78+
"tqqq_volatility_delever_allocation_detail": "杠杆仓位:TQQQ 保留 {retained_ratio},{redirect_symbol} {redirected_ratio}",
79+
"tqqq_signal_reason_entry_trend": "原因:QQQ 高于 MA200,MA20 斜率为正",
80+
"tqqq_signal_reason_entry_pullback": "原因:QQQ 低于 MA200,但站上 MA20 且回撤反弹确认",
81+
"tqqq_signal_reason_hold_trend": "原因:已持有风险仓位,QQQ 仍高于 MA200",
82+
"tqqq_signal_reason_exit_ma200": "原因:QQQ 跌破 MA200 退出线",
83+
"tqqq_signal_reason_idle_waiting": "原因:等待 QQQ 站上 MA200 且 MA20 斜率转正",
84+
"tqqq_signal_reason_macro_delever": "原因:宏观风控降低杠杆",
85+
"tqqq_signal_reason_macro_defense": "原因:宏观风控转入防守",
86+
"tqqq_signal_reason_crisis_defense": "原因:危机防御转入避险仓位",
7887
"buy_deferred_small_cash": "{symbol} 目标差额 ${diff},但可投资现金 ${investable} 不足买入 1 股(价格 ${price})",
7988
"buy_deferred_cash_limit": "{symbol} 目标差额 ${diff},预算可买 {budget_qty} 股,但券商估算可买数量为 0;可能有未完成挂单、结算或购买力占用",
8089
"buy_deferred_cash_sweep_cash_limit": "{symbol} 剩余可投资现金 ${investable},预算可回补 {budget_qty} 股,但券商估算可买数量为 0;可能有未完成挂单、结算或购买力占用",
@@ -219,10 +228,19 @@
219228
"buy_deferred_non_usd_cash": "Non-USD cash is present ({currencies}), but this US-equity strategy has USD cash ${available} and investable cash ${investable}; convert or deposit USD before buying",
220229
"buy_deferred_small_target_gap": "{symbol} target gap ${diff} does not exceed the 1-share price ${price}; skipped to avoid exceeding the target allocation",
221230
"buy_deferred_small_account_cash_substitution": "{symbol} target ${diff} is below the 1-share price ${price}; to avoid exceeding the target allocation, this cycle keeps cash (cash substitute: {cash_symbols})",
222-
"risk_control_tqqq_volatility_delever_applied": "🛡️ Risk control: QQQ {window}d annualized volatility {volatility} is above {threshold}; {source_symbol} redirects to {redirect_symbol}",
223-
"risk_control_tqqq_volatility_delever_applied_dynamic": "🛡️ Risk control: QQQ {window}d annualized volatility {volatility} is above effective threshold {threshold} ({threshold_detail}); {source_symbol} redirects to {redirect_symbol}",
224-
"risk_control_tqqq_volatility_delever_hysteresis": "🛡️ Risk control: QQQ {window}d annualized volatility {volatility} remains above the exit threshold {exit_threshold}; keep {source_symbol} redirected to {redirect_symbol}",
225-
"risk_control_tqqq_volatility_delever_hysteresis_dynamic": "🛡️ Risk control: QQQ {window}d annualized volatility {volatility} remains above exit threshold {exit_threshold}; entry effective threshold {threshold} ({threshold_detail}); keep {source_symbol} redirected to {redirect_symbol}",
231+
"risk_control_tqqq_volatility_delever_applied": "🛡️ Risk control: QQQ {window}d annualized volatility {volatility} is above {threshold}; {source_symbol} redirects to {redirect_symbol} ({allocation_detail})",
232+
"risk_control_tqqq_volatility_delever_applied_dynamic": "🛡️ Risk control: QQQ {window}d annualized volatility {volatility} is above effective threshold {threshold} ({threshold_detail}); {source_symbol} redirects to {redirect_symbol} ({allocation_detail})",
233+
"risk_control_tqqq_volatility_delever_hysteresis": "🛡️ Risk control: QQQ {window}d annualized volatility {volatility} remains above the exit threshold {exit_threshold}; keep {source_symbol} redirected to {redirect_symbol} ({allocation_detail})",
234+
"risk_control_tqqq_volatility_delever_hysteresis_dynamic": "🛡️ Risk control: QQQ {window}d annualized volatility {volatility} remains above exit threshold {exit_threshold}; entry effective threshold {threshold} ({threshold_detail}); keep {source_symbol} redirected to {redirect_symbol} ({allocation_detail})",
235+
"tqqq_volatility_delever_allocation_detail": "leveraged sleeve: TQQQ retained {retained_ratio}, {redirect_symbol} {redirected_ratio}",
236+
"tqqq_signal_reason_entry_trend": "reason: QQQ is above MA200 and MA20 slope is positive",
237+
"tqqq_signal_reason_entry_pullback": "reason: QQQ is below MA200 but above MA20 with a confirmed pullback rebound",
238+
"tqqq_signal_reason_hold_trend": "reason: existing risk sleeve remains active while QQQ stays above MA200",
239+
"tqqq_signal_reason_exit_ma200": "reason: QQQ fell below the MA200 exit line",
240+
"tqqq_signal_reason_idle_waiting": "reason: waiting for QQQ to reclaim MA200 with positive MA20 slope",
241+
"tqqq_signal_reason_macro_delever": "reason: macro risk governor reduced leverage",
242+
"tqqq_signal_reason_macro_defense": "reason: macro risk governor moved the strategy defensive",
243+
"tqqq_signal_reason_crisis_defense": "reason: crisis defense moved the strategy to the safe sleeve",
226244
"buy_deferred_small_cash": "{symbol} target gap ${diff}, but investable cash ${investable} is not enough for 1 share at ${price}",
227245
"buy_deferred_cash_limit": "{symbol} target gap ${diff}, budget supports {budget_qty} shares, but broker estimate returned 0; an open order, settlement, or buying-power hold may still be blocking funds",
228246
"buy_deferred_cash_sweep_cash_limit": "{symbol} residual investable cash ${investable}, budget supports {budget_qty} tail-rebuy shares, but broker estimate returned 0; an open order, settlement, or buying-power hold may still be blocking funds",

requirements.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
flask
22
gunicorn
33
quant-platform-kit @ git+https://github.com/QuantStrategyLab/QuantPlatformKit.git@2a711adf60b585ca02932bab9ee1bac7ce1df7c6
4-
us-equity-strategies @ git+https://github.com/QuantStrategyLab/UsEquityStrategies.git@085f6010883b3f7c66a1c16f96749c0251410f93
4+
us-equity-strategies @ git+https://github.com/QuantStrategyLab/UsEquityStrategies.git@fdd39ef0313181bee9083319b87b7175c32b364d
55
hk-equity-strategies @ git+https://github.com/QuantStrategyLab/HkEquityStrategies.git@02af62bc7af7b8ffdbe8575421434e455ab00d66
66
pandas
77
requests

tests/test_notifications.py

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,8 @@ def test_heartbeat_renders_tqqq_volatility_delever_risk_control(self):
194194
"dual_drive_volatility_delever_dynamic_floor": 0.24,
195195
"dual_drive_volatility_delever_dynamic_cap": 0.36,
196196
"dual_drive_volatility_delever_redirect_symbol": "QQQ",
197+
"dual_drive_volatility_delever_retained_ratio": 0.25,
198+
"dual_drive_volatility_delever_redirected_ratio": 0.75,
197199
},
198200
skip_logs=(),
199201
note_logs=(),
@@ -218,6 +220,8 @@ def test_heartbeat_renders_tqqq_volatility_delever_risk_control(self):
218220
"dual_drive_volatility_delever_dynamic_floor": 0.24,
219221
"dual_drive_volatility_delever_dynamic_cap": 0.36,
220222
"dual_drive_volatility_delever_redirect_symbol": "QQQ",
223+
"dual_drive_volatility_delever_retained_ratio": 0.25,
224+
"dual_drive_volatility_delever_redirected_ratio": 0.75,
221225
},
222226
skip_logs=(),
223227
note_logs=(),
@@ -228,11 +232,11 @@ def test_heartbeat_renders_tqqq_volatility_delever_risk_control(self):
228232
)
229233

230234
self.assertIn(
231-
"🛡️ 风控: QQQ 5 日年化波动率 31.2% 高于实际阈值 30.0%(动态 p90,252日窗口,范围 24.0%-36.0%,样本 252),TQQQ 转向 QQQ",
235+
"🛡️ 风控: QQQ 5 日年化波动率 31.2% 高于实际阈值 30.0%(动态 p90,252日窗口,范围 24.0%-36.0%,样本 252),TQQQ 转向 QQQ(杠杆仓位:TQQQ 保留 25.0%,QQQ 75.0%)",
232236
zh_rendered.compact_text,
233237
)
234238
self.assertIn(
235-
"🛡️ Risk control: QQQ 5d annualized volatility 31.2% is above effective threshold 30.0% (dynamic p90, 252d lookback, bounded 24.0%-36.0%, samples 252); TQQQ redirects to QQQ",
239+
"🛡️ Risk control: QQQ 5d annualized volatility 31.2% is above effective threshold 30.0% (dynamic p90, 252d lookback, bounded 24.0%-36.0%, samples 252); TQQQ redirects to QQQ (leveraged sleeve: TQQQ retained 25.0%, QQQ 75.0%)",
236240
en_rendered.compact_text,
237241
)
238242

@@ -255,6 +259,8 @@ def test_heartbeat_renders_tqqq_volatility_delever_hysteresis_risk_control(self)
255259
"dual_drive_volatility_delever_dynamic_cap": 0.36,
256260
"dual_drive_volatility_delever_trigger_reason": "hysteresis_hold",
257261
"dual_drive_volatility_delever_redirect_symbol": "QQQM",
262+
"dual_drive_volatility_delever_retained_ratio": 0.0,
263+
"dual_drive_volatility_delever_redirected_ratio": 1.0,
258264
},
259265
skip_logs=(),
260266
note_logs=(),
@@ -281,6 +287,8 @@ def test_heartbeat_renders_tqqq_volatility_delever_hysteresis_risk_control(self)
281287
"dual_drive_volatility_delever_dynamic_cap": 0.36,
282288
"dual_drive_volatility_delever_trigger_reason": "hysteresis_hold",
283289
"dual_drive_volatility_delever_redirect_symbol": "QQQM",
290+
"dual_drive_volatility_delever_retained_ratio": 0.0,
291+
"dual_drive_volatility_delever_redirected_ratio": 1.0,
284292
},
285293
skip_logs=(),
286294
note_logs=(),
@@ -291,11 +299,11 @@ def test_heartbeat_renders_tqqq_volatility_delever_hysteresis_risk_control(self)
291299
)
292300

293301
self.assertIn(
294-
"🛡️ 风控: QQQ 5 日年化波动率 26.2% 仍高于退出阈值 24.0%;入场实际阈值 30.0%(动态 p90,252日窗口,范围 24.0%-36.0%,样本 252),维持 TQQQ 转向 QQQM",
302+
"🛡️ 风控: QQQ 5 日年化波动率 26.2% 仍高于退出阈值 24.0%;入场实际阈值 30.0%(动态 p90,252日窗口,范围 24.0%-36.0%,样本 252),维持 TQQQ 转向 QQQM(杠杆仓位:TQQQ 保留 0.0%,QQQM 100.0%)",
295303
zh_rendered.compact_text,
296304
)
297305
self.assertIn(
298-
"🛡️ Risk control: QQQ 5d annualized volatility 26.2% remains above exit threshold 24.0%; entry effective threshold 30.0% (dynamic p90, 252d lookback, bounded 24.0%-36.0%, samples 252); keep TQQQ redirected to QQQM",
306+
"🛡️ Risk control: QQQ 5d annualized volatility 26.2% remains above exit threshold 24.0%; entry effective threshold 30.0% (dynamic p90, 252d lookback, bounded 24.0%-36.0%, samples 252); keep TQQQ redirected to QQQM (leveraged sleeve: TQQQ retained 0.0%, QQQM 100.0%)",
299307
en_rendered.compact_text,
300308
)
301309

0 commit comments

Comments
 (0)