Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 66 additions & 0 deletions application/signal_snapshot.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,72 @@
"blend_gate_volatility_delever_dynamic_cap",
"blend_gate_volatility_delever_metric",
"blend_gate_volatility_delever_triggered",
"blend_gate_volatility_delever_retention_ratio",
"blend_gate_volatility_delever_redirect_symbol",
"blend_gate_volatility_delever_removed_ratio",
"dual_drive_volatility_delever_enabled",
"dual_drive_volatility_delever_window",
"dual_drive_volatility_delever_threshold_mode",
"dual_drive_volatility_delever_threshold",
"dual_drive_volatility_delever_exit_threshold",
"dual_drive_volatility_delever_dynamic_threshold",
"dual_drive_volatility_delever_dynamic_sample_count",
"dual_drive_volatility_delever_dynamic_lookback",
"dual_drive_volatility_delever_dynamic_percentile",
"dual_drive_volatility_delever_dynamic_min_periods",
"dual_drive_volatility_delever_dynamic_floor",
"dual_drive_volatility_delever_dynamic_cap",
"dual_drive_volatility_delever_metric",
"dual_drive_volatility_delever_triggered",
"dual_drive_volatility_delever_entry_triggered",
"dual_drive_volatility_delever_hysteresis_triggered",
"dual_drive_volatility_delever_trigger_reason",
"dual_drive_volatility_delever_applied",
"dual_drive_volatility_delever_vetoed",
"dual_drive_volatility_delever_veto_reason",
"dual_drive_volatility_delever_taco_veto_enabled",
"dual_drive_volatility_delever_taco_rebound_context_active",
"dual_drive_volatility_delever_true_crisis_active",
"dual_drive_volatility_delever_redirect_symbol",
"dual_drive_volatility_delever_removed_value",
"dual_drive_macro_risk_governor_enabled",
"dual_drive_macro_risk_governor_found",
"dual_drive_macro_risk_governor_route",
"dual_drive_macro_risk_governor_active",
"dual_drive_macro_risk_governor_applied",
"dual_drive_macro_risk_governor_leverage_scalar",
"dual_drive_macro_risk_governor_risk_asset_scalar",
"dual_drive_macro_risk_governor_removed_value",
"dual_drive_macro_risk_governor_redirected_to_unlevered",
"dual_drive_crisis_defense_enabled",
"dual_drive_crisis_defense_triggered",
"dual_drive_crisis_defense_applied",
"dual_drive_crisis_defense_destination",
"dual_drive_crisis_defense_removed_value",
"market_regime_control_enabled",
"market_regime_control_found",
"market_regime_control_source",
"market_regime_control_schema_version",
"market_regime_control_route",
"market_regime_control_route_source",
"market_regime_control_active",
"market_regime_control_applied",
"market_regime_control_route_allowed",
"market_regime_control_risk_scalar",
"market_regime_control_risk_budget_scalar",
"market_regime_control_leverage_scalar",
"market_regime_control_risk_asset_scalar",
"market_regime_control_taco_allowed",
"market_regime_control_local_delever_veto_allowed",
"market_regime_control_crisis_defense_required",
"market_regime_control_blocked_actions",
"market_regime_control_vetoes",
"market_regime_control_reason_codes",
"market_regime_control_removed_weight",
"market_regime_control_removed_ratio",
"market_regime_control_redirected_to_unlevered_ratio",
"market_regime_control_safe_haven",
"market_regime_control_risk_symbols",
)


