add get symbol repository
This commit is contained in:
parent
fde3e3606f
commit
d587e95442
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"editor.fontFamily": "Fira Code, 'Fira Code', monospace"
|
||||
}
|
|
@ -4,3 +4,7 @@ Etcd:
|
|||
Hosts:
|
||||
- 127.0.0.1:2379
|
||||
Key: blockchain.rpc
|
||||
Binance:
|
||||
Key: ""
|
||||
Secret: ""
|
||||
TestMode: true
|
10
go.mod
10
go.mod
|
@ -3,12 +3,22 @@ module blockchain
|
|||
go 1.24.4
|
||||
|
||||
require (
|
||||
github.com/alicebob/miniredis/v2 v2.35.0
|
||||
github.com/zeromicro/go-zero v1.8.5
|
||||
google.golang.org/grpc v1.74.2
|
||||
google.golang.org/protobuf v1.36.6
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/bitly/go-simplejson v0.5.0 // indirect
|
||||
github.com/gorilla/websocket v1.5.3 // indirect
|
||||
github.com/jpillora/backoff v1.0.0 // indirect
|
||||
github.com/shopspring/decimal v1.4.0 // indirect
|
||||
github.com/yuin/gopher-lua v1.1.1 // indirect
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/adshao/go-binance/v2 v2.8.3
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
|
|
12
go.sum
12
go.sum
|
@ -1,9 +1,15 @@
|
|||
github.com/adshao/go-binance/v2 v2.8.3 h1:jwPRcX2u7FIO1pPoXgocyXpXhBI81A41kcmSDzS6uzo=
|
||||
github.com/adshao/go-binance/v2 v2.8.3/go.mod h1:XkkuecSyJKPolaCGf/q4ovJYB3t0P+7RUYTbGr+LMGM=
|
||||
github.com/alicebob/miniredis/v2 v2.35.0 h1:QwLphYqCEAo1eu1TqPRN2jgVMPBweeQcR21jeqDCONI=
|
||||
github.com/alicebob/miniredis/v2 v2.35.0/go.mod h1:TcL7YfarKPGDAthEtl5NBeHZfeUQj6OXMm/+iu5cLMM=
|
||||
github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8=
|
||||
github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
|
||||
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||
github.com/bitly/go-simplejson v0.5.0 h1:6IH+V8/tVMab511d5bn4M7EwGXZf9Hj6i2xSwkNEM+Y=
|
||||
github.com/bitly/go-simplejson v0.5.0/go.mod h1:cXHtHw4XUPsvGaxgjIAn8PhEWG9NfngEKAMDJEczWVA=
|
||||
github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 h1:DDGfHa7BWjL4YnC6+E63dPcxHo2sUxDIu8g3QgEJdRY=
|
||||
github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4=
|
||||
github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
|
||||
github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
|
||||
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
|
||||
|
@ -60,6 +66,8 @@ github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1 h1:K6RDEckDVWvDI9JAJY
|
|||
github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
|
||||
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/grafana/pyroscope-go v1.2.2 h1:uvKCyZMD724RkaCEMrSTC38Yn7AnFe8S2wiAIYdDPCE=
|
||||
github.com/grafana/pyroscope-go v1.2.2/go.mod h1:zzT9QXQAp2Iz2ZdS216UiV8y9uXJYQiGE1q8v1FyhqU=
|
||||
github.com/grafana/pyroscope-go/godeltaprof v0.1.8 h1:iwOtYXeeVSAeYefJNaxDytgjKtUuKQbJqgAIjlnicKg=
|
||||
|
@ -70,6 +78,8 @@ github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542 h1:2VTzZjLZBgl62/EtslC
|
|||
github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542/go.mod h1:Ow0tF8D4Kplbc8s8sSb3V2oUCygFHVp8gC3Dn6U4MNI=
|
||||
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
|
||||
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
|
||||
github.com/jpillora/backoff v1.0.0 h1:uvFg412JmmHBHw7iwprIxkPMI+sGQ4kzOWsMeHnm2EA=
|
||||
github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4=
|
||||
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
|
||||
|
@ -125,6 +135,8 @@ github.com/redis/go-redis/v9 v9.11.0 h1:E3S08Gl/nJNn5vkxd2i78wZxWAPNZgUNTp8WIJUA
|
|||
github.com/redis/go-redis/v9 v9.11.0/go.mod h1:huWgSWd8mW6+m0VPhJjSSQ+d6Nh1VICQ6Q5lHuCH/Iw=
|
||||
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
|
||||
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
|
||||
github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k=
|
||||
github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME=
|
||||
github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI=
|
||||
github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
|
||||
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
||||
|
|
|
@ -4,4 +4,11 @@ import "github.com/zeromicro/go-zero/zrpc"
|
|||
|
||||
type Config struct {
|
||||
zrpc.RpcServerConf
|
||||
Binance
|
||||
}
|
||||
|
||||
type Binance struct {
|
||||
Key string
|
||||
Secret string
|
||||
TestMode bool
|
||||
}
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
package blockchain
|
||||
|
||||
const RedisKeySymbolList = "symbol:all"
|
||||
|
||||
const SymbolExpire = 3600 // 秒(目前為一小時)
|
|
@ -0,0 +1,38 @@
|
|||
package entity
|
||||
|
||||
// Symbol 代表交易對資訊
|
||||
type Symbol struct {
|
||||
Symbol string `json:"symbol"` // 交易對名稱 (BTCUSDT)
|
||||
Status string `json:"status"` // 狀態(如 "TRADING" 表示可交易)
|
||||
BaseAsset string `json:"base_asset"` // 主幣種(如 BTCUSDT 的 BTC)
|
||||
BaseAssetPrecision int `json:"base_asset_precision"` // 主幣的小數點精度
|
||||
QuoteAsset string `json:"quote_asset"` // 報價幣種(如 BTCUSDT 的 USDT)
|
||||
QuoteAssetPrecision int `json:"quote_asset_precision"` // 報價資產顯示的小數位數
|
||||
}
|
||||
|
||||
func (s *Symbol) TableName() string {
|
||||
return "symbol"
|
||||
}
|
||||
|
||||
|
||||
// Symbol 這個是幣安的
|
||||
//type Symbol struct {
|
||||
// Symbol string `json:"symbol"` // 交易對名稱(如 "BTCUSDT")
|
||||
// Status string `json:"status"` // 狀態(如 "TRADING" 表示可交易)
|
||||
// BaseAsset string `json:"baseAsset"` // 主幣種(如 BTCUSDT 的 BTC)
|
||||
// BaseAssetPrecision int `json:"baseAssetPrecision"` // 主幣的小數點精度
|
||||
// QuoteAsset string `json:"quoteAsset"` // 報價幣種(如 BTCUSDT 的 USDT)
|
||||
// QuotePrecision int `json:"quotePrecision"` // 報價幣的小數點精度
|
||||
// QuoteAssetPrecision int `json:"quoteAssetPrecision"` // 報價資產顯示的小數位數
|
||||
// BaseCommissionPrecision int32 `json:"baseCommissionPrecision"` // 主幣手續費精度
|
||||
// QuoteCommissionPrecision int32 `json:"quoteCommissionPrecision"` // 報價幣手續費精度
|
||||
// OrderTypes []string `json:"orderTypes"` // 支援的下單類型(如 "LIMIT", "MARKET", "STOP_LOSS" 等)
|
||||
// IcebergAllowed bool `json:"icebergAllowed"` // 是否允許冰山單
|
||||
// OcoAllowed bool `json:"ocoAllowed"` // 是否允許 OCO 單(條件單組合)
|
||||
// QuoteOrderQtyMarketAllowed bool `json:"quoteOrderQtyMarketAllowed"` // 市價單是否可直接輸入報價幣金額
|
||||
// IsSpotTradingAllowed bool `json:"isSpotTradingAllowed"` // 是否允許現貨交易
|
||||
// IsMarginTradingAllowed bool `json:"isMarginTradingAllowed"` // 是否允許槓桿/融資交易
|
||||
// Filters []map[string]interface{} `json:"filters"` // 下單限制規則(如最小下單量、價格間隔等)
|
||||
// Permissions []string `json:"permissions"` // 可用權限(如 "SPOT", "MARGIN")
|
||||
// PermissionSets [][]string `json:"permissionSets"` // 權限組合(通常不常用)
|
||||
//}
|
|
@ -0,0 +1,10 @@
|
|||
package repository
|
||||
|
||||
import (
|
||||
"blockchain/internal/domain/entity"
|
||||
"context"
|
||||
)
|
||||
|
||||
type DataSourceRepository interface {
|
||||
GetSymbols(ctx context.Context) ([]entity.Symbol, error)
|
||||
}
|
|
@ -0,0 +1,124 @@
|
|||
package repository
|
||||
|
||||
import (
|
||||
"blockchain/internal/config"
|
||||
"blockchain/internal/domain/blockchain"
|
||||
"blockchain/internal/domain/entity"
|
||||
"blockchain/internal/domain/repository"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/adshao/go-binance/v2"
|
||||
"github.com/zeromicro/go-zero/core/logx"
|
||||
"github.com/zeromicro/go-zero/core/stores/redis"
|
||||
"github.com/zeromicro/go-zero/core/syncx"
|
||||
)
|
||||
|
||||
type BinanceRepositoryParam struct {
|
||||
Conf *config.Binance
|
||||
Redis *redis.Redis
|
||||
}
|
||||
|
||||
type BinanceRepository struct {
|
||||
Client *binance.Client
|
||||
rds *redis.Redis
|
||||
barrier syncx.SingleFlight
|
||||
}
|
||||
|
||||
func MustBinanceRepository(param BinanceRepositoryParam) repository.DataSourceRepository {
|
||||
apiKey := ""
|
||||
secret := ""
|
||||
if param.Conf.TestMode {
|
||||
binance.UseTestnet = true
|
||||
}
|
||||
client := binance.NewClient(apiKey, secret)
|
||||
|
||||
return &BinanceRepository{
|
||||
Client: client,
|
||||
rds: param.Redis,
|
||||
barrier: syncx.NewSingleFlight(),
|
||||
}
|
||||
}
|
||||
|
||||
func (repo *BinanceRepository) GetSymbols(ctx context.Context) ([]entity.Symbol, error) {
|
||||
// 優先從 redis hash 拿
|
||||
cached, err := repo.rds.Hgetall(blockchain.RedisKeySymbolList)
|
||||
if err == nil && len(cached) > 0 {
|
||||
symbols := make([]entity.Symbol, 0, len(cached))
|
||||
canUseCache := true
|
||||
for _, v := range cached {
|
||||
var symbol entity.Symbol
|
||||
if err := json.Unmarshal([]byte(v), &symbol); err == nil {
|
||||
symbols = append(symbols, symbol)
|
||||
} else {
|
||||
// 如果任何一個反序列化失敗,代表快取可能已損壞,最好是回源重新拉取
|
||||
canUseCache = false
|
||||
break
|
||||
}
|
||||
}
|
||||
if canUseCache {
|
||||
return symbols, nil
|
||||
}
|
||||
}
|
||||
// 用 SingleFlight 保證只有一個請求真的去 Binance
|
||||
val, err := repo.barrier.Do(blockchain.RedisKeySymbolList, func() (any, error) {
|
||||
// 拉 source
|
||||
srcSymbols, err := repo.getSymbolsFromSource(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result := make([]entity.Symbol, 0, len(srcSymbols))
|
||||
hashData := make(map[string]string, len(srcSymbols))
|
||||
for _, s := range srcSymbols {
|
||||
// 只挑目前需要的欄位
|
||||
symbolEntity := entity.Symbol{
|
||||
Symbol: s.Symbol,
|
||||
Status: s.Status,
|
||||
BaseAsset: s.BaseAsset,
|
||||
BaseAssetPrecision: s.BaseAssetPrecision,
|
||||
QuoteAsset: s.QuoteAsset,
|
||||
QuoteAssetPrecision: s.QuoteAssetPrecision,
|
||||
}
|
||||
result = append(result, symbolEntity)
|
||||
|
||||
// 將單一 symbol 序列化,準備寫入 hash
|
||||
raw, err := json.Marshal(symbolEntity)
|
||||
if err != nil {
|
||||
logx.Error("failed to marshal symbol entity")
|
||||
|
||||
continue
|
||||
}
|
||||
hashData[symbolEntity.Symbol] = string(raw)
|
||||
}
|
||||
|
||||
if len(hashData) > 0 {
|
||||
// 使用 HMSET 一次寫入多個欄位到 hash
|
||||
if err := repo.rds.Hmset(blockchain.RedisKeySymbolList, hashData); err == nil {
|
||||
// 再對整個 key 設置過期時間
|
||||
_ = repo.rds.Expire(blockchain.RedisKeySymbolList, blockchain.SymbolExpire)
|
||||
}
|
||||
}
|
||||
|
||||
return result, nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return val.([]entity.Symbol), nil
|
||||
}
|
||||
|
||||
func (repo *BinanceRepository) getSymbolsFromSource(ctx context.Context) ([]binance.Symbol, error) {
|
||||
if repo.Client == nil {
|
||||
return nil, fmt.Errorf("binance client not initialized")
|
||||
}
|
||||
|
||||
// 取得幣安交易所資訊
|
||||
info, err := repo.Client.NewExchangeInfoService().Do(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return info.Symbols, nil
|
||||
}
|
|
@ -0,0 +1,83 @@
|
|||
package repository
|
||||
|
||||
import (
|
||||
"blockchain/internal/config"
|
||||
"context"
|
||||
"fmt"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
miniredis "github.com/alicebob/miniredis/v2"
|
||||
"github.com/zeromicro/go-zero/core/stores/redis"
|
||||
)
|
||||
|
||||
func setupMiniRedis() (*miniredis.Miniredis, *redis.Redis) {
|
||||
// 啟動 setupMiniRedis 作為模擬的 Redis 服務
|
||||
mr, err := miniredis.Run()
|
||||
if err != nil {
|
||||
panic("failed to start miniRedis: " + err.Error())
|
||||
}
|
||||
|
||||
// 使用 setupMiniRedis 的地址配置 go-zero Redis 客戶端
|
||||
redisConf := redis.RedisConf{
|
||||
Host: mr.Addr(),
|
||||
Type: "node",
|
||||
}
|
||||
r := redis.MustNewRedis(redisConf)
|
||||
|
||||
return mr, r
|
||||
}
|
||||
|
||||
func TestGetSymbolsFromSource_TableDriven(t *testing.T) {
|
||||
mr, rdb := setupMiniRedis()
|
||||
defer mr.Close()
|
||||
|
||||
cases := []struct {
|
||||
name string
|
||||
wantSymbols []string
|
||||
expectErr bool
|
||||
}{
|
||||
{
|
||||
name: "ok",
|
||||
//wantSymbols: []string{"BTCUSDT", "ETHUSDT"},
|
||||
expectErr: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
repo := MustBinanceRepository(BinanceRepositoryParam{
|
||||
Conf: &config.Binance{
|
||||
Key: "",
|
||||
Secret: "",
|
||||
TestMode: true,
|
||||
},
|
||||
Redis: rdb,
|
||||
})
|
||||
|
||||
var wg sync.WaitGroup
|
||||
total := 10
|
||||
|
||||
for range total {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
symbols, err := repo.GetSymbols(context.Background())
|
||||
fmt.Println(symbols)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}()
|
||||
}
|
||||
wg.Wait()
|
||||
//got, err := repo.GetSymbolsFromSource(context.Background())
|
||||
//if tc.expectErr {
|
||||
// assert.Error(t, err)
|
||||
// assert.Nil(t, got)
|
||||
//} else {
|
||||
// assert.NoError(t, err)
|
||||
// assert.Equal(t, tc.wantSymbols, got)
|
||||
//}
|
||||
})
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue