Skip to content

Commit a144315

Browse files
Pigbibiclaude
andcommitted
refactor(notifications): migrate to shared renderer_base (-253 lines)
Replace locally duplicated functions with imports from quant_platform_kit.notifications.renderer_base. Keep thin wrappers for IBKR-specific data access patterns (e.g. execution_annotations flattening, hardcoded cash labels). Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 98a877e commit a144315

1 file changed

Lines changed: 37 additions & 290 deletions

File tree

notifications/renderers.py

Lines changed: 37 additions & 290 deletions
Original file line numberDiff line numberDiff line change
@@ -3,52 +3,35 @@
33
from __future__ import annotations
44

55
from collections.abc import Mapping
6-
import re
76

87
from notifications.events import RenderedNotification
98
from quant_platform_kit.common.quantity import format_quantity
109
from quant_platform_kit.common.notification_localization import (
1110
localize_notification_text as _base_localize_notification_text,
12-
translator_uses_zh as _base_translator_uses_zh,
1311
)
14-
15-
_PRICE_SOURCE_LABELS = {
16-
"longbridge_candlesticks": ("LongBridge 日线K线", "LongBridge daily candlesticks"),
17-
"schwab_daily_history_with_live_quote_overlay": ("Schwab 日线历史", "Schwab daily history"),
18-
"firstrade_ohlc_with_live_quote_overlay": ("Firstrade OHLC", "Firstrade OHLC"),
19-
"market_quote": ("实时行情报价", "market quote"),
20-
"mixed_market_quote_snapshot_close": (
21-
"实时行情报价 + 快照收盘价回补",
22-
"market quote + snapshot close fallback",
23-
),
24-
"mixed_market_quote_historical_close": (
25-
"实时行情报价 + 历史收盘价回补",
26-
"market quote + historical close fallback",
27-
),
28-
"snapshot_close": ("快照收盘价", "snapshot close"),
29-
"historical_close": ("历史收盘价", "historical close"),
30-
"market_data": ("市场数据", "market data"),
31-
}
32-
33-
try:
34-
from quant_platform_kit.common.notification_localization import (
35-
localize_price_source_label as _shared_localize_price_source_label,
36-
)
37-
except ImportError: # pragma: no cover - compatibility with older pinned shared wheels
38-
_shared_localize_price_source_label = None
39-
40-
41-
def _localize_price_source_label(value, *, translator=None, locale=None):
42-
source = str(value or "").strip()
43-
use_zh = _base_translator_uses_zh(translator) if translator is not None else str(locale or "").startswith("zh")
44-
if not source:
45-
return "未知" if use_zh else "unknown"
46-
label = _PRICE_SOURCE_LABELS.get(source)
47-
if label is not None:
48-
return label[0] if use_zh else label[1]
49-
if _shared_localize_price_source_label is not None:
50-
return _shared_localize_price_source_label(source, translator=translator, locale=locale)
51-
return source.replace("_", " ")
12+
from quant_platform_kit.notifications.renderer_base import (
13+
as_float_or_none as _as_float_or_none,
14+
build_timing_audit_lines as _build_timing_audit_lines_shared,
15+
build_tqqq_risk_control_lines as _build_tqqq_risk_control_lines_shared,
16+
compact_dashboard_lines,
17+
effective_volatility_delever_threshold as _effective_volatility_delever_threshold,
18+
format_percent as _format_percent,
19+
format_percentile as _format_percentile,
20+
format_sample_count as _format_sample_count,
21+
format_signal_snapshot_line as _format_signal_snapshot_line_shared,
22+
format_tqqq_volatility_delever_allocation_detail as _format_tqqq_volatility_delever_allocation_detail,
23+
format_volatility_delever_threshold_detail as _format_volatility_delever_threshold_detail,
24+
is_compact_dashboard_audit_line,
25+
is_truthy as _is_truthy,
26+
localize_price_source_label as _localize_price_source_label,
27+
localize_timing_contract as _localize_timing_contract,
28+
present as _present,
29+
relabel_dashboard_cash_labels as _relabel_dashboard_cash_labels_shared,
30+
resolve_execution,
31+
split_detail_segment as _split_detail_segment,
32+
split_labeled_text as _split_labeled_text,
33+
translator_uses_zh as _translator_uses_zh,
34+
)
5235

5336
_EXTRA_ZH_REASON_REPLACEMENTS = (
5437
("pending_orders_detected", "检测到未完成订单"),
@@ -60,8 +43,6 @@ def _localize_price_source_label(value, *, translator=None, locale=None):
6043
("fail_reason=", "失败原因="),
6144
("decision=", "决策="),
6245
)
63-
_DETAIL_FIELD_SPLIT_RE = re.compile(r"\s+(?=[^\s=::]+[=::])")
64-
6546

