blockchain/internal/repository/binance_adapter.go

150 lines
5.1 KiB
Go
Raw Normal View History

2025-08-09 16:36:24 +00:00
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"` // 保留欄位(可忽略)
}