package repository import ( "blockchain/internal/domain/blockchain" "blockchain/internal/domain/repository" "fmt" "github.com/goccy/go-json" "strings" "time" ) type BinanceAdapterParam struct { Name string WsURL string ClientPingInterval time.Duration ReadDeadline time.Duration } type BinanceAdapter struct { name string wsUrl string clientPingInterval time.Duration readDeadline time.Duration } func NewBinanceAdapter(param BinanceAdapterParam) repository.ExchangeAdapter { return &BinanceAdapter{ name: param.Name, wsUrl: param.WsURL, clientPingInterval: param.ClientPingInterval, readDeadline: param.ReadDeadline, } } func (repo *BinanceAdapter) Name() string { return repo.name } func (repo *BinanceAdapter) URL() string { return repo.wsUrl } // NormalizeSymbol 我系統內部也是統一用 大寫 BTCUSDT func (repo *BinanceAdapter) NormalizeSymbol(internal string) string { // 驗證:必須全部是大寫英文,且長度至少 6(如 BTCUSDT) if !isAllUpperAlpha(internal) { panic(fmt.Sprintf("invalid symbol format: %s (must be all uppercase letters, e.g., BTCUSDT)", internal)) } // Binance 訂閱格式是小寫 return strings.ToLower(internal) } func (repo *BinanceAdapter) DenormalizeSymbol(external string) string { // Binance 回傳小寫,轉回內部格式(大寫) return strings.ToUpper(external) } func (repo *BinanceAdapter) BuildSubscribe(symbols []string, interval blockchain.Interval) ([][]byte, error) { params := make([]string, 0, len(symbols)) for _, s := range symbols { params = append(params, fmt.Sprintf("%s@kline_%s", repo.NormalizeSymbol(s), interval)) } req := map[string]any{"method": "SUBSCRIBE", "params": params, "id": time.Now().UnixNano()} b, _ := json.Marshal(req) return [][]byte{b}, nil } func (repo *BinanceAdapter) BuildUnsubscribe(symbols []string, interval blockchain.Interval) ([][]byte, error) { params := make([]string, 0, len(symbols)) for _, s := range symbols { params = append(params, fmt.Sprintf("%s@kline_%s", repo.NormalizeSymbol(s), interval)) } req := map[string]any{"method": "UNSUBSCRIBE", "params": params, "id": time.Now().UnixNano()} b, _ := json.Marshal(req) return [][]byte{b}, nil } func (repo *BinanceAdapter) ParseKLines(msg []byte) ([]blockchain.Kline, error) { res := BinanceKlineEvent{} if err := json.Unmarshal(msg, &res); err != nil { return nil, nil } // 不是 kline 就略過 if res.EventType != "kline" { return nil, nil } return []blockchain.Kline{{ Exchange: repo.Name(), Symbol: repo.DenormalizeSymbol(res.Symbol), Interval: blockchain.Interval(res.K.Interval), OpenTime: res.K.StartTime, CloseTime: res.K.CloseTime, Open: res.K.Open, High: res.K.High, Low: res.K.Low, Close: res.K.Close, Volume: res.K.Volume, Final: res.K.Final, Raw: msg, }}, nil } func (repo *BinanceAdapter) ClientPingInterval() time.Duration { return repo.clientPingInterval } func (repo *BinanceAdapter) ReadDeadline() time.Duration { return repo.readDeadline } // 工具函式:檢查是否全為大寫英文字母 func isAllUpperAlpha(s string) bool { if len(s) == 0 { return false } for _, r := range s { if r < 'A' || r > 'Z' { return false } } return true } // BinanceKlineEvent 代表幣安 WebSocket 推送的 kline 事件 // 範例:{"e":"kline","E":..., "s":"BTCUSDT", "k":{...}} type BinanceKlineEvent struct { EventType string `json:"e"` // 事件類型,固定為 "kline" EventTime int64 `json:"E"` // 事件時間 (毫秒 UNIX 時戳) Symbol string `json:"s"` // 交易對,例如 "BTCUSDT" K BinanceKlineBody `json:"k"` // K 線細節 } // BinanceKlineBody 對應 "k" 物件(單一根 K 線的詳細資訊) type BinanceKlineBody struct { StartTime int64 `json:"t"` // 本根 K 線開盤時間 (ms) CloseTime int64 `json:"T"` // 本根 K 線關閉時間/結束時間 (ms) Symbol string `json:"s"` // 交易對(與外層 s 相同) Interval string `json:"i"` // 週期,例如 "1m","5m","1h","1d","1M"(月線) FirstTradeID int64 `json:"f"` // 本根K線包含的第一筆成交ID LastTradeID int64 `json:"L"` // 本根K線包含的最後一筆成交ID Open string `json:"o"` // 開盤價(字串,避免浮點誤差) Close string `json:"c"` // 收盤價(字串) High string `json:"h"` // 最高價(字串) Low string `json:"l"` // 最低價(字串) Volume string `json:"v"` // 交易量(Base 資產數量,字串) TradeCount int64 `json:"n"` // 成交筆數 Final bool `json:"x"` // 是否已收盤(true=此根K線已完成;false=仍在形成中) QuoteAssetVolume string `json:"q"` // 交易額(Quote 資產成交額,字串) TakerBuyBaseVolume string `json:"V"` // 主動買單成交量(Base),字串 TakerBuyQuoteVolume string `json:"Q"` // 主動買單成交額(Quote),字串 Ignore string `json:"B"` // 保留欄位(可忽略) }