mirror of
https://github.com/ZhuLinsen/daily_stock_analysis
synced 2026-09-20 02:43:35 +08:00
fix: address P6 signal linkage gaps (#1724)
This commit is contained in:
@@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/).
|
||||
## [Unreleased]
|
||||
|
||||
- [新功能] #1390 P6 将 DecisionSignal 复用到告警、通知和组合风险:告警触发关联 latest active 信号或创建最小 alert 信号,通知追加低敏信号摘要,持仓风险聚合 active sell/reduce/alert 信号并保持 fail-open。
|
||||
- [修复] #1722 修复 #1390 P6 DecisionSignal 在组合风险快照语义和默认聚合通知展示中的遗漏。
|
||||
- [新功能] #1707 资讯源新增 `newsnow` 类型、`NEWSNOW_BASE_URL` 配置和 `/api/v1/intelligence/sources/defaults` 默认源初始化接口,内置财联社热门、雪球热门股票、华尔街见闻快讯、金十数据和格隆汇事件等财经热点源,可直接拉取落库并进入既有分析证据链路;官方 NewsNow 部署指南见 https://github.com/qqhann/newsnow,生产环境建议自建实例而非使用公开示例。
|
||||
|
||||
- [修复] AlphaSift 热点题材刷新在 EastMoney 瞬断且无缓存时返回友好空态,并让桌面更新保留 AlphaSift 热点缓存。
|
||||
|
||||
@@ -850,6 +850,9 @@ class NotificationService(
|
||||
f"{labels['score_label']} {r.sentiment_score} | "
|
||||
f"{localize_trend_prediction(r.trend_prediction, report_language)}"
|
||||
)
|
||||
signal_excerpt = self._decision_signal_excerpt(r, report_language)
|
||||
if signal_excerpt:
|
||||
report_lines.append(signal_excerpt)
|
||||
else:
|
||||
report_lines.extend([f"## 📈 {labels['report_title']}", ""])
|
||||
# 逐个股票的详细分析
|
||||
@@ -866,6 +869,9 @@ class NotificationService(
|
||||
f"**Confidence:{confidence_stars}**",
|
||||
"",
|
||||
])
|
||||
signal_excerpt = self._decision_signal_excerpt(result, report_language)
|
||||
if signal_excerpt:
|
||||
report_lines.extend([signal_excerpt, ""])
|
||||
self._append_market_snapshot(report_lines, result)
|
||||
|
||||
# 核心看点
|
||||
@@ -1099,6 +1105,9 @@ class NotificationService(
|
||||
f"{labels['score_label']} {r.sentiment_score} | "
|
||||
f"{localize_trend_prediction(r.trend_prediction, report_language)}"
|
||||
)
|
||||
signal_excerpt = self._decision_signal_excerpt(r, report_language)
|
||||
if signal_excerpt:
|
||||
report_lines.append(signal_excerpt)
|
||||
report_lines.extend([
|
||||
"",
|
||||
"---",
|
||||
@@ -1401,6 +1410,9 @@ class NotificationService(
|
||||
f"{labels['score_label']} {r.sentiment_score} | "
|
||||
f"{localize_trend_prediction(r.trend_prediction, report_language)}"
|
||||
)
|
||||
signal_excerpt = self._decision_signal_excerpt(r, report_language)
|
||||
if signal_excerpt:
|
||||
lines.append(signal_excerpt)
|
||||
else:
|
||||
for result in sorted_results:
|
||||
signal_text, signal_emoji, _ = self._get_signal_level(result)
|
||||
@@ -1421,6 +1433,10 @@ class NotificationService(
|
||||
if one_sentence:
|
||||
lines.append(f"📌 **{one_sentence[:80]}**")
|
||||
lines.append("")
|
||||
signal_excerpt = self._decision_signal_excerpt(result, report_language)
|
||||
if signal_excerpt:
|
||||
lines.append(signal_excerpt)
|
||||
lines.append("")
|
||||
|
||||
# 重要信息区(舆情+基本面)
|
||||
info_lines = []
|
||||
@@ -1551,6 +1567,9 @@ class NotificationService(
|
||||
f"{labels['score_label']}:{result.sentiment_score} | "
|
||||
f"{localize_trend_prediction(result.trend_prediction, report_language)}"
|
||||
)
|
||||
signal_excerpt = self._decision_signal_excerpt(result, report_language)
|
||||
if signal_excerpt:
|
||||
lines.append(signal_excerpt)
|
||||
|
||||
# 操作理由(截断)
|
||||
if hasattr(result, 'buy_reason') and result.buy_reason:
|
||||
|
||||
@@ -115,6 +115,7 @@ class DecisionSignalService:
|
||||
expires_to: Optional[Any] = None,
|
||||
holding_only: bool = False,
|
||||
account_id: Optional[int] = None,
|
||||
stock_identities: Optional[List[Tuple[str, str]]] = None,
|
||||
page: int = 1,
|
||||
page_size: int = 20,
|
||||
) -> Dict[str, Any]:
|
||||
@@ -133,9 +134,27 @@ class DecisionSignalService:
|
||||
expires_from_dt = self._parse_datetime(expires_from)
|
||||
expires_to_dt = self._parse_datetime(expires_to)
|
||||
stock_codes = self._stock_filter_codes(stock_code, market=market_norm)
|
||||
stock_identities = None
|
||||
stock_identity_filters: Optional[List[Tuple[str, str]]] = None
|
||||
|
||||
if holding_only:
|
||||
if stock_identities is not None:
|
||||
# Explicit identities come from a caller-owned snapshot; skip cached holdings entirely.
|
||||
requested_codes = set(stock_codes or [])
|
||||
normalized_identities: set[Tuple[str, str]] = set()
|
||||
for identity_market, identity_code in stock_identities:
|
||||
if not str(identity_code or "").strip():
|
||||
continue
|
||||
identity_market_norm = self._normalize_market(identity_market)
|
||||
if market_norm and identity_market_norm != market_norm:
|
||||
continue
|
||||
identity_code_norm = self._normalize_stock_code(identity_code, market=identity_market_norm)
|
||||
if requested_codes and identity_code_norm not in requested_codes:
|
||||
continue
|
||||
normalized_identities.add((identity_market_norm, identity_code_norm))
|
||||
stock_identity_filters = sorted(normalized_identities)
|
||||
stock_codes = None
|
||||
if not stock_identity_filters:
|
||||
return {"items": [], "total": 0, "page": safe_page, "page_size": safe_page_size}
|
||||
elif holding_only:
|
||||
held_identities = self._cached_holding_identities(account_id=account_id)
|
||||
if market_norm:
|
||||
held_identities = {
|
||||
@@ -146,14 +165,14 @@ class DecisionSignalService:
|
||||
held_identities = {
|
||||
identity for identity in held_identities if identity[1] in requested_codes
|
||||
}
|
||||
stock_identities = sorted(held_identities)
|
||||
stock_identity_filters = sorted(held_identities)
|
||||
stock_codes = None
|
||||
if not stock_identities:
|
||||
if not stock_identity_filters:
|
||||
return {"items": [], "total": 0, "page": safe_page, "page_size": safe_page_size}
|
||||
|
||||
rows, total = self.repo.list(
|
||||
stock_codes=stock_codes,
|
||||
stock_identities=stock_identities,
|
||||
stock_identities=stock_identity_filters,
|
||||
market=market_norm,
|
||||
action=action_norm,
|
||||
market_phase=market_phase_norm,
|
||||
@@ -183,12 +202,13 @@ class DecisionSignalService:
|
||||
created_to=created_to_dt,
|
||||
expires_from=expires_from_dt,
|
||||
expires_to=expires_to_dt,
|
||||
stock_identities=stock_identity_filters,
|
||||
holding_only=holding_only,
|
||||
):
|
||||
self._backfill_analysis_signal_from_history(source_report_id_norm)
|
||||
rows, total = self.repo.list(
|
||||
stock_codes=stock_codes,
|
||||
stock_identities=stock_identities,
|
||||
stock_identities=stock_identity_filters,
|
||||
market=market_norm,
|
||||
action=action_norm,
|
||||
market_phase=market_phase_norm,
|
||||
@@ -276,6 +296,7 @@ class DecisionSignalService:
|
||||
created_to: Optional[datetime],
|
||||
expires_from: Optional[datetime],
|
||||
expires_to: Optional[datetime],
|
||||
stock_identities: Optional[List[Tuple[str, str]]],
|
||||
holding_only: bool,
|
||||
) -> bool:
|
||||
"""Only lazy-backfill for the exact report section query used by Web."""
|
||||
@@ -296,6 +317,7 @@ class DecisionSignalService:
|
||||
created_to,
|
||||
expires_from,
|
||||
expires_to,
|
||||
stock_identities,
|
||||
holding_only,
|
||||
)
|
||||
)
|
||||
|
||||
@@ -82,7 +82,7 @@ class PortfolioRiskService:
|
||||
lookback_days=thresholds["lookback_days"],
|
||||
)
|
||||
stop_loss = self._build_stop_loss(snapshot, thresholds)
|
||||
decision_signal_risk = self._build_decision_signal_risk(snapshot, account_id=account_id)
|
||||
decision_signal_risk = self._build_decision_signal_risk(snapshot)
|
||||
|
||||
return {
|
||||
"as_of": as_of_date.isoformat(),
|
||||
@@ -100,21 +100,22 @@ class PortfolioRiskService:
|
||||
def _build_decision_signal_risk(
|
||||
self,
|
||||
snapshot: Dict[str, Any],
|
||||
*,
|
||||
account_id: Optional[int],
|
||||
) -> Dict[str, Any]:
|
||||
try:
|
||||
held_positions = self._held_position_identities(snapshot)
|
||||
if not held_positions:
|
||||
return self._empty_decision_signal_risk(available=True)
|
||||
stock_identities = sorted({
|
||||
(position["market"], position["signal_stock_code"])
|
||||
for position in held_positions
|
||||
})
|
||||
|
||||
defensive_actions = set(DEFENSIVE_DECISION_SIGNAL_ACTIONS)
|
||||
latest_by_identity: Dict[Tuple[str, str], Dict[str, Any]] = {}
|
||||
page = 1
|
||||
while True:
|
||||
response = self.decision_signal_service.list_signals(
|
||||
holding_only=True,
|
||||
account_id=account_id,
|
||||
stock_identities=stock_identities,
|
||||
status="active",
|
||||
page=page,
|
||||
page_size=100,
|
||||
|
||||
@@ -10,6 +10,10 @@
|
||||
|
||||
{% for e in enriched %}
|
||||
{{ e.signal_emoji }} **{{ e.stock_name }}({{ e.result.code }})**: {{ e.localized_operation_advice }} | {{ labels.score_label }} {{ e.result.sentiment_score }} | {{ e.localized_trend_prediction }}
|
||||
{% set signal_excerpt = decision_signal_excerpt(e.result) %}
|
||||
{% if signal_excerpt %}
|
||||
{{ signal_excerpt }}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
|
||||
---
|
||||
|
||||
@@ -9,6 +9,10 @@
|
||||
**📊 {{ labels.summary_heading }}**
|
||||
{% for e in enriched %}
|
||||
{{ e.signal_emoji }} **{{ e.stock_name }}({{ e.result.code }})**: {{ e.localized_operation_advice }} | {{ labels.score_label }} {{ e.result.sentiment_score }} | {{ e.localized_trend_prediction }}
|
||||
{% set signal_excerpt = decision_signal_excerpt(e.result) %}
|
||||
{% if signal_excerpt %}
|
||||
{{ signal_excerpt }}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
{% for e in enriched %}
|
||||
@@ -24,6 +28,10 @@
|
||||
{% if one_sentence %}
|
||||
📌 **{{ one_sentence[:80] }}**
|
||||
{% endif %}
|
||||
{% set signal_excerpt = decision_signal_excerpt(result) %}
|
||||
{% if signal_excerpt %}
|
||||
{{ signal_excerpt }}
|
||||
{% endif %}
|
||||
|
||||
{% if intel.get('earnings_outlook') %}
|
||||
📊 {{ labels.earnings_outlook_label }}: {{ intel.earnings_outlook[:60] }}
|
||||
|
||||
@@ -494,6 +494,84 @@ def test_list_signals_does_not_backfill_ambiguous_history_default_decision_type_
|
||||
assert session.query(DecisionSignalRecord).count() == 0
|
||||
|
||||
|
||||
def test_list_signals_explicit_stock_identities_override_holding_only_and_intersect_filters(isolated_db) -> None:
|
||||
service = DecisionSignalService(db_manager=isolated_db)
|
||||
service.create_signal(
|
||||
_payload(
|
||||
source_report_id=171501,
|
||||
trace_id="trace-explicit-identity-000001",
|
||||
stock_code="000001",
|
||||
stock_name="平安银行",
|
||||
action="sell",
|
||||
)
|
||||
)
|
||||
service.create_signal(
|
||||
_payload(
|
||||
source_report_id=171502,
|
||||
trace_id="trace-explicit-identity-600519",
|
||||
stock_code="600519",
|
||||
action="reduce",
|
||||
)
|
||||
)
|
||||
|
||||
listed = service.list_signals(
|
||||
stock_identities=[("cn", "000001")],
|
||||
holding_only=True,
|
||||
status="active",
|
||||
)
|
||||
|
||||
assert listed["total"] == 1
|
||||
assert listed["items"][0]["stock_code"] == "000001"
|
||||
assert listed["items"][0]["action"] == "sell"
|
||||
|
||||
mismatched_stock_filter = service.list_signals(
|
||||
stock_code="600519",
|
||||
market="cn",
|
||||
stock_identities=[("cn", "000001")],
|
||||
status="active",
|
||||
)
|
||||
|
||||
assert mismatched_stock_filter == {"items": [], "total": 0, "page": 1, "page_size": 20}
|
||||
|
||||
|
||||
def test_list_signals_explicit_empty_stock_identities_returns_empty_without_widening(isolated_db) -> None:
|
||||
service = DecisionSignalService(db_manager=isolated_db)
|
||||
service.create_signal(
|
||||
_payload(
|
||||
source_report_id=171503,
|
||||
trace_id="trace-empty-identity-600519",
|
||||
stock_code="600519",
|
||||
action="sell",
|
||||
)
|
||||
)
|
||||
|
||||
listed = service.list_signals(stock_identities=[], status="active")
|
||||
|
||||
assert listed == {"items": [], "total": 0, "page": 1, "page_size": 20}
|
||||
|
||||
|
||||
def test_list_signals_explicit_stock_identities_do_not_trigger_history_backfill(isolated_db) -> None:
|
||||
record_id = isolated_db.save_analysis_history(
|
||||
result=_history_result(operation_advice="卖出", decision_type="sell", action="sell", action_label="卖出"),
|
||||
query_id="query-explicit-identity-no-backfill",
|
||||
report_type="simple",
|
||||
news_content="新闻摘要",
|
||||
context_snapshot={"market_phase_summary": {"phase": "postmarket"}},
|
||||
save_snapshot=True,
|
||||
)
|
||||
service = DecisionSignalService(db_manager=isolated_db)
|
||||
|
||||
listed = service.list_signals(
|
||||
source_type="analysis",
|
||||
source_report_id=record_id,
|
||||
stock_identities=[("cn", "600519")],
|
||||
)
|
||||
|
||||
assert listed == {"items": [], "total": 0, "page": 1, "page_size": 20}
|
||||
with isolated_db.get_session() as session:
|
||||
assert session.query(DecisionSignalRecord).count() == 0
|
||||
|
||||
|
||||
def test_service_plan_quality_slots_and_explicit_override(isolated_db) -> None:
|
||||
service = DecisionSignalService(db_manager=isolated_db)
|
||||
|
||||
|
||||
@@ -50,6 +50,16 @@ def _make_response(status_code: int, json: Optional[dict] = None) -> requests.Re
|
||||
return response
|
||||
|
||||
|
||||
def _attach_decision_signal_summary(result: AnalysisResult) -> AnalysisResult:
|
||||
result.decision_signal_summary = {
|
||||
"action": "sell",
|
||||
"action_label": "卖出",
|
||||
"horizon": "1d",
|
||||
"reason": "技术面走弱",
|
||||
}
|
||||
return result
|
||||
|
||||
|
||||
def _make_feishu_message() -> BotMessage:
|
||||
return BotMessage(
|
||||
platform="feishu",
|
||||
@@ -629,6 +639,118 @@ class TestNotificationServiceReportGeneration(unittest.TestCase):
|
||||
|
||||
self.assertIn("*分析模型:gemini/gemini-2.5-flash*", out)
|
||||
|
||||
@mock.patch("src.notification.get_config")
|
||||
def test_generate_dashboard_report_appends_decision_signal_excerpt_fallback(
|
||||
self, mock_get_config: mock.MagicMock
|
||||
):
|
||||
mock_get_config.return_value = _make_config(report_renderer_enabled=False)
|
||||
service = NotificationService()
|
||||
result = _attach_decision_signal_summary(AnalysisResult(
|
||||
code="600519",
|
||||
name="贵州茅台",
|
||||
sentiment_score=72,
|
||||
trend_prediction="看多",
|
||||
operation_advice="持有",
|
||||
analysis_summary="稳健",
|
||||
))
|
||||
|
||||
out = service.generate_dashboard_report([result], report_date="2026-02-01")
|
||||
|
||||
self.assertIn("AI 决策信号", out)
|
||||
self.assertIn("动作: 卖出", out)
|
||||
self.assertIn("周期: 1d", out)
|
||||
self.assertIn("理由: 技术面走弱", out)
|
||||
|
||||
@mock.patch("src.notification.get_config")
|
||||
def test_generate_daily_report_appends_decision_signal_excerpt_fallback(
|
||||
self, mock_get_config: mock.MagicMock
|
||||
):
|
||||
mock_get_config.return_value = _make_config(report_renderer_enabled=False)
|
||||
result = _attach_decision_signal_summary(AnalysisResult(
|
||||
code="600519",
|
||||
name="贵州茅台",
|
||||
sentiment_score=72,
|
||||
trend_prediction="看多",
|
||||
operation_advice="持有",
|
||||
analysis_summary="稳健",
|
||||
))
|
||||
|
||||
for summary_only in (True, False):
|
||||
service = NotificationService()
|
||||
service._report_summary_only = summary_only
|
||||
out = service.generate_daily_report([result], report_date="2026-02-01")
|
||||
self.assertEqual(out.count("AI 决策信号"), 1)
|
||||
self.assertIn("动作: 卖出", out)
|
||||
self.assertIn("周期: 1d", out)
|
||||
self.assertIn("理由: 技术面走弱", out)
|
||||
|
||||
@mock.patch("src.notification.get_config")
|
||||
def test_generate_wechat_dashboard_appends_decision_signal_excerpt_fallback(
|
||||
self, mock_get_config: mock.MagicMock
|
||||
):
|
||||
mock_get_config.return_value = _make_config(report_renderer_enabled=False)
|
||||
result = _attach_decision_signal_summary(AnalysisResult(
|
||||
code="600519",
|
||||
name="贵州茅台",
|
||||
sentiment_score=72,
|
||||
trend_prediction="看多",
|
||||
operation_advice="持有",
|
||||
analysis_summary="稳健",
|
||||
))
|
||||
|
||||
for summary_only in (True, False):
|
||||
service = NotificationService()
|
||||
service._report_summary_only = summary_only
|
||||
out = service.generate_wechat_dashboard([result])
|
||||
self.assertIn("AI 决策信号", out)
|
||||
self.assertIn("动作: 卖出", out)
|
||||
self.assertIn("周期: 1d", out)
|
||||
self.assertIn("理由: 技术面走弱", out)
|
||||
|
||||
@mock.patch("src.notification.get_config")
|
||||
def test_generate_wechat_summary_appends_decision_signal_excerpt(
|
||||
self, mock_get_config: mock.MagicMock
|
||||
):
|
||||
mock_get_config.return_value = _make_config(report_renderer_enabled=False)
|
||||
service = NotificationService()
|
||||
result = _attach_decision_signal_summary(AnalysisResult(
|
||||
code="600519",
|
||||
name="贵州茅台",
|
||||
sentiment_score=72,
|
||||
trend_prediction="看多",
|
||||
operation_advice="持有",
|
||||
analysis_summary="稳健",
|
||||
))
|
||||
|
||||
out = service.generate_wechat_summary([result])
|
||||
|
||||
self.assertEqual(out.count("AI 决策信号"), 1)
|
||||
self.assertIn("动作: 卖出", out)
|
||||
self.assertIn("周期: 1d", out)
|
||||
self.assertIn("理由: 技术面走弱", out)
|
||||
|
||||
@mock.patch("src.notification.get_config")
|
||||
def test_generate_dashboard_report_appends_decision_signal_excerpt_with_renderer(
|
||||
self, mock_get_config: mock.MagicMock
|
||||
):
|
||||
mock_get_config.return_value = _make_config(report_renderer_enabled=True)
|
||||
service = NotificationService()
|
||||
result = _attach_decision_signal_summary(AnalysisResult(
|
||||
code="600519",
|
||||
name="贵州茅台",
|
||||
sentiment_score=72,
|
||||
trend_prediction="看多",
|
||||
operation_advice="持有",
|
||||
analysis_summary="稳健",
|
||||
))
|
||||
|
||||
out = service.generate_dashboard_report([result], report_date="2026-02-01")
|
||||
|
||||
self.assertIn("AI 决策信号", out)
|
||||
self.assertIn("动作: 卖出", out)
|
||||
self.assertIn("周期: 1d", out)
|
||||
self.assertIn("理由: 技术面走弱", out)
|
||||
|
||||
@mock.patch("src.notification.get_config")
|
||||
def test_aggregate_reports_show_compact_market_status_only(self, mock_get_config: mock.MagicMock):
|
||||
mock_get_config.return_value = _make_config(report_renderer_enabled=False)
|
||||
|
||||
@@ -561,6 +561,73 @@ class PortfolioPr2TestCase(unittest.TestCase):
|
||||
self.assertEqual(signal_actions["300750"], "reduce")
|
||||
self.assertEqual(signal_actions["000001"], "alert")
|
||||
|
||||
def test_risk_report_uses_requested_snapshot_for_decision_signal_filters(self) -> None:
|
||||
account = self.service.create_account(name="Main", broker="Demo", market="cn", base_currency="CNY")
|
||||
aid = account["id"]
|
||||
self.service.record_cash_ledger(
|
||||
account_id=aid,
|
||||
event_date=date(2026, 1, 1),
|
||||
direction="in",
|
||||
amount=100000.0,
|
||||
currency="CNY",
|
||||
)
|
||||
self.service.record_trade(
|
||||
account_id=aid,
|
||||
symbol="600519",
|
||||
trade_date=date(2026, 1, 1),
|
||||
side="buy",
|
||||
quantity=10,
|
||||
price=100,
|
||||
market="cn",
|
||||
currency="CNY",
|
||||
)
|
||||
self._save_close("600519", date(2026, 1, 1), 100.0)
|
||||
self._save_close("600519", date(2026, 1, 2), 100.0)
|
||||
self.service.record_trade(
|
||||
account_id=aid,
|
||||
symbol="000001",
|
||||
trade_date=date(2026, 1, 2),
|
||||
side="buy",
|
||||
quantity=10,
|
||||
price=20,
|
||||
market="cn",
|
||||
currency="CNY",
|
||||
)
|
||||
self._save_close("000001", date(2026, 1, 2), 20.0)
|
||||
self._create_signal("000001", "sell")
|
||||
|
||||
original = self.risk_service.decision_signal_service.list_signals
|
||||
with patch.object(
|
||||
self.risk_service.decision_signal_service,
|
||||
"list_signals",
|
||||
wraps=original,
|
||||
) as spy:
|
||||
report = self.risk_service.get_risk_report(
|
||||
account_id=aid,
|
||||
as_of=date(2026, 1, 2),
|
||||
cost_method="fifo",
|
||||
)
|
||||
|
||||
block = report["decision_signal_risk"]
|
||||
self.assertTrue(block["available"])
|
||||
self.assertEqual(block["total"], 1)
|
||||
self.assertEqual(block["items"][0]["symbol"], "000001")
|
||||
self.assertEqual(block["items"][0]["signal"]["action"], "sell")
|
||||
for call in spy.mock_calls:
|
||||
self.assertIsNot(call.kwargs.get("holding_only"), True)
|
||||
identity_calls = [
|
||||
call.kwargs.get("stock_identities")
|
||||
for call in spy.mock_calls
|
||||
if call.kwargs.get("stock_identities") is not None
|
||||
]
|
||||
observed_identities = {
|
||||
identity
|
||||
for ids in identity_calls
|
||||
for identity in ids
|
||||
}
|
||||
self.assertIn(("cn", "000001"), observed_identities)
|
||||
self.assertIn(("cn", "600519"), observed_identities)
|
||||
|
||||
def test_risk_report_decision_signal_fail_open(self) -> None:
|
||||
account = self.service.create_account(name="Main", broker="Demo", market="cn", base_currency="CNY")
|
||||
aid = account["id"]
|
||||
|
||||
@@ -59,6 +59,16 @@ def _make_renderer_config(show_llm_model: bool = True) -> MagicMock:
|
||||
return config
|
||||
|
||||
|
||||
def _with_decision_signal_summary(result: AnalysisResult) -> AnalysisResult:
|
||||
result.decision_signal_summary = {
|
||||
"action": "sell",
|
||||
"action_label": "卖出",
|
||||
"horizon": "1d",
|
||||
"reason": "技术面走弱",
|
||||
}
|
||||
return result
|
||||
|
||||
|
||||
class TestReportRenderer(unittest.TestCase):
|
||||
"""Report renderer tests."""
|
||||
|
||||
@@ -80,6 +90,17 @@ class TestReportRenderer(unittest.TestCase):
|
||||
self.assertIn("作战计划", out)
|
||||
self.assertNotIn("盘中决策护栏", out)
|
||||
|
||||
def test_render_markdown_includes_decision_signal_excerpt(self) -> None:
|
||||
"""Markdown summary and full templates include DecisionSignal excerpts."""
|
||||
for summary_only in (True, False):
|
||||
r = _with_decision_signal_summary(_make_result())
|
||||
out = render("markdown", [r], summary_only=summary_only)
|
||||
self.assertIsNotNone(out)
|
||||
self.assertIn("AI 决策信号", out)
|
||||
self.assertIn("动作: 卖出", out)
|
||||
self.assertIn("周期: 1d", out)
|
||||
self.assertIn("理由: 技术面走弱", out)
|
||||
|
||||
def test_render_markdown_phase_decision_section(self) -> None:
|
||||
"""Markdown renders phase_decision when present."""
|
||||
r = _make_result(
|
||||
@@ -137,6 +158,17 @@ class TestReportRenderer(unittest.TestCase):
|
||||
self.assertIsNotNone(out)
|
||||
self.assertIn("贵州茅台", out)
|
||||
|
||||
def test_render_wechat_includes_decision_signal_excerpt(self) -> None:
|
||||
"""Wechat summary and full templates include DecisionSignal excerpts."""
|
||||
for summary_only in (True, False):
|
||||
r = _with_decision_signal_summary(_make_result())
|
||||
out = render("wechat", [r], summary_only=summary_only)
|
||||
self.assertIsNotNone(out)
|
||||
self.assertIn("AI 决策信号", out)
|
||||
self.assertIn("动作: 卖出", out)
|
||||
self.assertIn("周期: 1d", out)
|
||||
self.assertIn("理由: 技术面走弱", out)
|
||||
|
||||
def test_render_brief(self) -> None:
|
||||
"""Brief platform renders 3-5 sentence summary."""
|
||||
r = _make_result()
|
||||
|
||||
Reference in New Issue
Block a user