业务服务集合,涵盖量化评分、任务队列、系统配置、历史管理、技术指标、行业分析等。
| 文件 | 职责 |
|---|---|
scoring/ (子包) |
评分引擎薄壳 + ScoringService 主入口:纯算法已下沉到 quantcore.scoring;本子包仅保留向后兼容的 re-export + 注入 CBOE fetcher 的 ScoringService 子类(2026-05 旧的 scoring_service.py 顶层 shim 已删除,统一从 stocklens.services.scoring 导入) |
scoring/_cboe_fetcher.py |
fetch_cboe_options() 网络调用(CBOE 期权链 delayed quotes API),唯一保留在 stocklens 侧的网络函数 |
task_queue.py |
AnalysisTaskQueue 单例任务队列(线程池 TASK_QUEUE) |
task_service.py |
TaskService 单例任务服务(线程池 TASK_SERVICE) |
system_config_service.py |
SystemConfigService 运行时系统配置 + .env 读写(核心服务,545 行) |
system_config_validators.py |
LLM 通道与运行时模型校验(从 service 抽出,约 559 行,纯函数) |
history_service.py |
HistoryService 分析历史查询与管理 |
history_comparison_service.py |
分析历史对比 |
bollinger_service.py |
fetch_bollinger_multi_timeframe() — akshare/雪球抓取 + 两级缓存;纯计算(compute_bollinger + 各种 resample)已下沉到 quantcore.indicators.bollinger |
finviz_service.py |
FinvizService 前瞻数据/同行/英文新闻(仅美股) |
tech_indicators_service.py |
compute_correlation() — 通过 akshare 抓取 SPY/QQQ 日线 + 4h 共享缓存;其他纯算法(Fibonacci/ATR/OBV/VWAP/K 线形态/Sharpe/MaxDD)已下沉到 quantcore.indicators.tech 与 quantcore.stats |
industry_service.py |
行业分类与对比 |
social_sentiment_service.py |
社交情绪(仅美股) |
stock_service.py |
股票综合查询 |
stock_code_utils.py |
股票代码工具 |
name_to_code_resolver.py |
名称 → 代码 |
image_stock_extractor.py |
OCR 图片识别股票 |
import_parser.py |
导入数据解析 |
report_renderer.py |
报告 Markdown 渲染 |
agent_model_service.py |
Agent 模型管理 |
analysis_service.py |
分析服务薄壳(API 入口) |
拆包公告(2026-05):纯量化算法已全部下沉到独立的
quantcore/包(见quantcore.md)。 本子包现在只是一个向后兼容的薄壳:所有from stocklens.services.scoring.* import *的旧调用仍然有效, 实际实现位于quantcore.scoring.*。新代码请直接
from quantcore.scoring import ScoringEngine, ...。
stocklens/services/scoring/ 仅保留以下两部分有"实际逻辑"的代码:
| 文件 | 内容 |
|---|---|
__init__.py |
ScoringService(quantcore.scoring.ScoringEngine) 子类,构造时注入 _cboe_fetcher.fetch_cboe_options,让 ScoringService().evaluate(context) 在拿不到 cboe_data 时自动从 CBOE 抓 |
_cboe_fetcher.py |
fetch_cboe_options(symbol) — 唯一保留的网络函数(调 CBOE delayed quotes 公开接口) |
其余六个文件(types.py / fundamental.py / valuation.py / sentiment.py / technical.py / engine.py)已经退化为对 quantcore.scoring.* 的纯 re-export shim — 内部不再有业务逻辑。
| quantcore 文件 | 内容(即原 stocklens.services.scoring.* 的实际实现) |
|---|---|
quantcore/scoring/engine.py |
ScoringEngine.evaluate() 主入口 + calc_composite_score() 静态方法 |
quantcore/scoring/types.py |
18 个 dataclass 结果类型 + _sf / _unwrap_block / classify_industry helpers |
quantcore/scoring/fundamental.py |
calc_piotroski、calc_distress(Altman Z + Ohlson O + 综合困境) |
quantcore/scoring/valuation.py |
calc_valuation(含 Rule of 40 + PEG)、calc_beneish、calc_dupont、calc_earnings_quality、calc_magic_formula、calc_dividend_safety |
quantcore/scoring/sentiment.py |
calc_insider_activity、calc_options_sentiment(纯计算)、calc_momentum、calc_earnings_surprise、calc_short_interest |
quantcore/scoring/technical.py |
calc_sctr、calc_risk、calc_macro、calc_market_regime、calc_peer_rank |
from stocklens.services.scoring.engine import ScoringService
svc = ScoringService()
overview = svc.evaluate(enhanced_context) # 跑 18 模型
score = ScoringService.calc_composite_score(overview) # 0-100score = macro 25 # 宏观 + 市场状态
+ fundamental 20 # Piotroski / 盈余质量 / DuPont
+ valuation 20 # Rule of 40 / PEG / Magic Formula
+ technical+momentum 15 # SCTR / 12-1m 动量
+ distress 10 # Altman / Ohlson 等
+ sentiment 10 # 期权 / 内部人 / 做空 / 派息
满分 100,被 tests/unit/test_composite_score.py 14 用例锁定行为(边界、单调性、上下限)。
| 模型 | 函数 | 说明 |
|---|---|---|
| Piotroski F-Score | calc_piotroski |
9 项财务健康信号 |
| Altman Z-Score | calc_distress 内 |
破产风险 |
| Ohlson O-Score | calc_distress 内 |
违约概率 |
| Rule of 40 | calc_valuation 内 |
营收增速 + 净利率 |
| PEG Fair Value | calc_valuation 内 |
基于增速的合理估值 |
| Beneish M-Score | calc_beneish |
财务造假检测 |
| DuPont 分析 | calc_dupont |
ROE = 净利率 × 周转 × 杠杆 |
| 盈余质量 | calc_earnings_quality |
(NI - OCF) / TA |
| Magic Formula | calc_magic_formula |
盈利收益率 + ROIC |
| 派息安全性 | calc_dividend_safety |
派息率 + FCF 覆盖 |
| 机构持股 | calc_insider_activity |
持股 + 内部人 + 做空 |
| 期权情绪 | calc_options_sentiment |
P/C ratio + IV skew |
| 动量 | calc_momentum |
12-1m 动量 |
| 盈利惊喜 | calc_earnings_surprise |
SUE |
| 做空 | calc_short_interest |
days to cover |
| SCTR | calc_sctr |
技术综合评分 |
| 风险指标 | calc_risk |
Sharpe / Sortino / MaxDD |
| 宏观 + 市场状态 | calc_macro + calc_market_regime |
利率 / VIX / 趋势 |
进程内单例,异步任务队列。
submit(...) -> task_idget_task(task_id)/list_tasks()cancel(task_id)- 状态:
pending → running → completed / failed / cancelled - 线程池
TASK_QUEUE(默认 cpu_count,可调)
API 友好的任务服务封装,单例。线程池 TASK_SERVICE。
运行时系统配置:
get_all()— 按分类分组set(key, value)— 写入.env同时更新内存set_batch(updates)get_field_meta(key)— 字段元数据(来自config/registry.py)validate(key, value)get_llm_status()
fetch_bollinger_multi_timeframe(stock_code) 编排多达 7 个时间框架的布林带(1H / 2H / 4H / Daily / Weekly / Monthly / Yearly)。
- 抓取(仅本文件做):akshare
stock_us_daily取日线;60m kline 通过stocklens.data_service.LegacyManagerShim.fetch_bars()获取(Xueqiu HTTP 调用封装在 shim 层)。 - 缓存(仅本文件做):kvcache
bollingernamespace,4 小时 TTL。 - 计算(委托 quantcore):
quantcore.indicators.bollinger.compute_bollinger(df, timeframe)— 单周期 BBquantcore.indicators.bollinger.resample_to_{weekly,monthly,yearly}(df)— 周期 resamplequantcore.indicators.bollinger.resample_hourly(df_60m, hours)— 60m → 2H/4Hquantcore.indicators.bollinger.update_position_signal(result, daily_price, label)— 用日收盘统一 current_price
返回 quantcore.indicators.MultiBollingerSnapshot(dataclass,含 to_prompt_text() / has_data())。
仅美股:
get_forward_data(code)— 前瞻 PE / PEG / EPS 预期 / 目标价 / 评级get_peer_comparison(code)— 同行对比表(动态发现)get_news(code, limit)— 英文新闻
compute_correlation(stock_code, df) — 与 SPY/QQQ 的相关性 / Beta / Alpha:
- 抓取(仅本文件做):akshare
stock_us_daily取 SPY/QQQ 日线,4h 模块级缓存(_bm_cache)。 - 计算(委托
quantcore.stats.compute_correlation_pair):成对返回序列 → CorrelationResult。
其余纯计算(
compute_tech_indicators/detect_swing_points/select_fibonacci_swing/ 各 dataclass)已下沉到quantcore.indicators.tech。本文件仅 re-export 兼容旧导入路径。
- 历史查询、按代码/日期范围筛选、删除、统计
- 历史信号对比(同股 N 次分析的差异)
仅美股;接入第三方社交情绪源(受配置 SOCIAL_SENTIMENT_* 控制)。
- 依赖:
config、storage、repositories、tickbridge、search、market、utils.concurrency - 被依赖:
pipeline、api、agent
ScoringService的评分是确定性的(纯 Python 计算),LLM 不可修改- Finviz 数据通过
finvizfinance库抓取,仅支持美股;可 lazy import AnalysisTaskQueue与TaskService都是单例,import 即获取SystemConfigService修改配置会同时写入.env文件和内存- 布林带分析的小时级数据通过日线数据模拟(非真实分时数据)
- 评分引擎被 246 单测中的 50 用例覆盖(types + composite score)
- fail-open 日志(2026-05):服务层(
bollinger_service/image_stock_extractor/import_parser等)所有"退化到次路径"的try/except统一调stocklens.utils.fail_open.fail_open(logger, op, exc),输出格式[fail-open] <op>: <ExcType>: <msg>。新增 fail-over 路径请直接 import 这个 helper,不要手写logger.debug("silent except ...")
industry_service.fetch_industry_snapshot并行化:sections 4-9(insider trades / analyst ratings / description / peer tickers / etf holders / news)从 6 次顺序 finvizfinance 调用 改为ThreadPoolExecutor(max_workers=4)并行执行,预计单股节省 4-8 秒墙钟时间。Finviz 站点 的 throttle(tickbridge/domain_sources/finviz._REQUEST_INTERVAL=0.5s)控制了实际并发度。scoring/_cboe_fetcher.py改用 httpx.Client 单例:_get_client()模块级 client 通过httpx.Limits(max_keepalive_connections=8, max_connections=16)复用连接, 消除每只美股期权情绪评分时的 TCP+TLS 握手开销。
stocklens/services/history_service.py(881 行)的 Markdown 报告渲染逻辑
(_generate_single_stock_markdown 234 行 + 4 个静态 helper)抽离到新文件
stocklens/services/history_report_renderer.py:
| 函数 | 职责 |
|---|---|
render_single_stock_markdown(result, record) |
顶层入口,编排 5 个 section |
_render_intel_block / _render_core_conclusion / _render_data_perspective / _render_battle_plan / _render_legacy_flat_sections |
各自独立的 section builder |
escape_md / clean_sniper_value / safe_format_number / get_signal_level / append_market_snapshot_to_report |
模块级 helper(含纯函数版的 signal-level 5-band 映射) |
HistoryService._generate_single_stock_markdown 现在只是一个委托薄壳;
旧的 in-class 实现保留为 _generate_single_stock_markdown_legacy(紧急回滚用,
soak 期后删除)。HistoryService 本身保持数据访问职责(query / pagination /
news intel lookup),与渲染解耦。
stocklens/utils/feishu_doc.py 的 FeishuDocManager(飞书云文档发布)原本错误地
放在 utils/ 下,已迁移到新建的 stocklens/integrations/feishu_doc.py(integrations/
是新增子包,专门承载第三方 SaaS SDK 的 wrapper:飞书 / Notion 等)。utils/feishu_doc.py
保留为 backward-compat shim(from stocklens.integrations.feishu_doc import FeishuDocManager),
现存调用点零改动。
name_to_code_resolver.py 此前用模块级 _akshare_cache: Optional[tuple[float, Dict[str, str]]] = None + _AKSHARE_CACHE_TTL = 3600 这种 Dict + time.time() 模式,违反了 CLAUDE.md 中「禁止手写 TTL 缓存」的项目规则。
修复:
stocklens/utils/cache_bootstrap.py注册新 namespacename_to_code(TTL=3600s,policy=stale_while_revalidate,max_entries=4)。_get_akshare_name_to_code()改为通过kvcache.get_manager().namespace("name_to_code")读写;_fetch_akshare_name_to_code()拆出无缓存的真实 fetch 实现给 caller 包。- kvcache 不可用(manager 没 configure 的测试场景)时 fallback 直接调
_fetch_akshare_name_to_code(),保持单元测试可运行。
公开 API(resolve_name_to_code(name))零改动。
把过去散在 4 个文件的「评分→标签/Emoji/tag」映射收口为单模块:
| 函数 | 用途 | 历史调用点 |
|---|---|---|
get_sentiment_label(score) |
英文 5-band 标签(API 响应用) | analysis_service._get_sentiment_label / history_service._get_sentiment_label |
get_signal_level_en(score) → (label, emoji, tag) |
英文 markdown 渲染 | history_report_renderer.get_signal_level |
get_signal_level_zh(advice, score) → (text, emoji, tag) |
中文推送渲染(advice 优先,score 兜底) | report_renderer._get_signal_level |
旧函数全部成为新模块的 1 行 delegate(保留以满足向后兼容)。新增加 band 或调整阈值只需要改这一个文件。31 个回归测试 (tests/unit/test_scoring_labels.py) 锁住所有 band 边界 + advice/score 兜底逻辑。
原本 image_stock_extractor.py 内联 18 行的 _normalize_code(A 股 6 位 / HK 5 位 / US 1-5 字母 / .SH/.SZ 后缀),与 stock_code_utils.normalize_code 几乎一致但缺少 SH600519 / HK00700 这种 exchange-prefix 形式。改为 1 行 delegate _shared_normalize_code(raw),行为等价但现在多识别 prefix 形式。
原 330 行的 god function 拆成:
| 角色 | 函数 |
|---|---|
| 编排器 | fetch_industry_snapshot (~30 行,4 步顺序调用) |
| Section 1 (fundamentals) | _fetch_ticker_fundament + _populate_ticker_fundament + _TICKER_FUNDAMENT_FIELDS 表 |
| Section 2 (sector) | _fetch_sector_overview + _populate_sector_row + _row_to_sector_payload |
| Section 3 (peers) | _fetch_peer_companies + _format_market_cap |
| Sections 4-9 (并行) | _fetch_industry_subsections(保留 ThreadPoolExecutor,6 个内部 closure) |
_TICKER_FUNDAMENT_FIELDS 是 60 元组的 (snapshot_attr, finviz_key, coerce_blank) 表,替换原 80 行 snapshot.x = info.get(...) 样板。新增字段直接往 tuple 里加一行。
两个 task 服务的 ergonomics 不同(API+SSE+轮询 vs Bot 会话回复 fire-and-forget),不强行合并。在 task_service.py 模块头部加了「Why this exists alongside task_queue」段落,明确告诉新调用者优先用 task_queue.get_task_queue(),仅当需要 bot 会话回复路径时才走 TaskService。