// Package coinglass — CoinGlass V2 私有 API 客户端实现。 // spec:ai/specs/coinglass.md。 package coinglass import ( "encoding/json" "fmt" "math" "strconv" "cryptoHermes/internal/entity" ) // CGEnvelope 是 CoinGlass V2 endpoint 的兼容外壳。 // HTTP body 解密前有 success/code/data;解密后的 plaintext 通常已经是 data 本体。 // 测试和旧 dump 可能仍喂 envelope,mapper 保留兼容。 type CGEnvelope struct { Success *bool `json:"success"` Code json.RawMessage `json:"code"` Msg string `json:"msg"` Data json.RawMessage `json:"data"` } // MapLiqHeatMap 把解密后的 plaintext 映射成 entity.CGLiqHeatMap。 // // 真实响应的 plaintext 是 data 本体(例如 {"instrument":...,"liq":[...]})。 // 为兼容早期 fixture,如 plaintext 仍带 success/code/data envelope,则先拆 data。 // PR-1 阶段做最薄映射:data 整体进 RawGrid; // 若顶层有 rangeLow/rangeHigh/minPrice/maxPrice/priceMin/priceMax 字段则抽出(容忍命名漂移)。 // 真实 schema 经 spike dump 后回填到 spec §5(mapper 同步加显式字段)。 // // 数值边界(ADR-0004 D3):解 JSON number 默认 float64 是 transient, // 通过 formatFloat 转 string 进 entity;entity 字段全 string(G12.4 兜底)。 func MapLiqHeatMap(symbol string, interval string, ts int64, plaintext []byte) (*entity.CGLiqHeatMap, error) { raw, err := liqHeatMapDataPayload(plaintext) if err != nil { return nil, err } out := &entity.CGLiqHeatMap{ Symbol: symbol, Interval: interval, Timestamp: ts, RawGrid: append(json.RawMessage(nil), raw...), } var probe map[string]json.RawMessage if err := json.Unmarshal(raw, &probe); err == nil { out.PriceMin = pickFloatField(probe, "priceMin", "rangeLow", "minPrice", "min") out.PriceMax = pickFloatField(probe, "priceMax", "rangeHigh", "maxPrice", "max") } return out, nil } func liqHeatMapDataPayload(plaintext []byte) (json.RawMessage, error) { if !json.Valid(plaintext) { return nil, fmt.Errorf("liq_heatmap plaintext: invalid json") } var env CGEnvelope if err := json.Unmarshal(plaintext, &env); err == nil && env.Success != nil { if !*env.Success { return nil, fmt.Errorf("%w: code=%s msg=%q", ErrAPIBusiness, envelopeCode(env.Code), env.Msg) } if len(env.Data) == 0 || string(env.Data) == "null" { return nil, fmt.Errorf("%w: empty data code=%s msg=%q", ErrFakeSuccess, envelopeCode(env.Code), env.Msg) } return append(json.RawMessage(nil), env.Data...), nil } return append(json.RawMessage(nil), plaintext...), nil } // pickFloatField 在 probe map 里按候选 key 顺序找第一个能解成 float64 的字段, // 用 formatFloat 转回 string。找不到返回 ""。 func pickFloatField(probe map[string]json.RawMessage, keys ...string) string { for _, k := range keys { raw, ok := probe[k] if !ok { continue } var f float64 if err := json.Unmarshal(raw, &f); err == nil { return formatFloat(f) } var s string if err := json.Unmarshal(raw, &s); err == nil { return s } } return "" } // formatFloat 是 mapper 出口的唯一数值格式化点(ADR-0002 / ADR-0004 D3)。 // NaN / Inf 统一映射成 "",下游消费方自行判空,避免 entity 出现非法字面量。 func formatFloat(f float64) string { if math.IsNaN(f) || math.IsInf(f, 0) { return "" } return strconv.FormatFloat(f, 'f', -1, 64) }