docs/decision-signals.md
本页收口 #1390 P7,说明 DSA 如何把个股分析、Agent、告警和组合风险中的 AI 建议沉淀为可查询、可反馈、可后验评估的 DecisionSignal 资产。它是报告之上的结构化索引,不替代 Markdown 报告、operation_advice、三态 decision_type、告警规则或真实交易系统。
DecisionSignal 只记录建议、证据摘要、风险、观察条件、生命周期和来源,不执行下单或调仓。decision_profile 字段化并修正 server-side filter、去重、续期和 active 失效语义;不新增环境变量、config registry 项或 .env.example 内容。DECISION_SIGNAL_* 开关;信号功能的关闭或回滚通过 revert 对应代码完成。核心字段由 api/v1/schemas/decision_signals.py 定义,主要包括:
stock_code、stock_name、market、source_type、source_agent、source_report_id、trace_id、decision_profile、trigger_source。action、action_label、confidence、score、horizon、market_phase、plan_quality、status。entry_low、entry_high、stop_loss、target_price、invalidation、watch_conditions、reason、risk_summary、catalyst_summary。evidence、data_quality_summary、metadata。expires_at、created_at、updated_at。枚举取值:
| 字段 | 取值 |
|---|---|
market | cn、hk、us、jp、kr、tw |
source_type | analysis、agent、alert、market_review、manual |
market_phase | premarket、intraday、lunch_break、closing_auction、postmarket、non_trading、unknown |
action | buy、add、hold、reduce、sell、watch、avoid、alert |
horizon | intraday、1d、3d、5d、10d、swing、long |
decision_profile | conservative、balanced、aggressive;数据库 NULL 表示 legacy / unknown |
plan_quality | complete、partial、minimal、unknown |
status | active、expired、invalidated、closed、archived |
Web 展示必须把这些 wire value 映射为当前 UI 语言的用户可读标签;API 响应继续保留原始枚举值。
个股分析、技术评分 fallback、报告展示 fallback 与 DecisionSignal 提取共用 decision-scale-v1 口径。decision_type 只保留 buy|hold|sell 兼容统计;更细的可执行语义以八态 action 为准。
operation_advice 保留文本口径(如“持有观察”),action 作为统一 8 态决策口径(如 hold/watch/reduce)用于风控、回测与列表展示。新生成或最终保存前重算的个股报告应优先让两者保持一致;历史记录或兼容载荷仍出现语义冲突时,默认以 action 为列表、回测、DecisionSignal 等结构化展示的优先字段,operation_advice 仅作说明文本保留。| score | signal key | action | legacy decision_type | 语义 |
|---|---|---|---|---|
| 80-100 | strong_buy | buy | buy | 强烈买入,高胜率机会,可执行买入/加仓计划 |
| 60-79 | buy | buy | buy | 偏积极机会,允许少量待确认项 |
| 40-59 | watch | watch | hold | 信号分歧或确认不足,等待触发条件 |
| 20-39 | reduce | reduce | sell | 风险明显抬升,优先降低暴露 |
| 0-19 | sell | sell | sell | 趋势或风险显著恶化,优先退出 |
如果 score >= 60 但最终 action 是 hold/watch,或 score < 40 但最终 action 仍是 hold/watch,必须有明确 guardrail 解释,例如 dashboard.decision_stability.reason、dashboard.decision_score_calibration.guardrail_reason 或 metadata.guardrail_reason。风控降级会保留 raw_score、adjusted_score、raw_action、final_action 和原因;没有明确原因的中性动作在 DecisionSignal 提取时会按 canonical score 对齐为 buy/reduce/sell。
src/services/decision_signal_service.py 是信号生命周期的主入口:
horizon 和 expires_at 显式传入时优先。horizon 时,alert 或盘前/盘中/午间休市/集合竞价阶段默认 intraday,盘后、非交易时段、未知阶段或缺少阶段时默认 3d。intraday 过期时间优先读取低敏 metadata.market_phase_summary.minutes_to_close/minutes_to_open;缺失时按市场 fallback TTL。expired、invalidated、closed、archived 不能通过 PATCH /status 直接恢复为 active。(source_report_id, source_type, market, stock_code, decision_profile, action, horizon, market_phase);没有 report 但有 trace_id 时使用 trace 维度。decision_profile 参与信号身份:NULL 只与 NULL 匹配,非空 profile 只与相同 profile 匹配。Exact dedup、relaxed dedup、horizon/phase fill、expired refresh、active invalidation 和 stale backfill invalidation 都遵循该 same-profile 语义。invalidated,并把失效来源写入 metadata。不同非 NULL profile 可并存,即使 action 相反。decision_profile,只能刷新同 profile 记录。当前公开接口由 api/v1/endpoints/decision_signals.py 和 docs/architecture/api_spec.json 描述:
POST /api/v1/decision-signals:创建或按同源键去重,返回 { item, created }。GET /api/v1/decision-signals:分页查询,支持市场、股票、动作、阶段、decision_profile、来源、状态、时间范围和持仓过滤。省略或传空 decision_profile 不加 profile 条件,返回所有 profile;decision_profile=unknown 查询 NULL 行;合法 profile 精确匹配。GET /api/v1/decision-signals/{signal_id}:查询单条。PATCH /api/v1/decision-signals/{signal_id}/status:更新状态和可选 metadata。GET /api/v1/decision-signals/latest/{stock_code}:查询股票最新 active 信号。POST /api/v1/decision-signals/outcomes/run:显式触发后验评估。GET /api/v1/decision-signals/outcomes、GET /api/v1/decision-signals/outcomes/stats、GET /api/v1/decision-signals/{signal_id}/outcomes:查询后验结果与统计。GET/PUT /api/v1/decision-signals/{signal_id}/feedback:查询或写入 useful / not useful 反馈。POST /api/v1/decision-signals/reassess:基于来源历史报告预览不同决策风格下的信号,不写库。这些接口继承现有 /api/v1/* 管理员鉴权;ADMIN_AUTH_ENABLED=true 时需要有效管理员会话 Cookie。
reassess 第一版只做 preview,不创建或更新 DecisionSignal。
请求只支持:
{
"source_report_id": 123,
"decision_profile": "aggressive",
"persist": false
}
契约边界:
source_report_id 是唯一事实来源,重评估只读取对应持久化历史报告快照。signal_id,也不接受客户端提交 action、score、confidence、价格、metadata 或 guardrail 结果;额外字段会被请求校验拒绝。persist=true 当前固定返回 HTTP 400,错误码为 unsupported_operation。保存重评估结果留给 #1757。high、medium、low、poor、unknown,guardrail 只使用归一化后的等级。guardrail_result 是机器审计数据,记录 raw/final action、是否通过、violations 和 adjustments;warnings 是用户可读摘要,测试和客户端逻辑应优先依赖稳定 code。blocked_reason,不能把它当作普通可执行信号。Web 入口位于 /decision-signals:
status=active。GET /api/v1/decision-signals list API,不新增 timeline endpoint。时间线必须先应用非空当前股票后才会查询;没有当前股票时只显示引导态,不拉取 market-only 或 global timeline。30d、90d、180d 三个时间范围,默认 90d;每次最多请求 100 条。若返回 total > items.length,Web 会显示“仅展示最近 100 条信号,请缩小时间范围”,避免静默展示不完整轨迹。all 与 active:all 不传 status,active 传 status=active。P1 不提供 terminal status filter,也不做前端 terminal 过滤。decision_profile 查询;unknown 只用于筛选和展示 legacy NULL 行。普通高级列表不新增 profile filter。0/- 指标。decision_profile 字段,只有字段缺失时才回退 legacy metadata;历史缺失或非法 profile 的信号显示为 unknown,不会误标为 balanced。cn/hk/us/jp/kr/tw;jp/kr/tw 的前端本地化标签均已补齐,tw 信号可经 API 正常写入、按 market=tw 查询,并可在 Web DecisionSignal 页面通过市场筛选项选择台股(tw);告警(大盘红绿灯)市场支持 cn/hk/us/jp/kr。closed、invalidated 或 archived,不提供 terminal 状态恢复为 active。source_type=analysis 信号,也不会因打开报告详情触发 source_report_id 信号查询;需要查看报告来源信号时统一进入 /decision-signals 页面按来源报告 ID 精确筛选,或打开 /decision-signals?sourceReportId=<recordId> deep link。该筛选和 deep link 都会使用 source_type=analysis + source_report_id 的精确查询,以保留旧报告的 best-effort 懒回填入口。所有用户可见枚举必须使用 i18n 标签;技术 ID、股票代码、API 字段名、env key、URL 示例可以保留英文。
#1756 后 decision_profile 是 decision_signals 的正式 nullable 字段,同时 metadata 保留兼容字段:
decision_profile=balanced
profile_source=auto_default:普通新分析生成路径。
profile_source=backfill_defaulted:历史报告 lazy backfill 路径。
profile_policy_version=decision-profile-v1
signal_generation_version=legacy-report-extractor-v1
decision_signal_metadata_version=decision-signal-metadata-v1
新写入时,顶层合法 decision_profile 优先;顶层显式 null、空值或非法值直接拒绝。顶层缺失时才 fallback 合法 metadata.decision_profile;二者都缺失或 metadata profile 非法时默认写入 balanced。
新写入会同步 metadata.decision_profile 为正式字段值,避免双源冲突;metadata 省略或显式 null 均按无 metadata 处理,object 会浅复制,非 object 会被拒绝。
PATCH metadata 省略时保留原值,显式 null 时清空为 SQL NULL,object 时整包替换。正式 profile 非 NULL 时会覆盖 metadata 中的冲突值;正式 profile 为 legacy NULL 时会移除请求 object 中的 profile key,且不会提升正式字段。
自动失效写入同样遵循正式字段权威语义:正式 profile 非 NULL 时同步 metadata profile;legacy NULL 时只追加失效信息,保留原 legacy metadata,不注入或删除 profile。
Legacy / unknown 只用数据库 NULL 表示。profile_policy_version 只表示默认 profile metadata contract version,不代表已经实现独立 profile policy engine、scoring engine 或多 profile 生成。P1/P2 不写入 scoring_version 或 scoring_breakdown;这些字段如需引入,应由后续 reassess / scoring issue 定义。
Lazy backfill 语义:省略 profile 保留旧的 source_type=analysis + source_report_id 懒回填;decision_profile=balanced 可生成 balanced 回填;decision_profile=unknown、conservative、aggressive 不自动创建行。
普通个股分析和 Agent 个股分析如果携带 market_structure_context,自动提取 DecisionSignal 时会把以下低敏字段追加到 metadata:
market_structure_versionmarket_theme_versionstock_market_position_versionmarket_structure_statusprimary_themetheme_phasestock_rolemarket_structure_risk_tags这些字段只用于解释信号所处题材背景,不参与 action、score、horizon、同源去重键或生命周期计算。它们也不是题材龙头证明;当 market_structure_risk_tags 或缺失证据显示成分股、leader stocks 不完整时,客户端和后验分析应按降级题材证据处理。
快照字段中的 provider / dataset 来自市场结构抽取链路元数据,属于运行后持久化证据,不参与 LLM provider/model 路由、base URL 解析、.env 写回或配置迁移;可核验范围见 src/schemas/market_structure.py。
decision_signal_summary 写入 alert_triggers.diagnostics。source_type=alert/action=alert 信号。trace_id=alert-rule-<hash> 只用于同源重试的 best-effort 去重,不覆盖 active 信号本体。action、horizon、reason、watch_conditions、risk_summary、source_report_id。reason 在脱敏后完整展示,避免固定字符数在句中截断;watch_conditions 和 risk_summary 仍保持紧凑摘要上限。metadata、evidence、raw diagnostics、webhook URL、token 或 cookie。GET /api/v1/portfolio/risk 的 decision_signal_risk 只统计当前持仓中的 active sell/reduce/alert 信号,查询失败时 fail-open。更多告警和通知细节见 docs/alerts.md 与 docs/notifications.md。
P5 通过 sidecar 表保存用户反馈和后验结果,不扩展 decision_signals 主表:
decision_signal_feedback 保存每个信号最新的 useful|not_useful 反馈、可选原因/备注和来源。decision_signal_outcomes 按 (signal_id, horizon, engine_version) 幂等保存后验评估结果。engine_version=decision-signal-v1。1d/3d/5d/10d;intraday/swing/long、非方向动作、缺价和 forward bars 不足会写入 eval_status=unable 与明确 unable_reason。信号写入和状态更新使用 src/utils/sanitize.py 中的 sanitize_decision_signal_text() 与 sanitize_decision_signal_payload():
trace_id 是同源去重身份字段;如果包含会被脱敏的 credential,API 会拒绝请求,而不是保存被 redaction 破坏后的身份值。P7 的全局验收是确认信号池、通知摘要和 Web 展示不泄露 token、cookie、webhook URL、API key、邮箱密码等敏感信息。
#1756 对 SQLite 执行非破坏性 migration。
迁移说明:
.env、.env.example 或 Web 设置项。ALTER TABLE ADD COLUMN decision_profile,不会 drop/rebuild decision_signals,也不会删除旧 index。metadata_json:仅合法 metadata.decision_profile 回填到正式字段;invalid JSON、非 object 或非法 profile 保持 NULL。启动日志会记录 backfilled、invalid JSON、non-object、invalid profile 和 skipped existing profile 统计,这些统计只用于诊断,不阻断启动。source_type=analysis + source_report_id 且无命中时,才会 best-effort 懒回填。decision_signals、feedback 和 outcome 数据保持兼容。回滚说明:
DECISION_SIGNAL_* 开关;关闭信号提取/写入的回滚方式是 revert 相关代码。decision_signals、decision_signal_feedback 或 decision_signal_outcomes 数据;如需清理,应由维护者单独制定数据清理策略。