150 lines
5.1 KiB
Go
150 lines
5.1 KiB
Go
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"` // 保留欄位(可忽略)
|
||
}
|