feat: 接入 CoinGlass 清算热力图隔离链路
Some checks failed
ci / build + vet + guard + race (push) Has been cancelled
Some checks failed
ci / build + vet + guard + race (push) Has been cancelled
This commit is contained in:
81
internal/repo/webapi/coinglass/mapper.go
Normal file
81
internal/repo/webapi/coinglass/mapper.go
Normal file
@@ -0,0 +1,81 @@
|
||||
// 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 的统一外壳。
|
||||
// 解密后的 plaintext JSON 形如:{"success":true,"code":"0","msg":"","data":{...}}
|
||||
type CGEnvelope struct {
|
||||
Success bool `json:"success"`
|
||||
Code json.Number `json:"code"`
|
||||
Msg string `json:"msg"`
|
||||
Data json.RawMessage `json:"data"`
|
||||
}
|
||||
|
||||
// MapLiqHeatMap 把解密后的 plaintext envelope 映射成 entity.CGLiqHeatMap。
|
||||
//
|
||||
// PR-1 阶段未拿到真实 schema dump,先做最薄映射:data 整体进 RawGrid;
|
||||
// 若顶层有 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) {
|
||||
var env CGEnvelope
|
||||
if err := json.Unmarshal(plaintext, &env); err != nil {
|
||||
return nil, fmt.Errorf("liq_heatmap envelope: %w", err)
|
||||
}
|
||||
if !env.Success {
|
||||
return nil, fmt.Errorf("liq_heatmap business error code=%s msg=%s", env.Code, env.Msg)
|
||||
}
|
||||
out := &entity.CGLiqHeatMap{
|
||||
Symbol: symbol,
|
||||
Interval: interval,
|
||||
Timestamp: ts,
|
||||
RawGrid: append(json.RawMessage(nil), env.Data...),
|
||||
}
|
||||
|
||||
var probe map[string]json.RawMessage
|
||||
if err := json.Unmarshal(env.Data, &probe); err == nil {
|
||||
out.PriceMin = pickFloatField(probe, "priceMin", "minPrice", "min")
|
||||
out.PriceMax = pickFloatField(probe, "priceMax", "maxPrice", "max")
|
||||
}
|
||||
return out, 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)
|
||||
}
|
||||
Reference in New Issue
Block a user