6647
def _format_text(value, *, fallback: str) -> str:
6748
text = str(value).strip() if value is not None else ""
@@ -79,10 +60,6 @@ def _format_symbol_preview(symbols, *, limit: int = 3) -> str:
7960
return ",".join(shown)
8061

8162

82-
def _translator_uses_zh(translator) -> bool:
83-
return _base_translator_uses_zh(translator)
84-
85-
8663
def _extra_notification_lines(extra_notification_lines) -> list[str]:
8764
return [str(line).strip() for line in extra_notification_lines or () if str(line).strip()]
8865

@@ -95,42 +72,6 @@ def _localize_notification_text(text: str, *, translator) -> str:
9572
)
9673

9774

98-
def _localize_timing_contract(contract: str, *, translator) -> str:
99-
value = str(contract or "").strip()
100-
if not value:
101-
return ""
102-
if value == "same_trading_day":
103-
return "当日执行" if _translator_uses_zh(translator) else "same trading day"
104-
if value == "next_trading_day":
105-
return "次一交易日执行" if _translator_uses_zh(translator) else "next trading day"
106-
match = re.fullmatch(r"next_(\d+)_trading_days", value)
107-
if match:
108-
count = int(match.group(1))
109-
if _translator_uses_zh(translator):
110-
return f"{count}个交易日后执行"
111-
return f"next {count} trading days"
112-
return _localize_notification_text(value, translator=translator)
113-
114-
115-
def _split_detail_segment(text: str) -> list[str]:
116-
value = str(text or "").strip()
117-
if not value:
118-
return []
119-
if "=" not in value and ":" not in value and ":" not in value:
120-
return [value]
121-
return [part.strip() for part in _DETAIL_FIELD_SPLIT_RE.split(value) if part.strip()]
122-
123-
124-
def _split_labeled_text(text: str) -> list[str]:
125-
segments = [segment.strip() for segment in str(text or "").split(" | ") if segment.strip()]
126-
if not segments:
127-
return []
128-
lines = [segments[0]]
129-
for segment in segments[1:]:
130-
lines.extend(_split_detail_segment(segment))
131-
return lines
132-
133-
13475
def _format_prefixed_text(prefix: str, text: str) -> list[str]:
13576
parts = _split_labeled_text(text)
13677
if not parts:
@@ -445,235 +386,41 @@ def _format_dashboard_text(text) -> str:
445386

446387

