这一章跟着一只股票走完全程:从「今天该不该跑」开始,到数据、指标、新闻、Prompt、模型、守卫、落库、推送。每一站都告诉你它为什么存在,以及失败时会发生什么。
先记住一个判断
DSA 的可靠性不来自「模型很聪明」,而来自把不确定性切成很多小段,每段都允许降级。行情失败用收盘价,基本面超时用缺失标记,模型 JSON 坏了换模型,结论太激进用规则压回来。整条链路的设计哲学是:不追求完美输入,只追求可解释的次优输出。
全景:八站流水线
这条线由 StockAnalysisPipeline.analyze_stock() 串起来,外层是 run() 的线程池与批次调度。下面按站点拆解。
预检:今天该跑吗?这只股票能跑吗?
流水线真正开始前,先做三件「不产生分析内容、但决定是否继续」的事:
按股票所属市场查交易所日历。A 股休市就跳过 A 股,港股、美股各自判断;--force-run 可强制。指数并入 cn 市场语义。
如果 SQLite 里已经有该股票当日行情,抓数步骤可以直接跳过;整轮运行会冻结一个「目标交易日」,避免跨零点时前后数据日期不一致。
代码、名称、市场、资产类型(股票/指数/ETF)被解析成结构化目标。裸 6 位默认股票,显式指数才会进入指数路径。
失败会怎样
交易日不匹配 → 跳过该股票并记录原因;股票列表为空 → 整轮失败并写 empty_stock_list;单个代码解析失败 → 只影响该目标,不阻断同批其他股票。
取数:把「现在」和「过去」都拿到
一只股票需要四类输入。它们来自不同供应商,失败策略也不同:
| 数据 | 用途 | 失败策略 |
|---|---|---|
| 实时行情 | 盘中最新价、涨跌幅、量比,替换最后一根日线 | 失败退回最近收盘价,并标注数据来源 |
| 近 90 天日线 | 均线、MACD、RSI、支撑压力、历史对比 | 多源降级;仍失败则该股无法分析 |
| 筹码与资金 | 获利盘、集中度、主力资金流 | 不支持/超时 → 字段标记缺失,继续 |
| 基本面 | 财务快照、估值、板块归属、公司事件 | 独立预算(默认约 8 秒),超时降级 |
日线通常已落库,所以这里同时承担「增量更新」职责:先查当天是否已有数据,没有才走网络。数据源的选择与降级细节见数据源与降级。
失败会怎样
实时价失败不是致命错误;日线失败会触发多源切换;基本面超时会留下「数据缺失」标记,Prompt 会明确告诉模型「这块没有」,而不是让模型自由发挥。
指标:先算出一份确定性观点
StockTrendAnalyzer 用 pandas/numpy 计算均线、MACD、RSI、乖离率与量能,输出趋势状态、买入信号、0–100 的技术分和风险因素。这个分数不依赖 LLM,是模型之外的第二意见。
| 维度 | 分值 | 看什么 |
|---|---|---|
| 趋势 | 30 | 均线排列、MA20 方向、趋势强度 |
| 乖离率 | 20 | 离 MA5/MA10/MA20 的距离,MA5 阈值默认 5% |
| 量能 | 15 | 量比、放量/缩量、与均量比较 |
| 支撑 | 10 | 价格与均线/关键位的距离 |
| MACD | 15 | 金叉/死叉、零轴位置、柱体变化 |
| RSI | 10 | 超买超卖、多周期一致性 |
总分映射为买入信号(强买/买入/观望/卖出等)。盘中可选「实时价叠加」:把最后一根收盘价替换为最新价,让均线更贴近当下;代价是数据未收盘,稳定性略差。
为什么先算指标,再让 LLM 分析?
因为 LLM 对数字的敏感度不稳定。先把均线、MACD、RSI、乖离率算成明确的结构化字段,再喂给模型,模型的任务从「计算」变成「解释与综合」。这是降低幻觉的关键工程手段。
情报:给模型一双看新闻的眼睛
新闻不是「越多越好」,而是「越相关越好」。系统会围绕股票名称、代码、行业与事件,最多发起约 5 组检索,把新闻、公告、舆情汇总成情报上下文;可选接入社交舆情(如 Reddit/X/Polymarket,部分市场)。
- 命中的新闻会落库,后续可以复用,避免每轮重复抓取。
- 没有配置搜索源时不会报错,只是情报为空——报告的风险与催化部分会更保守。
- 新闻内容会作为「外部证据」进入 Prompt,并在某些路径里参与证据约束,减少模型凭空编造催化。
失败会怎样
搜索服务不可用 → 跳过情报,报告仍能生成;单条新闻抓取失败 → 丢弃该条;社交舆情未配置 → 不进入上下文。整个阶段是 fail-open 的。
上下文:把「一只股票」放进「今天的市场」
只看个股很容易误判:大盘暴跌时,再漂亮的多头排列也可能被砸穿。上下文装配会把至少五层信息拼在一起:
市场阶段
盘前、盘中、午休、收盘竞价、盘后。盘前不能假装知道今天怎么收盘。
当日大盘上下文
指数涨跌、市场宽度、板块强弱,给个股一个「背景板」。
市场结构
龙头、题材、资金风格;没有成分股证据时,不允许模型宣称「龙头」。
上下文包
把技术面、基本面、情报、数据质量打包成结构化摘要,控制长度与优先级。
这些片段不是简单拼接,而是各自带 schema 版本与质量状态。旧版本或 not_supported 的块会被跳过,避免脏数据污染 Prompt。
Prompt 的本质是「有损压缩」
90 天日线不可能全塞进去,必须压缩成均线、MACD、关键位、量能与结论。压缩得好,模型抓重点;压缩得差,模型只能猜。上下文包的工程价值就在这里。
LLM:按 JSON 契约生成决策仪表盘
分析器把系统提示词、行情表、技术指标、筹码、新闻、市场阶段拼成一条完整请求,要求模型返回严格 JSON。请求不是一次性的:
主模型 + 回退模型依次尝试;主模型超时、限流、JSON 不合法都会切下一个。
优先流式,能更早发现空响应;流式失败可退回非流式。
先提取 JSON 对象,用 json-repair 修常见格式问题;再做最小契约校验(关键字段是否齐全)。
完全无法解析时走文本解析,给一个保守分数并标记 success=False;这类结果不会作为正常分析落库。
模型输出包含六大块:核心结论、数据透视、情报、作战计划、阶段决策、信号归因。每个字段都有枚举或格式约束,方便前端、通知与 DecisionSignal 复用。
失败会怎样
所有模型都失败 → 该股分析失败,但同批其他股票继续;JSON 缺字段 → 有完整性重试,仍缺则用占位文本而不是崩溃;模型只返回散文 → 文本解析保底,但可信度下降。
守卫:让结论「落地」而不是「飘着」
这是 DSA 与其他「把股票数据丢给 GPT」的项目最不一样的地方。模型给出结论后,至少还有四道确定性检查:
| 守卫 | 检查什么 | 典型动作 |
|---|---|---|
| 结构守卫 | 现价相对支撑/压力的位置:破位 <0.985、近支撑 ≤1.03、突破 >1.01、近压力 ≥0.97 | 在压力位附近买入但资金没流入 → 降为持有/观望;在支撑位附近卖出但无重大风险 → 降为持有 |
| 阶段守卫 | 盘前/盘中/收盘竞价/盘后,能说什么 | 盘前不允许编造当日走势;数据 stale 时不允许高置信度 |
| 大盘守卫 | 当日市场上下文与个股结论是否冲突 | 大盘系统性风险下,压低激进买入 |
| 评分校准 | 持有/观望的分数区间 | 把 hold/watch 分数夹到 45–59,避免「观望 85 分」这种自相矛盾 |
买入还要看资金流:没有资金流入的「突破」会被降级,资金流出且未突破的「强势」也会被降级。这些规则不保证赚钱,但能显著减少「模型被一根阳线冲昏头」的概率。
一句话理解
LLM 负责写「分析稿」,守卫负责当「风控编辑」。稿件可以精彩,但不能越过红线。
交付:落库、报告、信号与推送
分析成功后,系统会做四件事:
- 写历史:整份结果序列化进
analysis_history,Web、回测、历史对比都从这里读。 - 抽信号:把结论转成
DecisionSignal,带动作、评分、周期、入场区间、止损、目标价与过期时间。 - 生成报告:默认在通知层拼装 Markdown;开启
REPORT_RENDERER_ENABLED时使用 Jinja2 模板按平台渲染。 - 推送:按渠道能力分片、降噪、转图片,再逐个发送;单个渠道失败不影响其他渠道。
批量模式下,单股报告会先汇总成「决策仪表盘」,再与大盘复盘合并推送;同时本地 reports/ 会保留一份 Markdown。
失败会怎样
落库失败 → 标记 report_save_failed;所有通知渠道失败 → 报告仍在本地;某个渠道超时 → 状态记为 partial_failed,其他渠道照常。
报告长什么样:六个问题,一份答案
一份完整报告本质上在回答六个问题:发生了什么?证据是什么?结论是什么?在哪里买/卖?风险在哪?下一步做什么? 对应到结构大致是:
| 报告区块 | 回答的问题 | 谁消费 |
|---|---|---|
| 摘要与评分 | 这只股票今天值不值得看? | 推送首屏、仪表盘 |
| 情报速览 | 有什么新闻、业绩、催化、风险? | 人、后续追问 |
| 核心结论 | 买/卖/观望,理由是什么? | 决策信号、回测 |
| 数据透视 | 行情、均线、量能、筹码怎么组合? | 人、调试 |
| 作战计划 | 狙击点、止损、目标、检查清单 | 实际执行 |
| 信号归因 / 历史对比 | 为什么是现在?和上次有什么不同? | 复盘、策略改进 |
评分不是「上涨概率」
统一的五档刻度是:80–100 强烈买入、60–79 买入、40–59 观望、20–39 减仓、0–19 卖出。它衡量的是「当前证据下建议动作的强度」,不是「明天涨的概率」。把它当概率用,是初学者最常见的误读。
DecisionSignal:把「观点」变成「可追踪的承诺」
报告是给人看的,信号是给系统追踪的。每份分析会抽取一条结构化信号,核心字段包括:动作、评分、持有周期、入场区间、止损价、目标价、过期时间、来源报告。它有三个好处:
- 可回测:过期后可以对照真实价格,评估这次建议是否有效。
- 可告警:价格触发入场/止损时,告警中心可以通知你。
- 可复盘:同一只股票的历史信号形成一条决策轨迹,而不是散落在聊天记录里。
注意「会过期」是特性不是缺陷。三天前的买入区间,今天可能已经完全失效;不设过期时间的信号,等于鼓励你刻舟求剑。
批量与并发:三只股票同时跑,但不互相拖累
默认 MAX_WORKERS=3,这是一个刻意的保守值:数据源大多免费或有限流,并发太高容易触发封禁。批量执行时:
- 每只股票独立处理,单只异常被捕获,不影响同批其他结果。
- 股票数量较多时会尝试批量预取行情,减少重复请求。
- 进度通过回调上报,Web 端用 SSE 实时显示,不必轮询。
- 成功结果才会进入汇总与推送;失败结果会出现在日志与诊断里。
并发调高不是「更快更好」。如果你的数据源是 Tushare 这类有明确频率限制的服务,贸然开 10 个线程只会让整批一起限流。
失败学:一份「不会全军覆没」的清单
| 如果…… | 系统会…… | 你该…… |
|---|---|---|
| 某个数据源挂了 | 按优先级切下一个,连续失败 3 次熔断 5 分钟 | 看数据质量页,必要时配更强数据源 |
| LLM 超时 | 切回退模型;仍失败则该股失败 | 降低并发、换更稳的模型或缩短 Prompt |
| JSON 不合法 | json-repair → 契约校验 → 完整性重试 → 文本兜底 | 看日志里的模型名与错误类型 |
| 盘中进程重启 | 已落库的当日数据可跳过抓取;目标交易日冻结 | 直接重跑,通常能续上 |
| 定时任务重复触发 | 文件锁 / 原子认领尽量保证至多一次 | 确保只有一个调度入口与共享数据库 |
模型给出「买入」,但现价已经非常接近压力位,且资金流没有明显流入。结构守卫最可能做什么?