Files
cryptoHermes/ai/specs/coinglass.md
dela 1b61541ff5
Some checks failed
ci / build + vet + guard + race (push) Has been cancelled
feat: 接入 CoinGlass 清算热力图隔离链路
2026-05-25 16:57:22 +08:00

30 KiB
Raw Permalink Blame History

spec — CoinGlass V2 数据源接入v3 phase-1

状态AcceptedD1D7 全锁§17 用户确认 2026-05-25等 v3-PR-1 实施) 日期2026-05-25 作者cryptoHermes 维护者 适用范围internal/repo/webapi/coinglass/(新)、internal/entity/coinglass.go(新)、internal/usecase/coinglass_*.go(新)、internal/controller/restapi/v1/coinglass_routes.go(新)、internal/worker/coinglass_collector.go(新)、config/config.go(扩 v3 段)

参考:

  • docs/coinglass/cg.mdV2 反混淆笔记6 分支 dispatcher + 双阶段解密 + data= 签名)
  • docs/coinglass/coinglass_client.pyPython 参考实现2026-05 验证可跑)
  • docs/coinglass/endpoint.json209 endpoint 存活报告154 ok
  • ai/harness-health.md line 84新数据源 → 必填本 spec
  • README.md 路线图 v3 段CoinGlass + ETF Flow + CME gap + … 多源聚合

1. 目标

把 CoinGlass V2 私有 API 中跨交易所聚合的 4 个核心信号接入 cryptoHermes作为 v3 的第一波交付。结束后下游 Hermes 量化引擎能从 /v1/coinglass/* 拿到 Binance 单交易所视角看不到的数据(清算热力图 + 多交易所 OI / 多空比 / funding 聚合)。

为什么是 CoinGlass 而不是 ETF Flow / CME / 链上

  • 反混淆窗口期已开cg.md + Python ref 都已在 2026-05 验证),错过这窗口下次 CoinGlass 版本号变V3又要重新逆向。
  • 4 个 endpoint 一次拿下,覆盖 README v3 段里的"多交易所聚合"子项ETF Flow / CME gap 在 CoinGlass 自己的 endpoint 里也有phase-2 直接复用本 phase 的 client 即可。
  • 链上 / 宏观日历是不同性质的数据源(不需要 TLS 指纹绕过、不需要私有解密),独立铺设更经济。

2. Phase 1 endpoint 范围(用户锁定 2026-05-25

Path 用途 参考 params
/api/index/v2/liqHeatMap 清算热力图(价格 × 时间 × 清算量) merge=true symbol=Binance_BTCUSDT interval=5 limit=288
/api/openInterest/v3/chart 多交易所 OI 历史 symbol=BTC interval=h1 timeType=1 type=0
/api/futures/longShortRate 跨交易所多空比 symbol=BTC interval=h1 timeType=1
/api/fundingRate/v2/home 多交易所 funding 聚合 (空)

非目标phase 2+ETF Flow、CVD、CME gap、宏观日历、链上稳定币流动性、其它 200 个 CoinGlass endpoint 中的 150 个。


3. 决策锁定

ID 决策 锁定时间 备注
D1 TLS 指纹库用 github.com/enetx/surf v1.0.144 2026-05-25用户后经 surf spike 修订) surf Firefox profile 已真打 CoinGlass liqHeatMap能拿到 v/user/data 并解出 plaintextpin v1.0.144,避免 latest 抬 Go 1.25
D2 Phase 1 = 4 个 endpoint见 §2 2026-05-25用户 全部对应 Python ref __main__ smoke test
D3 TOTP_SECRET 走 config 注入env COINGLASS_TOTP_SECRETenabled=true 时必填 2026-05-25作者 公开 bundle 里能抠出 → 不算账户 secret但 CoinGlass 可能旋转env 化保旋转友好Go client 不提供代码默认值
D4 新增 /v1/coinglass/* namespace折进 /v1/market/context 2026-05-25作者 CoinGlass 故障模式fake-success / 密钥旋转 / TLS 失效)独立,要求物理隔离;下游 Hermes 可独立降级
D5 每个 endpoint 独立 PG 表 + 独立 cron 周期 2026-05-25作者 热力图频次需要更细5min、funding 聚合更粗30minschema 形状差异大
D6 解密管线产生的 float64 数值字段,在 repo 出口处全部 strconv.FormatFloat 转 string 进 entity 2026-05-25作者 沿用 ADR-0001 D5 全链 string + ADR-0002 数值边界白名单语义。详见 §6.3
D7 新增独立 CoinglassProvider interfaceDerivativesProvider 2026-05-25作者 失败语义不同fake-success retry返回类型不同聚合后的 list 不是单 symbol

D1 / D2 由用户锁定D3-D7 由作者基于现有架构推断;任何一条用户不同意都可单点回旋。


4. 架构

4.1 分层Clean Arch 边界沿用)

controller/restapi/v1/coinglass_routes.go
        │
        ▼
usecase/coinglass_query.go        ← 只读路径repo 拉历史
usecase/coinglass_signal.go       ← 纯函数:聚合多交易所 OI/funding → 语义信号
        │
        ▼
repo/persistent/postgres/coinglass_*.go   ← 5 个新表的 UpsertMany + FindRecent
repo/webapi/coinglass/                    ← V2 client + 4 个 endpoint mapper
        │
        ▼
worker/coinglass_collector.go     ← cron周期性 fetch → upsert

G6边界守卫不变usecase 不导入 repo/webapi/coinglassrepo/persistent/postgres,只对 ports。 G12数值边界守卫扩白名单coinglass_signal.go 类比 derivatives_signal.go,允许 float64;其余 usecase 文件仍禁。

4.2 ports

// internal/usecase/ports.go 追加
type CoinglassProvider interface {
    GetLiqHeatMap(ctx context.Context, symbol string, intervalMin int, limit int) (*entity.CGLiqHeatMap, error)
    GetOpenInterestChart(ctx context.Context, symbol, interval string) (*entity.CGOpenInterestChart, error)
    GetLongShortRate(ctx context.Context, symbol, interval string) (*entity.CGLongShortRate, error)
    GetFundingRateHome(ctx context.Context) (*entity.CGFundingHome, error)
}

type CoinglassRepository interface {
    UpsertLiqHeatMap(ctx context.Context, m *entity.CGLiqHeatMap) error
    FindRecentLiqHeatMap(ctx context.Context, symbol string, limit int) ([]entity.CGLiqHeatMap, error)
    UpsertOpenInterestChart(ctx context.Context, m *entity.CGOpenInterestChart) error
    FindRecentOpenInterestChart(ctx context.Context, symbol, interval string, limit int) ([]entity.CGOpenInterestChart, error)
    UpsertLongShortRate(ctx context.Context, m *entity.CGLongShortRate) error
    FindRecentLongShortRate(ctx context.Context, symbol, interval string, limit int) ([]entity.CGLongShortRate, error)
    UpsertFundingHome(ctx context.Context, m *entity.CGFundingHome) error
    FindRecentFundingHome(ctx context.Context, limit int) ([]entity.CGFundingHome, error)
}

4 endpoint × (Upsert + FindRecent) = 8 repo 方法。每个独立表D5

4.3 V2 dispatcher 6 分支 — 必须全实现

cg.md §"Known branches (2026-05)" 写得很清楚:服务器per-responsev,同一 endpoint 连续两次调用 v 可能不同。客户端必须能处理所有 6 个分支,不能 pin

v seed 来源 已确认
"0" base64("coinglass"+path+"coinglass")[:16] V1-compat需要时再启
"1" path(待验证派生形状) cg.md 标"shape — verify"
"2" response.headers.time V2 实战出现
"55" "170b070da9654622" bundle 常量
"66" "d6537d845a964081" bundle 常量
"77" "863f08689c97435b" bundle 常量

实现要点(沿用 Python ref _resolve_seed

func resolveSeed(v, path, reqCacheTsV2, respTime string) (string, error) {
    switch v {
    case "0": return reqCacheTsV2, nil
    case "1": return path, nil
    case "2": return respTime, nil
    case "55": return "170b070da9654622", nil
    case "66": return "d6537d845a964081", nil
    case "77": return "863f08689c97435b", nil
    default:  return "", fmt.Errorf("unknown coinglass v=%q (bundle may have changed)", v)
    }
}

未知 v → 返回错误(不重试不降级),让 collector log 一行 warn等人工 review bundle。这是已知"以后会出现的故障模式",预先把 hook 留好。

4.4 双阶段解密universal pipeline

Stage 1: user_key = base64(seed)[:16]                              16 字节 ASCII
         token    = strip_quotes(gunzip(unpad(aes_ecb_decrypt(b64dec(headers["user"]), user_key))))
Stage 2: body_key = token[:16]**原 ASCII****不再 base64**
         plain    = strip_quotes(gunzip(unpad(aes_ecb_decrypt(b64dec(body["data"]), body_key))))
         result   = json.Unmarshal(plain)

Go 实现位置:internal/repo/webapi/coinglass/crypto.go,纯函数,与 fetch 解耦,方便单测喂样本。 依赖stdlib crypto/aes + crypto/cipherECB 自己拼 block 循环stdlib 没有 ECB 模式)+ compress/gzip + encoding/base64。零第三方。

PKCS#7 unpad 自己写 8 行ECB decrypt 自己写 ~15 行(cipher.Block.Decrypt(dst, src) 一块一块走)。

4.5 请求侧 data= 签名TOTP

ts        = unix_seconds_now()
totp_code = TOTP(secret=COINGLASS_TOTP_SECRET, period=30s, digits=6).At(ts)
plaintext = fmt.Sprintf("%d,%s", ts, totp_code)
cipher    = AES-256-ECB-PKCS7(DATA_AES_KEY)
data_b64  = base64(cipher.Encrypt(pad(plaintext)))
url      ?= ...&data=<data_b64>

依赖:github.com/pquerna/otp/totpGo TOTP 事实标准)。 DATA_AES_KEY = "1f68efd73f8d4921acc0dead41dd39bc"32 字节 ASCIIAES-256从 bundle 抠出,作为代码常量;与 TOTP_SECRET 不同 — DATA_AES_KEY 旋转概率显著低,且即便旋转也是同步换的。仍保留 env override COINGLASS_DATA_AES_KEY 作为 escape hatch。

4.6 TLS 指纹D1 = github.com/enetx/surf v1.0.144

client := surf.NewClient().Builder().
    Impersonate().FireFox(). // v1.0.144 Firefox/144 profile
    Timeout(15 * time.Second).
    Build()

请求头按 coinglass_client.py _base_headers 复写,但 User-Agent 由 surf Firefox profile 负责,避免 UA 与 TLS 指纹版本脱节。Origin / Referer = https://www.coinglass.comEncryption: trueCache-Ts-V2: <ms ts>

2026-05-25 spike 结果:surf@v1.0.144 在 Go 1.24.1 下真打 /api/index/v2/liqHeatMap?merge=true&symbol=Binance_BTCUSDT&interval=5&limit=288,返回 status=200v=0user header、非空 data,并可用 §4.4 双阶段解密得到 plaintext。surf@latest v1.0.200 也可用,但会把模块 go 版本抬到 1.25.0PR-1 不采用。

Fake-success 检测cg.md §"fake success trap"

判定(在 client 层,先于解密):

resp.status == 200 AND body.success == true AND body.data IS empty/null
  → fake-success → 等 2s 重试 1 次 → 仍 fake → 抛 ErrFakeSuccess

不区分"业务 errorsuccess=false + 明确 msg"与"fake-success",因为业务 error 调 retry 没用。两者必须明确分类。

4.7 配置(config/config.go 扩段)

coinglass:
  enabled: false                                 # 默认关,老 v1/v2 用户零侵入
  base_url: "https://capi.coinglass.com"
  totp_secret: "${COINGLASS_TOTP_SECRET}"        # env 注入必填enabled=true 时)
  data_aes_key: "${COINGLASS_DATA_AES_KEY:-1f68efd73f8d4921acc0dead41dd39bc}"
  tls_profile: "surf_firefox"                    # 当前实现固定 surf v1.0.144 Firefox/144 profile
  request_timeout_sec: 15
  fake_success_retry: 1
  fake_success_retry_wait_ms: 2000
  collectors:
    liq_heatmap_interval_sec: 300   # 5 min
    oi_chart_interval_sec: 600      # 10 min
    long_short_rate_interval_sec: 600
    funding_home_interval_sec: 1800 # 30 min
    startup_jitter_max_sec: 30      # 启动时随机延迟 [0, max),避免整点齐打
    backoff_initial_sec: 30         # 失败后退避起点
    backoff_max_sec: 600            # 退避上限
    single_flight: true             # 任一 endpoint 不允许并发 fetch

cron 行为硬约束§9 Collector 实现必须满足)

  1. 不重叠single-flight:同一 endpoint 上一次 fetch 未结束前,下一次 tick 跳过log skip 一行),不堆积。
  2. 失败退避fetch 失败(任意错误类)后按 backoff_initial_sec 起步 ×2 增长到 backoff_max_sec 封顶;成功一次后重置回 interval。
  3. 启动 jitter:每个 collector 启动时 sleep rand.Intn(startup_jitter_max_sec) 秒再开始第一次 tick避免 4 endpoint 在整点齐打 CoinGlass。

PR-1 范围内:只启 liqHeatMap 的 cron(其它 3 个 endpoint cron 留到 v3-PR-2 接入时一起启)。但 §4.7 的 config schema 与 §9 的 backoff/jitter/single-flight 框架必须在 PR-1 落地,避免 PR-2 还要回头改 config 结构。

enabled: false 默认保护:未配置 TOTP_SECRET 时 cron 不启动、controller 路由 503。G1无私钥守卫不变TOTP_SECRET 不是账户级签名,是 CoinGlass 站点级的反爬密钥,不签账户行为,不需要进 G1 灰名单。但 spec 内对此事实必须记下来:

CoinGlass TOTP_SECRET 是站点反爬签名密钥(用于 data= 参数),与"账户级 API 签名"性质不同,不在 G1 守卫范围内。其唯一暴露面是 CoinGlass 反爬规则,泄露不影响用户资产。AGENTS.md §"绝不做"不变。


5. 数据模型(internal/entity/coinglass.go

ADR-0001 D5所有数值字段全链 string。下文 type 都是 string 除非显式标注。

package entity

// CGLiqHeatMap — /api/index/v2/liqHeatMap 响应建模。
// 注意CoinGlass 返回的 prices/volumes 是 2D 网格,要展平成行存或保 JSONB
// 这里建议保 JSONB一次拉一帧按 Symbol+Timestamp 单行)。
type CGLiqHeatMap struct {
    Symbol    string `json:"symbol"`       // "Binance_BTCUSDT"
    Interval  string `json:"interval"`     // "5"(分钟)
    Timestamp int64  `json:"timestamp"`    // 拉取时间ms
    PriceMin  string `json:"priceMin"`
    PriceMax  string `json:"priceMax"`
    // RawGrid 是 CoinGlass 原 JSON 序列化字节;下游需要时再解析。
    // 不在 Go entity 层重建价格×时间二维网格,避免不必要 schema 锁定。
    RawGrid []byte `json:"rawGrid"`
}

// CGOpenInterestChart — 多交易所 OI 时序。一帧多交易所。
type CGOpenInterestChart struct {
    Symbol    string                 `json:"symbol"`   // "BTC"
    Interval  string                 `json:"interval"` // "h1"
    Timestamp int64                  `json:"timestamp"`
    Exchanges []CGExchangeOIPoint    `json:"exchanges"`
}

type CGExchangeOIPoint struct {
    Exchange string `json:"exchange"`  // "Binance" / "OKX" / "Bybit" ...
    OI       string `json:"oi"`        // USD value
}

// CGLongShortRate — 多交易所多空比。
type CGLongShortRate struct {
    Symbol    string                       `json:"symbol"`
    Interval  string                       `json:"interval"`
    Timestamp int64                        `json:"timestamp"`
    Exchanges []CGExchangeLongShortPoint   `json:"exchanges"`
}

type CGExchangeLongShortPoint struct {
    Exchange  string `json:"exchange"`
    LongRate  string `json:"longRate"`   // 百分比,如 "55.32"
    ShortRate string `json:"shortRate"`  // 百分比
    Ratio     string `json:"ratio"`      // long/short
}

// CGFundingHome — 多交易所 funding 聚合home page format
type CGFundingHome struct {
    Timestamp int64               `json:"timestamp"`
    Items     []CGFundingHomeItem `json:"items"`
}

type CGFundingHomeItem struct {
    Symbol         string `json:"symbol"`        // "BTCUSDT"
    Exchange       string `json:"exchange"`      // "Binance"
    FundingRate    string `json:"fundingRate"`   // e.g. "0.0001"
    NextFundingTs  int64  `json:"nextFundingTs"`
    Interval       string `json:"interval"`      // "8h" / "4h"
}

确认前提:上面字段名是基于 Python ref 4 个 smoke endpoint 的"预期形状"。真 schema 必须等 Go 端跑通 client + dump 一次解密后的 JSON 才能定。spec 阶段先占位PR-1 spike 跑通后回填本节并把 spec 标 Accepted。

5.1 JSONB 字段使用纪律(强约束)

CGLiqHeatMap.RawGrid 是本 spec 唯一允许的 JSONB 字段。约束如下:

  1. JSONB 只承载供应商原始网格 / 元信息CoinGlass 返回的 2D 价格×时间×清算量网格因 schema 频繁微调且行数爆炸(一帧 288 × N 价位),关系化得不偿失,故保 JSONB。
  2. usecase 层禁止直接解析 RawGrid 任意 JSON 结构:如 coinglass_signal.go 需要"清算密集价位"等派生字段,必须先在 mapper.go 里展开成 entity 字段(如未来加 LiqCluster []CGLiqLevel),再由 usecase 消费。json.Unmarshal(RawGrid, &map[string]any{}) 这种 ad-hoc 解析禁止出现在 usecase
  3. JSONB → 稳定 entity 字段的演进路径当某个派生指标稳定≥2 个 PR 反复用mapper 加一个显式字段;不要让 RawGrid 的形状沿伸到 controller / 下游 Hermes 合约。
  4. 其它 3 个 entityOI / LSR / Funding保持拍平结构,不引入新 JSONB 字段。新增 JSONB 字段需开 ADR 锁定。

6. Repo 层(internal/repo/webapi/coinglass/

6.1 目录结构

internal/repo/webapi/coinglass/
├── client.go        # surf wrapper + data= TOTP signing + dispatch GET + retry
├── crypto.go        # AES-ECB + PKCS7 + gzip零外部依赖
├── crypto_test.go   # 喂 fixed ciphertext + key → 解出已知 plaintext
├── dispatcher.go    # resolveSeed 6 分支
├── mapper.go        # CoinGlass JSON → entityper endpoint
├── mapper_test.go   # table-driven
├── liq_heatmap.go   # GetLiqHeatMap
├── oi_chart.go      # GetOpenInterestChart
├── long_short.go    # GetLongShortRate
└── funding_home.go  # GetFundingRateHome

6.2 错误分类client 层导出)

var (
    ErrFakeSuccess  = errors.New("coinglass fake-success (TLS fingerprint suspected)")
    ErrUnknownV     = errors.New("coinglass unknown v branch")
    ErrDecryptFail  = errors.New("coinglass decrypt pipeline failed")
    ErrAPIBusiness  = errors.New("coinglass API business error") // success=false + msg
)

Collector 见到 ErrFakeSuccess 连发 3 次 → 全局 disable 1 小时 + log alert避免 IP 进黑名单)。

6.3 数值边界G12 白名单扩张)

internal/repo/webapi/coinglass/mapper.go 内部允许 transient float64(解 JSON 时 number 默认 float64但 mapper 出口(写入 entity前必须 strconv.FormatFloat(f, 'f', -1, 64) 回 string。

G12 守卫扩白名单Makefile guard 目标的 G12.1 规则增加 coinglass 排除:

@echo "G12.1 (usecase float)..." && ! { grep -rn --include="*.go" "float64\|float32" internal/usecase | grep -vE "indicator|derivatives_signal|coinglass" | grep -q . ; }

同时新增 G12.7 / G12.8 覆盖:coinglass_signal.goderivatives_signal.go 同款no repo / no DB import

G12.4entity 不能含 floatCoinGlass entity 全 stringG12.4 不破。

6.4 测试策略

文件 测试范围 工具
crypto_test.go 喂 fixed (key, ciphertext) → 期望 plaintextPKCS7 unpad 边界0/16 块大小) table-driven + testify
dispatcher_test.go 6 个 v 分支各一例 + unknown v → ErrUnknownV table-driven
mapper_test.go 喂 fixed JSON从 Python ref dump 出来)→ 期望 entity 字段NaN/Inf/缺字段处理 table-drivenfixture 放 testdata/
client_test.go httptest.NewServer 起 fake CoinGlass验证 data= TOTP 签名、retry / fake-success 分类 httptest不需要 surf 真起 Firefox 指纹,测的是上层逻辑)
integration_test.gobuild tag integration_coinglass默认关 真打 CoinGlass仅在手动 + nightly 跑;不进 push CI CGO=0 即可

PR-C 已经被跳,但 CoinGlass integration test 因为打外部服务,必须有 build tag 隔离。这是 D7 顺带的硬约束。


7. Usecaseinternal/usecase/coinglass_*.go

7.1 coinglass_query.go(薄)

只读路径:给 controller 调,从 repo 拉历史,组装 response不做计算

type CoinglassQueryUsecase struct {
    repo CoinglassRepository
    log  *slog.Logger
}

func (u *CoinglassQueryUsecase) GetLiqHeatMap(ctx context.Context, symbol string, limit int) ([]entity.CGLiqHeatMap, error) { ... }
// ... 4 个 endpoint 各一个 Get

7.2 coinglass_signal.go(厚,纯函数)

类比 derivatives_signal.go:把多交易所原始数据翻译成可读语义。

// CoinglassSignal 把 4 个 endpoint 的原始数据聚合成上游 Hermes 直接消费的语义字段。
type CoinglassSignal struct {
    OIDominance     string `json:"oiDominance"`     // e.g. "binance:0.42,okx:0.28,bybit:0.18"
    OIShiftBias     string `json:"oiShiftBias"`     // accumulating_binance / fleeing_to_okx / balanced
    LSRDispersion   string `json:"lsrDispersion"`   // narrow / wide — 交易所间多空比方差
    FundingExtreme  string `json:"fundingExtreme"`  // 哪个 exchange funding 是 outlier
    LiqClusterPrice string `json:"liqClusterPrice"` // 清算热力图里最密集的价位(中位数)
}

type CoinglassSignalComputer interface {
    Compute(
        heat *entity.CGLiqHeatMap,
        oi *entity.CGOpenInterestChart,
        lsr *entity.CGLongShortRate,
        fund *entity.CGFundingHome,
    ) (*CoinglassSignal, []string) // signal + warnings
}

这是 phase 1 真正的"算法增量"。前面 4 个 endpoint 是底料;这里把"7 个交易所 OI 谁在涨"翻成"oi_shift_bias=accumulating_binance"才是给 Hermes 看的成品。

7.3 MarketContextUsecase 增强(可选 phase-1.5

ADR-0003 的 mirror 思路可以借用:/v1/market/context 增一个 coinglass *CGSummary 字段可选omitemptyCoinglassSignal 嵌入。不在 phase-1 必做范围phase-1 先把 /v1/coinglass/* 独立起来,跑稳了再考虑要不要折回 context。


8. Controllerinternal/controller/restapi/v1/coinglass_routes.go

GET /v1/coinglass/liq-heatmap?symbol=BTCUSDT&limit=288
GET /v1/coinglass/open-interest?symbol=BTC&interval=h1&limit=200
GET /v1/coinglass/long-short?symbol=BTC&interval=h1&limit=200
GET /v1/coinglass/funding-home
GET /v1/coinglass/signal?symbol=BTC                ← 聚合 4 endpoint 的 latest调 CoinglassSignalComputer

coinglass.enabled=false 时全部返 503 service-unavailable: coinglass disabled in config。 G11controller 不直依 derivatives repo类比controller 不直依 CoinglassRepository只调 CoinglassQueryUsecase。新增 G11.2 守卫覆盖:

@echo "G11.2 (ctrl coinglass)..." && ! grep -rqn "CoinglassRepo\|coinglassRepo" internal/controller

9. Worker / Collector

internal/worker/coinglass_collector.go4 个独立 cron间隔见 §4.7 config。复用 internal/worker/collector.go 的 ticker 模式。

每个 endpoint 错误隔离 — 一个 endpoint 持续 fake-success 不影响其它 3 个的轮询。ErrFakeSuccess 连 3 次 → 该 endpoint disable 1h + log error。


10. 迁移与回滚

10.1 schema 迁移

新增 4 个表 + 0 个旧表改动:

migrations/00XX_create_coinglass_liq_heatmap.up.sql
migrations/00XX_create_coinglass_open_interest_chart.up.sql
migrations/00XX_create_coinglass_long_short_rate.up.sql
migrations/00XX_create_coinglass_funding_home.up.sql

每个表对应一个 down.sql。migrations/ 已有 5 张 Binance 表;编号顺延。

10.2 回滚策略

  • 配置层关:coinglass.enabled=false → cron 不跑、路由 503但表保留。运维零代码回滚。
  • 代码层关revert PR-1 起的 4 个 commit表保留下次启用时重新 collect 填)。

11. 失败模式 & 检测

症状 含义 处置
持续 HTTP 200 + success:true + 无 data TLS 指纹漂移 / IP 进灰名单 该 endpoint disable 1h + log alert运维换 IP / 升级 surf profile
ErrUnknownV CoinGlass bundle 出了新 v 分支 log + alert人工 re-read bundle + 加分支spec §4.3 表追加
ErrDecryptFailgzip / unpad / utf-8 任一失败) 密钥派生错 / 算法变 log + alert先重试 1 次(可能是网络截断);仍失败 → human inspect dump
业务 success:false code=40001 params 错(如缺 symbol 不重试,直接 propagate 出错(编码 bug
业务 success:false code=40000 之前 fake-success 后被风控 同 fake-success 处置
静默 schema 变(新字段 / 字段重命名) CoinGlass 后端微调 mapper 容忍(按 key 选字段,不依顺序);新字段 phase-2 决定要不要接

监控 metricphase-1.5 落地,不阻塞 phase-1

  • coinglass_request_total{endpoint, outcome=ok|fake_success|business_err|unknown_v|decrypt_fail}
  • coinglass_endpoint_last_success_age_seconds{endpoint}

12. 守卫扩张总结

守卫 改动
G1无账户签名 不变。TOTP_SECRET 不在范围(站点反爬密钥 ≠ 账户签名密钥spec §4.7 已记
G6边界 不变。新 repo 仍只被 usecase 通过接口调
G11controller 不直依 repo 新增 G11.2 = controller 不见 CoinglassRepo
G12.1usecase float 白名单) coinglassgrep -vE "indicator|derivatives_signal|coinglass"
G12.7(新) coinglass_signal.go 不 import internal/repo
G12.8(新) coinglass_signal.go 不 import binance/postgres/pgx
G12.9(新) internal/repo/webapi/coinglass/mapper.go 出口前必须 formatPrice/FormatFloatgrep 模式mapper.go 内 float64 必须与 FormatFloat 共现) — 可选phase-1 先靠 mapper_test 覆盖
G13.x 不变

13. PR 切分(建议,可调)

PR 范围 验收
v3-PR-1 TLS spike + crypto.go + dispatcher.go + 1 endpointliqHeatMapvertical sliceclient / repo / usecase / controller / migration / 单 cron + §4.7 config 全骨架(含 jitter/backoff/single-flight 硬门槛(a) TLS fake-success 能被正确识别并抛 ErrFakeSuccessfake-success unit test 必过);(b) curl /v1/coinglass/liq-heatmap?symbol=BTCUSDT 返回非空真实响应;(c) 不触碰 /v1/market/context 任何字段v2 合约不动)
v3-PR-2 剩 3 endpointOI / longShort / fundingHome+ 各自 mapper + 表 + cron 4 个 endpoint 都能 curl 出数据4 个 cron 都遵守 single-flight / backoff / jitter
v3-PR-3 CoinglassSignalComputer 实现 + /v1/coinglass/signal + signal 单测;如需 RawGrid 派生指标,新增显式 entity 字段§5.1 纪律) signal 字段语义稳,单测覆盖 ≥95%usecase 层零 RawGrid 直接解析
v3-PR-4 守卫扩张落 Makefile + 文档对齐AGENTS §2、harness-health 复评、README v3 + spec 标 Accepted make guard 全过README v3 段第一项

/v1/market/context 折回机制phase-1 内绝不讨论CoinglassSignalComputer 稳定后≥2 周连续运行 + signal 字段被下游 Hermes 实际消费),再开 ADR 评估是否扩 MarketContext schema。PR-1/2/3 路径上任何 PR 都不能改 /v1/market/context 响应体。


14. 非目标(明确写下来)

  • ETF Flow / CME gap / CVD / 链上 / 宏观日历 — phase 2+。
  • WebSocket 推送 — 仍在 README "绝不做v1 范围外)" 范围v3 也不开。
  • CoinGlass 账户级私有接口(如 portfolio— 不接。CoinGlass 没有"账户级"概念,所有数据都是聚合公开;不存在 G1 violations 风险。
  • 历史回填 — Python ref 没做CoinGlass V2 也没标 history backfill endpointphase-1 起从启动那刻开始 collect。
  • TLS profile 自定义(构造非常用浏览器指纹) — phase-1 沿用 firefox_133profile 调优留 phase-2。

15. 复评触发器

  • firefox_133 失效CoinGlass 升级反爬)→ 评估升级 profile 或换 client。
  • 4 个 endpoint 任一持续 7 天 ≥10% fake-success 率 → 评估 IP 池 / 代理策略。
  • CoinGlass 出 V3 dispatcher即出现 7+ 个 v 分支)→ 整段 §4.3 重做。
  • 下游 Hermes 要求把 CoinGlass 折回 /v1/market/context → phase-1.5 启动。

16. 关联 ADR / 文档

  • ADR-0001架构基础D5 全链 string本 spec 严格沿用。
  • ADR-0002数值边界本 spec §6.3 扩白名单到 coinglass需要新 ADR(暂列 ADR-0004 — 等 PR-1 落地前写定)。
  • ADR-0003per-interval 技术结构无直接冲突CoinGlass 数据可未来作为新 interval/source 接入 TechnicalStructure 但本 phase 不做。
  • ai/risk-guardrails.mdG11/G12 扩张要同步更新v3-PR-4
  • docs/coinglass/cg.md:本 spec 的协议事实底稿,保持单一来源;本 spec 引用而非复述。
  • docs/coinglass/coinglass_client.py:本 spec 的算法参考实现Go 端是 1:1 移植。

17. 决策记录2026-05-25 用户确认,全部 Accepted

# 决策 内容
Q1 namespace 独立 /v1/coinglass/... 单独 namespace。/v1/market/context 继续 Binance-onlyPR-1/2/3 路径上绝不碰 v2 合约;后续融合需 CoinglassSignalComputer 稳定后通过显式版本/字段扩展开 ADR 讨论,不在 phase-1 范围
Q2 cron cadence 接受 5/10/10/30 minliqHeatMap / OI / LSR / Funding采用。强制三条实现约束single-flight 不重叠 + 失败指数退避 + 启动 jitter。详见 §4.7 / §9。PR-1 范围内仅启 liqHeatMap 的 cron。
Q3 entity 形状方向 OK RawGrid JSONB 适用于 liqHeatMap避免行爆炸其它 3 个 entity 拍平。string-numeric 全链路保持,零 float64 出 mapper 边界。新增强约束:JSONB 仅供应商原始网格 / 元信息usecase 不允许直接解析任意 JSON 结构,需要稳定字段时由 mapper 展开成 entity 字段§5.1)。
Q4 保持 4 PR 不合并 PR-2 进 PR-1。PR-1 已含 TLS spike + 解密 + 1 endpoint + repo/usecase/controller/cron风险足够大额外 3 endpoint 塞入会模糊失败归因。按 §13 4 PR 切分推进。

v3-PR-1 硬门槛(再强调,对应 §13 PR-1 验收):

  1. TLS fake-success 识别通过(ErrFakeSuccess 单测覆盖 + 真打 CoinGlass 时能正确分类)。
  2. liqHeatMap vertical slice 打通client → repo → usecase → controller → cron 全链,curl /v1/coinglass/liq-heatmap 出真实非空数据)。
  3. v2 合约零变更(/v1/market/context 响应体 diff = 0

任一未达标PR-1 不合。