Expand Down
66 changes: 66 additions & 0 deletions decision_mapper.py
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,72 @@ def map_strategy_decision_to_plan(
"blend_gate_volatility_delever_dynamic_cap",
"blend_gate_volatility_delever_metric",
"blend_gate_volatility_delever_triggered",
"blend_gate_volatility_delever_retention_ratio",
"blend_gate_volatility_delever_redirect_symbol",
"blend_gate_volatility_delever_removed_ratio",
"dual_drive_volatility_delever_enabled",
"dual_drive_volatility_delever_window",
"dual_drive_volatility_delever_threshold_mode",
"dual_drive_volatility_delever_threshold",
Comment on lines +257 to +260

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve new fields from execution annotations

When a strategy supplies these new risk-control audit values under diagnostics["execution_annotations"] (the same normalized path this mapper already supports for signal_display, thresholds, benchmark values, etc.), they are dropped because the new pass-through list is only copied from top-level diagnostics in the loop below. In that scenario the TQQQ notification and signal snapshot never see dual_drive_volatility_delever_applied or its thresholds, so the risk-control line added in this commit is silently omitted; check execution_annotations as a fallback for these fields or include them in the execution-field mapping.

Useful? React with 👍 / 👎.

"dual_drive_volatility_delever_exit_threshold",
"dual_drive_volatility_delever_dynamic_threshold",
"dual_drive_volatility_delever_dynamic_sample_count",
"dual_drive_volatility_delever_dynamic_lookback",
"dual_drive_volatility_delever_dynamic_percentile",
"dual_drive_volatility_delever_dynamic_min_periods",
"dual_drive_volatility_delever_dynamic_floor",
"dual_drive_volatility_delever_dynamic_cap",
"dual_drive_volatility_delever_metric",
"dual_drive_volatility_delever_triggered",
"dual_drive_volatility_delever_entry_triggered",
"dual_drive_volatility_delever_hysteresis_triggered",
"dual_drive_volatility_delever_trigger_reason",
"dual_drive_volatility_delever_applied",
"dual_drive_volatility_delever_vetoed",
"dual_drive_volatility_delever_veto_reason",
"dual_drive_volatility_delever_taco_veto_enabled",
"dual_drive_volatility_delever_taco_rebound_context_active",
"dual_drive_volatility_delever_true_crisis_active",
"dual_drive_volatility_delever_redirect_symbol",
"dual_drive_volatility_delever_removed_value",
"dual_drive_macro_risk_governor_enabled",
"dual_drive_macro_risk_governor_found",
"dual_drive_macro_risk_governor_route",
"dual_drive_macro_risk_governor_active",
"dual_drive_macro_risk_governor_applied",
"dual_drive_macro_risk_governor_leverage_scalar",
"dual_drive_macro_risk_governor_risk_asset_scalar",
"dual_drive_macro_risk_governor_removed_value",
"dual_drive_macro_risk_governor_redirected_to_unlevered",
"dual_drive_crisis_defense_enabled",
"dual_drive_crisis_defense_triggered",
"dual_drive_crisis_defense_applied",
"dual_drive_crisis_defense_destination",
"dual_drive_crisis_defense_removed_value",
"market_regime_control_enabled",
"market_regime_control_found",
"market_regime_control_source",
"market_regime_control_schema_version",
"market_regime_control_route",
"market_regime_control_route_source",
"market_regime_control_active",
"market_regime_control_applied",
"market_regime_control_route_allowed",
"market_regime_control_risk_scalar",
"market_regime_control_risk_budget_scalar",
"market_regime_control_leverage_scalar",
"market_regime_control_risk_asset_scalar",
"market_regime_control_taco_allowed",
"market_regime_control_local_delever_veto_allowed",
"market_regime_control_crisis_defense_required",
"market_regime_control_blocked_actions",
"market_regime_control_vetoes",
"market_regime_control_reason_codes",
"market_regime_control_removed_weight",
"market_regime_control_removed_ratio",
"market_regime_control_redirected_to_unlevered_ratio",
"market_regime_control_safe_haven",
"market_regime_control_risk_symbols",
):
if field_name in diagnostics:
execution[field_name] = diagnostics[field_name]
Expand Down
122 changes: 122 additions & 0 deletions notifications/renderers.py
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,112 @@ def _build_timing_audit_lines(execution, *, translator) -> list[str]:
return [f"{label}: {value}"]


def _format_percent(value) -> str:
try:
return f"{float(value) * 100:.1f}%"
except (TypeError, ValueError):
return "n/a"


