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