mirror of
https://github.com/ZhuLinsen/daily_stock_analysis
synced 2026-09-20 10:53:33 +08:00
* feat: add daily market context for stock analysis * fix(review-feedback-1623): Share the market context service across worker threads and Use the * fix(review-feedback-1623): Avoid overwriting per-region context reports and Expand history lookup * fix(review-feedback-1623): add position even though the guardrail supposedly softened the * fix(review-feedback-1623): Avoid generating the CLI market review twice * fix(review-feedback-1623): 修正 * fix(review-feedback-1623): 修复普通 CLI/调度运行中大盘复盘被上下文预热摘要替代的问题,并收敛 PR 描述与当前实现范围 * fix(review-feedback-1623): 修正保守护栏的误判风险,并同步收敛 PR 描述范围 * fix(review-feedback-1623): 处理并发生成大盘上下文的锁语义风险,并澄清/补充外部模型或运行时配置风险检测的依据 * fix(review-feedback-1623): 收敛大盘上下文预热对用户可见大盘复盘的行为影响,并补齐外部模型/API 与运行时配置检测风险的澄清或验证证据 * fix(review-feedback-1623): Do not skip multi-market reviews after one cached context and Do not * fix(review-feedback-1623): Prime context with the effective trading date * fix(review-feedback-1623): 收敛 PR 描述与实际 diff 的范围不一致,并澄清结构化检测提示的 provider/model/base * fix(review-feedback-1623): 补一句“本 PR 不修改 LLM provider/model/base url、默认模型、配置清理或迁移逻辑”,避免后续审查继续误判 * fix(review-feedback-1623): 收敛 PR 描述与实际 diff 的范围矛盾 * fix(review-feedback-1623): 补齐这些承诺的实现 * fix(review-feedback-1623): 收敛 PR 描述与实际 diff 范围,避免以未实现能力作为验收项 * fix(review-feedback-1623): 收敛 PR 描述与实际范围,并澄清外部模型/API 与运行时配置检测风险 * fix(review-feedback-1623): Match freshly saved reviews to the target trade date * fix(review-feedback-1623): preserve structured market-light risk signals * fix(review-feedback-1623): Escape market-summary sentinels before prompting * fix(review-feedback-1623): 修复同日缓存复用时把低敏摘要当完整大盘复盘输出的行为风险,并收敛 PR 描述与实际 diff * fix(review-feedback-1623): 收敛 * fix(review-feedback-1623): 收敛 PR 描述与实际 scope * fix(review-feedback-1623): 修正大盘复盘复用语义,并让 PR 描述与实际 scope 对齐 * fix(review-feedback-1623): 修正同日大盘上下文缓存复用语义,并让 PR 描述与实际 scope 对齐 * fix(review-feedback-1623): Disable cached market context when skipping market review * fix(review-feedback-1623): 收敛 PR 描述与实际 scope,并补充外部模型/API 与运行时配置语义未变化的可核验证据 * fix(review-feedback-1623): 修正 PR 描述与实际 scope 的实质性不一致,并补齐或澄清 LLM/provider/model/base URL * fix(review-feedback-1623): 修正 operation advice 契约漂移,并让 PR 描述/验收范围与当前实际 diff 对齐 * fix(review-feedback-1623): 收敛 main.py 中大盘复盘复用导致的通知/执行语义变化,并补对应回归测试 * fix(review-feedback-1623): 修复生成型大盘上下文未绑定当前 query id 的行为风险,并收敛 PR body 与实际 diff 范围 * fix(review-feedback-1623): 修复缓存语言匹配问题,并收敛 PR 描述与实际 diff/验收范围的一致性 * fix(review-feedback-1623): 收敛当前实现与描述/文档中的验收边界,并澄清 schedule/CLI 首次运行无历史大盘复盘时是否应生成并注入上下文 * fix(review-feedback-1623): 收敛 PR 描述与 docs/CHANGELOG.md 中关于验收边界的表述,避免与实际 diff 实质性矛盾 * fix(review-feedback-1623): 修正 PR 描述与实际 diff 的范围漂移 * fix(review-feedback-1623): Reuse the cached runtime market context on fallback * fix(review-feedback-1623): PR 描述:当前完整改动文件列表没有 api/ 、apps/dsa-web/ 、日报状态表或四阶段日报持久化相关实现,但 PR * fix(review-feedback-1623): 收敛描述范围 * fix(review-feedback-1623): 收敛并发生成复用路径的正确性风险,并修正 PR 描述的实际范围与验收项 * fix(review-feedback-1623): 收敛描述和验收范围 * fix(review-feedback-1623): PR 描述的 Background / Acceptance Criteria 仍包含独立 API、Web * fix(review-feedback-1623): 收敛 PR 描述与实际实现边界,并补齐 LLM/provider/base url 相关兼容性说明或验证证据 * fix(review-feedback-1623): PR 描述与实际 diff 范围不一致:描述和验收项仍写有独立 API、Web * fix(review-feedback-1623): Wait long enough for in-flight market reviews * fix(review-feedback-1623): 修复共享 market review lock 释放后仍不生成匹配上下文的行为风险,并同步收敛 PR description 的实际范围 * fix(review-feedback-1623): 修复锁竞争 fallback 下丢失运行时服务的问题,并同步收敛 PR description 与实际 diff 范围 * fix(review-feedback-1623): 收敛 PR description 与实际改动范围 * fix(review-feedback-1623): PR 描述与实际 diff 存在实质性矛盾:描述声称提供独立 API、Web * fix(review-feedback-1623): reuse runtime market context and handle English negations * fix: honor daily market context config * docs: remove unrelated changelog entries * fix(review-feedback-1623): preserve the skipped market-review artifact * fix(review-feedback-1623): add low position cap and are not in CONSERVATIVE TEXT MARKERS , so an
128 lines
4.5 KiB
Python
128 lines
4.5 KiB
Python
# -*- coding: utf-8 -*-
|
|
"""
|
|
Regression tests for prefetch behavior in StockAnalysisPipeline.run().
|
|
"""
|
|
|
|
import os
|
|
import sys
|
|
import unittest
|
|
from datetime import date, datetime, timezone
|
|
from types import SimpleNamespace
|
|
from unittest.mock import MagicMock, call
|
|
|
|
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
|
|
|
|
from tests.litellm_stub import ensure_litellm_stub
|
|
|
|
ensure_litellm_stub()
|
|
|
|
from src.core.pipeline import StockAnalysisPipeline
|
|
|
|
|
|
class TestPipelinePrefetchBehavior(unittest.TestCase):
|
|
@staticmethod
|
|
def _build_pipeline(process_result):
|
|
pipeline = StockAnalysisPipeline.__new__(StockAnalysisPipeline)
|
|
pipeline.max_workers = 1
|
|
pipeline.fetcher_manager = MagicMock()
|
|
pipeline.db = MagicMock()
|
|
pipeline.db.has_today_data.return_value = False
|
|
pipeline.process_single_stock = MagicMock(return_value=process_result)
|
|
pipeline.config = SimpleNamespace(
|
|
stock_list=["000001"],
|
|
refresh_stock_list=lambda: None,
|
|
single_stock_notify=False,
|
|
report_type="simple",
|
|
analysis_delay=0,
|
|
)
|
|
return pipeline
|
|
|
|
def test_run_dry_run_skips_stock_name_prefetch(self):
|
|
pipeline = self._build_pipeline(process_result=None)
|
|
|
|
pipeline.run(stock_codes=["000001"], dry_run=True, send_notification=False)
|
|
|
|
pipeline.fetcher_manager.prefetch_stock_names.assert_not_called()
|
|
|
|
def test_run_non_dry_run_prefetches_stock_names(self):
|
|
pipeline = self._build_pipeline(process_result=SimpleNamespace(code="000001"))
|
|
|
|
pipeline.run(stock_codes=["000001"], dry_run=False, send_notification=False)
|
|
|
|
pipeline.fetcher_manager.prefetch_stock_names.assert_called_once_with(
|
|
["000001"], use_bulk=False
|
|
)
|
|
|
|
def test_run_dry_run_counts_existing_data_by_effective_trading_date(self):
|
|
pipeline = self._build_pipeline(process_result=None)
|
|
pipeline._resolve_resume_target_date = MagicMock(
|
|
side_effect=[date(2026, 3, 27), date(2026, 3, 26)]
|
|
)
|
|
pipeline.db.has_today_data.side_effect = [True, False]
|
|
|
|
pipeline.run(
|
|
stock_codes=["600519", "AAPL"],
|
|
dry_run=True,
|
|
send_notification=False,
|
|
)
|
|
|
|
self.assertEqual(
|
|
pipeline.db.has_today_data.call_args_list,
|
|
[
|
|
call("600519", date(2026, 3, 27)),
|
|
call("AAPL", date(2026, 3, 26)),
|
|
],
|
|
)
|
|
|
|
def test_run_uses_one_frozen_reference_time_for_tasks_and_dry_run_stats(self):
|
|
pipeline = self._build_pipeline(process_result=None)
|
|
pipeline._resolve_resume_target_date = MagicMock(
|
|
side_effect=[date(2026, 3, 27), date(2026, 3, 26)]
|
|
)
|
|
pipeline.db.has_today_data.side_effect = [True, False]
|
|
|
|
pipeline.run(
|
|
stock_codes=["600519", "AAPL"],
|
|
dry_run=True,
|
|
send_notification=False,
|
|
)
|
|
|
|
task_reference_times = [
|
|
call.kwargs["current_time"]
|
|
for call in pipeline.process_single_stock.call_args_list
|
|
]
|
|
stats_reference_times = [
|
|
call.kwargs["current_time"]
|
|
for call in pipeline._resolve_resume_target_date.call_args_list
|
|
]
|
|
|
|
self.assertEqual(len(task_reference_times), 2)
|
|
self.assertEqual(len(stats_reference_times), 2)
|
|
self.assertEqual(len({id(value) for value in task_reference_times}), 1)
|
|
self.assertEqual(len({id(value) for value in stats_reference_times}), 1)
|
|
self.assertIs(task_reference_times[0], stats_reference_times[0])
|
|
|
|
def test_run_uses_supplied_reference_time_for_tasks_and_dry_run_stats(self):
|
|
pipeline = self._build_pipeline(process_result=None)
|
|
reference_time = datetime(2026, 3, 27, 1, 30, tzinfo=timezone.utc)
|
|
pipeline._resolve_resume_target_date = MagicMock(
|
|
side_effect=[date(2026, 3, 27), date(2026, 3, 26)]
|
|
)
|
|
pipeline.db.has_today_data.side_effect = [True, False]
|
|
|
|
pipeline.run(
|
|
stock_codes=["600519", "AAPL"],
|
|
dry_run=True,
|
|
send_notification=False,
|
|
current_time=reference_time,
|
|
)
|
|
|
|
for process_call in pipeline.process_single_stock.call_args_list:
|
|
self.assertIs(process_call.kwargs["current_time"], reference_time)
|
|
for resolve_call in pipeline._resolve_resume_target_date.call_args_list:
|
|
self.assertIs(resolve_call.kwargs["current_time"], reference_time)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|