def _format_percentile(value) -> str:
try:
percentile = float(value) * 100
except (TypeError, ValueError):
return "p?"
if float(percentile).is_integer():
return f"p{int(percentile)}"
return f"p{percentile:.1f}"


def _format_sample_count(value) -> str:
try:
count = float(value)
except (TypeError, ValueError):
return "n/a"
if float(count).is_integer():
return str(int(count))
return f"{count:.1f}"


def _present(value) -> bool:
return value not in (None, "")


def _is_truthy(value) -> bool:
if isinstance(value, bool):
return value
return str(value or "").strip().lower() in {"1", "true", "yes", "y"}


def _effective_volatility_delever_threshold(execution, *, prefix: str):
mode = str(execution.get(f"{prefix}_threshold_mode") or "").strip().lower()
dynamic_threshold = execution.get(f"{prefix}_dynamic_threshold")
if mode == "rolling_percentile" and _present(dynamic_threshold):
return dynamic_threshold
return execution.get(f"{prefix}_threshold")


def _format_volatility_delever_threshold_detail(execution, *, prefix: str, translator) -> str:
mode = str(execution.get(f"{prefix}_threshold_mode") or "").strip().lower()
fixed_threshold = execution.get(f"{prefix}_threshold")
dynamic_threshold = execution.get(f"{prefix}_dynamic_threshold")
if mode == "rolling_percentile":
kwargs = {
"percentile": _format_percentile(execution.get(f"{prefix}_dynamic_percentile")),
"lookback": _format_sample_count(execution.get(f"{prefix}_dynamic_lookback")),
"min_periods": _format_sample_count(execution.get(f"{prefix}_dynamic_min_periods")),
"sample_count": _format_sample_count(execution.get(f"{prefix}_dynamic_sample_count")),
"floor": _format_percent(execution.get(f"{prefix}_dynamic_floor")),
"cap": _format_percent(execution.get(f"{prefix}_dynamic_cap")),
"fixed_threshold": _format_percent(fixed_threshold),
}
if _present(dynamic_threshold):
return translator("blend_gate_volatility_threshold_detail_dynamic", **kwargs)
return translator("blend_gate_volatility_threshold_detail_dynamic_fallback", **kwargs)
return translator(
"blend_gate_volatility_threshold_detail_fixed",
threshold=_format_percent(fixed_threshold),
)


def _build_tqqq_risk_control_lines(execution, *, translator) -> list[str]:
prefix = "dual_drive_volatility_delever"
if not _is_truthy(execution.get(f"{prefix}_applied")):
return []
redirect_symbol = str(execution.get(f"{prefix}_redirect_symbol") or "QQQ").strip().upper()
window = str(execution.get(f"{prefix}_window") or "5").strip()
threshold = _effective_volatility_delever_threshold(execution, prefix=prefix)
threshold_detail = _format_volatility_delever_threshold_detail(
execution,
prefix=prefix,
translator=translator,
)
if str(execution.get(f"{prefix}_trigger_reason") or "").strip() == "hysteresis_hold":
return [
translator(
"risk_control_tqqq_volatility_delever_hysteresis_dynamic",
window=window,
volatility=_format_percent(execution.get(f"{prefix}_metric")),
exit_threshold=_format_percent(execution.get(f"{prefix}_exit_threshold")),
threshold=_format_percent(threshold),
threshold_detail=threshold_detail,
source_symbol="TQQQ",
redirect_symbol=redirect_symbol or "QQQ",
)
]
return [
translator(
"risk_control_tqqq_volatility_delever_applied_dynamic",
window=window,
volatility=_format_percent(execution.get(f"{prefix}_metric")),
threshold=_format_percent(threshold),
threshold_detail=threshold_detail,
source_symbol="TQQQ",
redirect_symbol=redirect_symbol or "QQQ",
)
]


