33from __future__ import annotations
44
55from collections .abc import Mapping
6- import re
76
87from notifications .events import RenderedNotification
98from quant_platform_kit .common .quantity import format_quantity
109from 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
6647def _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-
8663def _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-
13475def _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
447388def _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
473395def _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
488404def _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
608411def _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
654418def _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
679426def _strategy_dashboard_text (signal_metadata , * , translator ) -> str :
0 commit comments