447388
def _relabel_dashboard_cash_labels(text: str, *, cash_only_execution: bool) -> str:
448-
value = str(text or "")
449-
if cash_only_execution:
450-
value = value.replace("总资产(策略净值)", "总资产(策略标的+现金,不含融资额度)")
451-
value = value.replace(
452-
"Total assets (strategy net liquidation)",
453-
"Total assets (strategy symbols + cash, ex-margin)",
454-
)
455-
value = value.replace("购买力", "可用现金")
456-
value = value.replace("Buying power", "Available cash")
457-
return value
458-
value = value.replace("总资产(策略标的+现金,不含融资额度)", "总资产(策略净值)")
459-
value = value.replace("总资产(策略标的+现金)", "总资产(策略净值)")
460-
value = value.replace(
461-
"Total assets (strategy symbols + cash, ex-margin)",
462-
"Total assets (strategy net liquidation)",
463-
)
464-
value = value.replace(
465-
"Total assets (strategy symbols + cash)",
466-
"Total assets (strategy net liquidation)",
389+
"""Delegates to shared renderer_base; IBKR uses hardcoded labels (no translator)."""
390+
return _relabel_dashboard_cash_labels_shared(
391+
text, cash_only_execution=cash_only_execution, translator=None,
467392
)
468-
value = value.replace("可用现金", "购买力")
469-
value = value.replace("Available cash", "Buying power")
470-
return value
471393

472394

473395
def _format_compact_dashboard_text(text) -> str:
474396
lines = []
475397
for line in _format_dashboard_text(text).splitlines():
476-
stripped = line.strip()
477-
lowered = stripped.lower()
478-
if stripped.startswith(("⏱", "🧾", "🛡️", "📊", "🎯")):
479-
continue
480-
if lowered.startswith(("signal:", "signal:", "market status:")):
481-
continue
482-
if stripped.startswith(("信号:", "信号:", "市场状态:", "市场状态:")):
398+
if not line.strip() or is_compact_dashboard_audit_line(line):
483399
continue
484400
lines.append(line)
485401
return "\n".join(lines)
486402

487403

488404
def _build_timing_audit_lines(signal_metadata, *, translator) -> list[str]:
489-
metadata = signal_metadata if isinstance(signal_metadata, Mapping) else {}
490-
raw_annotations = metadata.get("execution_annotations")
491-
annotations = raw_annotations if isinstance(raw_annotations, Mapping) else {}
492-
signal_date = str(annotations.get("signal_date") or metadata.get("signal_date") or "").strip()
493-
effective_date = str(annotations.get("effective_date") or metadata.get("effective_date") or "").strip()
494-
contract = str(
495-
annotations.get("execution_timing_contract")
496-
or metadata.get("execution_timing_contract")
497-
or ""
498-
).strip()
499-
if not signal_date and not effective_date and not contract:
500-
return []
501-
label = "⏱ 执行时点" if _translator_uses_zh(translator) else "⏱ Timing"
502-
localized_contract = _localize_timing_contract(contract, translator=translator)
503-
if signal_date and effective_date:
504-
value = f"{signal_date} -> {effective_date}"
505-
else:
506-
value = signal_date or effective_date or localized_contract
507-
if localized_contract and localized_contract not in value:
508-
value = f"{value} ({localized_contract})" if value else localized_contract
509-
return [f"{label}: {value}"]
510-
511-
512-
def _format_percent(value) -> str:
513-
try:
514-
return f"{float(value) * 100:.1f}%"
515-
except (TypeError, ValueError):
516-
return "n/a"
517-
518-
519-
def _as_float_or_none(value):
520-
try:
521-
return float(value)
522-
except (TypeError, ValueError):
523-
return None
524-
525-
526-
def _format_percentile(value) -> str:
527-
try:
528-
percentile = float(value) * 100
529-
except (TypeError, ValueError):
530-
return "p?"
531-
if float(percentile).is_integer():
532-
return f"p{int(percentile)}"
533-
return f"p{percentile:.1f}"
534-
535-
536-
def _format_sample_count(value) -> str:
537-
try:
538-
count = float(value)
539-
except (TypeError, ValueError):
540-
return "n/a"
541-
if float(count).is_integer():
542-
return str(int(count))
543-
return f"{count:.1f}"
544-
545-
546-
def _present(value) -> bool:
547-
return value not in (None, "")
548-
549-
550-
def _is_truthy(value) -> bool:
551-
if isinstance(value, bool):
552-
return value
553-
return str(value or "").strip().lower() in {"1", "true", "yes", "y"}
554-
555-
556-
def _effective_volatility_delever_threshold(signal_metadata, *, prefix: str):
557-
mode = str(signal_metadata.get(f"{prefix}_threshold_mode") or "").strip().lower()
558-
dynamic_threshold = signal_metadata.get(f"{prefix}_dynamic_threshold")
559-
if mode == "rolling_percentile" and _present(dynamic_threshold):
560-
return dynamic_threshold
561-
return signal_metadata.get(f"{prefix}_threshold")
562-
563-
564-
def _format_volatility_delever_threshold_detail(signal_metadata, *, prefix: str, translator) -> str:
565-
mode = str(signal_metadata.get(f"{prefix}_threshold_mode") or "").strip().lower()
566-
fixed_threshold = signal_metadata.get(f"{prefix}_threshold")
567-
dynamic_threshold = signal_metadata.get(f"{prefix}_dynamic_threshold")
568-
if mode == "rolling_percentile":
569-
kwargs = {
570-
"percentile": _format_percentile(signal_metadata.get(f"{prefix}_dynamic_percentile")),
571-
"lookback": _format_sample_count(signal_metadata.get(f"{prefix}_dynamic_lookback")),
572-
"min_periods": _format_sample_count(signal_metadata.get(f"{prefix}_dynamic_min_periods")),
573-
"sample_count": _format_sample_count(signal_metadata.get(f"{prefix}_dynamic_sample_count")),
574-
"floor": _format_percent(signal_metadata.get(f"{prefix}_dynamic_floor")),
575-
"cap": _format_percent(signal_metadata.get(f"{prefix}_dynamic_cap")),
576-
"fixed_threshold": _format_percent(fixed_threshold),
577-
}
578-
if _present(dynamic_threshold):
579-
return translator("blend_gate_volatility_threshold_detail_dynamic", **kwargs)
580-
return translator("blend_gate_volatility_threshold_detail_dynamic_fallback", **kwargs)
581-
return translator(
582-
"blend_gate_volatility_threshold_detail_fixed",
583-
threshold=_format_percent(fixed_threshold),
584-
)
585-
586-
587-
def _format_tqqq_volatility_delever_allocation_detail(
588-
signal_metadata,
589-
*,
590-
prefix: str,
591-
redirect_symbol: str,
592-
translator,
593-
) -> str:
594-
retained_ratio = _as_float_or_none(signal_metadata.get(f"{prefix}_retained_ratio"))
595-
redirected_ratio = _as_float_or_none(signal_metadata.get(f"{prefix}_redirected_ratio"))
596-
if retained_ratio is None:
597-
retained_ratio = _as_float_or_none(signal_metadata.get(f"{prefix}_retention_ratio"))
598-
if redirected_ratio is None and retained_ratio is not None:
599-
redirected_ratio = max(0.0, min(1.0, 1.0 - retained_ratio))
600-
return translator(
601-
"tqqq_volatility_delever_allocation_detail",
602-
retained_ratio=_format_percent(retained_ratio),
603-
redirected_ratio=_format_percent(redirected_ratio),
604-
redirect_symbol=redirect_symbol or "QQQ",
405+
execution = resolve_execution(
406+
signal_metadata if isinstance(signal_metadata, Mapping) else {},
605407
)
408+
return _build_timing_audit_lines_shared(execution, translator=translator)
606409

607410

608411
def _build_tqqq_risk_control_lines(signal_metadata, *, translator) -> list[str]:
609-
prefix = "dual_drive_volatility_delever"
610-
if not _is_truthy(signal_metadata.get(f"{prefix}_applied")):
611-
return []
612-
redirect_symbol = str(signal_metadata.get(f"{prefix}_redirect_symbol") or "QQQ").strip().upper()
613-
window = str(signal_metadata.get(f"{prefix}_window") or "5").strip()
614-
threshold = _effective_volatility_delever_threshold(signal_metadata, prefix=prefix)
615-
threshold_detail = _format_volatility_delever_threshold_detail(
616-
signal_metadata,
617-
prefix=prefix,
618-
translator=translator,
619-
)
620-
allocation_detail = _format_tqqq_volatility_delever_allocation_detail(
621-
signal_metadata,
622-
prefix=prefix,
623-
redirect_symbol=redirect_symbol or "QQQ",
412+
return _build_tqqq_risk_control_lines_shared(
413+
signal_metadata if isinstance(signal_metadata, Mapping) else {},
624414
translator=translator,
625415
)
626-
if str(signal_metadata.get(f"{prefix}_trigger_reason") or "").strip() == "hysteresis_hold":
627-
return [
628-
translator(
629-
"risk_control_tqqq_volatility_delever_hysteresis_dynamic",
630-
window=window,
631-
volatility=_format_percent(signal_metadata.get(f"{prefix}_metric")),
632-
exit_threshold=_format_percent(signal_metadata.get(f"{prefix}_exit_threshold")),
633-
threshold=_format_percent(threshold),
634-
threshold_detail=threshold_detail,
635-
source_symbol="TQQQ",
636-
redirect_symbol=redirect_symbol or "QQQ",
637-
allocation_detail=allocation_detail,
638-
)
639-
]
640-
return [
641-
translator(
642-
"risk_control_tqqq_volatility_delever_applied_dynamic",
643-
window=window,
644-
volatility=_format_percent(signal_metadata.get(f"{prefix}_metric")),
645-
threshold=_format_percent(threshold),
646-
threshold_detail=threshold_detail,
647-
source_symbol="TQQQ",
648-
redirect_symbol=redirect_symbol or "QQQ",
649-
allocation_detail=allocation_detail,
650-
)
651-
]
652416

653417

654418
def _format_signal_snapshot_line(snapshot, *, translator) -> str:
655-
if not isinstance(snapshot, Mapping):
656-
return ""
657-
market_date = str(snapshot.get("market_date") or snapshot.get("signal_as_of") or "").strip()
658-
source = str(snapshot.get("latest_price_source") or "").strip()
659-
warning = snapshot.get("data_freshness_warning")
660-
if not market_date and not source and warning in (None, "", False):
661-
return ""
662-
if _translator_uses_zh(translator):
663-
parts = [
664-
f"日期 {market_date or '未知'}",
665-
f"数据源 {_localize_price_source_label(source, translator=translator)}",
666-
]
667-
if warning not in (None, "", False):
668-
parts.append(f"提示 {_localize_notification_text(warning, translator=translator)}")
669-
return "🧾 信号快照: " + " | ".join(parts)
670-
parts = [
671-
f"date {market_date or 'unknown'}",
672-
f"source {_localize_price_source_label(source, translator=translator)}",
673-
]
674-
if warning not in (None, "", False):
675-
parts.append(f"warning {warning}")
676-
return "🧾 Signal snapshot: " + " | ".join(parts)
419+
return _format_signal_snapshot_line_shared(
420+
snapshot,
421+
translator=translator,
422+
localize_text=_localize_notification_text,
423+
)
677424

678425

679426
def _strategy_dashboard_text(signal_metadata, *, translator) -> str:

0 commit comments

Comments
 (0)