def _format_signal_snapshot_line(snapshot, *, translator) -> str:
if not isinstance(snapshot, Mapping):
return ""
Expand Down Expand Up @@ -261,6 +367,7 @@ def _build_compact_trade_message(
signal_display,
timing_lines,
signal_snapshot_line,
risk_control_lines,
trade_logs,
) -> str:
lines = [
Expand All @@ -284,6 +391,7 @@ def _build_compact_trade_message(
status_summary = _first_detail_line(status_display)
if status_summary:
lines.append(_format_market_status_line(status_summary, translator=translator))
lines.extend(risk_control_lines)
signal_summary = _first_detail_line(signal_display)
if signal_summary:
lines.append(f"🎯 {translator('signal_label')}: {signal_summary}")
Expand All @@ -307,6 +415,7 @@ def _build_compact_heartbeat_message(
signal_display,
timing_lines,
signal_snapshot_line,
risk_control_lines,
) -> str:
lines = [
translator("heartbeat_header"),
Expand All @@ -330,6 +439,7 @@ def _build_compact_heartbeat_message(
status_summary = _first_detail_line(status_display)
if status_summary:
lines.append(_format_market_status_line(status_summary, translator=translator))
lines.extend(risk_control_lines)
signal_summary = _first_detail_line(signal_display)
if signal_summary:
lines.append(f"🎯 {translator('signal_label')}: {signal_summary}")
Expand Down Expand Up @@ -357,12 +467,16 @@ def render_trade_notification(
execution.get("signal_snapshot"),
translator=translator,
)
risk_control_lines = _build_tqqq_risk_control_lines(execution, translator=translator)
separator = str(execution["separator"])
status_line = (
"\n".join(_split_labeled_text(_format_market_status_line(status_display, translator=translator))) + "\n"
if status_display
else ""
)
risk_control_block = "\n".join(risk_control_lines)
if risk_control_block:
risk_control_block += "\n"
dashboard_block = f"{dashboard_text}\n{separator}\n" if dashboard_text else ""
trade_signal_lines = _format_label_value_lines(f"🎯 {translator('signal_label')}", signal_display)
trade_signal_block = "\n".join(trade_signal_lines)
Expand All @@ -376,6 +490,7 @@ def render_trade_notification(
f"{chr(10).join(timing_lines) + chr(10) if timing_lines else ''}"
f"{signal_snapshot_line + chr(10) if signal_snapshot_line else ''}"
f"{status_line}"
f"{risk_control_block}"
f"{trade_signal_block}\n\n"
f"{dashboard_block}"
+ "\n".join(trade_logs)
Expand All @@ -392,6 +507,7 @@ def render_trade_notification(
signal_display=signal_display,
timing_lines=timing_lines,
signal_snapshot_line=signal_snapshot_line,
risk_control_lines=risk_control_lines,
trade_logs=trade_logs,
)
return RenderedNotification(detailed_text=detailed_text, compact_text=compact_text)
Expand All @@ -416,6 +532,7 @@ def render_heartbeat_notification(
execution.get("signal_snapshot"),
translator=translator,
)
risk_control_lines = _build_tqqq_risk_control_lines(execution, translator=translator)
separator = str(execution["separator"])
total_equity = float(portfolio["total_equity"])
portfolio_rows = tuple(portfolio["portfolio_rows"])
Expand All @@ -425,6 +542,9 @@ def render_heartbeat_notification(
if status_display
else ""
)
risk_control_block = "\n".join(risk_control_lines)
if risk_control_block:
risk_control_block += "\n"
dashboard_block = f"{dashboard_text}\n{separator}\n" if dashboard_text else ""
benchmark_lines = _format_benchmark_lines(execution, translator=translator)
benchmark_block = "\n".join(benchmark_lines) + "\n" if benchmark_lines else ""
Expand All @@ -449,6 +569,7 @@ def render_heartbeat_notification(
f"{chr(10).join(timing_lines) + chr(10) if timing_lines else ''}"
f"{signal_snapshot_line + chr(10) if signal_snapshot_line else ''}"
f"{status_line}"
f"{risk_control_block}"
f"{heartbeat_signal_block}\n"
f"{benchmark_block}"
f"{separator}\n"
Expand All @@ -467,5 +588,6 @@ def render_heartbeat_notification(
signal_display=signal_display,
timing_lines=timing_lines,
signal_snapshot_line=signal_snapshot_line,
risk_control_lines=risk_control_lines,
)
return RenderedNotification(detailed_text=detailed_text, compact_text=compact_text)
8 changes: 8 additions & 0 deletions notifications/telegram.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,10 @@
"signal_blend_gate_defensive": "{trend_symbol} 跌破门槛线,防守持有 SOXX {soxx_ratio}",
"market_status_blend_gate_overlay_capped": "🧯 风控降档({asset})",
"signal_blend_gate_overlay_capped": "{trend_symbol} 仍在 {window} 日门槛线上方,但触发风控降档({reasons}),目标仓位 {allocation_text}",
"risk_control_tqqq_volatility_delever_applied": "🛡️ 风控: QQQ {window} 日年化波动率 {volatility} 高于 {threshold},{source_symbol} 转向 {redirect_symbol}",
"risk_control_tqqq_volatility_delever_applied_dynamic": "🛡️ 风控: QQQ {window} 日年化波动率 {volatility} 高于实际阈值 {threshold}({threshold_detail}),{source_symbol} 转向 {redirect_symbol}",
"risk_control_tqqq_volatility_delever_hysteresis": "🛡️ 风控: QQQ {window} 日年化波动率 {volatility} 仍高于退出阈值 {exit_threshold},维持 {source_symbol} 转向 {redirect_symbol}",
"risk_control_tqqq_volatility_delever_hysteresis_dynamic": "🛡️ 风控: QQQ {window} 日年化波动率 {volatility} 仍高于退出阈值 {exit_threshold};入场实际阈值 {threshold}({threshold_detail}),维持 {source_symbol} 转向 {redirect_symbol}",
"blend_gate_reason_rsi_cap": "RSI 超阈值",
"blend_gate_reason_bollinger_cap": "突破布林上轨",
"blend_gate_reason_volatility_delever": "{symbol} {window} 日年化波动率 {volatility} 高于 {threshold},SOXL 转向 {redirect_symbol}",
Expand Down Expand Up @@ -164,6 +168,10 @@
"signal_blend_gate_defensive": "{trend_symbol} below gated entry, hold defensive SOXX {soxx_ratio}",
"market_status_blend_gate_overlay_capped": "🧯 RISK-CAP ({asset})",
"signal_blend_gate_overlay_capped": "{trend_symbol} stays above the {window}d gate, but risk cap ({reasons}) cuts exposure to {allocation_text}",
"risk_control_tqqq_volatility_delever_applied": "🛡️ Risk control: QQQ {window}d annualized volatility {volatility} is above {threshold}; {source_symbol} redirects to {redirect_symbol}",
"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}",
"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}",
"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}",
"blend_gate_reason_rsi_cap": "RSI over threshold",
"blend_gate_reason_bollinger_cap": "price above upper band",
"blend_gate_reason_volatility_delever": "{symbol} {window}d annualized volatility {volatility} is above {threshold}; redirect SOXL to {redirect_symbol}",
Expand Down
4 changes: 2 additions & 2 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
flask
gunicorn
quant-platform-kit @ git+https://github.com/QuantStrategyLab/QuantPlatformKit.git@3b6a0a9bedde72773e188041e0dc48516b38aadc
us-equity-strategies @ git+https://github.com/QuantStrategyLab/UsEquityStrategies.git@8278048366f1cd83e29e0c921e4048e7e25ae227
quant-platform-kit @ git+https://github.com/QuantStrategyLab/QuantPlatformKit.git@023641c88506c732624a7329e48b51b9dbbe3c2a
us-equity-strategies @ git+https://github.com/QuantStrategyLab/UsEquityStrategies.git@7d35772d1125b534d0bcca557cb6dbaf28914719
pandas
requests
pandas_market_calendars
Expand Down
Loading