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

112 lines
6.1 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# ADR 0003 — Per-interval 技术结构升级
**状态**Accepted
**日期**2026-05-25
**决策者**:项目维护者
**适用范围**`internal/entity/market_context.go``internal/usecase/indicator.go``internal/usecase/ports.go``internal/usecase/market_context.go`
---
## 背景
v2.0 把技术结构第一波pivot S/R + P95/P5 区间 + LSR 穿越价位中位数)做了出来,但只算 primary `1h``TechnicalStructure.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仅在 `Intervals``Box`Support / Resistance / Range / LSLine 继续仅在 primary。
- 承认 README "per-interval 化" 范围过宽,缩减 v2.x 范围。
---
## 决策
**方案 A扩展 + 顶层镜像**
具体 entity 形态:
```go
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/context``technical.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 自验里必须全过。