diff --git a/internal/domain/entity/kline.go b/internal/domain/entity/kline.go new file mode 100644 index 0000000..6e2ee3b --- /dev/null +++ b/internal/domain/entity/kline.go @@ -0,0 +1,21 @@ +package entity + +type Kline struct { + OpenTime int64 `csv:"open_time"` // 開盤時間(毫秒) + Open string `csv:"open"` // 開盤價 + High string `csv:"high"` // 最高價 + Low string `csv:"low"` // 最低價 + Close string `csv:"close"` // 收盤價 + Volume string `csv:"volume"` // 成交量 + CloseTime int64 `csv:"close_time"` // 收盤時間(毫秒) + QuoteAssetVolume string `csv:"quote_asset_volume"` // 成交額(以報價資產計) + NumberOfTrades int `csv:"number_of_trades"` // 交易筆數 + TakerBuyBaseAssetVolume string `csv:"taker_buy_base_asset_volume"` // 主動買入成交量 + TakerBuyQuoteAssetVolume string `csv:"taker_buy_quote_asset_volume"` // 主動買入成交額 + Symbol string // 交易對 + Interval string // 12h,15m,1d,1h,1m,1s,2h,30m,3m,4h,5m,6h,8h +} + +func (s *Kline) TableName() string { + return "symbol" +} diff --git a/internal/domain/repository/data_source.go b/internal/domain/repository/data_source.go index 271da25..e56f614 100644 --- a/internal/domain/repository/data_source.go +++ b/internal/domain/repository/data_source.go @@ -7,4 +7,10 @@ import ( type DataSourceRepository interface { GetSymbols(ctx context.Context) ([]*entity.Symbol, error) + KlineDownloader +} + +type KlineDownloader interface { + // FetchHistoryKline 抓歷史K線資料,startMillis=0 表示從最早,endMillis=0 表示到最新 + FetchHistoryKline(ctx context.Context, symbol string, interval string, startMillis, endMillis int64) ([]*entity.Kline, error) } diff --git a/internal/repository/data_source_binance.go b/internal/repository/data_source_binance.go index ea01ee1..25f8664 100644 --- a/internal/repository/data_source_binance.go +++ b/internal/repository/data_source_binance.go @@ -1,13 +1,22 @@ package repository import ( + "archive/zip" "blockchain/internal/config" "blockchain/internal/domain/blockchain" "blockchain/internal/domain/entity" "blockchain/internal/domain/repository" "context" + "encoding/csv" "encoding/json" "fmt" + "io" + "net/http" + "os" + "path/filepath" + "strconv" + "strings" + "time" "github.com/adshao/go-binance/v2" "github.com/zeromicro/go-zero/core/logx" @@ -122,3 +131,87 @@ func (repo *BinanceRepository) getSymbolsFromSource(ctx context.Context) ([]bina return info.Symbols, nil } + +func (repo *BinanceRepository) FetchHistoryKline(ctx context.Context, symbol string, interval string, startMillis, endMillis int64) ([]*entity.Kline, error) { + const baseURL = "https://data.binance.vision/data/spot/daily/klines" + // 計算時間範圍 + var startDate, endDate time.Time + if startMillis == 0 { + // 若沒指定,直接假設 2009-01-01(比特幣最早有記錄的位置,其他幣不太可能早過這個) + startDate = time.Date(2009, 1, 1, 0, 0, 0, 0, time.UTC) + } else { + startDate = time.UnixMilli(startMillis) + } + if endMillis == 0 { + endDate = time.Now() + } else { + endDate = time.UnixMilli(endMillis) + } + symbol = strings.ToUpper(symbol) + + var result []*entity.Kline + + // 逐天下載 + for d := startDate; !d.After(endDate); d = d.AddDate(0, 0, 1) { + select { + case <-ctx.Done(): + return result, ctx.Err() + default: + } + dateStr := d.Format("2006-01-02") + zipFile := fmt.Sprintf("%s-%s-%s.zip", symbol, interval, dateStr) + url := fmt.Sprintf("%s/%s/%s/%s", baseURL, symbol, interval, zipFile) + + resp, err := http.Get(url) + if err != nil || resp.StatusCode != 200 { + continue // 檔案不存在就跳過 + } + tmpPath := filepath.Join(os.TempDir(), zipFile) + out, _ := os.Create(tmpPath) + io.Copy(out, resp.Body) + out.Close() + resp.Body.Close() + + // 解壓縮 + r, err := zip.OpenReader(tmpPath) + if err != nil { + os.Remove(tmpPath) + continue + } + for _, f := range r.File { + rc, _ := f.Open() + reader := csv.NewReader(rc) + for { + record, err := reader.Read() + if err == io.EOF { + break + } + if err != nil || len(record) < 12 { + continue + } + ot, _ := strconv.ParseInt(record[0], 10, 64) + ct, _ := strconv.ParseInt(record[6], 10, 64) + num, _ := strconv.Atoi(record[8]) + result = append(result, &entity.Kline{ + OpenTime: ot, + Open: record[1], + High: record[2], + Low: record[3], + Close: record[4], + Volume: record[5], + CloseTime: ct, + QuoteAssetVolume: record[7], + NumberOfTrades: num, + TakerBuyBaseAssetVolume: record[9], + TakerBuyQuoteAssetVolume: record[10], + Ignore: record[11], + }) + } + rc.Close() + } + r.Close() + os.Remove(tmpPath) + } + + return result, nil +}