Files
cryptoHermes/ai/adr/0003-per-interval-technical-structure.md
dela 21b3078094 feat(indicator): per-interval S/R + Range + LSLine + Donchian 箱体(ADR-0003)
按 ADR-0003 把 Support / Resistance / Range / LSLine 从顶层下沉进
IntervalTechnicals,每个周期独立计算;顶层 5 字段保留为 Intervals[primary]
镜像(单一来源,不双写)以维持向后兼容的 JSON shape。

新增 BoxBreak(Donchian 风格,lookback=20 根不含当前根,K 线 < 21 → nil),
落到 IntervalTechnicals.Box。

IndicatorComputer.Compute 签名升级到 longShortByInterval map:
market_context 改为并发拉取 15m/1h/4h/1d 的全局 LSR,1w 不在 Binance LSR
支持列表 → 加 warning lsr_unsupported_interval:1w。

测试:boxBreak 6 个 case(inside/break_up/break_down/insufficient/2 个 parse
失败)+ per-interval Support/Resistance/Range 跨周期断言 + 镜像不变量 +
缺 LSR key → LongShortLine nil。indicator.go 平均覆盖 97.26%(boxBreak 100%)。
2026-05-25 10:44:33 +08:00

6.1 KiB
Raw Blame History

ADR 0003 — Per-interval 技术结构升级

状态Accepted 日期2026-05-25 决策者:项目维护者 适用范围internal/entity/market_context.gointernal/usecase/indicator.gointernal/usecase/ports.gointernal/usecase/market_context.go


背景

v2.0 把技术结构第一波pivot S/R + P95/P5 区间 + LSR 穿越价位中位数)做了出来,但只算 primary 1hTechnicalStructure.Support / Resistance / RangeHigh / RangeLow / LongShortLine 全部在顶层、只有一份。

v2.x 路线图README 末尾)要把 MA / Bollinger / Vegas / 箱体做成 per-interval 指标。第一批 Bollinger / Vegas 已经落到 IntervalTechnicals{Bollinger, Vegas} 里,跑通了 Intervals map[string]IntervalTechnicals 的承载层ADR-0002 顺带确立)。

剩下的问题Support / Resistance / Range / LSLine 与新加的 Box 是否也应该 per-interval如果要顶层那 5 个字段怎么处理?这是 schema 决策,影响下游 Hermes 的读路径。


候选方案

  1. A扩展 + 顶层镜像

    • IntervalTechnicals 加 6 字段Support / Resistance / RangeHigh / RangeLow / LongShortLine / Box
    • TechnicalStructure 顶层那 5 个原有字段保留,作为 Intervals[primary] 的镜像。
    • 镜像由 indicator 出口处单点赋值(取 Intervals[primaryInterval],避免双写漂移)。
  2. B顶层只剩 Primary 字段 + 全下沉

    • TechnicalStructure 顶层只留 Primary string 指针 + Intervals,原 5 字段全部下沉。
    • 结构最干净,无冗余。
  3. C暂时不升级只新增 Box

    • 保留 IntervalTechnicals 当前的 BB + Vegas仅在 IntervalsBoxSupport / Resistance / Range / LSLine 继续仅在 primary。
    • 承认 README "per-interval 化" 范围过宽,缩减 v2.x 范围。

决策

方案 A扩展 + 顶层镜像

具体 entity 形态:

type TechnicalStructure struct {
    // primary interval 镜像,向后兼容
    Support       []TechnicalLevel              `json:"support"`
    Resistance    []TechnicalLevel              `json:"resistance"`
    RangeHigh     *string                       `json:"rangeHigh"`
    RangeLow      *string                       `json:"rangeLow"`
    LongShortLine *string                       `json:"longShortLine"`
    // per-interval 真正承载层
    Intervals     map[string]IntervalTechnicals `json:"intervals"`
}

type IntervalTechnicals struct {
    Bollinger     *Bollinger        `json:"bollinger,omitempty"`
    Vegas         *Vegas            `json:"vegas,omitempty"`
    Support       []TechnicalLevel  `json:"support,omitempty"`
    Resistance    []TechnicalLevel  `json:"resistance,omitempty"`
    RangeHigh     *string           `json:"rangeHigh,omitempty"`
    RangeLow      *string           `json:"rangeLow,omitempty"`
    LongShortLine *string           `json:"longShortLine,omitempty"`
    Box           *BoxBreak         `json:"box,omitempty"`
}

镜像规则:indicator.Compute 计算完所有周期后,把 Intervals[primaryInterval] 的 5 个对应字段拷一份到顶层。这是唯一写顶层的位置,避免双写。


理由

向后兼容是当前最贵的属性

下游 Hermes 已经在消费 /v1/market/contexttechnical.support / resistance / rangeHigh / ...。v2.x 是"指标第二波扩充",不是"breaking 改版"。方案 B 的清爽换不来等额收益——下游必须改一次读路径,而我们目前没有 v1→v2 schema 迁移的成本预算。

ADR-0002 已经预设了承载层

ADR-0002 引入 IntervalTechnicals 时,把它定义成"per-interval 指标的新家"。当时只填了 Bollinger / Vegas但留出的空位本来就是给后续 per-interval 算法的。方案 A 沿着这条已建好的轨道走,方案 B 会推翻刚立的承载层语义。

单一来源的镜像 vs 真正的冗余

担心"顶层和 Intervals[primary] 不一致"是合理的——这正是为什么镜像必须由 indicator 出口处单点赋值,而不是两条路径分别计算。只要这条规则进 indicator.go 内部约束(不暴露 SetTopLevel API漂移风险可控。代码审查可以加一条简单 grep除了 Compute 函数返回前那一段,internal/usecase/ 不许再写 TechnicalStructure.{Support,Resistance,RangeHigh,RangeLow,LongShortLine} =

方案 C 的代价更隐蔽

如果选 CREADME 路线图要先改(把"per-interval 化"从 v2.x 范围里删掉)。改文档比改代码便宜,但 ai/harness-health 的复评触发器、未来 v3 接 CoinGlass per-symbol 数据时的扩展点都会被这次妥协绊住。不如一次做对。


代价

  • IntervalTechnicals 字段从 2 个涨到 8 个类型本身变重。omitempty 让缺数据周期(如 1w 缺 LSLine不出现在 JSON 里。
  • IndicatorComputer.Compute 签名要从 longShort []LongShortRatio 改成 longShortByInterval map[string][]LongShortRatio——breaking 的内部 API 变化,调用方只有 market_context.go 一处,但要同步改 market_context 内的 LSR 多周期拉取(详见 PR-A 实施细节)。
  • 镜像规则需要单测保护:必须有用例断言"顶层字段 == Intervals[primary] 对应字段"。

何时重新评估

  • 下游消费方明确说"不再读顶层字段,只走 Intervals" → 把顶层 5 字段标 deprecated下个大版本删除升级到方案 B
  • IntervalTechnicals 字段涨到 15+ 且出现明显的"高频读子集 vs 全量读子集"分化 → 考虑拆 IntervalTechnicalsCore + IntervalTechnicalsExtended
  • 出现"顶层与 Intervals[primary] 不一致"的 bug → 把镜像断言移入 G14 守卫,加进 make guard

与其它决策的关系

  • ADR-0001 D2Clean Arch 边界)entity 字段扩展不影响层间依赖方向。
  • ADR-0002指标数值边界:本 ADR 不引入新 float 出口,所有 per-interval 字段沿用既有 string 表示与 G12 白名单G12 无需修改。
  • 守卫 G6 / G11 / G12:本次变更不应触发任何已有守卫违规;make guard 在 PR-A 自验里必须全过。