├── binance ├── readme.md ├── biz_order_book_test.go ├── testdata │ ├── gock.json │ ├── order_book_shot.json │ └── ccxt_book.log ├── base_test.go ├── biz_account.go ├── biz_ticker_test.go ├── common_test.go ├── AGENTS.md ├── biz_order_book.go ├── common.go ├── biz_order_test.go ├── biz_order_algo.go ├── ws_biz_test.go ├── ws_order_test.go ├── biz_order_algo_test.go └── biz_order_create.go ├── china ├── data.go ├── biz_test.go ├── readme.md ├── types.go ├── entry.go └── common.go ├── utils ├── data.go ├── text.go ├── num_utils.go ├── common_test.go ├── tf_utils_test.go ├── exg_test.go ├── misc_test.go ├── exg.go ├── common.go ├── file.go ├── crypto_test.go ├── crypto.go ├── tf_utils.go ├── dec_precs.go └── dec_precs_test.go ├── errs ├── types.go ├── main.go └── data.go ├── bex ├── common.go ├── entrys.go └── entrys_test.go ├── log ├── readme.md ├── mlogger.go ├── zap_text_core.go ├── config.go ├── capture.go ├── global.go └── log.go ├── exts.go ├── bybit ├── biz_test.go ├── base_test.go ├── biz_ticker.go └── types.go ├── .gitignore ├── common_test.go ├── LICENSE ├── go.mod ├── base.go ├── biz_test.go ├── AGENTS.md ├── go.sum ├── intf.go ├── data.go └── readme.cn.md /binance/readme.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /china/data.go: -------------------------------------------------------------------------------- 1 | package china 2 | 3 | import "time" 4 | 5 | var ( 6 | defTimeLoc, _ = time.LoadLocation("Asia/Shanghai") 7 | ) 8 | -------------------------------------------------------------------------------- /utils/data.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | var ( 4 | tfSecsCache = make(map[string]int) 5 | ) 6 | 7 | const ( 8 | UriEncodeSafe = "~()*!.'" 9 | ) 10 | -------------------------------------------------------------------------------- /errs/types.go: -------------------------------------------------------------------------------- 1 | package errs 2 | 3 | type Error struct { 4 | Code int 5 | msg string 6 | Stack string 7 | err error 8 | BizCode int 9 | Data interface{} 10 | } 11 | -------------------------------------------------------------------------------- /bex/common.go: -------------------------------------------------------------------------------- 1 | package bex 2 | 3 | import ( 4 | "github.com/banbox/banexg" 5 | "github.com/banbox/banexg/errs" 6 | ) 7 | 8 | type FuncNewExchange = func(map[string]interface{}) (banexg.BanExchange, *errs.Error) 9 | 10 | var newExgs map[string]FuncNewExchange 11 | -------------------------------------------------------------------------------- /log/readme.md: -------------------------------------------------------------------------------- 1 | # 说明 2 | 此日志记录主要代码来自https://github.com/milvus-io/milvus/tree/master/pkg/log 3 | 4 | # 如何使用 5 | ```go 6 | imports github.com/banbox/banexg/log 7 | 8 | // Optional 9 | log.SetupByArgs(true, "D:/test.log") 10 | log.Debugf("debug msg") 11 | log.Infof("debug msg") 12 | ``` -------------------------------------------------------------------------------- /exts.go: -------------------------------------------------------------------------------- 1 | package banexg 2 | 3 | import ( 4 | "go.uber.org/zap/zapcore" 5 | "net/http" 6 | "strings" 7 | ) 8 | 9 | type HttpHeader http.Header 10 | 11 | func (h HttpHeader) MarshalLogObject(enc zapcore.ObjectEncoder) error { 12 | for k, v := range h { 13 | enc.AddString(k, strings.Join(v, ",")) 14 | } 15 | return nil 16 | } 17 | -------------------------------------------------------------------------------- /utils/text.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "golang.org/x/text/cases" 5 | "golang.org/x/text/language" 6 | "strings" 7 | ) 8 | 9 | func SnakeToCamel(input string) string { 10 | parts := strings.Split(input, "_") 11 | caser := cases.Title(language.English) 12 | for i, text := range parts { 13 | parts[i] = caser.String(text) 14 | } 15 | return strings.Join(parts, "") 16 | } 17 | -------------------------------------------------------------------------------- /binance/biz_order_book_test.go: -------------------------------------------------------------------------------- 1 | package binance 2 | 3 | import ( 4 | "github.com/banbox/banexg/log" 5 | "go.uber.org/zap" 6 | "testing" 7 | ) 8 | 9 | func TestFetchOrderBook(t *testing.T) { 10 | exg := getBinance(nil) 11 | symbol := "ETH/USDT" 12 | res, err := exg.FetchOrderBook(symbol, 100, nil) 13 | if err != nil { 14 | panic(err) 15 | } 16 | log.Info("fetch order book", zap.Any("v", res)) 17 | } 18 | -------------------------------------------------------------------------------- /utils/num_utils.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import "math" 4 | 5 | const thresFloat64Eq = 1e-9 6 | 7 | /* 8 | EqualNearly 判断两个float是否近似相等,解决浮点精读导致不等 9 | */ 10 | func EqualNearly(a, b float64) bool { 11 | return EqualIn(a, b, thresFloat64Eq) 12 | } 13 | 14 | /* 15 | EqualIn 判断两个float是否在一定范围内近似相等 16 | */ 17 | func EqualIn(a, b, thres float64) bool { 18 | if math.IsNaN(a) && math.IsNaN(b) { 19 | return true 20 | } 21 | return math.Abs(a-b) <= thres 22 | } 23 | -------------------------------------------------------------------------------- /bybit/biz_test.go: -------------------------------------------------------------------------------- 1 | package bybit 2 | 3 | import ( 4 | "fmt" 5 | "github.com/banbox/banexg/utils" 6 | "testing" 7 | ) 8 | 9 | func TestLoadMarkets(t *testing.T) { 10 | exg := getBybit(nil) 11 | markets, err := exg.LoadMarkets(false, nil) 12 | if err != nil { 13 | panic(err) 14 | } 15 | outPath := "D:/bybit_markets.json" 16 | err_ := utils.WriteJsonFile(outPath, markets) 17 | if err_ != nil { 18 | panic(err_) 19 | } 20 | fmt.Printf("dump markets at: %v", outPath) 21 | } 22 | -------------------------------------------------------------------------------- /log/mlogger.go: -------------------------------------------------------------------------------- 1 | package log 2 | 3 | import ( 4 | "go.uber.org/zap" 5 | "sync/atomic" 6 | ) 7 | 8 | // MLogger is a wrapper type of zap.Logger. 9 | type MLogger struct { 10 | *zap.Logger 11 | rl atomic.Value // *utils.ReconfigurableRateLimiter 12 | } 13 | 14 | // With encapsulates zap.Logger With method to return MLogger instance. 15 | func (l *MLogger) With(fields ...zap.Field) *MLogger { 16 | nl := &MLogger{ 17 | Logger: l.Logger.With(fields...), 18 | } 19 | return nl 20 | } 21 | -------------------------------------------------------------------------------- /binance/testdata/gock.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "url": "https://dapi.binance.com/dapi/v1/exchangeInfo", 4 | "rsp_path": "dapiPublicGetExchangeInfo.json" 5 | }, 6 | { 7 | "url": "https://fapi.binance.com/fapi/v1/exchangeInfo", 8 | "rsp_path": "fapiPublicGetExchangeInfo.json" 9 | }, 10 | { 11 | "url": "https://api.binance.com/api/v3/exchangeInfo", 12 | "rsp_path": "publicGetExchangeInfo.json" 13 | }, 14 | { 15 | "url": "https://api.binance.com/sapi/v1/capital/config/getall", 16 | "rsp_path": "sapiGetCapitalConfigGetall.json" 17 | } 18 | ] -------------------------------------------------------------------------------- /bybit/base_test.go: -------------------------------------------------------------------------------- 1 | package bybit 2 | 3 | import ( 4 | "github.com/banbox/banexg/log" 5 | "github.com/banbox/banexg/utils" 6 | ) 7 | 8 | func getBybit(param map[string]interface{}) *Bybit { 9 | log.Setup("info", "") 10 | args := utils.SafeParams(param) 11 | local := make(map[string]interface{}) 12 | err_ := utils.ReadJsonFile("local.json", &local, utils.JsonNumAuto) 13 | if err_ != nil { 14 | panic(err_) 15 | } 16 | for k, v := range local { 17 | args[k] = v 18 | } 19 | exg, err := New(args) 20 | if err != nil { 21 | panic(err) 22 | } 23 | return exg 24 | } 25 | -------------------------------------------------------------------------------- /binance/base_test.go: -------------------------------------------------------------------------------- 1 | package binance 2 | 3 | import ( 4 | "github.com/banbox/banexg/log" 5 | "github.com/banbox/banexg/utils" 6 | ) 7 | 8 | func getBinance(param map[string]interface{}) *Binance { 9 | log.Setup("info", "") 10 | args := utils.SafeParams(param) 11 | local := make(map[string]interface{}) 12 | err_ := utils.ReadJsonFile("local.json", &local, utils.JsonNumAuto) 13 | if err_ != nil { 14 | panic(err_) 15 | } 16 | for k, v := range local { 17 | args[k] = v 18 | } 19 | exg, err := New(args) 20 | if err != nil { 21 | panic(err) 22 | } 23 | return exg 24 | } 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | .claude 3 | **/local.json 4 | 5 | # If you prefer the allow list template instead of the deny list, see community template: 6 | # https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore 7 | # 8 | # Binaries for programs and plugins 9 | *.exe 10 | *.exe~ 11 | *.dll 12 | *.so 13 | *.dylib 14 | 15 | # Test binary, built with `go test -c` 16 | *.test 17 | 18 | # Output of the go coverage tool, specifically when used with LiteIDE 19 | *.out 20 | 21 | # Dependency directories (remove the comment below to include it) 22 | # vendor/ 23 | 24 | # Go workspace file 25 | go.work 26 | -------------------------------------------------------------------------------- /binance/biz_account.go: -------------------------------------------------------------------------------- 1 | package binance 2 | 3 | import ( 4 | "github.com/banbox/banexg" 5 | "github.com/banbox/banexg/errs" 6 | "github.com/banbox/banexg/utils" 7 | ) 8 | 9 | func (e *Binance) WatchAccountConfig(params map[string]interface{}) (chan *banexg.AccountConfig, *errs.Error) { 10 | _, client, err := e.getAuthClient(params) 11 | if err != nil { 12 | return nil, err 13 | } 14 | args := utils.SafeParams(params) 15 | chanKey := client.Prefix("accConfig") 16 | create := func(cap int) chan *banexg.AccountConfig { return make(chan *banexg.AccountConfig, cap) } 17 | out := banexg.GetWsOutChan(e.Exchange, chanKey, create, args) 18 | e.AddWsChanRefs(chanKey, "account") 19 | return out, nil 20 | } 21 | -------------------------------------------------------------------------------- /bex/entrys.go: -------------------------------------------------------------------------------- 1 | package bex 2 | 3 | import ( 4 | "github.com/banbox/banexg" 5 | "github.com/banbox/banexg/binance" 6 | "github.com/banbox/banexg/bybit" 7 | "github.com/banbox/banexg/china" 8 | "github.com/banbox/banexg/errs" 9 | "github.com/banbox/banexg/utils" 10 | ) 11 | 12 | func init() { 13 | newExgs = map[string]FuncNewExchange{ 14 | "binance": binance.NewExchange, 15 | "bybit": bybit.NewExchange, 16 | "china": china.NewExchange, 17 | } 18 | } 19 | 20 | func New(name string, options map[string]interface{}) (banexg.BanExchange, *errs.Error) { 21 | fn, ok := newExgs[name] 22 | if !ok { 23 | return nil, errs.NewMsg(errs.CodeBadExgName, "invalid exg name: %s", name) 24 | } 25 | return fn(utils.SafeParams(options)) 26 | } 27 | -------------------------------------------------------------------------------- /china/biz_test.go: -------------------------------------------------------------------------------- 1 | package china 2 | 3 | import ( 4 | "github.com/banbox/banexg" 5 | "testing" 6 | ) 7 | 8 | func TestChina_MapMarket(t *testing.T) { 9 | exg, err := NewExchange(map[string]interface{}{ 10 | banexg.OptMarketType: banexg.MarketLinear, 11 | banexg.OptContractType: banexg.MarketSwap, 12 | }) 13 | if err != nil { 14 | panic(err) 15 | } 16 | type Item struct { 17 | code string 18 | year int 19 | result string 20 | } 21 | items := []*Item{ 22 | {code: "AP005", year: 2019, result: "AP2005"}, 23 | {code: "AP005", year: 2020, result: "AP2005"}, 24 | {code: "ag2102", year: 2020, result: "AG2102"}, 25 | {code: "ag2102", year: 2021, result: "AG2102"}, 26 | } 27 | for _, it := range items { 28 | mar, err := exg.MapMarket(it.code, it.year) 29 | if err != nil { 30 | panic(err) 31 | } 32 | if mar.Symbol != it.result { 33 | t.Error(it.code, it.year, it.result, mar.Symbol) 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /china/readme.md: -------------------------------------------------------------------------------- 1 | # 期货交易信息 2 | ## 手续费 3 | [金信期货手续费](https://www.jinxinqh.com/article/3667) 4 | [东方财富期货手续费公告](https://www.eastmoneyfutures.com/pages/service/sxf.html) 5 | [东航期货手续费](https://www.cesfutures.com/page/gsgg/#/index/jyssxf/qh) 6 | 7 | ## 保证金 8 | [方正中期期货保证金公告](https://www.founderfu.com/qihuojiaoyiguize/259.html) 9 | [东航期货保证金](https://www.cesfutures.com/page/gsgg/#/index/jcbzj) 10 | [创元期货保证金](https://www.cyqh.com.cn/trading-tips/19373.html) 11 | 12 | 13 | ## 涨跌停&最小变动 14 | [东方财富期货品种规则](https://www.eastmoneyfutures.com/pages/service/jygz.html) 15 | [金信期货品种最小变动](https://www.jinxinqh.com/article/68) 16 | 17 | ## 异常交易监控 18 | [金信期货各交易所异常交易监控标准和处理程序](https://www.jinxinqh.com/article/5108) 19 | 20 | ## 日/夜盘交易时间 21 | [金信期货日夜盘交易时间表](https://www.jinxinqh.com/article/205) 22 | [方正中期期货交易时段汇总](https://www.founderfu.com/qihuojiaoyiguize/368.html) 23 | 24 | ## 具体品种详细信息 25 | [申银万国期货品种](https://www.sywgqh.com.cn/Pc/Invest_School/Future_School) 26 | -------------------------------------------------------------------------------- /bex/entrys_test.go: -------------------------------------------------------------------------------- 1 | package bex 2 | 3 | import ( 4 | "fmt" 5 | "github.com/banbox/banexg/utils" 6 | "testing" 7 | ) 8 | 9 | func getExchange(name string, param map[string]interface{}) map[string]interface{} { 10 | args := utils.SafeParams(param) 11 | local := make(map[string]interface{}) 12 | localpath := name + "/local.json" 13 | err := utils.ReadJsonFile(localpath, &local, utils.JsonNumAuto) 14 | if err != nil { 15 | panic(fmt.Sprintf("read %s fail: %v", localpath, err)) 16 | } 17 | for k, v := range local { 18 | args[k] = v 19 | } 20 | return args 21 | } 22 | 23 | func TestNewExg(t *testing.T) { 24 | exgName := "binance" 25 | args := getExchange(exgName, nil) 26 | exg, err := New(exgName, args) 27 | if err != nil { 28 | panic(err) 29 | } 30 | res, err := exg.FetchOHLCV("ETH/USDT:USDT", "1d", 0, 10, nil) 31 | if err != nil { 32 | panic(err) 33 | } 34 | for _, k := range res { 35 | fmt.Printf("%v, %v %v %v %v %v\n", k.Time, k.Open, k.High, k.Low, k.Close, k.Volume) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /utils/common_test.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestSplitParts(t *testing.T) { 8 | tests := []struct { 9 | args string 10 | want []*StrType 11 | }{ 12 | {args: "CI1211C432.54", want: []*StrType{ 13 | {Val: "CI", Type: StrStr}, 14 | {Val: "1211", Type: StrInt}, 15 | {Val: "C", Type: StrStr}, 16 | {Val: "432.54", Type: StrFloat}, 17 | }}, 18 | {args: "125DP", want: []*StrType{ 19 | {Val: "125", Type: StrInt}, 20 | {Val: "DP", Type: StrStr}, 21 | }}, 22 | } 23 | for _, tt := range tests { 24 | t.Run(tt.args, func(t *testing.T) { 25 | got := SplitParts(tt.args) 26 | if len(got) != len(tt.want) { 27 | t.Errorf("len wrong %v, want %v", len(got), len(tt.want)) 28 | return 29 | } 30 | for i, v := range tt.want { 31 | g := got[i] 32 | if v.Val != g.Val || v.Type != g.Type { 33 | t.Errorf("%v val wrong %v %v, want %v %v", i, g.Val, g.Type, v.Val, v.Type) 34 | break 35 | } 36 | } 37 | }) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /common_test.go: -------------------------------------------------------------------------------- 1 | package banexg 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | ) 7 | 8 | func TestSliceInsert(t *testing.T) { 9 | a := []int{1, 2, 3, 6, 8} 10 | index := 5 11 | val := 23 12 | a = append(a, 0) 13 | copy(a[index+1:], a[index:]) 14 | a[index] = val 15 | fmt.Printf("%v", a) 16 | } 17 | 18 | func TestOdBookSide(t *testing.T) { 19 | bids := NewOdBookSide(true, 100, [][2]float64{ 20 | {120, 10}, 21 | {119, 15}, 22 | {117, 20}, 23 | {115, 40}, 24 | {110, 80}, 25 | }) 26 | asks := NewOdBookSide(false, 100, [][2]float64{ 27 | {122, 10}, 28 | {123, 15}, 29 | {125, 20}, 30 | {127, 40}, 31 | {130, 80}, 32 | }) 33 | bids.Set(119, 16) 34 | bids.Set(118, 17) 35 | asks.Set(127, 43) 36 | asks.Set(135, 100) 37 | asks.Set(121.8, 5) 38 | avgBidPrice, lastBidPrice, _ := bids.AvgPrice(43) 39 | volSum, fillRate := asks.SumVolTo(127) 40 | volSum2, fillRate2 := bids.SumVolTo(116) 41 | if avgBidPrice != 118.83720930232558 || lastBidPrice != 118 { 42 | t.Errorf("AvgPrice fail") 43 | } 44 | if volSum != 50 || fillRate != 1 || volSum2 != 63 || fillRate2 != 1 { 45 | t.Errorf("SumVolTo fail") 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 anyongjin 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/banbox/banexg 2 | 3 | go 1.23.0 4 | 5 | require ( 6 | github.com/banbox/bntp v0.1.0 7 | github.com/bytedance/sonic v1.14.1 8 | github.com/go-viper/mapstructure/v2 v2.4.0 9 | github.com/gorilla/websocket v1.5.3 10 | github.com/h2non/gock v1.2.0 11 | github.com/sasha-s/go-deadlock v0.3.6 12 | github.com/shopspring/decimal v1.4.0 13 | go.uber.org/zap v1.27.0 14 | golang.org/x/text v0.28.0 15 | gopkg.in/natefinch/lumberjack.v2 v2.2.1 16 | gopkg.in/yaml.v3 v3.0.1 17 | ) 18 | 19 | require ( 20 | github.com/beevik/ntp v1.4.3 // indirect 21 | github.com/bytedance/gopkg v0.1.3 // indirect 22 | github.com/bytedance/sonic/loader v0.3.0 // indirect 23 | github.com/cloudwego/base64x v0.1.6 // indirect 24 | github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542 // indirect 25 | github.com/klauspost/cpuid/v2 v2.2.9 // indirect 26 | github.com/petermattis/goid v0.0.0-20250813065127-a731cc31b4fe // indirect 27 | github.com/twitchyliquid64/golang-asm v0.15.1 // indirect 28 | go.uber.org/multierr v1.11.0 // indirect 29 | golang.org/x/arch v0.0.0-20210923205945-b76863e36670 // indirect 30 | golang.org/x/net v0.37.0 // indirect 31 | golang.org/x/sys v0.31.0 // indirect 32 | ) 33 | -------------------------------------------------------------------------------- /base.go: -------------------------------------------------------------------------------- 1 | package banexg 2 | 3 | import ( 4 | "fmt" 5 | "github.com/banbox/banexg/errs" 6 | ) 7 | 8 | func (h *ExgHosts) GetHost(key string) string { 9 | var dict map[string]string 10 | if h.TestNet { 11 | dict = h.Test 12 | } else { 13 | dict = h.Prod 14 | } 15 | host, ok := dict[key] 16 | if !ok { 17 | panic(fmt.Sprintf("Entry not exist: %s", key)) 18 | } 19 | return host 20 | } 21 | 22 | func (c *Credential) CheckFilled(keys map[string]bool) *errs.Error { 23 | var requires []string 24 | if c.ApiKey == "" && keys["ApiKey"] { 25 | requires = append(requires, "ApiKey") 26 | } 27 | if c.Secret == "" && keys["Secret"] { 28 | requires = append(requires, "Secret") 29 | } 30 | if c.UID == "" && keys["UID"] { 31 | requires = append(requires, "UID") 32 | } 33 | if c.Password == "" && keys["Password"] { 34 | requires = append(requires, "Password") 35 | } 36 | if len(requires) > 0 { 37 | return errs.NewMsg(errs.CodeCredsRequired, "credential required %v", requires) 38 | } 39 | return nil 40 | } 41 | 42 | func IsContract(marketType string) bool { 43 | return marketType == MarketFuture || marketType == MarketSwap || 44 | marketType == MarketLinear || marketType == MarketInverse 45 | } 46 | -------------------------------------------------------------------------------- /utils/tf_utils_test.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | ) 7 | 8 | func TestAlignTfSecsOffset(t *testing.T) { 9 | cases := [][3]int64{ 10 | {604800, 1740988222, 1740960000}, // 2025-03-03 -> 2025-03-03 11 | {604800, 1741074622, 1740960000}, // 2025-03-04 12 | {604800, 1741161022, 1740960000}, // 2025-03-05 13 | {604800, 1741247422, 1740960000}, // 2025-03-06 14 | {604800, 1741333822, 1740960000}, // 2025-03-07 15 | {604800, 1741420222, 1740960000}, // 2025-03-08 16 | {604800, 1741506379, 1740960000}, // 2025-03-09 -> 2025-03-03 17 | {259200, 1623715200, 1623715200}, 18 | {259200, 1623801600, 1623715200}, // 2021-06-16 -> 2021-06-15 19 | {259200, 1623888000, 1623715200}, 20 | {259200, 1623974400, 1623974400}, // 2021-06-18 -> 2021-06-15 21 | {86400, 1741506379, 1741478400}, // 2025-03-09 22 | } 23 | for _, v := range cases { 24 | tfSecs := int(v[0]) 25 | _, offset := GetTfAlignOrigin(tfSecs) 26 | out := AlignTfSecsOffset(v[1], tfSecs, offset) 27 | if out != v[2] { 28 | t.Errorf("fail, input: %v, cur: %v, expect: %v", v[1], out, v[2]) 29 | } 30 | } 31 | // 2025-03-06T14:00:00.000Z -> 2025-03-07T00:00:00.000Z 32 | out := AlignTfSecs(1741269600+50400, 86400) 33 | fmt.Println(out) 34 | } 35 | -------------------------------------------------------------------------------- /china/types.go: -------------------------------------------------------------------------------- 1 | package china 2 | 3 | import "github.com/banbox/banexg" 4 | 5 | type China struct { 6 | *banexg.Exchange 7 | } 8 | 9 | type Exchange struct { 10 | Code string `yaml:"code"` 11 | Title string `yaml:"title"` 12 | IndexUrl string `yaml:"index"` 13 | Suffix string `yaml:"suffix"` 14 | CaseLower bool `yaml:"case_lower"` // 品种ID是否小写 15 | DateNum int `yaml:"date_num"` // 年月显示后几位?4或3 16 | OptionDash bool `yaml:"option_dash"` // 期权C/P左右两侧是否有短横线 17 | } 18 | 19 | type ItemMarket struct { 20 | Code string `yaml:"code"` 21 | Title string `yaml:"title"` 22 | Market string `yaml:"market"` 23 | Exchange string `yaml:"exchange"` 24 | Extend string `yaml:"extend"` 25 | Alias []string `yaml:"alias"` 26 | DayRanges []string `yaml:"day_ranges"` 27 | NightRanges []string `yaml:"night_ranges"` 28 | Fee *Fee `yaml:"fee"` 29 | Multiplier float64 `yaml:"multiplier"` // 合约乘数;价格单位是吨,每手含multiplier吨 30 | PriceTick float64 `yaml:"price_tick"` // 最小价格变动,单位:吨 31 | LimitChgPct float64 `yaml:"limit_chg_pct"` // 涨跌停板,单位:百分比 32 | MarginPct float64 `yaml:"margin_pct"` // 保证金比率,单位:百分比 33 | } 34 | 35 | type Fee struct { 36 | Unit string `yaml:"unit"` 37 | Val float64 `yaml:"val"` 38 | ValCT float64 `yaml:"val_ct"` // 平今 39 | ValTD float64 `yaml:"val_td"` // 日内 40 | } 41 | 42 | type CnMarkets struct { 43 | Exchanges map[string]*Exchange `yaml:"exchanges"` 44 | Contracts []*ItemMarket `yaml:"contracts"` 45 | Stocks []*ItemMarket `yaml:"stocks"` 46 | } 47 | -------------------------------------------------------------------------------- /binance/biz_ticker_test.go: -------------------------------------------------------------------------------- 1 | package binance 2 | 3 | import ( 4 | "fmt" 5 | "github.com/banbox/banexg" 6 | "github.com/banbox/banexg/utils" 7 | "testing" 8 | ) 9 | 10 | func TestFetchTicker(t *testing.T) { 11 | exg := getBinance(nil) 12 | ticker, err := exg.FetchTicker("BTC/USDT", nil) 13 | if err != nil { 14 | panic(err) 15 | } 16 | ticker.Info = nil 17 | fmt.Println(utils.MarshalString(ticker)) 18 | } 19 | 20 | func TestFetchTicker2(t *testing.T) { 21 | exg := getBinance(nil) 22 | ticker, err := exg.FetchTicker("BTC/USDT:USDT", nil) 23 | if err != nil { 24 | panic(err) 25 | } 26 | ticker.Info = nil 27 | fmt.Println(utils.MarshalString(ticker)) 28 | } 29 | 30 | func TestFetchTicker3(t *testing.T) { 31 | exg := getBinance(nil) 32 | ticker, err := exg.FetchTicker("BTC/USD:BTC", nil) 33 | if err != nil { 34 | panic(err) 35 | } 36 | ticker.Info = nil 37 | fmt.Println(utils.MarshalString(ticker)) 38 | } 39 | 40 | func TestFetchTickers(t *testing.T) { 41 | exg := getBinance(nil) 42 | tickers, err := exg.FetchTickers(nil, map[string]interface{}{ 43 | "market": banexg.MarketLinear, 44 | }) 45 | if err != nil { 46 | panic(err) 47 | } 48 | for _, ticker := range tickers { 49 | ticker.Info = nil 50 | } 51 | fmt.Println(utils.MarshalString(tickers)) 52 | } 53 | 54 | func TestFetchTickerPrice(t *testing.T) { 55 | exg := getBinance(nil) 56 | prices, err := exg.FetchTickerPrice("", map[string]interface{}{ 57 | "market": banexg.MarketLinear, 58 | }) 59 | if err != nil { 60 | panic(err) 61 | } 62 | fmt.Println(utils.MarshalString(prices)) 63 | } 64 | -------------------------------------------------------------------------------- /utils/exg_test.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | ) 7 | 8 | type TestCase struct { 9 | Input string 10 | Output int 11 | } 12 | 13 | func TestPrecisionFromString(t *testing.T) { 14 | var cases = []struct { 15 | Input string 16 | Output float64 17 | }{ 18 | {"1e-4", 4}, 19 | {"0.0001", 4}, 20 | {"0.00000001", 8}, 21 | } 22 | for _, it := range cases { 23 | out := PrecisionFromString(it.Input) 24 | if out == it.Output { 25 | t.Logf("pass %s %v", it.Input, out) 26 | } else { 27 | t.Errorf("FAIL %s %v expect: %v", it.Input, out, it.Output) 28 | } 29 | } 30 | } 31 | 32 | type DDD struct { 33 | data map[string]string 34 | } 35 | 36 | func TestSetFieldBy(t *testing.T) { 37 | holder := DDD{data: map[string]string{}} 38 | fmt.Printf("data: %v %v\n", holder.data, holder.data == nil) 39 | items := make(map[string]interface{}) 40 | SetFieldBy(&(holder.data), items, "123", nil) 41 | if holder.data == nil { 42 | t.Errorf("Fail SetFieldBy, holder.data should not be nil") 43 | } else { 44 | fmt.Printf("Pass SetFieldBy") 45 | } 46 | } 47 | 48 | func TestParseTimeFrame(t *testing.T) { 49 | var cases = []TestCase{ 50 | {"1m", 60}, 51 | {"5s", 5}, 52 | {"10S", 10}, 53 | {"2H", 7200}, 54 | {"2d", 172800}, 55 | {"3w", 1814400}, 56 | {"2M", 5184000}, 57 | {"2Y", 63072000}, 58 | } 59 | for _, it := range cases { 60 | out, err := ParseTimeFrame(it.Input) 61 | if err != nil { 62 | panic(err) 63 | } 64 | if out == it.Output { 65 | t.Logf("pass %s %v", it.Input, out) 66 | } else { 67 | t.Errorf("FAIL %s %v expect: %v", it.Input, out, it.Output) 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /binance/common_test.go: -------------------------------------------------------------------------------- 1 | package binance 2 | 3 | import ( 4 | "fmt" 5 | "github.com/banbox/banexg/utils" 6 | "github.com/h2non/gock" 7 | "strings" 8 | ) 9 | 10 | type GockItem struct { 11 | Method string `json:"method"` 12 | URL string `json:"url"` 13 | Status int `json:"status"` 14 | RspType string `json:"rsp_type"` 15 | RspPath string `json:"rsp_path"` 16 | } 17 | 18 | /* 19 | LoadGockItems 20 | 读取并设置需要mock的接口,这里只存储最常用的接口,如markets等 21 | */ 22 | func LoadGockItems(path string) error { 23 | var items = make([]GockItem, 0) 24 | err := utils.ReadJsonFile(path, &items, utils.JsonNumFloat) 25 | if err != nil { 26 | return err 27 | } 28 | for i, item := range items { 29 | if item.URL == "" { 30 | return fmt.Errorf("url is required for %d item", i+1) 31 | } 32 | if item.RspPath == "" { 33 | return fmt.Errorf("rsp_path is required for %d item", i+1) 34 | } 35 | idx := strings.Index(item.URL, ".") 36 | subIdx := strings.Index(item.URL[idx:], "/") 37 | p := item.URL[idx+subIdx:] 38 | domain := item.URL[:idx+subIdx] 39 | req := gock.New(domain) 40 | method := strings.ToLower(item.Method) 41 | if method == "" { 42 | method = "get" 43 | } 44 | switch method { 45 | case "get": 46 | req = req.Get(p) 47 | case "post": 48 | req = req.Post(p) 49 | case "put": 50 | req = req.Put(p) 51 | case "delete": 52 | req = req.Delete(p) 53 | default: 54 | return fmt.Errorf("invalid gock method: %s", method) 55 | } 56 | if item.Status == 0 { 57 | item.Status = 200 58 | } 59 | rsp := req.Reply(item.Status) 60 | if item.RspType != "" { 61 | rsp = rsp.Type(item.RspType) 62 | } 63 | rsp.File("testdata/" + item.RspPath) 64 | } 65 | return nil 66 | } 67 | -------------------------------------------------------------------------------- /utils/misc_test.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "fmt" 5 | "github.com/banbox/banexg/log" 6 | "go.uber.org/zap" 7 | "math" 8 | "testing" 9 | ) 10 | 11 | func TestSonicUnmarshal(t *testing.T) { 12 | // sonic默认反序列化json中的number时,使用float64,对于一些大的int64的值,会导致精度损失,这里是测试哪些类型会有精度损失 13 | // 使用utils.UnmarshalString替换后,大整数都能正常解析了 14 | runSonicItem("MaxInt64", math.MaxInt64) 15 | runSonicItem("MinInt64", math.MinInt64) 16 | runSonicItem("MaxInt32", math.MaxInt32) 17 | runSonicItem("MinInt32", math.MinInt32) 18 | runSonicItem("MaxFloat64", math.MaxFloat64) 19 | runSonicItem("MaxFloat32", math.MaxFloat32) 20 | } 21 | 22 | func runSonicItem[T comparable](name string, val T) { 23 | text, err := MarshalString(val) 24 | if err != nil { 25 | panic(fmt.Sprintf("marshal %v fail: %v", name, err)) 26 | } 27 | textWrap := fmt.Sprintf("{\"val\":%v}", text) 28 | var res = make(map[string]interface{}) 29 | // err2 := UnmarshalString(textWrap, &res) 30 | err2 := UnmarshalString(textWrap, &res, JsonNumAuto) 31 | if err2 != nil { 32 | log.Error("unmarshal fail", zap.String("name", name), zap.Error(err2)) 33 | } 34 | input := fmt.Sprintf("%v", val) 35 | output := fmt.Sprintf("%v", res["val"]) 36 | if input != output { 37 | log.Error("unmarshal wrong", zap.String("name", name), zap.String("input", input), 38 | zap.String("text", text), zap.String("output", output)) 39 | } 40 | } 41 | 42 | func TestGetSystemProxy(t *testing.T) { 43 | prx, err := GetSystemProxy() 44 | if err != nil { 45 | panic(err) 46 | } 47 | systemProxy := fmt.Sprintf("%s://%s:%s", prx.Protocol, prx.Host, prx.Port) 48 | 49 | envProxy := GetSystemEnvProxy() 50 | fmt.Printf("envProxy: %s, systemProxy: %s\n", envProxy, systemProxy) 51 | } 52 | -------------------------------------------------------------------------------- /biz_test.go: -------------------------------------------------------------------------------- 1 | package banexg 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestSetOptions(t *testing.T) { 8 | FakeApiKey := "123" 9 | e := Exchange{ 10 | Options: map[string]interface{}{ 11 | OptMarketType: MarketMargin, 12 | OptApiKey: FakeApiKey, 13 | }, 14 | } 15 | e.Init() 16 | _, creds, err := e.GetAccountCreds("default") 17 | if err != nil { 18 | panic(err) 19 | } 20 | if creds.ApiKey == FakeApiKey { 21 | t.Logf("Pass ApiKey") 22 | } else { 23 | t.Errorf("Fail ApiKey, cur %v, expect: %v", creds.ApiKey, FakeApiKey) 24 | } 25 | if e.MarketType == MarketMargin { 26 | t.Logf("Pass MarketType") 27 | } else { 28 | t.Errorf("Fail MarketType, cur %v, expect: %v", e.MarketType, MarketMargin) 29 | } 30 | } 31 | 32 | func TestCalcFee(t *testing.T) { 33 | symbol := "FOO/BAR" 34 | exg := Exchange{ 35 | ExgInfo: &ExgInfo{ 36 | Markets: map[string]*Market{ 37 | symbol: { 38 | ID: "foobar", 39 | Symbol: symbol, 40 | Base: "FOO", 41 | Quote: "BAR", 42 | Settle: "BAR", 43 | Taker: 0.002, 44 | Maker: 0.001, 45 | Precision: &Precision{ 46 | Price: 8, 47 | Amount: 8, 48 | ModePrice: PrecModeDecimalPlace, 49 | ModeAmount: PrecModeDecimalPlace, 50 | }, 51 | }, 52 | }, 53 | }, 54 | } 55 | amount := 10. 56 | price := 100. 57 | fee, err := exg.CalculateFee(symbol, OdTypeLimit, OdSideBuy, amount, price, false, nil) 58 | if err != nil { 59 | panic(err) 60 | } 61 | if fee.Cost != 2.0 { 62 | t.Errorf("taker fee: %v", fee) 63 | } 64 | fee, err = exg.CalculateFee(symbol, OdTypeLimit, OdSideBuy, amount, price, true, nil) 65 | if err != nil { 66 | panic(err) 67 | } 68 | if fee.Cost != 1.0 { 69 | t.Errorf("maker fee: %v", fee) 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /utils/exg.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "github.com/banbox/banexg/errs" 5 | "math" 6 | "strconv" 7 | "strings" 8 | ) 9 | 10 | /* 11 | PrecisionFromString 12 | 1e-4 -> 4 13 | 0.000001 -> 6 14 | 100 -> 0 15 | */ 16 | func PrecisionFromString(input string) float64 { 17 | input = strings.TrimSpace(input) 18 | if input == "" { 19 | return 0 20 | } 21 | val, err := strconv.ParseFloat(input, 64) 22 | if err != nil { 23 | return 0 24 | } 25 | prec := math.Round(math.Log10(val)) 26 | if prec < 0 { 27 | return prec * -1 28 | } 29 | return 0 30 | } 31 | 32 | func SafeParams(params map[string]interface{}) map[string]interface{} { 33 | result := map[string]interface{}{} 34 | if params != nil { 35 | for k, v := range params { 36 | result[k] = v 37 | } 38 | } 39 | return result 40 | } 41 | 42 | func ParseTimeFrame(timeframe string) (int, *errs.Error) { 43 | if val, ok := tfSecsCache[timeframe]; ok { 44 | return val, nil 45 | } 46 | length := len(timeframe) 47 | if length <= 1 { 48 | return 0, errs.NewMsg(errs.CodeInvalidTimeFrame, "invalid timeframe") 49 | } 50 | 51 | amount, err := strconv.Atoi(timeframe[:length-1]) 52 | if err != nil { 53 | return 0, errs.NewMsg(errs.CodeInvalidTimeFrame, "invalid timeframe") 54 | } 55 | 56 | unit := timeframe[length-1] 57 | var scale int 58 | 59 | switch unit { 60 | case 'y', 'Y': 61 | scale = 60 * 60 * 24 * 365 62 | case 'M': 63 | scale = 60 * 60 * 24 * 30 64 | case 'w', 'W': 65 | scale = 60 * 60 * 24 * 7 66 | case 'd', 'D': 67 | scale = 60 * 60 * 24 68 | case 'h', 'H': 69 | scale = 60 * 60 70 | case 'm': 71 | scale = 60 72 | case 's', 'S': 73 | scale = 1 74 | default: 75 | return 0, errs.NewMsg(errs.CodeInvalidTimeFrame, "invalid timeframe") 76 | } 77 | 78 | res := amount * scale 79 | tfSecsCache[timeframe] = res 80 | 81 | return res, nil 82 | } 83 | -------------------------------------------------------------------------------- /log/zap_text_core.go: -------------------------------------------------------------------------------- 1 | package log 2 | 3 | import ( 4 | "fmt" 5 | "go.uber.org/zap/zapcore" 6 | ) 7 | 8 | // NewTextCore creates a Core that writes logs to a WriteSyncer. 9 | func NewTextCore(enc zapcore.Encoder, ws zapcore.WriteSyncer, enab zapcore.LevelEnabler) zapcore.Core { 10 | return &textIOCore{ 11 | LevelEnabler: enab, 12 | enc: enc, 13 | out: ws, 14 | } 15 | } 16 | 17 | // textIOCore is a copy of zapcore.ioCore that only accept *TextEncoder 18 | // it can be removed after https://github.com/uber-go/zap/pull/685 be merged 19 | type textIOCore struct { 20 | zapcore.LevelEnabler 21 | enc zapcore.Encoder 22 | out zapcore.WriteSyncer 23 | } 24 | 25 | func (c *textIOCore) With(fields []zapcore.Field) zapcore.Core { 26 | clone := c.clone() 27 | // it's different to ioCore, here call TextEncoder#addFields to fix https://github.com/pingcap/log/issues/3 28 | switch e := clone.enc.(type) { 29 | case *TextEncoder: 30 | e.addFields(fields) 31 | case zapcore.ObjectEncoder: 32 | for _, field := range fields { 33 | field.AddTo(e) 34 | } 35 | default: 36 | panic(fmt.Sprintf("unsupported encode type: %T for With operation", clone.enc)) 37 | } 38 | return clone 39 | } 40 | 41 | func (c *textIOCore) Check(ent zapcore.Entry, ce *zapcore.CheckedEntry) *zapcore.CheckedEntry { 42 | if c.Enabled(ent.Level) { 43 | return ce.AddCore(ent, c) 44 | } 45 | return ce 46 | } 47 | 48 | func (c *textIOCore) Write(ent zapcore.Entry, fields []zapcore.Field) error { 49 | buf, err := c.enc.EncodeEntry(ent, fields) 50 | if err != nil { 51 | return err 52 | } 53 | _, err = c.out.Write(buf.Bytes()) 54 | buf.Free() 55 | if err != nil { 56 | return err 57 | } 58 | if ent.Level > zapcore.ErrorLevel { 59 | // Since we may be crashing the program, sync the output. Ignore Sync 60 | // errors, pending a clean solution to issue https://github.com/uber-go/zap/issues/370. 61 | c.Sync() 62 | } 63 | return nil 64 | } 65 | 66 | func (c *textIOCore) Sync() error { 67 | return c.out.Sync() 68 | } 69 | 70 | func (c *textIOCore) clone() *textIOCore { 71 | return &textIOCore{ 72 | LevelEnabler: c.LevelEnabler, 73 | enc: c.enc.Clone(), 74 | out: c.out, 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /errs/main.go: -------------------------------------------------------------------------------- 1 | package errs 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "runtime" 7 | "strconv" 8 | "strings" 9 | ) 10 | 11 | func NewFull(code int, err error, format string, a ...any) *Error { 12 | msg := fmt.Sprintf(format, a...) 13 | var res *Error 14 | if errors.As(err, &res) { 15 | res.msg = fmt.Sprintf("%s %s", res.msg, msg) 16 | return res 17 | } 18 | return &Error{Code: code, err: err, msg: msg, Stack: CallStack(3, 30)} 19 | } 20 | 21 | func NewMsg(code int, format string, a ...any) *Error { 22 | return &Error{Code: code, msg: fmt.Sprintf(format, a...), Stack: CallStack(3, 30)} 23 | } 24 | 25 | func New(code int, err error) *Error { 26 | var res *Error 27 | if errors.As(err, &res) { 28 | return res 29 | } 30 | return &Error{Code: code, err: err, Stack: CallStack(3, 30)} 31 | } 32 | 33 | func (e *Error) Short() string { 34 | if e == nil { 35 | return "" 36 | } 37 | if e.BizCode != 0 { 38 | return fmt.Sprintf("[%s(%d)] %s", e.CodeName(), e.BizCode, e.Message()) 39 | } 40 | return fmt.Sprintf("[%s] %s", e.CodeName(), e.Message()) 41 | } 42 | 43 | func (e *Error) CodeName() string { 44 | codeNameLock.Lock() 45 | name, ok := errCodeNames[e.Code] 46 | codeNameLock.Unlock() 47 | if ok { 48 | return name 49 | } 50 | return strconv.Itoa(e.Code) 51 | } 52 | 53 | func (e *Error) Error() string { 54 | if e == nil { 55 | return "" 56 | } 57 | if e.BizCode != 0 { 58 | return fmt.Sprintf("[%s(%d)] %s\n%s", e.CodeName(), e.BizCode, e.Message(), e.Stack) 59 | } 60 | return fmt.Sprintf("[%s] %s\n%s", e.CodeName(), e.Message(), e.Stack) 61 | } 62 | 63 | func (e *Error) Message() string { 64 | if e.err == nil { 65 | return e.msg 66 | } 67 | var errMsg string 68 | if PrintErr != nil { 69 | errMsg = PrintErr(e.err) 70 | } else { 71 | errMsg = e.err.Error() 72 | } 73 | if e.msg == "" { 74 | return errMsg 75 | } 76 | return fmt.Sprintf("%s %s", e.msg, errMsg) 77 | } 78 | 79 | func (e *Error) Unwrap() error { 80 | return e.err 81 | } 82 | 83 | func CallStack(skip, maxNum int) string { 84 | pc := make([]uintptr, maxNum) 85 | n := runtime.Callers(skip, pc) 86 | frames := runtime.CallersFrames(pc[:n]) 87 | var texts = make([]string, 0, 16) 88 | for { 89 | f, more := frames.Next() 90 | texts = append(texts, fmt.Sprintf(" at %v:%v %v", f.File, f.Line, f.Function)) 91 | if !more { 92 | break 93 | } 94 | } 95 | return strings.Join(texts, "\n") 96 | } 97 | 98 | func UpdateErrNames(data map[int]string) { 99 | codeNameLock.Lock() 100 | for k, v := range data { 101 | errCodeNames[k] = v 102 | } 103 | codeNameLock.Unlock() 104 | } 105 | -------------------------------------------------------------------------------- /AGENTS.md: -------------------------------------------------------------------------------- 1 | ## 项目概述 2 | 3 | BanExg 是一个用Go语言开发的多交易所统一SDK类库,旨在为加密货币交易提供统一的API接口。目前已完整支持Binance交易所,部分支持Bybit交易所,并为后续接入更多交易所(如OKX)奠定了坚实的架构基础。 4 | 5 | ## 一、整体文件组织架构 6 | 7 | ### 1.1 项目根目录结构 8 | ``` 9 | banexg/ 10 | ├── 核心接口层 11 | │ ├── intf.go # 核心接口定义 (BanExchange) 12 | │ ├── types.go # 核心数据结构 (Exchange, Market, Order等) 13 | │ ├── base.go # 基础功能实现 14 | │ ├── biz.go # 通用业务逻辑 15 | │ ├── common.go # 通用工具函数 16 | │ ├── data.go # 常量和配置数据 17 | │ ├── exts.go # 扩展工具函数 18 | │ └── websocket.go # WebSocket客户端实现 19 | ├── 交易所实现层 20 | │ ├── binance/ # Binance交易所完整实现 21 | │ ├── bybit/ # Bybit交易所部分实现 22 | │ └── china/ # 中国区域交易所(模拟) 23 | ├── 基础设施层 24 | │ ├── utils/ # 工具函数库 25 | │ ├── log/ # 日志系统 26 | │ ├── errs/ # 错误处理 27 | │ └── bex/ # 交易所工厂注册 28 | ├── 测试和文档 29 | │ ├── */testdata/ # 测试数据 30 | │ ├── */readme.md # 各模块文档 31 | │ └── contribute.md # 贡献指南 32 | └── 配置文件 33 | ├── go.mod # Go模块配置 34 | └── */local.json # 本地配置文件 35 | ``` 36 | 37 | ### 1.2 架构分层设计 38 | 39 | **四层架构模式:** 40 | 1. **接口抽象层** - 定义统一的交易所操作接口 41 | 2. **业务逻辑层** - 实现通用的交易业务逻辑 42 | 3. **适配器层** - 各交易所的具体实现适配 43 | 4. **基础设施层** - 工具、日志、错误处理等支撑服务 44 | 45 | ## 二、核心结构体和接口 46 | 47 | ### 2.1 最重要的核心接口 48 | 49 | #### BanExchange接口 (`intf.go`) 50 | **位置:** `intf.go` 51 | 52 | **作用:** 定义所有交易所必须实现的统一接口,包含100+个方法,涵盖: 53 | - **市场数据获取**:`LoadMarkets`, `FetchTicker`, `FetchOHLCV`, `FetchOrderBook` 54 | - **交易操作**:`CreateOrder`, `CancelOrder`, `EditOrder`, `FetchOrder` 55 | - **账户管理**:`FetchBalance`, `FetchPositions`, `SetLeverage` 56 | - **实时数据**:`WatchOrderBooks`, `WatchTrades`, `WatchBalance` 57 | - **工具方法**:`CalculateFee`, `CalcMaintMargin`, `PrecAmount` 58 | 59 | ### 2.2 最重要的核心结构体 60 | 61 | #### Exchange结构体 (`types.go:59`) 62 | **位置:** `types.go:59` 63 | 64 | **作用:** 交易所的基础实现结构,所有具体交易所都嵌入此结构 65 | **关键字段:** 66 | - `ExgInfo`: 交易所基本信息(ID、名称、版本) 67 | - `Apis`: API端点映射表 68 | - `Accounts`: 多账户管理 69 | - `Markets`: 市场信息缓存 70 | - `WSClients`: WebSocket客户端池 71 | - `Options`: 配置选项管理 72 | 73 | 74 | ## 三、项目整体架构风格 75 | 76 | ### 3.1 设计模式运用 77 | 78 | - 核心`BanExchange`接口定义统一标准 79 | - 通过`bex`模块支持动态交易所注册 80 | - 可配置的重试策略和错误处理 81 | - 基础`Exchange`提供通用业务流程 82 | - 统一的初始化和配置流程 83 | - 使用标准的状态和返回格式,各交易所按需转换 84 | 85 | ## 四、重要开发流程 86 | 87 | ### 4.1 添加新接口 88 | 1. 在`entry.go`下`Apis`添加对应的定义 89 | 2. 在`data.go`下添加`MethodXXX`方法名常量 90 | 3. 在合适位置如`biz_order_create.go`下添加对应的业务逻辑实现 91 | 4. 在`types.go`下添加返回数据类型定义 92 | 93 | 94 | ## AI准则 95 | - 严格遵守DRY准则,Dont Repeat Yourself,添加新代码前检查是否已有相似代码,有则提取为子函数 96 | - 始终用最少的代码完成任务,只生成当前需要的核心代码,不要过度设计,不要提前生成以后可能需要的代码。 97 | -------------------------------------------------------------------------------- /china/entry.go: -------------------------------------------------------------------------------- 1 | package china 2 | 3 | import ( 4 | "github.com/banbox/banexg" 5 | "github.com/banbox/banexg/errs" 6 | ) 7 | 8 | func New(Options map[string]interface{}) (*China, *errs.Error) { 9 | exg := &China{ 10 | Exchange: &banexg.Exchange{ 11 | ExgInfo: &banexg.ExgInfo{ 12 | ID: "china", 13 | Name: "China", 14 | Countries: []string{"CN"}, 15 | FixedLvg: true, 16 | }, 17 | RateLimit: 50, 18 | Options: Options, 19 | Hosts: &banexg.ExgHosts{}, 20 | Fees: &banexg.ExgFee{ 21 | Linear: &banexg.TradeFee{ 22 | FeeSide: "quote", 23 | TierBased: false, 24 | Percentage: true, 25 | Taker: 0.0002, 26 | Maker: 0.0002, 27 | }, 28 | }, 29 | Apis: map[string]*banexg.Entry{ 30 | "test": {Path: "", Host: "", Method: "GET"}, 31 | }, 32 | Has: map[string]map[string]int{ 33 | "": { 34 | banexg.ApiFetchTicker: banexg.HasFail, 35 | banexg.ApiFetchTickers: banexg.HasFail, 36 | banexg.ApiFetchTickerPrice: banexg.HasFail, 37 | banexg.ApiLoadLeverageBrackets: banexg.HasOk, 38 | banexg.ApiGetLeverage: banexg.HasOk, 39 | banexg.ApiFetchOHLCV: banexg.HasFail, 40 | banexg.ApiFetchOrderBook: banexg.HasFail, 41 | banexg.ApiFetchOrder: banexg.HasFail, 42 | banexg.ApiFetchOrders: banexg.HasFail, 43 | banexg.ApiFetchBalance: banexg.HasFail, 44 | banexg.ApiFetchAccountPositions: banexg.HasFail, 45 | banexg.ApiFetchPositions: banexg.HasFail, 46 | banexg.ApiFetchOpenOrders: banexg.HasFail, 47 | banexg.ApiCreateOrder: banexg.HasFail, 48 | banexg.ApiEditOrder: banexg.HasFail, 49 | banexg.ApiCancelOrder: banexg.HasFail, 50 | banexg.ApiSetLeverage: banexg.HasFail, 51 | banexg.ApiCalcMaintMargin: banexg.HasFail, 52 | banexg.ApiWatchOrderBooks: banexg.HasFail, 53 | banexg.ApiUnWatchOrderBooks: banexg.HasFail, 54 | banexg.ApiWatchOHLCVs: banexg.HasFail, 55 | banexg.ApiUnWatchOHLCVs: banexg.HasFail, 56 | banexg.ApiWatchMarkPrices: banexg.HasFail, 57 | banexg.ApiUnWatchMarkPrices: banexg.HasFail, 58 | banexg.ApiWatchTrades: banexg.HasFail, 59 | banexg.ApiUnWatchTrades: banexg.HasFail, 60 | banexg.ApiWatchMyTrades: banexg.HasFail, 61 | banexg.ApiWatchBalance: banexg.HasFail, 62 | banexg.ApiWatchPositions: banexg.HasFail, 63 | banexg.ApiWatchAccountConfig: banexg.HasFail, 64 | }, 65 | }, 66 | }, 67 | } 68 | err := exg.Init() 69 | if err != nil { 70 | return nil, err 71 | } 72 | exg.CalcFee = makeCalcFee(exg) 73 | return exg, nil 74 | } 75 | 76 | func NewExchange(Options map[string]interface{}) (banexg.BanExchange, *errs.Error) { 77 | return New(Options) 78 | } 79 | -------------------------------------------------------------------------------- /errs/data.go: -------------------------------------------------------------------------------- 1 | package errs 2 | 3 | import ( 4 | "github.com/sasha-s/go-deadlock" 5 | ) 6 | 7 | const ( 8 | CodeNetFail = -1*iota - 1 9 | CodeNotSupport 10 | CodeInvalidRequest 11 | CodeAccKeyError 12 | CodeMissingApiKey 13 | CodeCredsRequired 14 | CodeSignFail 15 | CodeRunTime 16 | CodeNotImplement 17 | CodeMarketNotLoad 18 | CodeNoMarketForPair 19 | CodeUnsupportMarket 20 | CodeSandboxApiNotSupport 21 | CodeApiNotSupport 22 | CodeInvalidResponse 23 | CodeMarshalFail 24 | CodeUnmarshalFail 25 | CodeParamRequired 26 | CodeParamInvalid 27 | CodeWsInvalidMsg 28 | CodeWsReadFail 29 | CodeConnectFail 30 | CodeInvalidTimeFrame 31 | CodePrecDecFail 32 | CodeBadExgName 33 | CodeIOWriteFail 34 | CodeIOReadFail 35 | CodeInvalidData 36 | CodeExpired 37 | CodeNetDisable 38 | CodeCancel 39 | CodeShutdown 40 | CodeTimeout 41 | CodeOOM 42 | CodeSystemBusy 43 | CodeUnauthorized 44 | CodeForbidden 45 | CodeDataNotFound 46 | CodeServerError 47 | CodeNoTrade 48 | ) 49 | 50 | var ( 51 | PrintErr func(e error) string // print string for common error 52 | codeNameLock = deadlock.Mutex{} 53 | ) 54 | 55 | var errCodeNames = map[int]string{ 56 | CodeNetFail: "NetFail", 57 | CodeNotSupport: "NotSupport", 58 | CodeInvalidRequest: "InvalidRequest", 59 | CodeAccKeyError: "AccKeyError", 60 | CodeMissingApiKey: "MissingApiKey", 61 | CodeCredsRequired: "CredsRequired", 62 | CodeSignFail: "SignFail", 63 | CodeRunTime: "RunTime", 64 | CodeNotImplement: "NotImplement", 65 | CodeMarketNotLoad: "MarketNotLoad", 66 | CodeNoMarketForPair: "NoMarketForPair", 67 | CodeUnsupportMarket: "UnsupportMarket", 68 | CodeSandboxApiNotSupport: "SandboxApiNotSupport", 69 | CodeApiNotSupport: "ApiNotSupport", 70 | CodeInvalidResponse: "InvalidResponse", 71 | CodeMarshalFail: "MarshalFail", 72 | CodeUnmarshalFail: "UnmarshalFail", 73 | CodeParamRequired: "ParamRequired", 74 | CodeParamInvalid: "ParamInvalid", 75 | CodeWsInvalidMsg: "WsInvalidMsg", 76 | CodeWsReadFail: "WsReadFail", 77 | CodeConnectFail: "ConnectFail", 78 | CodeInvalidTimeFrame: "InvalidTimeFrame", 79 | CodePrecDecFail: "PrecDecFail", 80 | CodeBadExgName: "BadExgName", 81 | CodeIOWriteFail: "IOWriteFail", 82 | CodeIOReadFail: "IOReadFail", 83 | CodeInvalidData: "InvalidData", 84 | CodeExpired: "Expired", 85 | CodeNetDisable: "NetDisable", 86 | CodeCancel: "Cancel", 87 | CodeShutdown: "Shutdown", 88 | CodeTimeout: "Timeout", 89 | CodeOOM: "OOM", 90 | CodeSystemBusy: "SystemBusy", 91 | CodeUnauthorized: "Unauthorized", 92 | CodeForbidden: "Forbidden", 93 | CodeDataNotFound: "DataNotFound", 94 | CodeServerError: "ServerError", 95 | CodeNoTrade: "NoTrade", 96 | } 97 | -------------------------------------------------------------------------------- /utils/common.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "fmt" 5 | "github.com/banbox/banexg/errs" 6 | "sort" 7 | "strings" 8 | "time" 9 | ) 10 | 11 | /* 12 | YMD 将13位时间戳转为日期 13 | 14 | separator 年月日的间隔字符 15 | fullYear 年是否使用4个数字 16 | */ 17 | func YMD(timestamp int64, separator string, fullYear bool) string { 18 | // 将13位时间戳转换为Time对象 19 | t := time.Unix(0, timestamp*int64(time.Millisecond)) 20 | 21 | yearFormat := "2006" 22 | if !fullYear { 23 | yearFormat = "06" 24 | } 25 | 26 | // 根据是否有分隔符和年份的格式来构建时间格式字符串 27 | layout := fmt.Sprintf("%s%s02%s01", yearFormat, separator, separator) 28 | 29 | // 返回格式化时间 30 | return t.Format(layout) 31 | } 32 | 33 | func ISO8601(millis int64) string { 34 | t := time.UnixMilli(millis) 35 | return t.Format(time.RFC3339) 36 | } 37 | 38 | const ( 39 | StrStr = 0 40 | StrInt = 1 41 | StrFloat = 2 42 | ) 43 | 44 | type StrType struct { 45 | Val string 46 | Type int 47 | } 48 | 49 | func SplitParts(text string) []*StrType { 50 | res := make([]*StrType, 0) 51 | var b strings.Builder 52 | var state = StrStr // 初始化状态为字符串类型 53 | 54 | addToResult := func() { 55 | res = append(res, &StrType{Val: b.String(), Type: state}) 56 | b.Reset() // 重置Builder以便累积下一个部分 57 | } 58 | 59 | for _, c := range text { 60 | switch { 61 | case c >= '0' && c <= '9': // 数字 62 | if state == StrStr { // 类型转换 63 | if b.Len() > 0 { 64 | addToResult() 65 | } 66 | state = StrInt 67 | } 68 | b.WriteRune(c) 69 | case c == '.': // 小数点,仅在字符串或整数后有效,转换为浮点数类型 70 | if state == StrInt { 71 | state = StrFloat 72 | } else if state == StrFloat { 73 | if b.Len() > 0 { 74 | addToResult() 75 | } 76 | state = StrStr 77 | } 78 | b.WriteRune(c) 79 | default: // 非数字字符,累积的数字(如有)加入结果,然后处理当前字符 80 | if state != StrStr { 81 | if b.Len() > 0 { 82 | addToResult() 83 | } 84 | state = StrStr 85 | } 86 | b.WriteRune(c) 87 | } 88 | } 89 | 90 | if b.Len() > 0 { 91 | addToResult() 92 | } 93 | 94 | return res 95 | } 96 | 97 | func ParseTimeRanges(items []string, loc *time.Location) ([][2]int64, *errs.Error) { 98 | result := make([][2]int64, 0, len(items)) 99 | for _, dt := range items { 100 | arr := strings.Split(dt, "-") 101 | t1, err_ := time.ParseInLocation("15:04", arr[0], loc) 102 | if err_ != nil { 103 | return nil, errs.NewMsg(errs.CodeParamInvalid, "time format must be 15:04, current:%s", arr[0]) 104 | } 105 | t2, err_ := time.ParseInLocation("15:04", arr[1], loc) 106 | if err_ != nil { 107 | return nil, errs.NewMsg(errs.CodeParamInvalid, "time format must be 15:04, current:%s", arr[1]) 108 | } 109 | msecs1 := int64(t1.Hour()*60+t1.Minute()) * 60000 110 | msecs2 := int64(t2.Hour()*60+t2.Minute()) * 60000 111 | if msecs1 > msecs2 { 112 | // 结束时间是次日 113 | msecs2 += 24 * 60 * 60000 114 | } 115 | result = append(result, [2]int64{msecs1, msecs2}) 116 | } 117 | sort.Slice(result, func(i, j int) bool { 118 | return result[i][0] < result[j][0] 119 | }) 120 | return result, nil 121 | } 122 | -------------------------------------------------------------------------------- /binance/AGENTS.md: -------------------------------------------------------------------------------- 1 | # Binance 模块开发指南 2 | 3 | ## 1. 核心文件职责 4 | 5 | | 文件名 | 核心职责 | 关键内容 | 6 | | :--- | :--- | :--- | 7 | | **`entry.go`** | **入口与路由** | `New`构造函数;**`Apis` 映射表** (定义所有REST API路由、Host、权重)。 | 8 | | **`data.go`** | **常量定义** | `MethodXXX` 方法名常量;Host类型常量;API枚举值映射 (如订单状态)。 | 9 | | **`types.go`** | **数据结构** | `Binance` 主结构体;**交易所原始JSON响应结构体** (通常以 `Bnb` 开头)。 | 10 | | **`biz_*.go`** | **业务逻辑** | REST API 具体实现。如 `biz_order.go` (订单), `biz_ticker.go` (行情)。 | 11 | | **`ws_*.go`** | **WebSocket** | WS 连接管理、订阅逻辑、消息路由 (`makeHandleWsMsg`)。 | 12 | | **`common.go`** | **通用工具** | 精度计算 `GetPrecision`;限额转换 `GetMarketLimits`。 | 13 | 14 | ## 2. 新增 REST API 开发流程 (Standard Flow) 15 | 16 | 添加新接口必须严格遵循以下 **4步走** 流程: 17 | 18 | ### Step 1: 定义方法常量 (`data.go`) 19 | 在 `data.go` 中添加唯一的 Method 常量。 20 | ```go 21 | // 命名规范: Method + ApiType(Sapi/Fapi/Public) + Action 22 | const MethodSapiGetNewFeature = "sapiGetNewFeature" 23 | ``` 24 | 25 | ### Step 2: 注册 API 路由 (`entry.go`) 26 | 在 `entry.go` 的 `Apis` 变量中注册接口配置。 27 | ```go 28 | MethodSapiGetNewFeature: { 29 | Path: "path/to/endpoint", // URL 相对路径 30 | Host: HostSApi, // 引用 data.go 中的 Host 常量 31 | Method: "GET", // HTTP 方法 32 | Cost: 1, // 权重消耗 33 | }, 34 | ``` 35 | 36 | ### Step 3: 定义原始数据结构 (`types.go`) 37 | 在 `types.go` 中定义接口返回的 **原始 JSON 结构**。 38 | ```go 39 | type BnbNewFeatureRsp struct { 40 | Id string `json:"id"` 41 | Value string `json:"value"` 42 | } 43 | ``` 44 | 45 | ### Step 4: 实现业务方法 (`biz_*.go`) 46 | 在合适的 `biz_` 文件中实现方法,需遵循统一模式: 47 | 48 | ```go 49 | func (e *Binance) FetchNewFeature(symbol string, params map[string]interface{}) (*banexg.StandardType, *errs.Error) { 50 | // 1. 预处理参数与市场 (必须) 51 | args, market, err := e.LoadArgsMarket(symbol, params) 52 | if err != nil { 53 | return nil, err 54 | } 55 | 56 | // 2. 准备请求参数 (使用 utils 提取) 57 | args["symbol"] = market.ID // Binance 通常需要转为内部 ID 58 | someVal := utils.PopMapVal(args, "someParam", "default") 59 | 60 | // 3. 发起请求 (自动处理签名与重试) 61 | // 根据市场类型选择 Method 常量 (如现货、U本位合约可能不同) 62 | method := MethodSapiGetNewFeature 63 | rsp := e.RequestApiRetry(context.Background(), method, args, 1) 64 | if rsp.Error != nil { 65 | return nil, rsp.Error 66 | } 67 | 68 | // 4. 解析与转换 (使用泛型解析器或手动转换) 69 | // 需将 Bnb 原始结构转换为 banexg 标准结构 70 | var raw BnbNewFeatureRsp 71 | if err := utils.Unmarshal(rsp.Data, &raw); err != nil { 72 | return nil, errs.NewMsg(errs.CodeDataLost, "parse error: %v", err) 73 | } 74 | 75 | return &banexg.StandardType{ 76 | ID: raw.Id, 77 | // ... 字段映射 78 | }, nil 79 | } 80 | ``` 81 | 82 | ## 3. 重要开发约定 83 | 84 | ### 3.1 市场与参数处理 85 | - **`LoadArgsMarket`**: 所有业务方法首行必须调用。它负责: 86 | - 复制 `params` 防止副作用。 87 | - 解析 `symbol` 字符串为 `*banexg.Market` 对象。 88 | - 校验市场是否存在。 89 | - **Generic Parsing**: 许多模块(如 Order, Ticker)使用了泛型解析函数(如 `parseOrder[*SpotOrder]`, `parseTickers[*LinearTicker]`),添加新类型时应优先复用这些模式。 90 | 91 | ### 3.2 Host 常量选择指南 92 | - **`HostPublic` / `HostPrivate`**: 现货 (Spot) 93 | - **`HostFApi...`**: U本位合约 (USDT-M Futures) 94 | - **`HostDApi...`**: 币本位合约 (COIN-M Futures) 95 | - **`HostSApi...`**: 杠杆/理财/通用 (Margin/Savings) 96 | - **`HostEApi...`**: 期权 (Options) 97 | 98 | ### 3.3 错误处理 99 | - 统一使用 `*errs.Error`。 100 | - 参数错误使用 `errs.NewMsg(errs.CodeParamInvalid, "msg")`。 101 | - 网络/API错误由 `RequestApiRetry` 自动封装。 102 | 103 | ### 3.4 WebSocket 开发 104 | - 消息路由位于 `ws_biz.go` 的 `makeHandleWsMsg` 函数。 105 | - 需根据 `item.Event` 字符串分发到具体的 `handleXXX` 方法。 106 | -------------------------------------------------------------------------------- /log/config.go: -------------------------------------------------------------------------------- 1 | package log 2 | 3 | import ( 4 | "go.uber.org/zap" 5 | "go.uber.org/zap/zapcore" 6 | "time" 7 | ) 8 | 9 | const ( 10 | defaultLogMaxSize = 300 // MB 11 | ) 12 | 13 | // FileLogConfig serializes file log related config in toml/json. 14 | type FileLogConfig struct { 15 | // Log logpath 16 | LogPath string `toml:"logpath" json:"logpath"` 17 | // Max size for a single file, in MB. 18 | MaxSize int `toml:"max-size" json:"max-size"` 19 | // Max log keep days, default is never deleting. 20 | MaxDays int `toml:"max-days" json:"max-days"` 21 | // Maximum number of old log files to retain. 22 | MaxBackups int `toml:"max-backups" json:"max-backups"` 23 | } 24 | 25 | // Config serializes log related config in toml/json. 26 | type Config struct { 27 | // Log level. 28 | Level string `toml:"level" json:"level"` 29 | // Log format. one of json, text, or console. 30 | Format string `toml:"format" json:"format"` 31 | // Disable automatic timestamps in output. 32 | DisableTimestamp bool `toml:"disable-timestamp" json:"disable-timestamp"` 33 | // Stdout enable or not. 34 | Stdout bool `toml:"stdout" json:"stdout"` 35 | // File log config. 36 | File *FileLogConfig `toml:"file" json:"file"` 37 | // Development puts the logger in development mode, which changes the 38 | // behavior of DPanicLevel and takes stacktraces more liberally. 39 | Development bool `toml:"development" json:"development"` 40 | // DisableCaller stops annotating logs with the calling function's file 41 | // name and line number. By default, all logs are annotated. 42 | DisableCaller bool `toml:"disable-caller" json:"disable-caller"` 43 | // DisableStacktrace completely disables automatic stacktrace capturing. By 44 | // default, stacktraces are captured for WarnLevel and above logs in 45 | // development and ErrorLevel and above in production. 46 | DisableStacktrace bool `toml:"disable-stacktrace" json:"disable-stacktrace"` 47 | // DisableErrorVerbose stops annotating logs with the full verbose error 48 | // message. 49 | DisableErrorVerbose bool `toml:"disable-error-verbose" json:"disable-error-verbose"` 50 | // SamplingConfig sets a sampling strategy for the logger. Sampling caps the 51 | // global CPU and I/O load that logging puts on your process while attempting 52 | // to preserve a representative subset of your logs. 53 | // 54 | // Values configured here are per-second. See zapcore.NewSampler for details. 55 | Sampling *zap.SamplingConfig `toml:"sampling" json:"sampling"` 56 | // custom log handlers 57 | Handlers []zapcore.Core 58 | } 59 | 60 | // ZapProperties records some information about zap. 61 | type ZapProperties struct { 62 | Core zapcore.Core 63 | Syncer zapcore.WriteSyncer 64 | Level zap.AtomicLevel 65 | } 66 | 67 | func newZapTextEncoder(cfg *Config) zapcore.Encoder { 68 | return NewTextEncoderByConfig(cfg) 69 | } 70 | 71 | func (cfg *Config) buildOptions(errSink zapcore.WriteSyncer) []zap.Option { 72 | opts := []zap.Option{zap.ErrorOutput(errSink)} 73 | 74 | if cfg.Development { 75 | opts = append(opts, zap.Development()) 76 | } 77 | 78 | if !cfg.DisableCaller { 79 | opts = append(opts, zap.AddCaller()) 80 | } 81 | 82 | stackLevel := zap.ErrorLevel 83 | if cfg.Development { 84 | stackLevel = zap.WarnLevel 85 | } 86 | if !cfg.DisableStacktrace { 87 | opts = append(opts, zap.AddStacktrace(stackLevel)) 88 | } 89 | 90 | if cfg.Sampling != nil { 91 | opts = append(opts, zap.WrapCore(func(core zapcore.Core) zapcore.Core { 92 | return zapcore.NewSamplerWithOptions(core, time.Second, cfg.Sampling.Initial, cfg.Sampling.Thereafter, zapcore.SamplerHook(cfg.Sampling.Hook)) 93 | })) 94 | } 95 | 96 | return opts 97 | } 98 | -------------------------------------------------------------------------------- /binance/biz_order_book.go: -------------------------------------------------------------------------------- 1 | package binance 2 | 3 | import ( 4 | "context" 5 | "github.com/banbox/banexg" 6 | "github.com/banbox/banexg/errs" 7 | "github.com/banbox/banexg/utils" 8 | "strconv" 9 | ) 10 | 11 | func (e *Binance) FetchOrderBook(symbol string, limit int, params map[string]interface{}) (*banexg.OrderBook, *errs.Error) { 12 | args, market, err := e.LoadArgsMarket(symbol, params) 13 | if err != nil { 14 | return nil, err 15 | } 16 | args["symbol"] = market.ID 17 | if limit > 0 { 18 | args["limit"] = limit 19 | } 20 | var method string 21 | if market.Option { 22 | method = MethodEapiPublicGetDepth 23 | } else if market.Linear { 24 | method = MethodFapiPublicGetDepth 25 | } else if market.Inverse { 26 | method = MethodDapiPublicGetDepth 27 | } else { 28 | method = MethodPublicGetDepth 29 | } 30 | tryNum := e.GetRetryNum("FetchOrderBook", 1) 31 | rsp := e.RequestApiRetry(context.Background(), method, args, tryNum) 32 | if rsp.Error != nil { 33 | return nil, rsp.Error 34 | } 35 | var book *banexg.OrderBook 36 | if market.Option { 37 | book, err = parseOrderBook[OptionOrderBook](market, rsp) 38 | } else if market.Linear { 39 | book, err = parseOrderBook[LinearOrderBook](market, rsp) 40 | } else if market.Inverse { 41 | book, err = parseOrderBook[InverseOrderBook](market, rsp) 42 | } else { 43 | book, err = parseOrderBook[SpotOrderBook](market, rsp) 44 | } 45 | if book != nil && book.TimeStamp == 0 { 46 | book.TimeStamp = e.MilliSeconds() 47 | } 48 | return book, err 49 | } 50 | 51 | func parseOrderBook[T IBnbOrderBook](m *banexg.Market, rsp *banexg.HttpRes) (*banexg.OrderBook, *errs.Error) { 52 | var data = new(T) 53 | err := utils.UnmarshalString(rsp.Content, &data, utils.JsonNumDefault) 54 | if err != nil { 55 | return nil, errs.New(errs.CodeUnmarshalFail, err) 56 | } 57 | result := (*data).ToStdOrderBook(m) 58 | return result, nil 59 | } 60 | 61 | func (o BaseOrderBook) ToStdOrderBook(market *banexg.Market) *banexg.OrderBook { 62 | var asks = make([][2]float64, len(o.Asks)) 63 | var bids = make([][2]float64, len(o.Bids)) 64 | for i, it := range o.Asks { 65 | item := [2]float64{} 66 | item[0], _ = strconv.ParseFloat(it[0], 64) 67 | item[1], _ = strconv.ParseFloat(it[1], 64) 68 | asks[i] = item 69 | } 70 | for i, it := range o.Bids { 71 | item := [2]float64{} 72 | item[0], _ = strconv.ParseFloat(it[0], 64) 73 | item[1], _ = strconv.ParseFloat(it[1], 64) 74 | bids[i] = item 75 | } 76 | var res = banexg.OrderBook{ 77 | Symbol: market.Symbol, 78 | Asks: banexg.NewOdBookSide(false, len(asks), asks), 79 | Bids: banexg.NewOdBookSide(true, len(bids), bids), 80 | Cache: make([]map[string]string, 0), 81 | } 82 | return &res 83 | } 84 | 85 | func (o OptionOrderBook) ToStdOrderBook(market *banexg.Market) *banexg.OrderBook { 86 | var res = o.BaseOrderBook.ToStdOrderBook(market) 87 | res.TimeStamp = o.Time 88 | res.Nonce = int64(o.UpdateID) 89 | return res 90 | } 91 | 92 | func (o LinearOrderBook) ToStdOrderBook(market *banexg.Market) *banexg.OrderBook { 93 | var res = o.BaseOrderBook.ToStdOrderBook(market) 94 | res.TimeStamp = o.Time 95 | res.Nonce = int64(o.UpdateID) 96 | return res 97 | } 98 | 99 | func (o InverseOrderBook) ToStdOrderBook(market *banexg.Market) *banexg.OrderBook { 100 | var res = o.LinearOrderBook.ToStdOrderBook(market) 101 | return res 102 | } 103 | 104 | func (o SpotOrderBook) ToStdOrderBook(market *banexg.Market) *banexg.OrderBook { 105 | var res = o.BaseOrderBook.ToStdOrderBook(market) 106 | res.Nonce = int64(o.UpdateID) 107 | return res 108 | } 109 | -------------------------------------------------------------------------------- /log/capture.go: -------------------------------------------------------------------------------- 1 | package log 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "io" 7 | "os" 8 | "sync" 9 | ) 10 | 11 | // OutCapture capture stdout/stderr and write to file 12 | type OutCapture struct { 13 | orgStdout *os.File 14 | orgStderr *os.File 15 | outFile *os.File 16 | errFile *os.File 17 | outReader *os.File 18 | outWriter *os.File 19 | errReader *os.File 20 | errWriter *os.File 21 | wg sync.WaitGroup 22 | } 23 | 24 | // NewOutCaptureToFile outPath, errPath can be same or different 25 | func NewOutCaptureToFile(outPath, errPath string) (*OutCapture, error) { 26 | // 创建输出文件 27 | outFile, err := os.Create(outPath) 28 | if err != nil { 29 | return nil, err 30 | } 31 | 32 | var errFile *os.File 33 | if errPath == "" || errPath == outPath { 34 | errFile = outFile 35 | } else { 36 | // 创建错误输出文件 37 | errFile, err = os.Create(errPath) 38 | if err != nil { 39 | outFile.Close() 40 | return nil, err 41 | } 42 | } 43 | var sta *OutCapture 44 | sta, err = NewOutCapture(outFile, errFile) 45 | if err != nil { 46 | outFile.Close() 47 | if outFile != errFile { 48 | errFile.Close() 49 | } 50 | return nil, err 51 | } 52 | return sta, nil 53 | } 54 | 55 | func NewOutCapture(outFile, errFile *os.File) (*OutCapture, error) { 56 | if outFile == nil || errFile == nil { 57 | return nil, errors.New("outFile and errFile are required") 58 | } 59 | // 保存原始的stdout和stderr 60 | orgStdout := os.Stdout 61 | orgStderr := os.Stderr 62 | 63 | // 为stdout创建pipe 64 | outReader, outWriter, err := os.Pipe() 65 | if err != nil { 66 | return nil, err 67 | } 68 | 69 | // 为stderr创建pipe 70 | errReader, errWriter, err := os.Pipe() 71 | if err != nil { 72 | outReader.Close() 73 | outWriter.Close() 74 | return nil, err 75 | } 76 | 77 | return &OutCapture{ 78 | orgStdout: orgStdout, 79 | orgStderr: orgStderr, 80 | outFile: outFile, 81 | errFile: errFile, 82 | outReader: outReader, 83 | outWriter: outWriter, 84 | errReader: errReader, 85 | errWriter: errWriter, 86 | }, nil 87 | } 88 | 89 | // Start start capture stdout/stderr 90 | func (c *OutCapture) Start() { 91 | // 将标准输出重定向到管道 92 | os.Stdout = c.outWriter 93 | os.Stderr = c.errWriter 94 | 95 | // 启动goroutine监听stdout并写入文件 96 | c.wg.Add(1) 97 | go func() { 98 | defer c.wg.Done() 99 | io.Copy(io.MultiWriter(c.outFile, c.orgStdout), c.outReader) 100 | }() 101 | 102 | // 启动goroutine监听stderr并写入文件 103 | c.wg.Add(1) 104 | go func() { 105 | defer c.wg.Done() 106 | io.Copy(io.MultiWriter(c.errFile, c.orgStderr), c.errReader) 107 | }() 108 | } 109 | 110 | // Stop stop capture stdout/stderr 111 | func (c *OutCapture) Stop() { 112 | // 恢复原始的stdout和stderr 113 | os.Stdout = c.orgStdout 114 | os.Stderr = c.orgStderr 115 | 116 | // 关闭写入端以触发读取goroutine的退出 117 | err := c.outWriter.Close() 118 | if err != nil { 119 | fmt.Printf("close outWriter fail %v\n", err) 120 | } 121 | err = c.errWriter.Close() 122 | if err != nil { 123 | fmt.Printf("close errWriter fail %v\n", err) 124 | } 125 | 126 | // 等待所有goroutine完成 127 | c.wg.Wait() 128 | 129 | // 关闭所有文件 130 | err = c.outReader.Close() 131 | if err != nil { 132 | fmt.Printf("close outReader fail %v\n", err) 133 | } 134 | err = c.errReader.Close() 135 | if err != nil { 136 | fmt.Printf("close outReader fail %v\n", err) 137 | } 138 | err = c.outFile.Close() 139 | if err != nil { 140 | fmt.Printf("close outFile fail %v\n", err) 141 | } 142 | if c.errFile != c.outFile { 143 | err = c.errFile.Close() 144 | if err != nil { 145 | fmt.Printf("close errFile fail %v\n", err) 146 | } 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /china/common.go: -------------------------------------------------------------------------------- 1 | package china 2 | 3 | import ( 4 | "github.com/banbox/banexg" 5 | "github.com/banbox/banexg/errs" 6 | "github.com/banbox/banexg/log" 7 | utils2 "github.com/banbox/banexg/utils" 8 | "go.uber.org/zap" 9 | "strings" 10 | ) 11 | 12 | func (m *ItemMarket) Resolve(bases map[string]*ItemMarket) { 13 | if m.Extend == "" { 14 | return 15 | } 16 | base, _ := bases[m.Extend] 17 | if base == nil { 18 | log.Warn("china market extend invalid", zap.String("val", m.Extend), zap.String("from", m.Code)) 19 | return 20 | } 21 | if m.Market == "" && base.Market != "" { 22 | m.Market = base.Market 23 | } 24 | if m.Exchange == "" && base.Exchange != "" { 25 | m.Exchange = base.Exchange 26 | } 27 | if m.DayRanges == nil && len(base.DayRanges) > 0 { 28 | m.DayRanges = base.DayRanges 29 | } 30 | if m.NightRanges == nil && len(base.NightRanges) > 0 { 31 | m.NightRanges = base.NightRanges 32 | } 33 | if m.Fee == nil && base.Fee != nil { 34 | m.Fee = base.Fee 35 | } 36 | if m.PriceTick == 0 && base.PriceTick != 0 { 37 | m.PriceTick = base.PriceTick 38 | } 39 | if m.LimitChgPct == 0 && base.LimitChgPct != 0 { 40 | m.LimitChgPct = base.LimitChgPct 41 | } 42 | if m.MarginPct == 0 && base.MarginPct != 0 { 43 | m.MarginPct = base.MarginPct 44 | } 45 | } 46 | 47 | func (m *ItemMarket) toSymbol(parts []*utils2.StrType, toStd bool) (string, *errs.Error) { 48 | if len(parts) == 0 { 49 | return "", errs.NewMsg(errs.CodeParamRequired, "parts is empty") 50 | } 51 | exchange := ctExgs[m.Exchange] 52 | var b strings.Builder 53 | if m.Market != banexg.MarketSpot { 54 | // 期货、期权 55 | p0, p1 := parts[0], parts[1] 56 | if p0.Type != utils2.StrStr { 57 | return "", errs.NewMsg(errs.CodeParamInvalid, "part0 should be str") 58 | } 59 | if toStd { 60 | b.WriteString(strings.ToUpper(p0.Val)) 61 | } else if exchange.CaseLower { 62 | b.WriteString(strings.ToLower(p0.Val)) 63 | } else { 64 | b.WriteString(p0.Val) 65 | } 66 | if p1.Type != utils2.StrInt { 67 | return "", errs.NewMsg(errs.CodeParamInvalid, "part1 should be int") 68 | } 69 | // 写入年月 70 | p1val := p1.Val 71 | if toStd || (p1val == "000" || p1val == "888" || p1val == "999") { 72 | b.WriteString(p1val) 73 | } else { 74 | b.WriteString(p1val[len(p1val)-exchange.DateNum:]) 75 | } 76 | // 判断是否期权 77 | if len(parts) == 4 && parts[2].Type == utils2.StrStr && len(parts[2].Val) == 1 && parts[3].Type == utils2.StrInt { 78 | // 第三个是C/P,第四个是价格 79 | if toStd { 80 | b.WriteString(strings.ReplaceAll(parts[2].Val, "-", "")) 81 | } else if exchange.OptionDash { 82 | b.WriteString("-") 83 | b.WriteString(parts[2].Val) 84 | b.WriteString("-") 85 | } else { 86 | b.WriteString(parts[2].Val) 87 | } 88 | b.WriteString(parts[3].Val) 89 | } else { 90 | for _, p := range parts[2:] { 91 | b.WriteString(p.Val) 92 | } 93 | if m.Market == banexg.MarketOption { 94 | return "", errs.NewMsg(errs.CodeParamInvalid, "invalid option symbol: %s", b.String()) 95 | } 96 | } 97 | return b.String(), nil 98 | } 99 | return "", errs.NewMsg(errs.CodeNotImplement, "method not implement") 100 | } 101 | 102 | /* 103 | ToStdSymbol 104 | 转为标准Symbol,注意期货的年月需要提前归一化为4位数字 105 | */ 106 | func (m *ItemMarket) ToStdSymbol(parts []*utils2.StrType) (string, *errs.Error) { 107 | return m.toSymbol(parts, true) 108 | } 109 | 110 | /* 111 | ToRawSymbol 112 | 转为交易所Symbol 113 | */ 114 | func (m *ItemMarket) ToRawSymbol(parts []*utils2.StrType) (string, *errs.Error) { 115 | return m.toSymbol(parts, false) 116 | } 117 | 118 | func (f *Fee) ParseStd() { 119 | if f.Val == -999 { 120 | f.Val = 0 121 | } else if f.Val == 0 { 122 | f.Val = -1 123 | } 124 | if f.ValCT == -999 { 125 | f.ValCT = 0 126 | } else if f.ValCT == 0 { 127 | f.ValCT = -1 128 | } 129 | if f.ValTD == -999 { 130 | f.ValTD = 0 131 | } else if f.ValTD == 0 { 132 | f.ValTD = -1 133 | } 134 | if f.ValTD >= 0 { 135 | // 日内交易手续费,转为平今格式 136 | f.ValCT = f.ValTD*2 - f.Val 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /binance/common.go: -------------------------------------------------------------------------------- 1 | package binance 2 | 3 | import ( 4 | "github.com/banbox/banexg" 5 | "github.com/banbox/banexg/utils" 6 | "strconv" 7 | ) 8 | 9 | func (mar *BnbMarket) GetPrecision() *banexg.Precision { 10 | var pre = banexg.Precision{} 11 | if mar.QuantityPrecision > 0 { 12 | pre.Amount = float64(mar.QuantityPrecision) 13 | pre.ModeAmount = banexg.PrecModeDecimalPlace 14 | } else if mar.QuantityScale > 0 { 15 | pre.Amount = float64(mar.QuantityScale) 16 | pre.ModeAmount = banexg.PrecModeDecimalPlace 17 | } 18 | if mar.PricePrecision > 0 { 19 | pre.Price = float64(mar.PricePrecision) 20 | pre.ModePrice = banexg.PrecModeDecimalPlace 21 | } else if mar.PriceScale > 0 { 22 | pre.Price = float64(mar.PriceScale) 23 | pre.ModePrice = banexg.PrecModeDecimalPlace 24 | } 25 | pre.Base = float64(mar.BaseAssetPrecision) 26 | pre.ModeBase = banexg.PrecModeDecimalPlace 27 | pre.Quote = float64(mar.QuotePrecision) 28 | pre.ModeQuote = banexg.PrecModeDecimalPlace 29 | return &pre 30 | } 31 | 32 | func (mar *BnbMarket) GetMarketLimits(p *banexg.Precision) *banexg.MarketLimits { 33 | minQty, _ := strconv.ParseFloat(mar.MinQty, 64) 34 | maxQty, _ := strconv.ParseFloat(mar.MaxQty, 64) 35 | var filters = make(map[string]BnbFilter) 36 | for _, flt := range mar.Filters { 37 | filters[utils.GetMapVal(flt, "filterType", "")] = flt 38 | } 39 | var res = banexg.MarketLimits{ 40 | Amount: &banexg.LimitRange{ 41 | Min: minQty, 42 | Max: maxQty, 43 | }, 44 | Leverage: &banexg.LimitRange{}, 45 | Price: &banexg.LimitRange{}, 46 | Cost: &banexg.LimitRange{}, 47 | Market: &banexg.LimitRange{}, 48 | } 49 | if flt, ok := filters["PRICE_FILTER"]; ok { 50 | // PRICE_FILTER reports zero values for maxPrice 51 | // since they updated filter types in November 2018 52 | // https://github.com/ccxt/ccxt/issues/4286 53 | // therefore limits['price']['max'] doesn't have any meaningful value except None 54 | res.Price.Min = utils.GetMapFloat(flt, "minPrice") 55 | res.Price.Max = utils.GetMapFloat(flt, "maxPrice") 56 | priceTick := utils.GetMapFloat(flt, "tickSize") 57 | if priceTick > 0 { 58 | p.Price = priceTick 59 | p.ModePrice = banexg.PrecModeTickSize 60 | } 61 | } 62 | if flt, ok := filters["LOT_SIZE"]; ok { 63 | res.Amount.Min = utils.GetMapFloat(flt, "minQty") 64 | res.Amount.Max = utils.GetMapFloat(flt, "maxQty") 65 | amountTick := utils.GetMapFloat(flt, "stepSize") 66 | if amountTick > 0 { 67 | p.Amount = amountTick 68 | p.ModeAmount = banexg.PrecModeTickSize 69 | } 70 | } 71 | if flt, ok := filters["MARKET_LOT_SIZE"]; ok { 72 | res.Market.Min = utils.GetMapFloat(flt, "minQty") 73 | res.Market.Max = utils.GetMapFloat(flt, "maxQty") 74 | } 75 | if flt, ok := filters["MIN_NOTIONAL"]; ok { 76 | res.Cost.Min = utils.GetMapFloat(flt, "notional") 77 | } else if flt, ok := filters["NOTIONAL"]; ok { 78 | res.Cost.Min = utils.GetMapFloat(flt, "minNotional") 79 | res.Cost.Max = utils.GetMapFloat(flt, "maxNotional") 80 | } 81 | return &res 82 | } 83 | 84 | func (b *LinearSymbolLvgBrackets) ToStdBracket() *SymbolLvgBrackets { 85 | var res = SymbolLvgBrackets{ 86 | NotionalCoef: b.NotionalCoef, 87 | Brackets: make([]*LvgBracket, len(b.Brackets)), 88 | } 89 | for i, item := range b.Brackets { 90 | res.Brackets[i] = &LvgBracket{ 91 | BaseLvgBracket: item.BaseLvgBracket, 92 | Capacity: item.NotionalCap, 93 | Floor: item.NotionalFloor, 94 | } 95 | } 96 | return &res 97 | } 98 | func (b *LinearSymbolLvgBrackets) GetSymbol() string { 99 | return b.Symbol 100 | } 101 | 102 | func (b *InversePairLvgBrackets) ToStdBracket() *SymbolLvgBrackets { 103 | var res = SymbolLvgBrackets{ 104 | NotionalCoef: b.NotionalCoef, 105 | Brackets: make([]*LvgBracket, len(b.Brackets)), 106 | } 107 | for i, item := range b.Brackets { 108 | res.Brackets[i] = &LvgBracket{ 109 | BaseLvgBracket: item.BaseLvgBracket, 110 | Capacity: item.QtyCap, 111 | Floor: item.QtylFloor, 112 | } 113 | } 114 | return &res 115 | } 116 | func (b *InversePairLvgBrackets) GetSymbol() string { 117 | return b.Symbol 118 | } 119 | -------------------------------------------------------------------------------- /utils/file.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "fmt" 5 | "github.com/banbox/banexg/errs" 6 | "os" 7 | "path/filepath" 8 | "runtime" 9 | "strconv" 10 | "strings" 11 | "time" 12 | ) 13 | 14 | func WriteFile(path string, data []byte) error { 15 | file, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644) 16 | if err != nil { 17 | return err 18 | } 19 | defer file.Close() 20 | 21 | // 将字符串写入文件 22 | _, err = file.Write(data) 23 | if err != nil { 24 | return err 25 | } 26 | 27 | // 将文件的缓冲区内容刷新到磁盘 28 | err = file.Sync() 29 | if err != nil { 30 | return err 31 | } 32 | return nil 33 | } 34 | 35 | func WriteJsonFile(path string, data interface{}) error { 36 | bytes, err := Marshal(data) 37 | if err != nil { 38 | return err 39 | } 40 | return WriteFile(path, bytes) 41 | } 42 | 43 | func ReadFile(path string) ([]byte, error) { 44 | _, err := os.Stat(path) 45 | if err != nil { 46 | return nil, err 47 | } 48 | file, err := os.Open(path) 49 | if err != nil { 50 | return nil, err 51 | } 52 | defer file.Close() 53 | 54 | // 读取文件内容 55 | stat, _ := file.Stat() 56 | fileSize := stat.Size() 57 | content := make([]byte, fileSize) 58 | _, err = file.Read(content) 59 | if err != nil { 60 | return nil, err 61 | } 62 | return content, nil 63 | } 64 | 65 | /* 66 | ReadJsonFile 67 | 68 | numType: JsonNumDefault(JsonNumFloat), JsonNumStr, JsonNumAuto 69 | */ 70 | func ReadJsonFile(path string, obj interface{}, numType int) error { 71 | data, err := ReadFile(path) 72 | if err != nil { 73 | return err 74 | } 75 | return Unmarshal(data, obj, numType) 76 | } 77 | 78 | func WriteCacheFile(key, content string, expSecs int) *errs.Error { 79 | cacheDir, err := GetCacheDir() 80 | if err != nil { 81 | return errs.New(errs.CodeIOReadFail, err) 82 | } 83 | path := filepath.Join(cacheDir, "banexg_"+key) 84 | file, err := os.Create(path) 85 | if err != nil { 86 | return errs.New(errs.CodeIOWriteFail, err) 87 | } 88 | expireAt := int64(0) 89 | if expSecs > 0 { 90 | expireAt = time.Now().UnixMilli() + int64(expSecs)*1000 91 | } 92 | _, err = file.WriteString(fmt.Sprintf("%v\n", expireAt)) 93 | if err != nil { 94 | return errs.New(errs.CodeIOWriteFail, err) 95 | } 96 | _, err = file.WriteString(content) 97 | if err != nil { 98 | return errs.New(errs.CodeIOWriteFail, err) 99 | } 100 | err = file.Close() 101 | if err != nil { 102 | return errs.New(errs.CodeIOWriteFail, err) 103 | } 104 | return nil 105 | } 106 | 107 | func ReadCacheFile(key string) (string, *errs.Error) { 108 | cacheDir, err := GetCacheDir() 109 | if err != nil { 110 | return "", errs.New(errs.CodeIOReadFail, err) 111 | } 112 | path := filepath.Join(cacheDir, "banexg_"+key) 113 | data, err := os.ReadFile(path) 114 | if err != nil { 115 | return "", errs.New(errs.CodeIOReadFail, err) 116 | } 117 | fileText := string(data) 118 | sepIdx := strings.Index(fileText, "\n") 119 | if sepIdx <= 0 { 120 | return "", errs.NewMsg(errs.CodeInvalidData, "newLineIdx should > 0, current: %v", sepIdx) 121 | } 122 | expireMS, err := strconv.ParseInt(fileText[:sepIdx], 10, 64) 123 | if err != nil { 124 | return "", errs.New(errs.CodeInvalidData, err) 125 | } 126 | content := fileText[sepIdx+1:] 127 | if expireMS > 0 && expireMS < time.Now().UnixMilli() { 128 | stamp := time.UnixMilli(expireMS) 129 | expDate := stamp.Format("2006-01-02 15:04:05") 130 | return content, errs.NewMsg(errs.CodeExpired, "expired at: %v", expDate) 131 | } 132 | return content, nil 133 | } 134 | 135 | func GetCacheDir() (string, error) { 136 | switch runtime.GOOS { 137 | case "windows": 138 | homeDir, err := os.UserHomeDir() 139 | if err != nil { 140 | return "", err 141 | } 142 | return filepath.Join(homeDir, "AppData", "Local"), nil 143 | case "darwin": // macOS 144 | homeDir, err := os.UserHomeDir() 145 | if err != nil { 146 | return "", err 147 | } 148 | return filepath.Join(homeDir, "Library", "Caches"), nil 149 | default: 150 | // 默认linux 151 | // 优先使用 XDG_CACHE_HOME 环境变量 152 | if xdgCache := os.Getenv("XDG_CACHE_HOME"); xdgCache != "" { 153 | return xdgCache, nil 154 | } 155 | // 回退到 $HOME/.cache 156 | homeDir, err := os.UserHomeDir() 157 | if err != nil { 158 | return "", err 159 | } 160 | return filepath.Join(homeDir, ".cache"), nil 161 | } 162 | } 163 | -------------------------------------------------------------------------------- /utils/crypto_test.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "github.com/banbox/banexg/log" 5 | "go.uber.org/zap" 6 | "testing" 7 | ) 8 | 9 | type SignCase struct { 10 | data string 11 | secret string 12 | method string 13 | hash string 14 | digest string 15 | sign string 16 | } 17 | 18 | var fakePrivatePem = `-----BEGIN RSA PRIVATE KEY----- 19 | MIIG5AIBAAKCAYEA3s4G3wS0j9yYJPsD6lO+rWK9iQmyXpORCw2Ix9bv1JII8xUv 20 | XxpMuMMokEvrUUvnTdfZAaCNmxe2nNUdjASIkSuyXqmFvCWnElyZQnw+NYBAlYH6 21 | wJQb3LQnBp5+lDq33SlBUHzRF8wPeXgIs7BwyZEvdw9UOqjW7sSDBiYibEV9Zwid 22 | ILkPyrGAnhZvF8lntBgm24pkxf6Typf2Q1XMSmLLrTOQZKmWyliOG6KSCGThYmv1 23 | Jf3Q+WohkSAtcjMnFm/PiOaWqGlEcuVza2zJSXTpRU0cIz0an1TfnI+mG6nloWGB 24 | 9IKtVG0ALqFdy+0YDqEleKTYegirUQJ1t5J7wXrPKX0VBl1M0yRHrkDogwszn8L3 25 | MPPlLjk3GOKQdCCaW0b8twt4sacgK30HAGKz8n82rGDviNiTcCPh8L8+J91V2DgK 26 | jofXzsbJtSZJFsYocU1t7AsCEZubdmoqn8YQjiuWoqWpysKDFHZIoTFTTH98iaBZ 27 | EsDTe45zJq5te/r/AgMBAAECggGBALnQGOrlgbB4yGoO7bT/IoZ3Upp2+8rkRpJx 28 | NyFyn5EoOU6A3IDz7ggouiudJSMnqj/BQ7mXrIErxaAGHB4pqbtoNdm8h0viGvO1 29 | RhusgjUcQMBvJjB1VMc7d1CN3gLA9ZX8UfxOHBM8m6sx8A/rliSEcJFat2Q0awu1 30 | 14/JPewOCAdlp6UisYjZf+pXy06LKXGIst9lC6YUKi1LjpWZeEaRHkvUNRe+V4Np 31 | Vxq0+hUGDPGIF2sXwrA/Ur81lrEm9mOFoIqIM6H99E22lDq3g/azxuRG/zSBUbNA 32 | HO7NUarKQRwsI36QzyPaNH+Ubn6AGKriGEz4f/VlrLdadJpz50iKn8zAJCxNAuCM 33 | hwYIi+jqXiK2fuN70ZMg4LKrWVtqhxQTEAh6B5PXwxfAQk13mMWiuISp7tUOBm8+ 34 | 5SuaXCstMKpjxGReKyqTnfntB8fKN/uEKOO1JKS/tvGyOnF08lHd9fLd5Fi2PfmV 35 | rwXdfUYmm0tLJyzCoHd+9J7zcF10AQKBwQD5YaVBWD0madrNV7Y7q+75OvCYW1pk 36 | lecSDR8kYqwbPjYA+oPIeJQQBJfZQuYj0Iw5ALW2F4VBQQu5M/ly1MFRTjGJnTbx 37 | 80mrfbljVk3ZPDD+6Y/IZeZrZUv+XdSBEY0vcpCeUZcugRUIeIQlWtpGR2kkBuAw 38 | m90P/urj1Ae5wzQBRfr/ogOUOYAVtlWSVIaFtSJI0jPRSG72fFSIkX2QXhNWuqJq 39 | GnREfPnv9TCTqA3YX7OjCx5kD+yICphwyg0CgcEA5LfQRiqJRc/x/EjvBDt2kxzn 40 | xqi/XiBHFxbQb+JVKttLBmkXwHpyWcwdki39Q3jD/ThnJWVlSuOkQNdFw1oyqTIf 41 | UEl0p15mdrd1Bn80NiUCkSou7kZnAnENfC+LWw8wjNTaSAq+EzBtiYWWk2pLTAOm 42 | +ma1a2/X9k1WegknHbiT8KE0ZCtNSwty7VMqbUcRG0aWsSuTrPg3D1LzTPD1lRu3 43 | E8TZjrIkxWPVdTkfjrTNfAOOsSmnoT1FIXMyypI7AoHAQFt7u1ZbSZuN1Opq5BFl 44 | 9bnJN3hz5nttC5KJU+mHAuzWIQCFm+nKRCv7SB1kqR974IYXXuvI/uMbdGs+V+0i 45 | CqqETEBfWqdvfqtOeZ1fL83B0zdRXOU3RsX4i6eJXNm7tt/5BHKH8n9rfyki6UT+ 46 | CZ8KOjrwBnti3GrsEWm5qK4AsMdvlCMqi0kfjfrlMINRyBXLyEE/ECaCRGgnpKrv 47 | XZ95nCtEGN/E25vpII0FQUXgdNOV12DaMfaOEzmwx4LNAoHARiWqFxsMpwCz8vBb 48 | fizOnSgMXf17U98KbqZsnyQHgvFm/TxWMI5da/USTLcWKg9r7MnTuMB0ZJeU1N4x 49 | Y0zSpNneiL0+reZh/p8doTR6SvDm7KbHZgTpqvIJdMEQOIlcFpVhrR6+VRxRPBBg 50 | si2zkki8eafulFjlH4FwuFT+TjtCBFcsvlwZhJ6qTOdo58MYGAl6RjRbQn2ORYDn 51 | Zf2xFF4/tCx3nTA93txTp3QxnY8ORq7AoM1pwCYOgcfXGBHpAoHBAJwcdWFgP1Wb 52 | D/DvOZCvmfz62+llQWVsIW6UVpFZKXqAG7z5tKKS1pWBy9QwROjoxtX8ZiSsde3J 53 | wFPib8wMKq6gD27bpdyR4d4oUxs2D/XXK17dv8JSCGLAeCWt20VoRpl43Y8wniGo 54 | 3zJcpJSxhCmsCMPDr9znljf4Bu+/hDyBY/DDb504NMW1CTrdbnM+IX4IFSXb7UQl 55 | 31QTdan+0NeUh6TrAFhptAAESwSj1vt9tcznt/lncarZy5NQ6H7tZg== 56 | -----END RSA PRIVATE KEY----- 57 | ` 58 | 59 | func TestSignature(t *testing.T) { 60 | 61 | fakeSecret := "ThisIsASecret" 62 | 63 | items := []SignCase{ 64 | { 65 | data: "timestamp=1702376740732&recvWindow=10000", 66 | secret: fakeSecret, 67 | method: "hmac", 68 | hash: "sha256", 69 | digest: "hex", 70 | sign: "721c211bd113874ac03604e7d9cc23e8ac28d557f509749fd68461f326f555b3", 71 | }, 72 | { 73 | data: "timestamp=1702383061579&recvWindow=10000", 74 | secret: fakePrivatePem, 75 | method: "rsa", 76 | hash: "sha256", 77 | sign: "s65Oyv%2BBwC9MWryDBCytJ51x%2BYhWE20EC2c7BKGW%2BolHx%2B886uHVQ3O%2F5tT6NKQDWZJBQ0jYcSERGDWcMgXTjFdNztmRuTzSMPBlP56q4R8iEfYPNnc4z8W3UZMXfdlA72AREylggHhbY8y40ailx%2FSWoe2DwZOzMwh88kcH7xrovouGZzq1ocUSRX%2FTVdEpu%2BIoYuyL2Ug4zgZ9puVAbVCCC9oo9bUNdb00z4gHZy58DLpf0GWvc1vSEOHdHeqRQzTuP0KpC3PNxI%2B6g5E7FaTinO5OshXgPYExJKXjOfW%2BOrRvW%2F2FHos1EBP2btSGy5wluC5cvV8TAyBka5sNks8Ob1fD8Wu2ATRdMUchnw2M63fweFh4g0EnRGjbUHzt1WDGpbu8Uiqir%2BZpKY1hxh%2B6bqnPXVvRasnNMH9UwzTeI40pocJtjqfiRQLrZvuyJGL6IwsrLTTddqkmENL%2FSuViK21gq0YbjfMLHEUhtTGvdxWyTkA6ieRBYK8oWcGr", 78 | }, 79 | } 80 | 81 | for _, item := range items { 82 | sign1, err := Signature(item.data, item.secret, item.method, item.hash, item.digest) 83 | if err == nil && sign1 == item.sign { 84 | log.Info("pass", zap.String("method", item.method), zap.String("sign", sign1)) 85 | } else { 86 | log.Error("FAIL", zap.String("method", item.method), zap.String("sign", sign1), 87 | zap.String("expect", item.sign)) 88 | } 89 | } 90 | 91 | } 92 | -------------------------------------------------------------------------------- /utils/crypto.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "bytes" 5 | "crypto" 6 | "crypto/ed25519" 7 | "crypto/hmac" 8 | "crypto/rand" 9 | "crypto/rsa" 10 | "crypto/sha256" 11 | "crypto/sha512" 12 | "crypto/x509" 13 | "encoding/base64" 14 | "encoding/hex" 15 | "encoding/pem" 16 | "errors" 17 | "fmt" 18 | "github.com/banbox/banexg/errs" 19 | "github.com/banbox/banexg/log" 20 | "golang.org/x/text/encoding/charmap" 21 | "golang.org/x/text/transform" 22 | "hash" 23 | "io" 24 | ) 25 | 26 | /* 27 | Signature 28 | method: rsa, eddsa, hmac 29 | 30 | hashName: 31 | rsa: sha256/sha384/sha512 32 | eddsa: 不需要 33 | hmac:sha256/sha384/sha512 34 | digest: hmac: base64/hex 35 | */ 36 | func Signature(data string, secret string, method string, hashName string, digest string) (string, *errs.Error) { 37 | secretBytes, err := EncodeToLatin1(secret) 38 | if err != nil { 39 | return "", errs.New(errs.CodeSignFail, err) 40 | } 41 | dataBytes, err := EncodeToLatin1(data) 42 | if err != nil { 43 | return "", errs.New(errs.CodeSignFail, err) 44 | } 45 | algoMap := map[string]crypto.Hash{ 46 | "sha256": crypto.SHA256, 47 | "sha384": crypto.SHA384, 48 | "sha512": crypto.SHA512, 49 | } 50 | hashType, ok := algoMap[hashName] 51 | var sign string 52 | if method == "rsa" { 53 | if !ok { 54 | return "", errs.NewMsg(errs.CodeSignFail, "unsupport hash type:"+hashName) 55 | } 56 | secretKey, err := loadPrivateKey(secretBytes) 57 | if err != nil { 58 | return "", errs.New(errs.CodeSignFail, err) 59 | } 60 | sign, err = rsaSign(dataBytes, secretKey, hashType) 61 | } else if method == "eddsa" { 62 | sign, err = Eddsa(dataBytes, secretBytes) 63 | } else if method == "hmac" { 64 | if !ok { 65 | return "", errs.NewMsg(errs.CodeSignFail, "unsupport hash type:"+hashName) 66 | } 67 | sign = HMAC(dataBytes, secretBytes, hashType.New, digest) 68 | return sign, nil 69 | } else { 70 | msgText := "unsupport sign method: " + method 71 | log.Panic(msgText) 72 | return "", errs.NewMsg(errs.CodeSignFail, msgText) 73 | } 74 | if err != nil { 75 | return "", errs.New(errs.CodeSignFail, err) 76 | } 77 | sign = EncodeURIComponent(sign, UriEncodeSafe) 78 | return sign, nil 79 | } 80 | 81 | // RSA签名 82 | func rsaSign(data []byte, privateKey *rsa.PrivateKey, hashType crypto.Hash) (string, error) { 83 | var hashed []byte 84 | var h hash.Hash 85 | 86 | switch hashType { 87 | case crypto.SHA256: 88 | h = sha256.New() 89 | case crypto.SHA384: 90 | h = sha512.New384() 91 | case crypto.SHA512: 92 | h = sha512.New() 93 | default: 94 | return "", errors.New("unsupported hash type") 95 | } 96 | 97 | h.Write(data) 98 | hashed = h.Sum(nil) 99 | 100 | signature, err := rsa.SignPKCS1v15(rand.Reader, privateKey, hashType, hashed) 101 | if err != nil { 102 | return "", err 103 | } 104 | 105 | return base64.StdEncoding.EncodeToString(signature), nil 106 | } 107 | 108 | // 加载PEM格式的私钥 109 | func loadPrivateKey(pemEncoded []byte) (*rsa.PrivateKey, error) { 110 | block, _ := pem.Decode(pemEncoded) 111 | if block == nil { 112 | return nil, errors.New("failed to parse PEM block containing the key") 113 | } 114 | 115 | privKey, err := x509.ParsePKCS1PrivateKey(block.Bytes) 116 | if err != nil { 117 | return nil, err 118 | } 119 | 120 | return privKey, nil 121 | } 122 | 123 | func Eddsa(request []byte, secret []byte) (string, error) { 124 | block, _ := pem.Decode(secret) 125 | if block == nil { 126 | return "", fmt.Errorf("failed to parse PEM block containing the key") 127 | } 128 | 129 | privateKey := ed25519.NewKeyFromSeed(block.Bytes) 130 | sig := ed25519.Sign(privateKey, request) 131 | return base64.StdEncoding.EncodeToString(sig), nil 132 | } 133 | 134 | func HMAC(request []byte, secret []byte, algorithm func() hash.Hash, digest string) string { 135 | h := hmac.New(algorithm, secret) 136 | h.Write(request) 137 | binary := h.Sum(nil) 138 | 139 | switch digest { 140 | case "hex": 141 | return hex.EncodeToString(binary) 142 | case "base64": 143 | return base64.StdEncoding.EncodeToString(binary) 144 | default: 145 | return string(binary) 146 | } 147 | } 148 | 149 | func EncodeToLatin1(input string) ([]byte, error) { 150 | encoder := charmap.ISO8859_1.NewEncoder() 151 | reader := transform.NewReader(bytes.NewReader([]byte(input)), encoder) 152 | return io.ReadAll(reader) 153 | } 154 | -------------------------------------------------------------------------------- /binance/testdata/order_book_shot.json: -------------------------------------------------------------------------------- 1 | {"lastUpdateId": 3743827050724, "E": 1703936451404, "T": 1703936451396, "bids": [["2287.40", "132.879"], ["2287.39", "22.325"], ["2287.38", "0.703"], ["2287.32", "0.436"], ["2287.31", "2.141"], ["2287.30", "5.466"], ["2287.29", "0.790"], ["2287.28", "0.653"], ["2287.25", "0.252"], ["2287.24", "0.191"], ["2287.23", "0.319"], ["2287.22", "0.009"], ["2287.21", "1.002"], ["2287.20", "2.010"], ["2287.19", "6.798"], ["2287.18", "6.771"], ["2287.17", "5.463"], ["2287.16", "1.178"], ["2287.14", "0.838"], ["2287.13", "17.327"], ["2287.12", "16.396"], ["2287.11", "28.732"], ["2287.10", "17.768"], ["2287.08", "17.106"], ["2287.06", "0.200"], ["2287.05", "0.439"], ["2287.04", "2.402"], ["2287.03", "6.038"], ["2287.02", "7.204"], ["2287.01", "24.708"], ["2287.00", "2.732"], ["2286.99", "5.519"], ["2286.98", "2.515"], ["2286.97", "18.535"], ["2286.96", "8.415"], ["2286.95", "1.214"], ["2286.94", "0.288"], ["2286.93", "0.874"], ["2286.92", "2.367"], ["2286.91", "9.848"], ["2286.90", "0.230"], ["2286.89", "7.469"], ["2286.88", "23.404"], ["2286.87", "23.652"], ["2286.86", "1.059"], ["2286.85", "6.651"], ["2286.84", "15.817"], ["2286.83", "1.329"], ["2286.82", "24.240"], ["2286.81", "31.842"], ["2286.80", "1.220"], ["2286.79", "19.312"], ["2286.77", "10.028"], ["2286.76", "4.839"], ["2286.75", "19.642"], ["2286.73", "2.104"], ["2286.72", "29.339"], ["2286.71", "11.677"], ["2286.70", "18.979"], ["2286.69", "1.244"], ["2286.68", "1.794"], ["2286.67", "18.638"], ["2286.66", "29.875"], ["2286.65", "21.279"], ["2286.64", "2.401"], ["2286.62", "22.905"], ["2286.61", "8.968"], ["2286.60", "5.712"], ["2286.59", "1.192"], ["2286.58", "1.374"], ["2286.57", "34.474"], ["2286.56", "26.531"], ["2286.55", "1.857"], ["2286.54", "36.265"], ["2286.53", "0.082"], ["2286.52", "0.207"], ["2286.51", "2.117"], ["2286.50", "0.475"], ["2286.49", "17.493"], ["2286.48", "15.869"], ["2286.47", "0.874"], ["2286.46", "43.729"], ["2286.45", "51.877"], ["2286.44", "19.093"], ["2286.43", "33.640"], ["2286.42", "1.200"], ["2286.41", "7.023"], ["2286.40", "5.442"], ["2286.39", "35.897"], ["2286.38", "0.224"], ["2286.37", "1.423"], ["2286.36", "1.035"], ["2286.35", "29.034"], ["2286.34", "64.867"], ["2286.33", "0.874"], ["2286.32", "1.458"], ["2286.31", "19.845"], ["2286.30", "8.496"], ["2286.29", "2.318"], ["2286.28", "2.388"]], "asks": [["2287.41", "15.956"], ["2287.42", "0.009"], ["2287.43", "8.581"], ["2287.49", "0.800"], ["2287.53", "1.247"], ["2287.55", "3.461"], ["2287.56", "0.800"], ["2287.58", "5.000"], ["2287.60", "4.211"], ["2287.63", "7.593"], ["2287.64", "2.630"], ["2287.66", "0.790"], ["2287.67", "0.024"], ["2287.68", "7.676"], ["2287.69", "0.280"], ["2287.70", "11.445"], ["2287.71", "7.174"], ["2287.73", "12.677"], ["2287.75", "2.527"], ["2287.76", "7.848"], ["2287.77", "2.630"], ["2287.78", "16.358"], ["2287.79", "0.129"], ["2287.80", "31.529"], ["2287.81", "1.906"], ["2287.82", "0.227"], ["2287.83", "18.033"], ["2287.84", "0.750"], ["2287.85", "0.264"], ["2287.86", "0.457"], ["2287.88", "0.085"], ["2287.89", "12.760"], ["2287.90", "17.413"], ["2287.91", "1.241"], ["2287.92", "1.851"], ["2287.93", "0.936"], ["2287.94", "18.140"], ["2287.95", "13.203"], ["2287.96", "5.659"], ["2287.97", "0.020"], ["2287.98", "4.863"], ["2288.00", "5.603"], ["2288.01", "15.120"], ["2288.02", "13.376"], ["2288.03", "2.623"], ["2288.04", "5.647"], ["2288.05", "5.104"], ["2288.06", "23.422"], ["2288.07", "18.983"], ["2288.08", "4.285"], ["2288.09", "5.375"], ["2288.10", "17.235"], ["2288.11", "18.350"], ["2288.12", "22.372"], ["2288.13", "2.116"], ["2288.14", "1.402"], ["2288.15", "45.287"], ["2288.16", "5.511"], ["2288.17", "3.972"], ["2288.18", "16.003"], ["2288.19", "17.954"], ["2288.20", "4.663"], ["2288.21", "4.157"], ["2288.22", "30.705"], ["2288.23", "49.394"], ["2288.24", "21.001"], ["2288.25", "17.542"], ["2288.26", "8.557"], ["2288.27", "3.698"], ["2288.28", "11.825"], ["2288.29", "18.058"], ["2288.30", "7.770"], ["2288.31", "2.147"], ["2288.32", "17.495"], ["2288.33", "4.696"], ["2288.34", "24.699"], ["2288.35", "63.844"], ["2288.36", "11.024"], ["2288.37", "1.120"], ["2288.38", "11.161"], ["2288.39", "25.059"], ["2288.40", "4.417"], ["2288.41", "5.496"], ["2288.42", "21.915"], ["2288.43", "17.518"], ["2288.44", "26.069"], ["2288.45", "13.684"], ["2288.46", "89.441"], ["2288.48", "21.224"], ["2288.49", "18.781"], ["2288.50", "1.790"], ["2288.51", "0.209"], ["2288.52", "2.468"], ["2288.53", "2.919"], ["2288.54", "5.500"], ["2288.55", "15.169"], ["2288.56", "5.015"], ["2288.57", "3.156"], ["2288.58", "0.727"], ["2288.59", "18.146"]]} -------------------------------------------------------------------------------- /binance/biz_order_test.go: -------------------------------------------------------------------------------- 1 | package binance 2 | 3 | import ( 4 | "fmt" 5 | "github.com/banbox/banexg" 6 | "github.com/banbox/banexg/log" 7 | "github.com/banbox/banexg/utils" 8 | "github.com/banbox/bntp" 9 | "go.uber.org/zap" 10 | "testing" 11 | "time" 12 | ) 13 | 14 | func TestFetchOrder(t *testing.T) { 15 | exg := getBinance(nil) 16 | cases := map[string]map[string]interface{}{ 17 | "25578760824": {"market": banexg.MarketLinear}, 18 | } 19 | 20 | symbol := "ETC/USDT:USDT" 21 | for orderId, item := range cases { 22 | text, _ := utils.MarshalString(item) 23 | res, err := exg.FetchOrder(symbol, orderId, item) 24 | if err != nil { 25 | panic(fmt.Errorf("%s Error: %v", text, err)) 26 | } 27 | resText, _ := utils.MarshalString(res) 28 | t.Logf("%s result: %s", text, resText) 29 | } 30 | } 31 | 32 | func TestFetchOrders(t *testing.T) { 33 | exg := getBinance(nil) 34 | now := time.Now().UnixMilli() 35 | loopIntv := int64(86400000 * 7) 36 | cases := []map[string]interface{}{ 37 | //{"market": banexg.MarketSpot}, 38 | {"market": banexg.MarketLinear, banexg.ParamUntil: now, banexg.ParamLoopIntv: loopIntv}, 39 | //{"market": banexg.MarketInverse}, 40 | //{"market": banexg.MarketOption}, 41 | } 42 | 43 | symbol := "XRP/USDT:USDT" 44 | since := now - loopIntv*4 // 4周前作为开始时间 45 | for _, item := range cases { 46 | text, _ := utils.MarshalString(item) 47 | res, err := exg.FetchOrders(symbol, since, 0, item) 48 | if err != nil { 49 | panic(fmt.Errorf("%s Error: %v", text, err)) 50 | } 51 | resText, _ := utils.MarshalString(res) 52 | t.Logf("%s result: %s", text, resText) 53 | } 54 | } 55 | 56 | func TestFetchOpenOrders(t *testing.T) { 57 | exg := getBinance(nil) 58 | cases := []map[string]interface{}{ 59 | //{"market": banexg.MarketSpot}, 60 | {"market": banexg.MarketLinear}, 61 | //{"market": banexg.MarketInverse}, 62 | //{"market": banexg.MarketOption}, 63 | } 64 | symbol := "ETC/USDT:USDT" 65 | since := time.Date(2025, 4, 6, 0, 0, 0, 0, time.UTC).UnixMilli() 66 | for _, item := range cases { 67 | text, _ := utils.MarshalString(item) 68 | res, err := exg.FetchOpenOrders(symbol, since, 0, item) 69 | if err != nil { 70 | panic(fmt.Errorf("%s Error: %v", text, err)) 71 | } 72 | resText, _ := utils.MarshalString(res) 73 | t.Logf("%s result: %s", text, resText) 74 | } 75 | } 76 | 77 | func printCreateOrder(symbol string, odType string, side string, amount float64, price float64, params map[string]interface{}) { 78 | exg := getBinance(nil) 79 | res, err := exg.CreateOrder(symbol, odType, side, amount, price, params) 80 | if err != nil { 81 | panic(err) 82 | } 83 | resStr, err2 := utils.MarshalString(res) 84 | if err2 != nil { 85 | panic(err2) 86 | } 87 | fmt.Printf(resStr) 88 | fmt.Printf("\n") 89 | } 90 | 91 | func TestBinance_CreateOrder(t *testing.T) { 92 | args := map[string]interface{}{ 93 | banexg.ParamPositionSide: "LONG", 94 | } 95 | symbol := "ETH/USDT:USDT" 96 | printCreateOrder(symbol, banexg.OdTypeLimit, banexg.OdSideBuy, 0.02, 1000, args) 97 | } 98 | 99 | func TestTriggerLongStop(t *testing.T) { 100 | bntp.LangCode = bntp.LangZhCN 101 | exg := getBinance(nil) 102 | symbol := "DOGE/USDT:USDT" 103 | priceMap, err := exg.FetchTickerPrice(symbol, nil) 104 | if err != nil { 105 | panic(err) 106 | } 107 | price := priceMap[symbol] 108 | if price == 0 { 109 | fmt.Printf("get ticker price fail: %v", priceMap) 110 | return 111 | } 112 | args := map[string]interface{}{ 113 | banexg.ParamPositionSide: "LONG", 114 | banexg.ParamTakeProfitPrice: price * 1.05, 115 | } 116 | amt := 10 / price 117 | printCreateOrder(symbol, banexg.OdTypeMarket, banexg.OdSideBuy, amt, 0, args) 118 | } 119 | 120 | func TestCallbackStop(t *testing.T) { 121 | bntp.LangCode = bntp.LangZhCN 122 | args := map[string]interface{}{ 123 | banexg.ParamPositionSide: "SHORT", 124 | banexg.ParamActivationPrice: 2830.0, 125 | banexg.ParamCallbackRate: 2.0, 126 | } 127 | symbol := "ETH/USDT:USDT" 128 | // 为空单添加追踪止损 129 | printCreateOrder(symbol, banexg.OdTypeTrailingStopMarket, banexg.OdSideBuy, 0.008, 0, args) 130 | } 131 | 132 | func TestSellOrder(t *testing.T) { 133 | symbol := "USDT/BRL" 134 | printCreateOrder(symbol, banexg.OdTypeMarket, banexg.OdSideSell, 10, 0, nil) 135 | } 136 | 137 | func TestCalcelOrder(t *testing.T) { 138 | exg := getBinance(nil) 139 | symbol := "ETH/USDT:USDT" 140 | 141 | res, err := exg.CancelOrder("8389765870487818124", symbol, nil) 142 | if err != nil { 143 | panic(err) 144 | } 145 | resStr, _ := utils.MarshalString(res) 146 | log.Info("cancel order", zap.String("res", resStr)) 147 | } 148 | -------------------------------------------------------------------------------- /binance/biz_order_algo.go: -------------------------------------------------------------------------------- 1 | package binance 2 | 3 | import ( 4 | "context" 5 | "strconv" 6 | "strings" 7 | 8 | "github.com/banbox/banexg" 9 | "github.com/banbox/banexg/errs" 10 | "github.com/banbox/banexg/utils" 11 | ) 12 | 13 | func (e *Binance) createAlgoOrder(market *banexg.Market, args map[string]interface{}, tryNum int) (*banexg.Order, *errs.Error) { 14 | utils.PopMapVal(args, banexg.ParamAlgoOrder, false) 15 | args["symbol"] = market.ID 16 | args["algoType"] = "CONDITIONAL" 17 | if v, ok := args["newClientOrderId"]; ok { 18 | args["clientAlgoId"] = v 19 | delete(args, "newClientOrderId") 20 | } 21 | if v, ok := args["stopPrice"]; ok { 22 | args["triggerPrice"] = v 23 | delete(args, "stopPrice") 24 | } 25 | 26 | if utils.GetMapVal(args, banexg.ParamClosePosition, false) { 27 | delete(args, "quantity") 28 | delete(args, banexg.ParamReduceOnly) 29 | } 30 | // Convert boolean params to string if necessary 31 | banexg.SetBoolArg(args, banexg.ParamClosePosition, banexg.BoolLower) 32 | banexg.SetBoolArg(args, banexg.ParamReduceOnly, banexg.BoolLower) 33 | banexg.SetBoolArg(args, banexg.ParamPriceProtect, banexg.BoolUpper) 34 | 35 | if _, ok := args[banexg.ParamPriceMatch]; ok { 36 | delete(args, "price") 37 | } 38 | if v, ok := args[banexg.ParamActivationPrice]; ok { 39 | if val, ok := v.(float64); ok && val > 0 { 40 | if pStr, err := e.PrecPrice(market, val); err == nil { 41 | args[banexg.ParamActivationPrice] = pStr 42 | } 43 | } 44 | } 45 | 46 | rsp := e.RequestApiRetry(context.Background(), MethodFapiPrivatePostAlgoOrder, args, tryNum) 47 | if rsp.Error != nil { 48 | return nil, rsp.Error 49 | } 50 | var mapSymbol = func(mid string) string { 51 | return market.Symbol 52 | } 53 | return parseOrder[*AlgoOrder](mapSymbol, rsp) 54 | } 55 | 56 | func (e *Binance) fetchAlgoOrder(id, clientOrderId string, args map[string]interface{}) (*banexg.Order, *errs.Error) { 57 | if clientOrderId != "" { 58 | args["clientAlgoId"] = clientOrderId 59 | } else { 60 | if strings.HasPrefix(id, "algo:") { 61 | id = id[5:] 62 | } 63 | args["algoId"] = id 64 | } 65 | tryNum := e.GetRetryNum("FetchOrder", 1) 66 | rsp := e.RequestApiRetry(context.Background(), MethodFapiPrivateGetAlgoOrder, args, tryNum) 67 | if rsp.Error != nil { 68 | return nil, rsp.Error 69 | } 70 | var mapSymbol = func(mid string) string { 71 | market := e.GetMarketById(mid, banexg.MarketLinear) 72 | return market.Symbol 73 | } 74 | return parseOrder[*AlgoOrder](mapSymbol, rsp) 75 | } 76 | 77 | func (e *Binance) fetchAlgoOrders(args map[string]interface{}, market *banexg.Market) ([]*banexg.Order, *errs.Error) { 78 | tryNum := e.GetRetryNum("FetchOrders", 1) 79 | rsp := e.RequestApiRetry(context.Background(), MethodFapiPrivateGetAllAlgoOrders, args, tryNum) 80 | if rsp.Error != nil { 81 | return nil, rsp.Error 82 | } 83 | var mapSymbol = func(mid string) string { 84 | return market.Symbol 85 | } 86 | return parseOrders[*AlgoOrder](mapSymbol, rsp) 87 | } 88 | 89 | func (e *Binance) fetchAlgoOpenOrders(args map[string]interface{}, market *banexg.Market) ([]*banexg.Order, *errs.Error) { 90 | if market != nil { 91 | args["symbol"] = market.ID 92 | } 93 | tryNum := e.GetRetryNum("FetchOpenOrders", 1) 94 | rsp := e.RequestApiRetry(context.Background(), MethodFapiPrivateGetOpenAlgoOrders, args, tryNum) 95 | if rsp.Error != nil { 96 | return nil, rsp.Error 97 | } 98 | var mapSymbol func(mid string) string 99 | if market != nil { 100 | mapSymbol = func(mid string) string { 101 | return market.Symbol 102 | } 103 | } else { 104 | var marketMap = make(map[string]*banexg.Market) 105 | mapSymbol = func(mid string) string { 106 | if market, ok := marketMap[mid]; ok { 107 | return market.Symbol 108 | } 109 | market := e.GetMarketById(mid, banexg.MarketLinear) 110 | marketMap[mid] = market 111 | return market.Symbol 112 | } 113 | } 114 | return parseOrders[*AlgoOrder](mapSymbol, rsp) 115 | } 116 | 117 | func (e *Binance) cancelAlgoOrder(id string, clientOrderId string, market *banexg.Market) (*banexg.Order, *errs.Error) { 118 | args := make(map[string]interface{}) 119 | args["symbol"] = market.ID 120 | if clientOrderId != "" { 121 | args["clientAlgoId"] = clientOrderId 122 | } else { 123 | if strings.HasPrefix(id, "algo:") { 124 | id = id[5:] 125 | } 126 | args["algoId"] = id 127 | } 128 | method := MethodFapiPrivateDeleteAlgoOrder 129 | tryNum := e.GetRetryNum("CancelAlgoOrder", 1) 130 | rsp := e.RequestApiRetry(context.Background(), method, args, tryNum) 131 | if rsp.Error != nil { 132 | return nil, rsp.Error 133 | } 134 | var data = new(DeleteAlgoOrderRsp) 135 | _, err := utils.UnmarshalStringMap(rsp.Content, &data) 136 | if err != nil { 137 | return nil, errs.New(errs.CodeUnmarshalFail, err) 138 | } 139 | if data.Code != "200" { 140 | return nil, errs.NewMsg(errs.CodeServerError, "cancel algo order failed: %s", data.Msg) 141 | } 142 | return &banexg.Order{ 143 | ID: strconv.FormatInt(data.AlgoId, 10), 144 | ClientOrderID: data.ClientAlgoId, 145 | Status: banexg.OdStatusCanceled, 146 | Symbol: market.Symbol, 147 | }, nil 148 | } 149 | -------------------------------------------------------------------------------- /bybit/biz_ticker.go: -------------------------------------------------------------------------------- 1 | package bybit 2 | 3 | import ( 4 | "github.com/banbox/banexg" 5 | "github.com/banbox/banexg/errs" 6 | "github.com/banbox/banexg/utils" 7 | "strconv" 8 | ) 9 | 10 | func (e *Bybit) FetchTickers(symbols []string, params map[string]interface{}) ([]*banexg.Ticker, *errs.Error) { 11 | args := utils.SafeParams(params) 12 | marketType, _, err := e.LoadArgsMarketType(args, symbols...) 13 | if err != nil { 14 | return nil, err 15 | } 16 | return e.fetchTickers(marketType, args) 17 | } 18 | 19 | func (e *Bybit) FetchTicker(symbol string, params map[string]interface{}) (*banexg.Ticker, *errs.Error) { 20 | args, market, err := e.LoadArgsMarket(symbol, params) 21 | if err != nil { 22 | return nil, err 23 | } 24 | args["symbol"] = market.ID 25 | var items []*banexg.Ticker 26 | items, err = e.fetchTickers(market.Type, args) 27 | if len(items) > 0 { 28 | return items[0], nil 29 | } 30 | return nil, err 31 | } 32 | 33 | func (e *Bybit) FetchTickerPrice(symbol string, params map[string]interface{}) (map[string]float64, *errs.Error) { 34 | return nil, errs.NewMsg(errs.CodeNotImplement, "not support FetchTickerPrice") 35 | } 36 | 37 | func (e *Bybit) fetchTickers(marketType string, args map[string]interface{}) ([]*banexg.Ticker, *errs.Error) { 38 | switch marketType { 39 | case banexg.MarketOption: 40 | args["category"] = "option" 41 | case banexg.MarketLinear: 42 | args["category"] = "linear" 43 | case banexg.MarketInverse: 44 | args["category"] = "inverse" 45 | default: 46 | args["category"] = "spot" 47 | } 48 | method := MethodPublicGetV5MarketTickers 49 | tryNum := e.GetRetryNum("FetchTicker", 1) 50 | if marketType == banexg.MarketOption { 51 | return parseTickers[*OptionTicker](e, marketType, method, args, tryNum) 52 | } else if marketType == banexg.MarketLinear || marketType == banexg.MarketInverse { 53 | return parseTickers[*FutureTicker](e, marketType, method, args, tryNum) 54 | } else { 55 | return parseTickers[*SpotTicker](e, marketType, method, args, tryNum) 56 | } 57 | } 58 | 59 | func parseTickers[T ITicker](e *Bybit, marketType, method string, args map[string]interface{}, tryNum int) ([]*banexg.Ticker, *errs.Error) { 60 | rsp := requestRetry[struct { 61 | Category string `json:"category"` 62 | List []map[string]interface{} `json:"list"` 63 | }](e, method, args, tryNum) 64 | if rsp.Error != nil { 65 | return nil, rsp.Error 66 | } 67 | items := rsp.Result.List 68 | var arr = make([]T, 0, len(items)) 69 | err := utils.DecodeStructMap(items, &arr, "json") 70 | if err != nil { 71 | return nil, errs.New(errs.CodeUnmarshalFail, err) 72 | } 73 | var result = make([]*banexg.Ticker, 0, len(items)) 74 | timeStamp := e.MilliSeconds() 75 | for i, item := range arr { 76 | ticker := item.ToStdTicker(e, marketType, items[i]) 77 | if ticker.Symbol == "" { 78 | continue 79 | } 80 | ticker.TimeStamp = timeStamp 81 | result = append(result, ticker) 82 | } 83 | return result, nil 84 | } 85 | 86 | func (t *BaseTicker) ToStdTicker(e *Bybit, marketType string, info map[string]interface{}) *banexg.Ticker { 87 | symbol := e.SafeSymbol(t.Symbol, "", marketType) 88 | bid1, _ := strconv.ParseFloat(t.Bid1Price, 64) 89 | bid1Vol, _ := strconv.ParseFloat(t.Bid1Size, 64) 90 | ask1, _ := strconv.ParseFloat(t.Ask1Price, 64) 91 | ask1Vol, _ := strconv.ParseFloat(t.Ask1Size, 64) 92 | high, _ := strconv.ParseFloat(t.HighPrice24h, 64) 93 | low, _ := strconv.ParseFloat(t.LowPrice24h, 64) 94 | vol, _ := strconv.ParseFloat(t.Volume24h, 64) 95 | quoteVol, _ := strconv.ParseFloat(t.Turnover24h, 64) 96 | lastPrice, _ := strconv.ParseFloat(t.LastPrice, 64) 97 | return &banexg.Ticker{ 98 | Symbol: symbol, 99 | Bid: bid1, 100 | BidVolume: bid1Vol, 101 | Ask: ask1, 102 | AskVolume: ask1Vol, 103 | High: high, 104 | Low: low, 105 | Close: lastPrice, 106 | Last: lastPrice, 107 | BaseVolume: vol, 108 | QuoteVolume: quoteVol, 109 | } 110 | } 111 | 112 | func (t *SpotTicker) ToStdTicker(e *Bybit, marketType string, info map[string]interface{}) *banexg.Ticker { 113 | res := t.BaseTicker.ToStdTicker(e, marketType, info) 114 | open, _ := strconv.ParseFloat(t.PrevPrice24h, 64) 115 | pcnt, _ := strconv.ParseFloat(t.Price24hPcnt, 64) 116 | // indexPrice, _ := strconv.ParseFloat(t.UsdIndexPrice, 64) 117 | res.Open = open 118 | res.Percentage = pcnt * 100 119 | res.Info = info 120 | return res 121 | } 122 | 123 | func (t *ContractTicker) ToStdTicker(e *Bybit, marketType string, info map[string]interface{}) *banexg.Ticker { 124 | res := t.BaseTicker.ToStdTicker(e, marketType, info) 125 | indexPrice, _ := strconv.ParseFloat(t.IndexPrice, 64) 126 | //delvPrice, _ := strconv.ParseFloat(t.PredictedDeliveryPrice, 64) 127 | markPrice, _ := strconv.ParseFloat(t.MarkPrice, 64) 128 | //openInterest, _ := strconv.ParseFloat(t.OpenInterest, 64) 129 | res.IndexPrice = indexPrice 130 | res.MarkPrice = markPrice 131 | return res 132 | } 133 | 134 | func (t *FutureTicker) ToStdTicker(e *Bybit, marketType string, info map[string]interface{}) *banexg.Ticker { 135 | res := t.ContractTicker.ToStdTicker(e, marketType, info) 136 | open, _ := strconv.ParseFloat(t.PrevPrice24h, 64) 137 | pcnt, _ := strconv.ParseFloat(t.Price24hPcnt, 64) 138 | // PrevPrice1h, OpenInterestValue, FundingRate, NextFundingTime, BasisRate, DeliveryFeeRate 139 | // DeliveryTime, Basis 140 | res.Open = open 141 | res.Percentage = pcnt * 100 142 | res.Info = info 143 | return res 144 | } 145 | 146 | func (t *OptionTicker) ToStdTicker(e *Bybit, marketType string, info map[string]interface{}) *banexg.Ticker { 147 | res := t.ContractTicker.ToStdTicker(e, marketType, info) 148 | // Bid1Iv, Ask1Iv, MarkIv, UnderlyingPrice, TotalVolume, TotalTurnover, Delta 149 | // Gamma, Vega, Theta, Change24h 150 | res.Info = info 151 | return res 152 | } 153 | -------------------------------------------------------------------------------- /utils/tf_utils.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "strconv" 7 | "sync" 8 | ) 9 | 10 | type TFOrigin struct { 11 | TFSecs int 12 | OffsetSecs int 13 | Origin string 14 | } 15 | 16 | var ( 17 | tfSecsMap = map[string]int{} 18 | secsTfMap = map[int]string{} 19 | tfLock sync.Mutex // 使用期间不涉及其他锁 20 | tfOrigins = []*TFOrigin{ 21 | // 从小到大的顺序 22 | {259200, 86400, "1970-01-02"}, 23 | {604800, 345600, "1970-01-05"}, 24 | } 25 | ) 26 | 27 | const ( 28 | SecsMin = 60 29 | SecsHour = SecsMin * 60 30 | SecsDay = SecsHour * 24 31 | SecsWeek = SecsDay * 7 32 | SecsMon = SecsDay * 30 33 | SecsQtr = SecsMon * 3 34 | SecsYear = SecsDay * 365 35 | ) 36 | 37 | func RegTfSecs(items map[string]int) { 38 | tfLock.Lock() 39 | for key, val := range items { 40 | tfSecsMap[key] = val 41 | secsTfMap[val] = key 42 | } 43 | tfLock.Unlock() 44 | } 45 | 46 | func parseTimeFrame(timeframe string) (int, error) { 47 | if len(timeframe) < 2 { 48 | return 0, errors.New("timeframe string too short") 49 | } 50 | 51 | amountStr := timeframe[:len(timeframe)-1] 52 | unit := timeframe[len(timeframe)-1] 53 | 54 | amount, err := strconv.Atoi(amountStr) 55 | if err != nil { 56 | return 0, err 57 | } 58 | 59 | var scale int 60 | switch unit { 61 | case 'y', 'Y': 62 | scale = SecsYear 63 | case 'q', 'Q': 64 | scale = SecsQtr 65 | case 'M': 66 | scale = SecsMon 67 | case 'w', 'W': 68 | scale = SecsWeek 69 | case 'd', 'D': 70 | scale = SecsDay 71 | case 'h', 'H': 72 | scale = SecsHour 73 | case 'm': 74 | scale = SecsMin 75 | case 's', 'S': 76 | scale = 1 77 | default: 78 | return 0, errors.New("timeframe unit " + string(unit) + " is not supported") 79 | } 80 | 81 | return amount * scale, nil 82 | } 83 | 84 | /* 85 | TFToSecs 86 | Convert the time cycle to seconds 87 | Supporting units: s, m, h, d, M, Q, Y 88 | 将时间周期转为秒 89 | 支持单位:s, m, h, d, M, Q, Y 90 | */ 91 | func TFToSecs(timeFrame string) int { 92 | secs, err := TFToSecSafe(timeFrame) 93 | if err != nil { 94 | panic(err) 95 | } 96 | return secs 97 | } 98 | 99 | func TFToSecSafe(timeFrame string) (int, error) { 100 | tfLock.Lock() 101 | secs, ok := tfSecsMap[timeFrame] 102 | var err error 103 | if !ok { 104 | secs, err = parseTimeFrame(timeFrame) 105 | if err == nil { 106 | tfSecsMap[timeFrame] = secs 107 | secsTfMap[secs] = timeFrame 108 | } 109 | } 110 | tfLock.Unlock() 111 | return secs, err 112 | } 113 | 114 | func GetTfAlignOrigin(secs int) (string, int) { 115 | for _, item := range tfOrigins { 116 | if secs < item.TFSecs { 117 | break 118 | } 119 | if secs%item.TFSecs == 0 { 120 | return item.Origin, item.OffsetSecs 121 | } 122 | } 123 | return "1970-01-01", 0 124 | } 125 | 126 | /* 127 | AlignTfSecsOffset 128 | Convert the given 10 second timestamp to the header start timestamp for the specified time period, using the specified offset 129 | 将给定的10位秒级时间戳,转为指定时间周期下,的头部开始时间戳,使用指定偏移 130 | */ 131 | func AlignTfSecsOffset(timeSecs int64, tfSecs int, offset int) int64 { 132 | if timeSecs > 1000000000000 { 133 | panic("10 digit timestamp is require for AlignTfSecs") 134 | } 135 | tfSecs64 := int64(tfSecs) 136 | if offset == 0 { 137 | return timeSecs / tfSecs64 * tfSecs64 138 | } 139 | offset64 := int64(offset) 140 | return (timeSecs-offset64)/tfSecs64*tfSecs64 + offset64 141 | } 142 | 143 | /* 144 | AlignTfSecs 145 | Convert the given 10 second timestamp to the header start timestamp for the specified time period 146 | 将给定的10位秒级时间戳,转为指定时间周期下,的头部开始时间戳 147 | */ 148 | func AlignTfSecs(timeSecs int64, tfSecs int) int64 { 149 | _, offset := GetTfAlignOrigin(tfSecs) 150 | return AlignTfSecsOffset(timeSecs, tfSecs, offset) 151 | } 152 | 153 | /* 154 | AlignTfMSecs 155 | Convert the given 13 millisecond timestamp to the header start timestamp for the specified time period 156 | 将给定的13位毫秒级时间戳,转为指定时间周期下,的头部开始时间戳 157 | */ 158 | func AlignTfMSecs(timeMSecs int64, tfMSecs int64) int64 { 159 | if timeMSecs < 100000000000 { 160 | panic(fmt.Sprintf("12 digit is required for AlignTfMSecs, : %v", timeMSecs)) 161 | } 162 | if tfMSecs < 1000 { 163 | panic("milliseconds tfMSecs is require for AlignTfMSecs") 164 | } 165 | return AlignTfSecs(timeMSecs/1000, int(tfMSecs/1000)) * 1000 166 | } 167 | 168 | func AlignTfMSecsOffset(timeMSecs, tfMSecs, offset int64) int64 { 169 | if timeMSecs < 100000000000 { 170 | panic(fmt.Sprintf("12 digit is required for AlignTfMSecsOffset, : %v", timeMSecs)) 171 | } 172 | if tfMSecs < 1000 { 173 | panic("milliseconds tfMSecs is require for AlignTfMSecs") 174 | } 175 | return AlignTfSecsOffset(timeMSecs/1000, int(tfMSecs/1000), int(offset/1000)) * 1000 176 | } 177 | 178 | /* 179 | SecsToTF 180 | Convert the seconds of a time period into a time period 181 | 将时间周期的秒数,转为时间周期 182 | */ 183 | func SecsToTF(tfSecs int) string { 184 | tfLock.Lock() 185 | timeFrame, ok := secsTfMap[tfSecs] 186 | invalid := false 187 | if !ok { 188 | switch { 189 | case tfSecs >= SecsYear: 190 | timeFrame = strconv.Itoa(tfSecs/SecsYear) + "y" 191 | case tfSecs >= SecsQtr: 192 | timeFrame = strconv.Itoa(tfSecs/SecsQtr) + "q" 193 | case tfSecs >= SecsMon: 194 | timeFrame = strconv.Itoa(tfSecs/SecsMon) + "M" 195 | case tfSecs >= SecsWeek: 196 | timeFrame = strconv.Itoa(tfSecs/SecsWeek) + "w" 197 | case tfSecs >= SecsDay: 198 | timeFrame = strconv.Itoa(tfSecs/SecsDay) + "d" 199 | case tfSecs >= SecsHour: 200 | timeFrame = strconv.Itoa(tfSecs/SecsHour) + "h" 201 | case tfSecs >= SecsMin: 202 | timeFrame = strconv.Itoa(tfSecs/SecsMin) + "m" 203 | case tfSecs >= 1: 204 | timeFrame = strconv.Itoa(tfSecs) + "s" 205 | default: 206 | invalid = true 207 | } 208 | secsTfMap[tfSecs] = timeFrame 209 | } 210 | tfLock.Unlock() 211 | if invalid { 212 | panic("unsupport tfSecs:" + strconv.Itoa(tfSecs)) 213 | } 214 | return timeFrame 215 | } 216 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/banbox/bntp v0.1.0 h1:kZROpbb3OME8XM8QeM+2SeQWQrHRDFN05okr8OITKAU= 2 | github.com/banbox/bntp v0.1.0/go.mod h1:w0WP7GROKQo07wHFBm/FVgH4xlZuOYCoro0ky4iTC3M= 3 | github.com/beevik/ntp v1.4.3 h1:PlbTvE5NNy4QHmA4Mg57n7mcFTmr1W1j3gcK7L1lqho= 4 | github.com/beevik/ntp v1.4.3/go.mod h1:Unr8Zg+2dRn7d8bHFuehIMSvvUYssHMxW3Q5Nx4RW5Q= 5 | github.com/bytedance/gopkg v0.1.3 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M= 6 | github.com/bytedance/gopkg v0.1.3/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM= 7 | github.com/bytedance/sonic v1.14.1 h1:FBMC0zVz5XUmE4z9wF4Jey0An5FueFvOsTKKKtwIl7w= 8 | github.com/bytedance/sonic v1.14.1/go.mod h1:gi6uhQLMbTdeP0muCnrjHLeCUPyb70ujhnNlhOylAFc= 9 | github.com/bytedance/sonic/loader v0.3.0 h1:dskwH8edlzNMctoruo8FPTJDF3vLtDT0sXZwvZJyqeA= 10 | github.com/bytedance/sonic/loader v0.3.0/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI= 11 | github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M= 12 | github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU= 13 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 14 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 15 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 16 | github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs= 17 | github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= 18 | github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= 19 | github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= 20 | github.com/h2non/gock v1.2.0 h1:K6ol8rfrRkUOefooBC8elXoaNGYkpp7y2qcxGG6BzUE= 21 | github.com/h2non/gock v1.2.0/go.mod h1:tNhoxHYW2W42cYkYb1WqzdbYIieALC99kpYr7rH/BQk= 22 | github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542 h1:2VTzZjLZBgl62/EtslCrtky5vbi9dd7HrQPQIx6wqiw= 23 | github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542/go.mod h1:Ow0tF8D4Kplbc8s8sSb3V2oUCygFHVp8gC3Dn6U4MNI= 24 | github.com/klauspost/cpuid/v2 v2.2.9 h1:66ze0taIn2H33fBvCkXuv9BmCwDfafmiIVpKV9kKGuY= 25 | github.com/klauspost/cpuid/v2 v2.2.9/go.mod h1:rqkxqrZ1EhYM9G+hXH7YdowN5R5RGN6NK4QwQ3WMXF8= 26 | github.com/nbio/st v0.0.0-20140626010706-e9e8d9816f32 h1:W6apQkHrMkS0Muv8G/TipAy/FJl/rCYT0+EuS8+Z0z4= 27 | github.com/nbio/st v0.0.0-20140626010706-e9e8d9816f32/go.mod h1:9wM+0iRr9ahx58uYLpLIr5fm8diHn0JbqRycJi6w0Ms= 28 | github.com/petermattis/goid v0.0.0-20250813065127-a731cc31b4fe h1:vHpqOnPlnkba8iSxU4j/CvDSS9J4+F4473esQsYLGoE= 29 | github.com/petermattis/goid v0.0.0-20250813065127-a731cc31b4fe/go.mod h1:pxMtw7cyUw6B2bRH0ZBANSPg+AoSud1I1iyJHI69jH4= 30 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 31 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 32 | github.com/sasha-s/go-deadlock v0.3.6 h1:TR7sfOnZ7x00tWPfD397Peodt57KzMDo+9Ae9rMiUmw= 33 | github.com/sasha-s/go-deadlock v0.3.6/go.mod h1:CUqNyyvMxTyjFqDT7MRg9mb4Dv/btmGTqSR+rky/UXo= 34 | github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k= 35 | github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME= 36 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 37 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 38 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 39 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 40 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 41 | github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 42 | github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= 43 | github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 44 | github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= 45 | github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= 46 | go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= 47 | go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= 48 | go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= 49 | go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= 50 | go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= 51 | go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= 52 | golang.org/x/arch v0.0.0-20210923205945-b76863e36670 h1:18EFjUmQOcUvxNYSkA6jO9VAiXCnxFY6NyDX0bHDmkU= 53 | golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= 54 | golang.org/x/net v0.37.0 h1:1zLorHbz+LYj7MQlSf1+2tPIIgibq2eL5xkrGk6f+2c= 55 | golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= 56 | golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= 57 | golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= 58 | golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng= 59 | golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU= 60 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 61 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 62 | gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc= 63 | gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc= 64 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 65 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 66 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 67 | -------------------------------------------------------------------------------- /log/global.go: -------------------------------------------------------------------------------- 1 | package log 2 | 3 | import ( 4 | "context" 5 | "go.uber.org/zap" 6 | "go.uber.org/zap/zapcore" 7 | ) 8 | 9 | type ctxLogKeyType struct{} 10 | 11 | var CtxLogKey = ctxLogKeyType{} 12 | 13 | // Debug logs a message at DebugLevel. The message includes any fields passed 14 | // at the log site, as well as any fields accumulated on the logger. 15 | func Debug(msg string, fields ...zap.Field) { 16 | L().Debug(msg, fields...) 17 | } 18 | 19 | // Info logs a message at InfoLevel. The message includes any fields passed 20 | // at the log site, as well as any fields accumulated on the logger. 21 | func Info(msg string, fields ...zap.Field) { 22 | L().Info(msg, fields...) 23 | } 24 | 25 | // Warn logs a message at WarnLevel. The message includes any fields passed 26 | // at the log site, as well as any fields accumulated on the logger. 27 | func Warn(msg string, fields ...zap.Field) { 28 | L().Warn(msg, fields...) 29 | } 30 | 31 | // Error logs a message at ErrorLevel. The message includes any fields passed 32 | // at the log site, as well as any fields accumulated on the logger. 33 | func Error(msg string, fields ...zap.Field) { 34 | L().Error(msg, fields...) 35 | } 36 | 37 | // Panic logs a message at PanicLevel. The message includes any fields passed 38 | // at the log site, as well as any fields accumulated on the logger. 39 | // 40 | // The logger then panics, even if logging at PanicLevel is disabled. 41 | func Panic(msg string, fields ...zap.Field) { 42 | L().Panic(msg, fields...) 43 | } 44 | 45 | // Fatal logs a message at FatalLevel. The message includes any fields passed 46 | // at the log site, as well as any fields accumulated on the logger. 47 | // 48 | // The logger then calls os.Exit(1), even if logging at FatalLevel is 49 | // disabled. 50 | func Fatal(msg string, fields ...zap.Field) { 51 | L().Fatal(msg, fields...) 52 | } 53 | 54 | // With creates a child logger and adds structured context to it. 55 | // Fields added to the child don't affect the parent, and vice versa. 56 | func With(fields ...zap.Field) *MLogger { 57 | return &MLogger{ 58 | Logger: L().With(fields...).WithOptions(zap.AddCallerSkip(-1)), 59 | } 60 | } 61 | 62 | // SetLevel alters the logging level. 63 | func SetLevel(l zapcore.Level) { 64 | _globalP.Load().(*ZapProperties).Level.SetLevel(l) 65 | } 66 | 67 | // GetLevel gets the logging level. 68 | func GetLevel() zapcore.Level { 69 | return _globalP.Load().(*ZapProperties).Level.Level() 70 | } 71 | 72 | // WithTraceID returns a context with trace_id attached 73 | func WithTraceID(ctx context.Context, traceID string) context.Context { 74 | return WithFields(ctx, zap.String("traceID", traceID)) 75 | } 76 | 77 | // WithReqID adds given reqID field to the logger in ctx 78 | func WithReqID(ctx context.Context, reqID int64) context.Context { 79 | fields := []zap.Field{zap.Int64("reqID", reqID)} 80 | return WithFields(ctx, fields...) 81 | } 82 | 83 | // WithModule adds given module field to the logger in ctx 84 | func WithModule(ctx context.Context, module string) context.Context { 85 | fields := []zap.Field{zap.String("module", module)} 86 | return WithFields(ctx, fields...) 87 | } 88 | 89 | // WithFields returns a context with fields attached 90 | func WithFields(ctx context.Context, fields ...zap.Field) context.Context { 91 | var zlogger *zap.Logger 92 | if ctxLogger, ok := ctx.Value(CtxLogKey).(*MLogger); ok { 93 | zlogger = ctxLogger.Logger 94 | } else { 95 | zlogger = ctxL() 96 | } 97 | mLogger := &MLogger{ 98 | Logger: zlogger.With(fields...), 99 | } 100 | return context.WithValue(ctx, CtxLogKey, mLogger) 101 | } 102 | 103 | // Ctx returns a logger which will log contextual messages attached in ctx 104 | func Ctx(ctx context.Context) *MLogger { 105 | if ctx == nil { 106 | return &MLogger{Logger: ctxL()} 107 | } 108 | if ctxLogger, ok := ctx.Value(CtxLogKey).(*MLogger); ok { 109 | return ctxLogger 110 | } 111 | return &MLogger{Logger: ctxL()} 112 | } 113 | 114 | // withLogLevel returns ctx with a leveled logger, notes that it will overwrite logger previous attached! 115 | func withLogLevel(ctx context.Context, level zapcore.Level) context.Context { 116 | var zlogger *zap.Logger 117 | switch level { 118 | case zap.DebugLevel: 119 | zlogger = debugL() 120 | case zap.InfoLevel: 121 | zlogger = infoL() 122 | case zap.WarnLevel: 123 | zlogger = warnL() 124 | case zap.ErrorLevel: 125 | zlogger = errorL() 126 | case zap.FatalLevel: 127 | zlogger = fatalL() 128 | default: 129 | zlogger = L() 130 | } 131 | return context.WithValue(ctx, CtxLogKey, &MLogger{Logger: zlogger}) 132 | } 133 | 134 | // WithDebugLevel returns context with a debug level enabled logger. 135 | // Notes that it will overwrite previous attached logger within context 136 | func WithDebugLevel(ctx context.Context) context.Context { 137 | return withLogLevel(ctx, zapcore.DebugLevel) 138 | } 139 | 140 | // WithInfoLevel returns context with a info level enabled logger. 141 | // Notes that it will overwrite previous attached logger within context 142 | func WithInfoLevel(ctx context.Context) context.Context { 143 | return withLogLevel(ctx, zapcore.InfoLevel) 144 | } 145 | 146 | // WithWarnLevel returns context with a warning level enabled logger. 147 | // Notes that it will overwrite previous attached logger within context 148 | func WithWarnLevel(ctx context.Context) context.Context { 149 | return withLogLevel(ctx, zapcore.WarnLevel) 150 | } 151 | 152 | // WithErrorLevel returns context with a error level enabled logger. 153 | // Notes that it will overwrite previous attached logger within context 154 | func WithErrorLevel(ctx context.Context) context.Context { 155 | return withLogLevel(ctx, zapcore.ErrorLevel) 156 | } 157 | 158 | // WithFatalLevel returns context with a fatal level enabled logger. 159 | // Notes that it will overwrite previous attached logger within context 160 | func WithFatalLevel(ctx context.Context) context.Context { 161 | return withLogLevel(ctx, zapcore.FatalLevel) 162 | } 163 | -------------------------------------------------------------------------------- /binance/testdata/ccxt_book.log: -------------------------------------------------------------------------------- 1 | ---------- 1703936456396 ---------- 2 | ask: 2287.430 41009.045040 3 | ask: 2287.440 20.586960 4 | ask: 2287.490 1829.992000 5 | ask: 2287.540 5631.923480 6 | ask: 2287.550 7917.210550 7 | ask: 2287.560 11828.972760 8 | ask: 2287.600 20.588400 9 | ask: 2287.630 5584.104830 10 | ask: 2287.640 20325.681400 11 | ask: 2287.670 32866.954890 12 | ask: 2287.680 11623.702080 13 | ask: 2287.690 619.963990 14 | ask: 2287.700 26182.726500 15 | ask: 2287.710 17032.000950 16 | ask: 2287.730 29292.094920 17 | ask: 2287.750 41.179500 18 | ask: 2287.760 22529.860480 19 | ask: 2287.770 7151.569020 20 | ask: 2287.780 24410.612600 21 | ask: 2287.790 512.464960 22 | ask: 2287.800 72349.387200 23 | ask: 2287.810 4360.565860 24 | ask: 2287.820 301.992240 25 | ask: 2287.830 56262.315360 26 | ask: 2287.840 1425.324320 27 | ask: 2287.850 603.992400 28 | ask: 2287.870 48013.239820 29 | ask: 2287.890 23867.268480 30 | ask: 2287.900 12908.331800 31 | ask: 2287.910 1793.721440 32 | ask: 2287.930 46293.975620 33 | ask: 2287.940 41503.231600 34 | ask: 2287.950 8723.953350 35 | ask: 2287.960 12917.822160 36 | ask: 2287.980 4262.506740 37 | ask: 2288.000 12819.664000 38 | ask: 2288.010 32967.936090 39 | ask: 2288.020 76978.144880 40 | ask: 2288.030 2999.607330 41 | ask: 2288.040 12920.561880 42 | ask: 2288.050 1478.080300 43 | ask: 2288.060 54506.165320 44 | ask: 2288.070 44139.158370 45 | ask: 2288.080 8431.574800 46 | ask: 2288.090 12298.483750 47 | ask: 2288.100 61746.666600 48 | ask: 2288.110 41986.818500 49 | ask: 2288.120 51210.413720 50 | ask: 2288.130 6326.679450 51 | ask: 2288.140 5793.570480 52 | ask: 2288.150 103165.819050 53 | ask: 2288.160 11694.785760 54 | ask: 2288.170 10456.936900 55 | ask: 2288.180 36617.744540 56 | ask: 2288.190 356.957640 57 | ask: 2288.200 11127.516600 58 | ask: 2288.210 51091.152880 59 | ask: 2288.220 33211.225080 60 | ask: 2288.230 111567.230110 61 | ask: 2288.240 48055.328240 62 | ask: 2288.250 50142.422250 63 | ask: 2288.260 18974.251920 64 | ask: 2288.270 15555.659460 65 | ask: 2288.280 27081.793800 66 | ask: 2288.290 43637.690300 67 | ask: 2288.300 8615.449500 68 | ask: 2288.310 3082.353570 69 | ask: 2288.320 10054.878080 70 | ask: 2288.330 40514.882650 71 | ask: 2288.340 72618.181560 72 | ask: 2288.350 128060.642700 73 | ask: 2288.360 25684.552640 74 | ask: 2288.370 1189.952400 75 | ask: 2288.380 25540.609180 76 | ask: 2288.390 56429.409010 77 | ask: 2288.400 38417.659200 78 | ask: 2288.410 12526.756340 79 | ask: 2288.420 49693.040300 80 | ask: 2288.430 26532.057420 81 | ask: 2288.440 54439.699160 82 | ask: 2288.450 3462.424850 83 | ask: 2288.460 72397.720560 84 | ask: 2288.470 186366.131390 85 | ask: 2288.480 72583.720160 86 | ask: 2288.490 43218.133650 87 | ask: 2288.500 5597.671000 88 | ask: 2288.510 478.298590 89 | ask: 2288.520 2444.139360 90 | ask: 2288.530 2332.012070 91 | ask: 2288.540 11213.846000 92 | ask: 2288.550 35172.724950 93 | ask: 2288.560 11477.128400 94 | ask: 2288.580 1663.797660 95 | ask: 2288.590 43931.773640 96 | ask: 2288.600 10131.632200 97 | ask: 2288.610 1373.166000 98 | ask: 2288.620 5517.862820 99 | ask: 2288.630 11694.899300 100 | ask: 2288.680 7099.485360 101 | ask: 2288.740 478.346660 102 | bid: 2287.420 333096.387820 103 | bid: 2287.410 1866.526560 104 | bid: 2287.400 50599.575400 105 | bid: 2287.390 24118.240160 106 | bid: 2287.380 1608.028140 107 | bid: 2287.310 4897.130710 108 | bid: 2287.300 12502.381800 109 | bid: 2287.290 5006.877810 110 | bid: 2287.280 1493.593840 111 | bid: 2287.270 1806.943300 112 | bid: 2287.250 576.387000 113 | bid: 2287.240 436.862840 114 | bid: 2287.230 729.626370 115 | bid: 2287.220 20.584980 116 | bid: 2287.210 2291.784420 117 | bid: 2287.200 4597.272000 118 | bid: 2287.190 349.940070 119 | bid: 2287.180 15486.495780 120 | bid: 2287.170 27292.799610 121 | bid: 2287.160 2694.274480 122 | bid: 2287.140 1916.623320 123 | bid: 2287.130 39629.101510 124 | bid: 2287.120 37499.619520 125 | bid: 2287.110 68192.471760 126 | bid: 2287.100 40637.192800 127 | bid: 2287.080 39122.790480 128 | bid: 2287.060 9516.456660 129 | bid: 2287.050 12132.800250 130 | bid: 2287.040 9285.382400 131 | bid: 2287.030 8148.687890 132 | bid: 2287.020 2287.020000 133 | bid: 2287.010 12194.337320 134 | bid: 2287.000 8246.922000 135 | bid: 2286.990 13621.312440 136 | bid: 2286.980 5728.884900 137 | bid: 2286.970 47388.305370 138 | bid: 2286.960 22245.259920 139 | bid: 2286.950 5314.871800 140 | bid: 2286.940 1116.026720 141 | bid: 2286.930 7425.661710 142 | bid: 2286.920 13906.760520 143 | bid: 2286.910 10592.967120 144 | bid: 2286.900 983.367000 145 | bid: 2286.890 14368.529870 146 | bid: 2286.880 41488.576960 147 | bid: 2286.870 48632.577420 148 | bid: 2286.860 2421.784740 149 | bid: 2286.850 13885.753200 150 | bid: 2286.840 8246.345040 151 | bid: 2286.830 34462.528100 152 | bid: 2286.820 24743.392400 153 | bid: 2286.810 73918.846440 154 | bid: 2286.800 2789.896000 155 | bid: 2286.790 58217.099820 156 | bid: 2286.780 4912.003440 157 | bid: 2286.770 8877.241140 158 | bid: 2286.760 11065.631640 159 | bid: 2286.750 44916.343500 160 | bid: 2286.730 4834.147220 161 | bid: 2286.720 70090.254720 162 | bid: 2286.710 27593.729570 163 | bid: 2286.700 44771.299300 164 | bid: 2286.690 3329.420640 165 | bid: 2286.680 32242.188000 166 | bid: 2286.670 43990.957460 167 | bid: 2286.660 70143.295500 168 | bid: 2286.650 42799.228050 169 | bid: 2286.640 2533.597120 170 | bid: 2286.630 3827.818620 171 | bid: 2286.620 22299.118240 172 | bid: 2286.610 21420.962480 173 | bid: 2286.600 9887.258400 174 | bid: 2286.590 2318.602260 175 | bid: 2286.580 2686.731500 176 | bid: 2286.570 78776.909640 177 | bid: 2286.560 60664.723360 178 | bid: 2286.550 5748.386700 179 | bid: 2286.540 82491.503580 180 | bid: 2286.530 187.495460 181 | bid: 2286.520 473.309640 182 | bid: 2286.510 4840.541670 183 | bid: 2286.500 1086.087500 184 | bid: 2286.490 39997.569570 185 | bid: 2286.480 6582.775920 186 | bid: 2286.470 34203.304730 187 | bid: 2286.460 100441.901340 188 | bid: 2286.450 117242.296650 189 | bid: 2286.440 43872.210720 190 | bid: 2286.430 77372.791200 191 | bid: 2286.410 59915.374050 192 | bid: 2286.400 16359.192000 193 | bid: 2286.390 34757.700780 194 | bid: 2286.380 1643.907220 195 | bid: 2286.370 2338.956510 196 | bid: 2286.360 1909.110600 197 | bid: 2286.350 19562.010600 198 | bid: 2286.340 1888.516840 199 | bid: 2286.300 165585.277500 200 | bid: 2286.290 5299.620220 201 | bid: 2286.210 36122.118000 202 | 203 | -------------------------------------------------------------------------------- /intf.go: -------------------------------------------------------------------------------- 1 | package banexg 2 | 3 | import ( 4 | "github.com/banbox/banexg/errs" 5 | "io" 6 | ) 7 | 8 | /* 9 | Exchange interface 10 | 交易所接口 11 | */ 12 | 13 | type BanExchange interface { 14 | LoadMarkets(reload bool, params map[string]interface{}) (MarketMap, *errs.Error) 15 | GetCurMarkets() MarketMap 16 | GetMarket(symbol string) (*Market, *errs.Error) 17 | /* 18 | Map the original variety ID of the exchange to a standard symbol, where year is the year where the K-line data is located 19 | 将交易所原始品种ID映射为标准symbol,year是K线数据所在年 20 | */ 21 | MapMarket(rawID string, year int) (*Market, *errs.Error) 22 | FetchTicker(symbol string, params map[string]interface{}) (*Ticker, *errs.Error) 23 | FetchTickers(symbols []string, params map[string]interface{}) ([]*Ticker, *errs.Error) 24 | FetchTickerPrice(symbol string, params map[string]interface{}) (map[string]float64, *errs.Error) 25 | LoadLeverageBrackets(reload bool, params map[string]interface{}) *errs.Error 26 | InitLeverageBrackets() *errs.Error 27 | GetLeverage(symbol string, notional float64, account string) (float64, float64) 28 | CheckSymbols(symbols ...string) ([]string, []string) 29 | Info() *ExgInfo 30 | 31 | FetchOHLCV(symbol, timeframe string, since int64, limit int, params map[string]interface{}) ([]*Kline, *errs.Error) 32 | FetchOrderBook(symbol string, limit int, params map[string]interface{}) (*OrderBook, *errs.Error) 33 | FetchLastPrices(symbols []string, params map[string]interface{}) ([]*LastPrice, *errs.Error) 34 | FetchFundingRate(symbol string, params map[string]interface{}) (*FundingRateCur, *errs.Error) 35 | FetchFundingRates(symbols []string, params map[string]interface{}) ([]*FundingRateCur, *errs.Error) 36 | FetchFundingRateHistory(symbol string, since int64, limit int, params map[string]interface{}) ([]*FundingRate, *errs.Error) 37 | 38 | // FetchOrder query given order 39 | FetchOrder(symbol, orderId string, params map[string]interface{}) (*Order, *errs.Error) 40 | // FetchOrders Get all account orders; active, canceled, or filled. (symbol required) 41 | FetchOrders(symbol string, since int64, limit int, params map[string]interface{}) ([]*Order, *errs.Error) 42 | FetchBalance(params map[string]interface{}) (*Balances, *errs.Error) 43 | // FetchAccountPositions Get account positions on all symbols 44 | FetchAccountPositions(symbols []string, params map[string]interface{}) ([]*Position, *errs.Error) 45 | // FetchPositions Get position risks (default) or account positions on all symbols 46 | FetchPositions(symbols []string, params map[string]interface{}) ([]*Position, *errs.Error) 47 | // FetchOpenOrders Get all open orders on a symbol or all symbol. 48 | FetchOpenOrders(symbol string, since int64, limit int, params map[string]interface{}) ([]*Order, *errs.Error) 49 | FetchIncomeHistory(inType string, symbol string, since int64, limit int, params map[string]interface{}) ([]*Income, *errs.Error) 50 | 51 | CreateOrder(symbol, odType, side string, amount, price float64, params map[string]interface{}) (*Order, *errs.Error) 52 | EditOrder(symbol, orderId, side string, amount, price float64, params map[string]interface{}) (*Order, *errs.Error) 53 | CancelOrder(id string, symbol string, params map[string]interface{}) (*Order, *errs.Error) 54 | 55 | SetFees(fees map[string]map[string]float64) 56 | CalculateFee(symbol, odType, side string, amount float64, price float64, isMaker bool, params map[string]interface{}) (*Fee, *errs.Error) 57 | SetLeverage(leverage float64, symbol string, params map[string]interface{}) (map[string]interface{}, *errs.Error) 58 | CalcMaintMargin(symbol string, cost float64) (float64, *errs.Error) 59 | Call(method string, params map[string]interface{}) (*HttpRes, *errs.Error) 60 | 61 | WatchOrderBooks(symbols []string, limit int, params map[string]interface{}) (chan *OrderBook, *errs.Error) 62 | UnWatchOrderBooks(symbols []string, params map[string]interface{}) *errs.Error 63 | WatchOHLCVs(jobs [][2]string, params map[string]interface{}) (chan *PairTFKline, *errs.Error) 64 | UnWatchOHLCVs(jobs [][2]string, params map[string]interface{}) *errs.Error 65 | WatchMarkPrices(symbols []string, params map[string]interface{}) (chan map[string]float64, *errs.Error) 66 | UnWatchMarkPrices(symbols []string, params map[string]interface{}) *errs.Error 67 | WatchTrades(symbols []string, params map[string]interface{}) (chan *Trade, *errs.Error) 68 | UnWatchTrades(symbols []string, params map[string]interface{}) *errs.Error 69 | WatchMyTrades(params map[string]interface{}) (chan *MyTrade, *errs.Error) 70 | WatchBalance(params map[string]interface{}) (chan *Balances, *errs.Error) 71 | WatchPositions(params map[string]interface{}) (chan []*Position, *errs.Error) 72 | WatchAccountConfig(params map[string]interface{}) (chan *AccountConfig, *errs.Error) 73 | 74 | // SetDump Record all websocket messages to the specified file 将websocket所有消息记录到指定文件 75 | SetDump(path string) *errs.Error 76 | // SetReplay Replay all websocket messages from the specified file 从指定文件重放所有websocket消息 77 | SetReplay(path string) *errs.Error 78 | // GetReplayTo Retrieve the 13 bit timestamp of the next message to be replayed, with sys. MaxInt64 indicating no next message 获取下一个要重放的消息13位时间戳,sys.MaxInt64表示无下一个消息 79 | GetReplayTo() int64 80 | // ReplayOne Replay the next websocket message 重放下一个websocket消息 81 | ReplayOne() *errs.Error 82 | // ReplayAll Replay all recorded websocket messages 重放所有记录的websocket消息 83 | ReplayAll() *errs.Error 84 | // SetOnWsChan Trigger callback when creating a new websocket message chan 创建新websocket消息chan时触发回调 85 | SetOnWsChan(cb FuncOnWsChan) 86 | 87 | PrecAmount(m *Market, amount float64) (float64, *errs.Error) 88 | PrecPrice(m *Market, price float64) (float64, *errs.Error) 89 | PrecCost(m *Market, cost float64) (float64, *errs.Error) 90 | PrecFee(m *Market, fee float64) (float64, *errs.Error) 91 | 92 | HasApi(key, market string) bool 93 | SetOnHost(cb func(n string) string) 94 | PriceOnePip(symbol string) (float64, *errs.Error) 95 | IsContract(marketType string) bool 96 | MilliSeconds() int64 97 | 98 | GetAccount(id string) (*Account, *errs.Error) 99 | SetMarketType(marketType, contractType string) *errs.Error 100 | GetExg() *Exchange 101 | Close() *errs.Error 102 | GetNetDisable() bool 103 | SetNetDisable(v bool) 104 | } 105 | 106 | type WsConn interface { 107 | Close() error 108 | WriteClose() error 109 | ReConnect() error 110 | NextWriter() (io.WriteCloser, error) 111 | ReadMsg() ([]byte, error) 112 | IsOK() bool 113 | GetID() int 114 | SetID(id int) 115 | } 116 | -------------------------------------------------------------------------------- /utils/dec_precs.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "github.com/shopspring/decimal" 7 | "regexp" 8 | "strconv" 9 | "strings" 10 | ) 11 | 12 | const ( 13 | PrecModeDecimalPlace = 2 // 保留小数点后指定位 14 | PrecModeSignifDigits = 3 // 保留全部有效数字个数 15 | PrecModeTickSize = 4 // 返回给定的数字的整数倍 16 | ) 17 | 18 | var ( 19 | regTrimEndZero = regexp.MustCompile(`0+$`) 20 | ) 21 | 22 | var ( 23 | ErrInvalidPrecision = errors.New("invalid precision") 24 | ErrNegPrecForTickSize = errors.New("negative precision for tick size") 25 | ) 26 | 27 | /* 28 | DecToPrec 29 | 将数字转换为指定精度返回 30 | 31 | num: 需要处理的数字字符串 32 | countMode: PrecModeDecimalPlace 保留小数点后指定位; 33 | PrecModeSignifDigits 保留全部有效数字个数 34 | PrecModeTickSize 返回给定的数字的整数倍 35 | precision: 结合countMode使用,正整数,负整数,正浮点数 36 | isRound: 是否四舍五入,默认截断 37 | padZero: 是否尾部填充0到指定长度 38 | */ 39 | func DecToPrec(num string, countMode int, precision string, isRound, padZero bool) (string, error) { 40 | if precision == "" { 41 | return "", ErrInvalidPrecision 42 | } 43 | if countMode < PrecModeDecimalPlace || countMode > PrecModeTickSize { 44 | return "", fmt.Errorf("invalid count mode %d", countMode) 45 | } 46 | precVal, err := decimal.NewFromString(precision) 47 | if err != nil { 48 | return "", fmt.Errorf("invalid precision %s %v", precision, err) 49 | } 50 | numVal, err := decimal.NewFromString(num) 51 | if err != nil { 52 | return "", fmt.Errorf("invalid num %s %v", num, err) 53 | } 54 | zeroVal := decimal.NewFromInt(0) 55 | tenVal := decimal.NewFromInt(10) 56 | negVal := decimal.NewFromInt(-1) 57 | 58 | powerOfTen := func(d *decimal.Decimal) decimal.Decimal { 59 | return tenVal.Pow(d.Mul(negVal)) 60 | } 61 | 62 | if precVal.LessThan(zeroVal) { 63 | if countMode == PrecModeTickSize { 64 | return "", ErrNegPrecForTickSize 65 | } 66 | nearest := powerOfTen(&precVal) 67 | if isRound { 68 | numDivVal := numVal.Div(nearest).String() 69 | midRes, err := DecToPrec(numDivVal, PrecModeDecimalPlace, "0", isRound, padZero) 70 | if err != nil { 71 | return "", err 72 | } 73 | midResVal, err := decimal.NewFromString(midRes) 74 | if err != nil { 75 | return "", err 76 | } 77 | return midResVal.Mul(nearest).String(), nil 78 | } else { 79 | numTruc := numVal.Sub(numVal.Mod(nearest)).String() 80 | return DecToPrec(numTruc, PrecModeDecimalPlace, "0", isRound, padZero) 81 | } 82 | } 83 | if countMode == PrecModeTickSize { 84 | missing := numVal.Abs().Mod(precVal) 85 | if !missing.Equal(zeroVal) { 86 | delta := missing 87 | if isRound { 88 | twoVal := decimal.NewFromInt(2) 89 | if missing.GreaterThanOrEqual(precVal.Div(twoVal)) { 90 | delta = delta.Sub(precVal) 91 | } 92 | if numVal.GreaterThan(zeroVal) { 93 | delta = delta.Mul(negVal) 94 | } 95 | } else { 96 | if numVal.GreaterThanOrEqual(zeroVal) { 97 | delta = delta.Mul(negVal) 98 | } 99 | } 100 | numVal = numVal.Add(delta) 101 | } 102 | parts := strings.Split(regTrimEndZero.ReplaceAllString(precVal.String(), ""), ".") 103 | newPrec := "0" 104 | if len(parts) > 1 { 105 | newPrec = strconv.Itoa(len(parts[1])) 106 | } else { 107 | match := regTrimEndZero.FindString(parts[0]) 108 | if match != "" { 109 | newPrec = strconv.Itoa(-len(match)) 110 | } 111 | } 112 | return DecToPrec(numVal.String(), PrecModeDecimalPlace, newPrec, true, padZero) 113 | } 114 | precise := zeroVal 115 | numExp := adjusted(numVal) 116 | if isRound { 117 | if countMode == PrecModeDecimalPlace { 118 | precise = numVal.Round(int32(precVal.IntPart())) 119 | } else if countMode == PrecModeSignifDigits { 120 | q := precVal.Sub(decimal.NewFromInt32(numExp + 1)) 121 | sigFig := powerOfTen(&q) 122 | if q.LessThan(zeroVal) { 123 | numPrecText := numVal.String()[:precVal.IntPart()] 124 | if numPrecText == "" { 125 | numPrecText = "0" 126 | } 127 | numPrecVal, err := decimal.NewFromString(numPrecText) 128 | if err != nil { 129 | return "", fmt.Errorf("numPrecText fail %v %v", numPrecText, err) 130 | } 131 | below := sigFig.Mul(numPrecVal) 132 | above := below.Add(sigFig) 133 | if below.Sub(numVal).Abs().LessThan(above.Sub(numVal).Abs()) { 134 | precise = below 135 | } else { 136 | precise = above 137 | } 138 | } else { 139 | precise = numVal.Round(int32(q.IntPart())) 140 | } 141 | } else { 142 | return "", fmt.Errorf("invalid cound mode: %v", countMode) 143 | } 144 | numExp = adjusted(precise) 145 | } else { 146 | if countMode == PrecModeDecimalPlace { 147 | precise = numVal.Truncate(int32(precVal.IntPart())) 148 | } else if countMode == PrecModeSignifDigits { 149 | if !precVal.Equal(zeroVal) { 150 | margin := tenVal.Pow(decimal.NewFromInt32(numExp)) 151 | dotVal := numVal.Div(margin).Truncate(int32(precVal.IntPart()) - 1) 152 | precise = dotVal.Mul(margin) 153 | } 154 | } else { 155 | return "", fmt.Errorf("invalid cound mode: %v", countMode) 156 | } 157 | } 158 | if !padZero { 159 | return precise.String(), nil 160 | } 161 | if countMode == PrecModeDecimalPlace { 162 | return precise.StringFixed(int32(precVal.IntPart())), nil 163 | } 164 | // PrecModeSignifDigits 165 | dotNum := int32(precVal.IntPart()) - numExp - 1 166 | if dotNum > 0 { 167 | return precise.StringFixed(dotNum), nil 168 | } 169 | return precise.String(), nil 170 | } 171 | 172 | // 获取与Python中Decimal.adjusted()类似的值 173 | func adjusted(dec decimal.Decimal) int32 { 174 | // 计算有效数字(coefficient) 175 | coefficient := dec.Coefficient() 176 | coefficient.Abs(coefficient) 177 | // 获取指数值 178 | exponent := dec.Exponent() 179 | 180 | // 计算Decimal的字符串表示中小数点前的数字个数 181 | adjustedExponent := int32(len(coefficient.String())) - 1 182 | 183 | // 返回指数+调整后(小数点前的数字个数-1) 184 | return adjustedExponent + exponent 185 | } 186 | 187 | /* 188 | PrecFloat64 189 | 对给定浮点数取近似值,精确到指定位数 190 | */ 191 | func PrecFloat64(num float64, prec float64, isRound bool, mode int) (float64, error) { 192 | resStr, err := PrecFloat64Str(num, prec, isRound, mode) 193 | if err != nil { 194 | return 0, err 195 | } 196 | return strconv.ParseFloat(resStr, 64) 197 | } 198 | 199 | func PrecFloat64Str(num float64, prec float64, isRound bool, mode int) (string, error) { 200 | numStr := strconv.FormatFloat(num, 'f', -1, 64) 201 | precStr := strconv.FormatFloat(prec, 'f', -1, 64) 202 | if mode == 0 { 203 | mode = PrecModeDecimalPlace 204 | } 205 | return DecToPrec(numStr, mode, precStr, isRound, false) 206 | } 207 | -------------------------------------------------------------------------------- /bybit/types.go: -------------------------------------------------------------------------------- 1 | package bybit 2 | 3 | import ( 4 | "github.com/banbox/banexg" 5 | ) 6 | 7 | type Bybit struct { 8 | *banexg.Exchange 9 | RecvWindow int // 允许的和服务器最大毫秒时间差 10 | } 11 | 12 | /* 13 | ***************************** CurrencyMap *********************************** 14 | */ 15 | 16 | type Currency struct { 17 | Name string `json:"name"` 18 | Coin string `json:"coin"` 19 | RemainAmount string `json:"remainAmount"` 20 | Chains []*Chain `json:"chains"` 21 | } 22 | 23 | type Chain struct { 24 | Chain string `json:"chain"` 25 | ChainType string `json:"chainType"` 26 | Confirmation string `json:"confirmation"` 27 | WithdrawFee string `json:"withdrawFee"` 28 | DepositMin string `json:"depositMin"` 29 | WithdrawMin string `json:"withdrawMin"` 30 | ChainDeposit string `json:"chainDeposit"` 31 | ChainWithdraw string `json:"chainWithdraw"` 32 | MinAccuracy string `json:"minAccuracy"` 33 | WithdrawPercentageFee string `json:"withdrawPercentageFee"` 34 | } 35 | 36 | /* 37 | ***************************** Markets *********************************** 38 | */ 39 | 40 | type BaseMarket struct { 41 | Symbol string `json:"symbol"` 42 | BaseCoin string `json:"baseCoin"` 43 | QuoteCoin string `json:"quoteCoin"` 44 | Status string `json:"status"` 45 | } 46 | 47 | type SpotMarket struct { 48 | BaseMarket 49 | Innovation string `json:"innovation"` 50 | MarginTrading string `json:"marginTrading"` 51 | LotSizeFilter *LotSizeFt `json:"lotSizeFilter"` 52 | PriceFilter *PriceFt `json:"priceFilter"` 53 | RiskParameters *RiskParams `json:"riskParameters"` 54 | } 55 | 56 | type LotSizeFt struct { 57 | BasePrecision string `json:"basePrecision"` 58 | QuotePrecision string `json:"quotePrecision"` 59 | MinOrderQty string `json:"minOrderQty"` 60 | MaxOrderQty string `json:"maxOrderQty"` 61 | MinOrderAmt string `json:"minOrderAmt"` 62 | MaxOrderAmt string `json:"maxOrderAmt"` 63 | } 64 | 65 | type PriceFt struct { 66 | MinPrice string `json:"minPrice"` // empty for spot 67 | MaxPrice string `json:"maxPrice"` // empty for spot 68 | TickSize string `json:"tickSize"` 69 | } 70 | 71 | type RiskParams struct { 72 | LimitParameter string `json:"limitParameter"` 73 | MarketParameter string `json:"marketParameter"` 74 | } 75 | 76 | type ContractMarket struct { 77 | BaseMarket 78 | SettleCoin string `json:"settleCoin"` 79 | LaunchTime string `json:"launchTime"` 80 | DeliveryTime string `json:"deliveryTime"` 81 | DeliveryFeeRate string `json:"deliveryFeeRate"` 82 | PriceFilter *PriceFt `json:"priceFilter"` 83 | } 84 | 85 | type FutureMarket struct { 86 | ContractMarket 87 | ContractType string `json:"contractType"` 88 | PriceScale string `json:"priceScale"` 89 | LeverageFilter *LeverageFt `json:"leverageFilter"` 90 | LotSizeFilter *FutureLotSizeFt `json:"lotSizeFilter"` 91 | UnifiedMarginTrade bool `json:"unifiedMarginTrade"` 92 | FundingInterval int `json:"fundingInterval"` 93 | CopyTrading string `json:"copyTrading"` 94 | UpperFundingRate string `json:"upperFundingRate"` 95 | LowerFundingRate string `json:"lowerFundingRate"` 96 | } 97 | 98 | type LeverageFt struct { 99 | MinLeverage string `json:"minLeverage"` 100 | MaxLeverage string `json:"maxLeverage"` 101 | LeverageStep string `json:"leverageStep"` 102 | } 103 | 104 | type OptionLotSizeFt struct { 105 | MinOrderQty string `json:"minOrderQty"` 106 | MaxOrderQty string `json:"maxOrderQty"` 107 | QtyStep string `json:"qtyStep"` 108 | } 109 | 110 | type FutureLotSizeFt struct { 111 | OptionLotSizeFt 112 | MaxMktOrderQty string `json:"maxMktOrderQty"` 113 | PostOnlyMaxOrderQty string `json:"postOnlyMaxOrderQty"` 114 | MinNotionalValue string `json:"minNotionalValue"` 115 | } 116 | 117 | type OptionMarket struct { 118 | ContractMarket 119 | OptionsType string `json:"optionsType"` 120 | LotSizeFilter *OptionLotSizeFt `json:"lotSizeFilter"` 121 | } 122 | 123 | /* 124 | ***************************** Tickers *********************************** 125 | */ 126 | 127 | type ITicker interface { 128 | ToStdTicker(e *Bybit, marketType string, info map[string]interface{}) *banexg.Ticker 129 | } 130 | 131 | type BaseTicker struct { 132 | Symbol string `json:"symbol"` 133 | Bid1Price string `json:"bid1Price"` 134 | Bid1Size string `json:"bid1Size"` 135 | Ask1Price string `json:"ask1Price"` 136 | Ask1Size string `json:"ask1Size"` 137 | LastPrice string `json:"lastPrice"` 138 | HighPrice24h string `json:"highPrice24h"` 139 | LowPrice24h string `json:"lowPrice24h"` 140 | Turnover24h string `json:"turnover24h"` 141 | Volume24h string `json:"volume24h"` 142 | } 143 | 144 | type SpotTicker struct { 145 | BaseTicker 146 | PrevPrice24h string `json:"prevPrice24h"` 147 | Price24hPcnt string `json:"price24hPcnt"` 148 | UsdIndexPrice string `json:"usdIndexPrice"` 149 | } 150 | 151 | type ContractTicker struct { 152 | BaseTicker 153 | IndexPrice string `json:"indexPrice"` 154 | PredictedDeliveryPrice string `json:"predictedDeliveryPrice"` 155 | MarkPrice string `json:"markPrice"` 156 | OpenInterest string `json:"openInterest"` 157 | } 158 | 159 | type OptionTicker struct { 160 | ContractTicker 161 | Bid1Iv string `json:"bid1Iv"` 162 | Ask1Iv string `json:"ask1Iv"` 163 | MarkIv string `json:"markIv"` 164 | UnderlyingPrice string `json:"underlyingPrice"` 165 | TotalVolume string `json:"totalVolume"` 166 | TotalTurnover string `json:"totalTurnover"` 167 | Delta string `json:"delta"` 168 | Gamma string `json:"gamma"` 169 | Vega string `json:"vega"` 170 | Theta string `json:"theta"` 171 | Change24h string `json:"change24h"` 172 | } 173 | 174 | type FutureTicker struct { 175 | ContractTicker 176 | PrevPrice24h string `json:"prevPrice24h"` 177 | Price24hPcnt string `json:"price24hPcnt"` 178 | PrevPrice1h string `json:"prevPrice1h"` 179 | OpenInterestValue string `json:"openInterestValue"` 180 | FundingRate string `json:"fundingRate"` 181 | NextFundingTime string `json:"nextFundingTime"` 182 | BasisRate string `json:"basisRate"` 183 | DeliveryFeeRate string `json:"deliveryFeeRate"` 184 | DeliveryTime string `json:"deliveryTime"` 185 | Basis string `json:"basis"` 186 | } 187 | 188 | type FundRate struct { 189 | Symbol string `json:"symbol"` 190 | FundingRate string `json:"fundingRate"` 191 | FundingRateTimestamp string `json:"fundingRateTimestamp"` 192 | } 193 | -------------------------------------------------------------------------------- /log/log.go: -------------------------------------------------------------------------------- 1 | package log 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "go.uber.org/zap" 7 | "go.uber.org/zap/zapcore" 8 | "gopkg.in/natefinch/lumberjack.v2" 9 | "os" 10 | "reflect" 11 | "sync" 12 | "sync/atomic" 13 | ) 14 | 15 | var _globalL, _globalP, _globalS atomic.Value 16 | 17 | var ( 18 | _globalLevelLogger sync.Map 19 | _namedRateLimiters sync.Map 20 | ) 21 | 22 | func init() { 23 | l, p := newStdLogger() 24 | 25 | replaceLeveledLoggers(l) 26 | _globalL.Store(l) 27 | _globalP.Store(p) 28 | 29 | s := _globalL.Load().(*zap.Logger).Sugar() 30 | _globalS.Store(s) 31 | 32 | } 33 | 34 | // InitLogger initializes a zap logger. 35 | func InitLogger(cfg *Config, opts ...zap.Option) (*zap.Logger, *ZapProperties, error) { 36 | var outputs []zapcore.WriteSyncer 37 | if cfg.File != nil && len(cfg.File.LogPath) > 0 { 38 | lg, err := initFileLog(cfg.File) 39 | if err != nil { 40 | return nil, nil, err 41 | } 42 | outputs = append(outputs, zapcore.AddSync(lg)) 43 | } 44 | if cfg.Stdout { 45 | stdOut, _, err := zap.Open([]string{"stdout"}...) 46 | if err != nil { 47 | return nil, nil, err 48 | } 49 | outputs = append(outputs, stdOut) 50 | } 51 | debugCfg := *cfg 52 | debugCfg.Level = "debug" 53 | outputsWriter := zap.CombineWriteSyncers(outputs...) 54 | debugL, r, err := InitLoggerWithWriteSyncer(&debugCfg, outputsWriter, cfg.Handlers, opts...) 55 | if err != nil { 56 | return nil, nil, err 57 | } 58 | replaceLeveledLoggers(debugL) 59 | level := zapcore.DebugLevel 60 | if err := level.UnmarshalText([]byte(cfg.Level)); err != nil { 61 | return nil, nil, err 62 | } 63 | r.Level.SetLevel(level) 64 | return debugL.WithOptions(zap.AddCallerSkip(1)), r, nil 65 | } 66 | 67 | // InitLoggerWithWriteSyncer initializes a zap logger with specified write syncer. 68 | func InitLoggerWithWriteSyncer(cfg *Config, output zapcore.WriteSyncer, handlers []zapcore.Core, opts ...zap.Option) (*zap.Logger, *ZapProperties, error) { 69 | level := zap.NewAtomicLevel() 70 | err := level.UnmarshalText([]byte(cfg.Level)) 71 | if err != nil { 72 | return nil, nil, fmt.Errorf("initLoggerWithWriteSyncer UnmarshalText cfg.Level errs:%w", err) 73 | } 74 | core := NewTextCore(newZapTextEncoder(cfg), output, level) 75 | if len(handlers) > 0 { 76 | handlers = append([]zapcore.Core{core}, handlers...) 77 | core = zapcore.NewTee(handlers...) 78 | } 79 | opts = append(cfg.buildOptions(output), opts...) 80 | lg := zap.New(core, opts...) 81 | r := &ZapProperties{ 82 | Core: core, 83 | Syncer: output, 84 | Level: level, 85 | } 86 | return lg, r, nil 87 | } 88 | 89 | // initFileLog initializes file based logging options. 90 | func initFileLog(cfg *FileLogConfig) (*lumberjack.Logger, error) { 91 | if st, err := os.Stat(cfg.LogPath); err == nil { 92 | if st.IsDir() { 93 | return nil, errors.New("can't use directory as log file name") 94 | } 95 | } 96 | if cfg.MaxSize == 0 { 97 | cfg.MaxSize = defaultLogMaxSize 98 | } 99 | 100 | // use lumberjack to logrotate 101 | return &lumberjack.Logger{ 102 | Filename: cfg.LogPath, 103 | MaxSize: cfg.MaxSize, 104 | MaxBackups: cfg.MaxBackups, 105 | MaxAge: cfg.MaxDays, 106 | LocalTime: false, 107 | }, nil 108 | } 109 | 110 | func newStdLogger() (*zap.Logger, *ZapProperties) { 111 | conf := &Config{Level: "debug", Stdout: true, DisableErrorVerbose: true} 112 | lg, r, _ := InitLogger(conf, zap.OnFatal(zapcore.WriteThenPanic)) 113 | return lg, r 114 | } 115 | 116 | // L returns the global Logger, which can be reconfigured with ReplaceGlobals. 117 | // It's safe for concurrent use. 118 | func L() *zap.Logger { 119 | return _globalL.Load().(*zap.Logger) 120 | } 121 | 122 | // S returns the global SugaredLogger, which can be reconfigured with 123 | // ReplaceGlobals. It's safe for concurrent use. 124 | func S() *zap.SugaredLogger { 125 | return _globalS.Load().(*zap.SugaredLogger) 126 | } 127 | 128 | func ctxL() *zap.Logger { 129 | level := _globalP.Load().(*ZapProperties).Level.Level() 130 | l, ok := _globalLevelLogger.Load(level) 131 | if !ok { 132 | return L() 133 | } 134 | return l.(*zap.Logger) 135 | } 136 | 137 | func debugL() *zap.Logger { 138 | v, _ := _globalLevelLogger.Load(zapcore.DebugLevel) 139 | return v.(*zap.Logger) 140 | } 141 | 142 | func infoL() *zap.Logger { 143 | v, _ := _globalLevelLogger.Load(zapcore.InfoLevel) 144 | return v.(*zap.Logger) 145 | } 146 | 147 | func warnL() *zap.Logger { 148 | v, _ := _globalLevelLogger.Load(zapcore.WarnLevel) 149 | return v.(*zap.Logger) 150 | } 151 | 152 | func errorL() *zap.Logger { 153 | v, _ := _globalLevelLogger.Load(zapcore.ErrorLevel) 154 | return v.(*zap.Logger) 155 | } 156 | 157 | func fatalL() *zap.Logger { 158 | v, _ := _globalLevelLogger.Load(zapcore.FatalLevel) 159 | return v.(*zap.Logger) 160 | } 161 | 162 | // ReplaceGlobals replaces the global Logger and SugaredLogger. 163 | // It's safe for concurrent use. 164 | func ReplaceGlobals(logger *zap.Logger, props *ZapProperties) { 165 | _globalL.Store(logger) 166 | _globalS.Store(logger.Sugar()) 167 | _globalP.Store(props) 168 | } 169 | 170 | func replaceLeveledLoggers(debugLogger *zap.Logger) { 171 | levels := []zapcore.Level{ 172 | zapcore.DebugLevel, zapcore.InfoLevel, zapcore.WarnLevel, zapcore.ErrorLevel, 173 | zapcore.DPanicLevel, zapcore.PanicLevel, zapcore.FatalLevel, 174 | } 175 | for _, level := range levels { 176 | levelL := debugLogger.WithOptions(zap.IncreaseLevel(level)) 177 | _globalLevelLogger.Store(level, levelL) 178 | } 179 | } 180 | 181 | // Sync flushes any buffered log entries. 182 | func Sync() error { 183 | if err := L().Sync(); err != nil { 184 | return err 185 | } 186 | if err := S().Sync(); err != nil { 187 | return err 188 | } 189 | var reterr error 190 | _globalLevelLogger.Range(func(key, val interface{}) bool { 191 | l := val.(*zap.Logger) 192 | if err := l.Sync(); err != nil { 193 | reterr = err 194 | return false 195 | } 196 | return true 197 | }) 198 | return reterr 199 | } 200 | 201 | func Level() zap.AtomicLevel { 202 | return _globalP.Load().(*ZapProperties).Level 203 | } 204 | 205 | // SetupLogger is used to initialize the log with config. 206 | func SetupLogger(cfg *Config) { 207 | logger, p, err := InitLogger(cfg) 208 | if err == nil { 209 | ReplaceGlobals(logger, p) 210 | } else { 211 | Fatal("initialize logger error", zap.Error(err)) 212 | } 213 | } 214 | 215 | func Setup(level, logFile string, handlers ...zapcore.Core) { 216 | var file *FileLogConfig 217 | if logFile != "" { 218 | file = &FileLogConfig{ 219 | LogPath: logFile, 220 | } 221 | } 222 | if level == "" { 223 | level = "info" 224 | } 225 | SetupLogger(&Config{ 226 | Stdout: true, 227 | Format: "text", 228 | Level: level, 229 | File: file, 230 | Handlers: handlers, 231 | }) 232 | } 233 | 234 | func Type(key string, v interface{}) zap.Field { 235 | return zap.Stringer(fmt.Sprintf("type_of_%s", key), reflect.TypeOf(v)) 236 | } 237 | -------------------------------------------------------------------------------- /binance/ws_biz_test.go: -------------------------------------------------------------------------------- 1 | package binance 2 | 3 | import ( 4 | "compress/gzip" 5 | "encoding/gob" 6 | "fmt" 7 | "github.com/banbox/banexg" 8 | "github.com/banbox/banexg/errs" 9 | "github.com/banbox/banexg/log" 10 | "github.com/banbox/bntp" 11 | "github.com/h2non/gock" 12 | "go.uber.org/zap" 13 | "os" 14 | "path/filepath" 15 | "strconv" 16 | "strings" 17 | "testing" 18 | "time" 19 | ) 20 | 21 | func TestWatchOHLCVs(t *testing.T) { 22 | testWatchOHLCVs(t, false) 23 | } 24 | 25 | func testWatchOHLCVs(t *testing.T, isFake bool) { 26 | if isFake { 27 | gock.DisableNetworking() 28 | err := LoadGockItems("testdata/gock.json") 29 | if err != nil { 30 | panic(err) 31 | } 32 | } 33 | exg := getBinance(map[string]interface{}{ 34 | banexg.OptDebugWs: true, 35 | }) 36 | if isFake { 37 | gock.InterceptClient(exg.HttpClient) 38 | } 39 | 40 | var err *errs.Error 41 | jobs := [][2]string{ 42 | {"ETH/USDT", "1s"}, 43 | } 44 | out, err_ := exg.WatchOHLCVs(jobs, nil) 45 | if err_ != nil { 46 | panic(err_) 47 | } 48 | count := 0 49 | mainFor: 50 | for { 51 | select { 52 | case k, ok := <-out: 53 | if !ok { 54 | log.Info("read out chan fail, break") 55 | break mainFor 56 | } 57 | count += 1 58 | if count == 10 { 59 | err2 := exg.UnWatchOHLCVs(jobs, nil) 60 | if err2 != nil { 61 | log.Error("unwatch fail", zap.Error(err)) 62 | } else { 63 | log.Info("unwatch jobs..") 64 | } 65 | } 66 | log.Info("ohlcv", zap.Int64("t", k.Time), 67 | zap.Float64("o", k.Open), 68 | zap.Float64("h", k.High), 69 | zap.Float64("l", k.Low), 70 | zap.Float64("c", k.Close), 71 | zap.Float64("v", k.Volume), 72 | ) 73 | } 74 | } 75 | } 76 | 77 | func TestWatchBalance(t *testing.T) { 78 | exg := getBinance(nil) 79 | exg.MarketType = banexg.MarketLinear 80 | out, err := exg.WatchBalance(nil) 81 | if err != nil { 82 | panic(err) 83 | } 84 | fmt.Println("start watching balances") 85 | mainFor: 86 | for { 87 | select { 88 | case b, ok := <-out: 89 | if !ok { 90 | log.Info("read out chan fail, break") 91 | break mainFor 92 | } 93 | builder := strings.Builder{} 94 | builder.WriteString("time:" + strconv.FormatInt(b.TimeStamp, 10) + "\n") 95 | for _, item := range b.Assets { 96 | builder.WriteString(item.Code + "\t\t") 97 | builder.WriteString(fmt.Sprintf("free: %f total: %f\n", item.Free, item.Total)) 98 | } 99 | fmt.Print(builder.String()) 100 | } 101 | } 102 | } 103 | 104 | func TestWatchPositions(t *testing.T) { 105 | exg := getBinance(nil) 106 | exg.MarketType = banexg.MarketLinear 107 | out, err := exg.WatchPositions(nil) 108 | if err != nil { 109 | panic(err) 110 | } 111 | fmt.Println("start watching positions") 112 | mainFor: 113 | for { 114 | select { 115 | case positions, ok := <-out: 116 | if !ok { 117 | log.Info("read out chan fail, break") 118 | break mainFor 119 | } 120 | builder := strings.Builder{} 121 | builder.WriteString("=============================\n") 122 | for _, pos := range positions { 123 | builder.WriteString(pos.Symbol + ", ") 124 | builder.WriteString(fmt.Sprintf("%v, ", pos.Contracts)) 125 | builder.WriteString(fmt.Sprintf("%v, ", pos.UnrealizedPnl)) 126 | builder.WriteString("\n") 127 | } 128 | fmt.Print(builder.String()) 129 | } 130 | } 131 | } 132 | 133 | func TestWatchMarkPrices(t *testing.T) { 134 | exg := getBinance(nil) 135 | exg.MarketType = banexg.MarketLinear 136 | symbols := []string{"BTC/USDT:USDT", "ETH/USDT:USDT"} 137 | out, err := exg.WatchMarkPrices(symbols, map[string]interface{}{ 138 | banexg.ParamInterval: "1s", 139 | }) 140 | // 监听所有币种,3s更新: 141 | // out, err := exg.WatchMarkPrices(nil, nil) 142 | if err != nil { 143 | panic(err) 144 | } 145 | fmt.Println("start watching markPrices") 146 | mainFor: 147 | for { 148 | select { 149 | case data, ok := <-out: 150 | if !ok { 151 | log.Info("read out chan fail, break") 152 | break mainFor 153 | } 154 | timeStr := bntp.Now().Format("2006-01-02 15:04:05") 155 | builder := strings.Builder{} 156 | builder.WriteString("============== " + timeStr + " ===============\n") 157 | for symbol, price := range data { 158 | builder.WriteString(fmt.Sprintf("%s: %v\n", symbol, price)) 159 | } 160 | fmt.Print(builder.String()) 161 | } 162 | } 163 | } 164 | 165 | func TestWsDump(t *testing.T) { 166 | exg := getBinance(map[string]interface{}{ 167 | banexg.OptDumpPath: getWsDumpPath(), 168 | }) 169 | exg.MarketType = banexg.MarketLinear 170 | symbols := []string{"BTC/USDT:USDT", "ETH/USDT:USDT"} 171 | _, err := exg.WatchOrderBooks(symbols, 500, nil) 172 | if err != nil { 173 | panic(err) 174 | } 175 | log.Info("watch order books ...") 176 | time.AfterFunc(time.Second*30, func() { 177 | _, err = exg.WatchMarkPrices(symbols, nil) 178 | if err != nil { 179 | panic(err) 180 | } 181 | log.Info("watch mark prices ...") 182 | }) 183 | time.Sleep(time.Second * 60) 184 | exg.Close() 185 | } 186 | 187 | func TestWsReplay(t *testing.T) { 188 | exg := getBinance(map[string]interface{}{ 189 | banexg.OptReplayPath: getWsDumpPath(), 190 | }) 191 | exg.MarketType = banexg.MarketLinear 192 | exg.SetOnWsChan(func(key string, out interface{}) { 193 | offset := strings.LastIndex(key, "@") 194 | method := key[offset+1:] 195 | if method == "depth" { 196 | chl := out.(chan *banexg.OrderBook) 197 | go func() { 198 | count := 0 199 | for range chl { 200 | count += 1 201 | } 202 | log.Info("got depth msg", zap.Int("num", count)) 203 | }() 204 | } else if method == "markPrice" { 205 | chl := out.(chan map[string]float64) 206 | go func() { 207 | count := 0 208 | for range chl { 209 | count += 1 210 | } 211 | log.Info("got markPrice msg", zap.Int("num", count)) 212 | }() 213 | } else { 214 | log.Info("got unknown ws chan", zap.String("key", key)) 215 | } 216 | }) 217 | err := exg.ReplayAll() 218 | if err != nil { 219 | panic(err) 220 | } 221 | exg.Close() 222 | time.Sleep(time.Second) 223 | } 224 | 225 | func getWsDumpPath() string { 226 | cacheDir, err_ := os.UserCacheDir() 227 | if err_ != nil { 228 | panic(err_) 229 | } 230 | return filepath.Join(cacheDir, "ban_ws_dump.gz") 231 | } 232 | 233 | func TestDumpCompress(t *testing.T) { 234 | inPath := getWsDumpPath() 235 | file, err := os.Open(inPath) 236 | if err != nil { 237 | panic(err) 238 | } 239 | defer file.Close() 240 | reader, err := gzip.NewReader(file) 241 | if err != nil { 242 | panic(err) 243 | } 244 | defer reader.Close() 245 | decoder := gob.NewDecoder(reader) 246 | 247 | out, err := os.OpenFile(inPath+"1", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644) 248 | if err != nil { 249 | panic(err) 250 | } 251 | defer out.Close() 252 | writer := gzip.NewWriter(out) 253 | defer writer.Close() 254 | encoder := gob.NewEncoder(writer) 255 | 256 | for { 257 | cache := make([]*banexg.WsLog, 0, 1000) 258 | if err_ := decoder.Decode(&cache); err_ != nil { 259 | // read done 260 | break 261 | } 262 | err = encoder.Encode(cache) 263 | if err != nil { 264 | panic(err) 265 | } 266 | } 267 | } 268 | -------------------------------------------------------------------------------- /binance/ws_order_test.go: -------------------------------------------------------------------------------- 1 | package binance 2 | 3 | import ( 4 | "bufio" 5 | "fmt" 6 | "github.com/banbox/banexg" 7 | "github.com/banbox/banexg/log" 8 | "github.com/banbox/banexg/utils" 9 | "github.com/h2non/gock" 10 | "github.com/sasha-s/go-deadlock" 11 | "go.uber.org/zap" 12 | "go.uber.org/zap/buffer" 13 | "io" 14 | "os" 15 | "strings" 16 | "testing" 17 | "time" 18 | ) 19 | 20 | func TestWatchOrderBook(t *testing.T) { 21 | testWatchOrderBook("ETH/USDT:USDT", 100) 22 | } 23 | 24 | func TestWatchOrderBookLimit(t *testing.T) { 25 | //testWatchOrderBook("ETH/USDT", 10) 26 | testWatchOrderBook("ETH/USDT:USDT", 10) 27 | } 28 | 29 | func testWatchOrderBook(symbol string, depthLimit int) { 30 | exg := getBinance(nil) 31 | out, err := exg.WatchOrderBooks([]string{symbol}, depthLimit, nil) 32 | if err != nil { 33 | panic(err) 34 | } 35 | for { 36 | select { 37 | case msg := <-out: 38 | msgText, err := utils.MarshalString(msg) 39 | if err != nil { 40 | log.Error("marshal msg fail", zap.Error(err)) 41 | continue 42 | } 43 | log.Info("ws", zap.String("msg", msgText)) 44 | } 45 | } 46 | } 47 | 48 | type MockWsConn struct { 49 | Path string 50 | file *os.File 51 | scanner *bufio.Scanner 52 | msgChan chan []byte 53 | lock deadlock.Mutex 54 | id int 55 | } 56 | 57 | func (c *MockWsConn) Close() error { 58 | if c.file != nil { 59 | err := c.file.Close() 60 | c.file = nil 61 | return err 62 | } 63 | return nil 64 | } 65 | 66 | func (c *MockWsConn) WriteClose() error { 67 | return nil 68 | } 69 | 70 | func (c *MockWsConn) ReConnect() error { 71 | return nil 72 | } 73 | 74 | func (c *MockWsConn) NextWriter() (io.WriteCloser, error) { 75 | return &mockWriter{conn: c}, nil 76 | } 77 | 78 | func (c *MockWsConn) ReadMsg() ([]byte, error) { 79 | c.lock.Lock() 80 | defer c.lock.Unlock() 81 | time.Sleep(time.Millisecond * 30) 82 | 83 | // 检查通道是否有数据 84 | select { 85 | case data, ok := <-c.msgChan: 86 | if ok { 87 | log.Info("receive msg", zap.String("msg", string(data))) 88 | var msgRaw map[string]interface{} 89 | _ = utils.Unmarshal(data, &msgRaw, utils.JsonNumStr) 90 | msg := utils.MapValStr(msgRaw) 91 | method, _ := utils.SafeMapVal(msg, "method", "") 92 | id, _ := utils.SafeMapVal(msg, "id", 0) 93 | var retData = fmt.Sprintf("{\"id\":%d,\"result\":null}", id) 94 | if method == "SUBSCRIBE" { 95 | 96 | } else { 97 | log.Error("unsupport ws method", zap.String("method", method)) 98 | } 99 | log.Info("ret msg", zap.String("msg", retData)) 100 | return []byte(retData), nil 101 | } 102 | default: 103 | } 104 | if c.scanner == nil { 105 | file, err := os.Open(c.Path) 106 | if err != nil { 107 | return nil, err 108 | } 109 | c.file = file 110 | c.scanner = bufio.NewScanner(file) 111 | } 112 | if !c.scanner.Scan() { 113 | err := c.scanner.Err() 114 | if err != nil { 115 | return nil, err 116 | } 117 | return nil, io.EOF 118 | } 119 | return c.scanner.Bytes(), nil 120 | } 121 | 122 | func (c *MockWsConn) IsOK() bool { 123 | return c.scanner != nil 124 | } 125 | 126 | func (c *MockWsConn) GetID() int { 127 | return c.id 128 | } 129 | 130 | func (c *MockWsConn) SetID(v int) { 131 | c.id = v 132 | } 133 | 134 | type mockWriter struct { 135 | conn *MockWsConn 136 | } 137 | 138 | func (w *mockWriter) Write(p []byte) (n int, err error) { 139 | w.conn.lock.Lock() 140 | defer w.conn.lock.Unlock() 141 | select { 142 | case w.conn.msgChan <- p: 143 | default: 144 | return 0, io.ErrShortWrite 145 | } 146 | return len(p), nil 147 | } 148 | 149 | func (w *mockWriter) Close() error { 150 | // 可以在这里添加关闭逻辑,如果需要 151 | return nil 152 | } 153 | 154 | func TestWatchOrderBookOut(t *testing.T) { 155 | err := LoadGockItems("testdata/gock.json") 156 | gock.DisableNetworking() 157 | if err != nil { 158 | panic(err) 159 | } 160 | gock.New("https://fapi.binance.com").Get("/fapi/v1/depth"). 161 | Reply(200).File("testdata/order_book_shot.json") 162 | 163 | var conn banexg.WsConn 164 | conn = &MockWsConn{ 165 | Path: "testdata/ws_odbook_msg.log", 166 | msgChan: make(chan []byte, 10), 167 | } 168 | exg := getBinance(map[string]interface{}{ 169 | banexg.OptWsConn: conn, 170 | }) 171 | // 模拟网络请求 172 | gock.InterceptClient(exg.HttpClient) 173 | symbol := "ETH/USDT:USDT" // 这里必须是ETH/USDT:USDT 174 | exg.WsIntvs["WatchOrderBooks"] = 100 175 | // 此处请求订单簿镜像时mock的testdata/order_book_shot.json 176 | out, err_ := exg.WatchOrderBooks([]string{symbol}, 100, nil) 177 | if err_ != nil { 178 | panic(err_) 179 | } 180 | data, err := utils.ReadFile("testdata/ccxt_book.log") 181 | if err != nil { 182 | fmt.Println("read fail:", err) 183 | return 184 | } 185 | expect := strings.Replace(string(data), "\r\n", "\n", -1) 186 | writer := buffer.Buffer{} 187 | var book *banexg.OrderBook 188 | mainFor: 189 | for { 190 | select { 191 | case tmp, ok := <-out: 192 | if !ok { 193 | log.Info("read out chan fail, break") 194 | break mainFor 195 | } 196 | book = tmp 197 | } 198 | } 199 | if book == nil { 200 | panic("no book received") 201 | } 202 | _, _ = writer.WriteString(fmt.Sprintf("---------- %v ----------\n", book.TimeStamp)) 203 | for i, price := range book.Asks.Price { 204 | size := book.Asks.Size[i] 205 | _, _ = writer.WriteString(fmt.Sprintf("ask: %.3f %.6f\n", price, price*size)) 206 | } 207 | for i, price := range book.Bids.Price { 208 | size := book.Bids.Size[i] 209 | _, _ = writer.WriteString(fmt.Sprintf("bid: %.3f %.6f\n", price, price*size)) 210 | } 211 | _, _ = writer.WriteString("\n") 212 | output := writer.String() 213 | if output != expect { 214 | outPath := "D:/banexg_odbook.log" 215 | t.Error("order book invalid, please check:" + outPath) 216 | err := os.WriteFile(outPath, []byte(output), 0644) 217 | if err != nil { 218 | panic(fmt.Sprintf("write bad order book fail:%s", outPath)) 219 | } 220 | } 221 | } 222 | 223 | func TestWatchTrades(t *testing.T) { 224 | exg := getBinance(nil) 225 | var symbols = []string{"ETC/USDT:USDT"} 226 | exg.MarketType = banexg.MarketLinear 227 | out, err := exg.WatchTrades(symbols, nil) 228 | if err != nil { 229 | panic(err) 230 | } 231 | fmt.Println("start watching trades") 232 | mainFor: 233 | for { 234 | select { 235 | case trade, ok := <-out: 236 | if !ok { 237 | log.Info("read out chan fail, break") 238 | break mainFor 239 | } 240 | builder := strings.Builder{} 241 | builder.WriteString(trade.Symbol + ", ") 242 | builder.WriteString(fmt.Sprintf("%v, ", trade.Amount)) 243 | builder.WriteString(fmt.Sprintf("%v, ", trade.Price)) 244 | builder.WriteString(fmt.Sprintf("%v, ", trade.Side)) 245 | builder.WriteString("\n") 246 | fmt.Print(builder.String()) 247 | } 248 | } 249 | } 250 | 251 | func TestWatchMyTrades(t *testing.T) { 252 | exg := getBinance(nil) 253 | exg.MarketType = banexg.MarketLinear 254 | out, err := exg.WatchMyTrades(nil) 255 | if err != nil { 256 | panic(err) 257 | } 258 | fmt.Println("start watching my trades") 259 | mainFor: 260 | for { 261 | select { 262 | case trade, ok := <-out: 263 | if !ok { 264 | log.Info("read out chan fail, break") 265 | break mainFor 266 | } 267 | builder := strings.Builder{} 268 | builder.WriteString(trade.Symbol + ", ") 269 | builder.WriteString(fmt.Sprintf("%v, ", trade.Amount)) 270 | builder.WriteString(fmt.Sprintf("%v, ", trade.Filled)) 271 | builder.WriteString(fmt.Sprintf("%v, ", trade.Price)) 272 | builder.WriteString(fmt.Sprintf("%v, ", trade.Average)) 273 | builder.WriteString(fmt.Sprintf("%v, ", trade.State)) 274 | builder.WriteString("\n") 275 | fmt.Print(builder.String()) 276 | } 277 | } 278 | } 279 | -------------------------------------------------------------------------------- /binance/biz_order_algo_test.go: -------------------------------------------------------------------------------- 1 | package binance 2 | 3 | import ( 4 | "strings" 5 | "testing" 6 | 7 | "github.com/banbox/banexg" 8 | "github.com/banbox/banexg/log" 9 | "github.com/banbox/banexg/utils" 10 | "github.com/banbox/bntp" 11 | "go.uber.org/zap" 12 | ) 13 | 14 | func getPosition(t *testing.T) (*Binance, *banexg.Position) { 15 | bntp.LangCode = bntp.LangZhCN 16 | exg := getBinance(nil) 17 | 18 | pos, err := exg.FetchAccountPositions(nil, map[string]interface{}{ 19 | banexg.ParamMarket: banexg.MarketLinear, 20 | }) 21 | if err != nil { 22 | panic(err) 23 | } 24 | if len(pos) == 0 { 25 | t.Fatal("please create position first") 26 | } 27 | return exg, pos[0] 28 | } 29 | 30 | // TestCreateAlgoOrder 测试创建策略单 31 | // 对应接口: POST /fapi/v1/algoOrder 32 | func TestCreateAlgoOrder(t *testing.T) { 33 | exg, pos := getPosition(t) 34 | symbol := pos.Symbol 35 | quantity := pos.Contracts 36 | curPrice := pos.Notional / quantity 37 | log.Info("get positions", zap.String("pair", symbol), zap.Float64("price", curPrice), zap.Float64("quantity", quantity)) 38 | 39 | // 1. 测试创建止损单 (STOP_MARKET) 40 | slPrice := curPrice * 0.8 41 | tpPrice := curPrice * 1.2 42 | args := map[string]interface{}{ 43 | banexg.ParamStopLossPrice: slPrice, 44 | banexg.ParamPositionSide: strings.ToUpper(banexg.PosSideLong), 45 | } 46 | // CreateOrder 内部会根据 Linear 市场和 OrderType 自动路由到 createAlgoOrder 47 | res, err := exg.CreateOrder(symbol, banexg.OdTypeStopMarket, banexg.OdSideSell, quantity, 0, args) 48 | if err != nil { 49 | t.Fatalf("Create STOP_MARKET Order Failed: %v", err) 50 | } 51 | resStr, _ := utils.MarshalString(res) 52 | t.Logf("Create STOP_MARKET Result: %s", resStr) 53 | 54 | // 2. 测试创建止盈单 (TAKE_PROFIT_MARKET) 55 | args2 := map[string]interface{}{ 56 | banexg.ParamTakeProfitPrice: tpPrice, 57 | banexg.ParamPositionSide: strings.ToUpper(banexg.PosSideLong), 58 | } 59 | res2, err := exg.CreateOrder(symbol, banexg.OdTypeTakeProfitMarket, banexg.OdSideSell, quantity, 0, args2) 60 | if err != nil { 61 | t.Fatalf("Create TAKE_PROFIT_MARKET Order Failed: %v", err) 62 | } 63 | resStr2, _ := utils.MarshalString(res2) 64 | t.Logf("Create TAKE_PROFIT_MARKET Result: %s", resStr2) 65 | } 66 | 67 | // TestFetchOpenAlgoOrders 测试查询当前挂单的策略单 68 | // 对应接口: GET /fapi/v1/openAlgoOrders 69 | func TestFetchOpenAlgoOrders(t *testing.T) { 70 | exg, pos := getPosition(t) 71 | symbol := pos.Symbol 72 | 73 | args := map[string]interface{}{ 74 | banexg.ParamAlgoOrder: true, // 关键参数,指示查询策略单 75 | } 76 | 77 | res, err := exg.FetchOpenOrders(symbol, 0, 0, args) 78 | if err != nil { 79 | t.Fatalf("Fetch Open Algo Orders Failed: %v", err) 80 | } 81 | resStr, _ := utils.MarshalString(res) 82 | t.Logf("Fetch Open Algo Orders Result: %s", resStr) 83 | 84 | if len(res) > 0 { 85 | // 测试获取条件单 86 | algoRes, err := exg.FetchOrder(symbol, res[0].ID, args) 87 | if err != nil { 88 | t.Logf("Fetch Algo Order Failed (Might be invalid ID): %v", err) 89 | } else { 90 | resStr, _ = utils.MarshalString(algoRes) 91 | t.Logf("Fetch Algo Order Result: %s", resStr) 92 | } 93 | } 94 | } 95 | 96 | // TestFetchAlgoOrdersHistory 测试查询策略单历史 97 | // 对应接口: GET /fapi/v1/allAlgoOrders 98 | func TestFetchAlgoOrdersHistory(t *testing.T) { 99 | exg, pos := getPosition(t) 100 | symbol := pos.Symbol 101 | 102 | args := map[string]interface{}{ 103 | banexg.ParamAlgoOrder: true, // 关键参数,指示查询策略单 104 | // banexg.ParamLimit: 10, 105 | } 106 | 107 | res, err := exg.FetchOrders(symbol, 0, 10, args) 108 | if err != nil { 109 | t.Fatalf("Fetch Algo Orders History Failed: %v", err) 110 | } 111 | resStr, _ := utils.MarshalString(res) 112 | t.Logf("Fetch Algo Orders History Result: %s", resStr) 113 | 114 | if len(res) > 0 { 115 | // 测试获取历史单详情 116 | algoRes, err := exg.FetchOrder(symbol, res[0].ID, args) 117 | if err != nil { 118 | t.Logf("Fetch Algo Order History Detail Failed: %v", err) 119 | } else { 120 | resStr, _ = utils.MarshalString(algoRes) 121 | t.Logf("Fetch Algo Order History Detail Result: %s", resStr) 122 | } 123 | } 124 | } 125 | 126 | // TestCancelAlgoOrder 测试撤销策略单 127 | // 对应接口: DELETE /fapi/v1/algoOrder 128 | func TestCancelAlgoOrder(t *testing.T) { 129 | exg, pos := getPosition(t) 130 | symbol := pos.Symbol 131 | quantity := pos.Contracts 132 | curPrice := pos.Notional / quantity 133 | 134 | // 1. 先创建一个策略单 135 | slPrice := curPrice * 0.8 136 | createArgs := map[string]interface{}{ 137 | banexg.ParamStopLossPrice: slPrice, 138 | banexg.ParamPositionSide: strings.ToUpper(banexg.PosSideLong), 139 | } 140 | order, err := exg.CreateOrder(symbol, banexg.OdTypeStopMarket, banexg.OdSideSell, quantity, 0, createArgs) 141 | if err != nil { 142 | t.Fatalf("Setup: Create Order Failed: %v", err) 143 | } 144 | algoId := order.ID 145 | t.Logf("Setup: Created order %s to cancel", algoId) 146 | 147 | // 2. 撤销该策略单 148 | args := map[string]interface{}{ 149 | banexg.ParamAlgoOrder: true, // 关键参数,指示操作策略单 150 | } 151 | 152 | res, err := exg.CancelOrder(algoId, symbol, args) 153 | if err != nil { 154 | t.Logf("Cancel Algo Order Failed: %v", err) 155 | } else { 156 | resStr, _ := utils.MarshalString(res) 157 | t.Logf("Cancel Algo Order Result: %s", resStr) 158 | } 159 | } 160 | 161 | // TestAlgoOrderLifecycle 综合测试:下单 -> 查询挂单 -> 撤单 -> 查询历史 162 | func TestAlgoOrderLifecycle(t *testing.T) { 163 | exg, pos := getPosition(t) 164 | symbol := pos.Symbol 165 | quantity := pos.Contracts 166 | curPrice := pos.Notional / quantity 167 | 168 | // 1. 下单 169 | slPrice := curPrice * 0.8 170 | createArgs := map[string]interface{}{ 171 | banexg.ParamStopLossPrice: slPrice, 172 | banexg.ParamPositionSide: strings.ToUpper(banexg.PosSideLong), 173 | } 174 | order, err := exg.CreateOrder(symbol, banexg.OdTypeStopMarket, banexg.OdSideSell, quantity, 0, createArgs) 175 | if err != nil { 176 | t.Fatalf("Lifecycle: Create Order Failed: %v", err) 177 | } 178 | t.Logf("Lifecycle: Created Order ID: %s", order.ID) 179 | 180 | algoId := order.ID 181 | 182 | // 2. 查询 Open Orders 确认存在 183 | openArgs := map[string]interface{}{ 184 | banexg.ParamAlgoOrder: true, 185 | } 186 | openOrders, err := exg.FetchOpenOrders(symbol, 0, 0, openArgs) 187 | if err != nil { 188 | t.Fatalf("Lifecycle: Fetch Open Orders Failed: %v", err) 189 | } 190 | found := false 191 | for _, o := range openOrders { 192 | if o.ID == algoId { 193 | found = true 194 | break 195 | } 196 | } 197 | if !found { 198 | t.Fatalf("Lifecycle: Order %s not found in open orders", algoId) 199 | } 200 | t.Logf("Lifecycle: Order %s found in open orders", algoId) 201 | 202 | // 3. 查询单个订单详情 203 | fetchArgs := map[string]interface{}{ 204 | banexg.ParamAlgoOrder: true, 205 | } 206 | fetchedOrder, err := exg.FetchOrder(symbol, algoId, fetchArgs) 207 | if err != nil { 208 | t.Fatalf("Lifecycle: Fetch Order Failed: %v", err) 209 | } 210 | if fetchedOrder.ID != algoId { 211 | t.Errorf("Lifecycle: Fetched Order ID mismatch, got %s want %s", fetchedOrder.ID, algoId) 212 | } 213 | t.Logf("Lifecycle: Fetched Order Details confirmed") 214 | 215 | // 4. 撤单 216 | cancelArgs := map[string]interface{}{ 217 | banexg.ParamAlgoOrder: true, 218 | } 219 | cancelRes, err := exg.CancelOrder(algoId, symbol, cancelArgs) 220 | if err != nil { 221 | t.Fatalf("Lifecycle: Cancel Order Failed: %v", err) 222 | } 223 | t.Logf("Lifecycle: Cancelled Order %s, Status: %s", cancelRes.ID, cancelRes.Status) 224 | 225 | // 5. 查询历史订单确认状态 (可能需要稍微等待一点时间) 226 | // time.Sleep(time.Second) 227 | // historyArgs := map[string]interface{}{ 228 | // banexg.ParamAlgoOrder: true, 229 | // banexg.ParamLimit: 20, 230 | // } 231 | // historyOrders, err := exg.FetchOrders(symbol, 0, 20, historyArgs) 232 | // if err != nil { 233 | // t.Fatalf("Lifecycle: Fetch History Failed: %v", err) 234 | // } 235 | // foundHistory := false 236 | // for _, o := range historyOrders { 237 | // if o.ID == algoId { 238 | // foundHistory = true 239 | // t.Logf("Lifecycle: Order found in history with status: %s", o.Status) 240 | // break 241 | // } 242 | // } 243 | // if !foundHistory { 244 | // t.Logf("Lifecycle: Order %s not found in history (might be delay)", algoId) 245 | // } 246 | } 247 | -------------------------------------------------------------------------------- /data.go: -------------------------------------------------------------------------------- 1 | package banexg 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/banbox/banexg/utils" 7 | "github.com/sasha-s/go-deadlock" 8 | ) 9 | 10 | const ( 11 | ParamClientOrderId = "clientOrderId" 12 | ParamOrderIds = "orderIdList" 13 | ParamOrigClientOrderIDs = "origClientOrderIdList" 14 | ParamSor = "sor" // smart order route, for create order in spot 15 | ParamPostOnly = "postOnly" 16 | ParamTimeInForce = "timeInForce" 17 | ParamTriggerPrice = "triggerPrice" 18 | ParamStopLossPrice = "stopLossPrice" 19 | ParamTakeProfitPrice = "takeProfitPrice" 20 | ParamTrailingDelta = "trailingDelta" 21 | ParamReduceOnly = "reduceOnly" 22 | ParamCost = "cost" 23 | ParamClosePosition = "closePosition" // 触发后全部平仓 24 | ParamActivationPrice = "activationPrice" 25 | ParamCallbackRate = "callbackRate" // 跟踪止损回调百分比 26 | ParamAlgoOrder = "algoOrder" 27 | ParamWorkingType = "workingType" 28 | ParamPriceMatch = "priceMatch" 29 | ParamPriceProtect = "priceProtect" 30 | ParamSelfTradePreventionMode = "selfTradePreventionMode" 31 | ParamGoodTillDate = "goodTillDate" 32 | ParamRolling = "rolling" 33 | ParamTest = "test" 34 | ParamMarginMode = "marginMode" 35 | ParamSymbol = "symbol" 36 | ParamSymbols = "symbols" 37 | ParamPositionSide = "positionSide" 38 | ParamProxy = "proxy" 39 | ParamName = "name" 40 | ParamMethod = "method" 41 | ParamInterval = "interval" 42 | ParamAccount = "account" 43 | ParamMarket = "market" 44 | ParamContract = "contract" 45 | ParamBrokerId = "brokerId" 46 | ParamLimit = "limit" 47 | ParamUntil = "until" 48 | ParamRetry = "retry" 49 | ParamLoopIntv = "loopIntv" 50 | ParamDirection = "direction" 51 | ParamDebug = "debug" 52 | ParamNoCache = "noCache" 53 | ) 54 | 55 | var ( 56 | DefReqHeaders = map[string]string{ 57 | "User-Agent": "Go-http-client/1.1", 58 | "Connection": "keep-alive", 59 | "Accept": "application/json", 60 | } 61 | DefCurrCodeMap = map[string]string{ 62 | "XBT": "BTC", 63 | "BCC": "BCH", 64 | "BCHSV": "BSV", 65 | } 66 | DefWsIntvs = map[string]int{ 67 | "WatchOrderBooks": 100, 68 | } 69 | DefRetries = map[string]int{ 70 | "FetchOrderBook": 1, 71 | "FetchPositionsRisk": 1, 72 | } 73 | HostRetryWaits = map[string]int64{} 74 | hostWaitLock deadlock.Mutex 75 | hostFlowChans = make(map[string]chan struct{}) 76 | hostFlowLock deadlock.Mutex 77 | HostHttpConcurr = 3 // Maximum concurrent number of HTTP requests per domain name 每个域名发起http请求最大并发数 78 | ) 79 | 80 | const ( 81 | DefTimeInForce = TimeInForceGTC 82 | ) 83 | 84 | const ( 85 | HasFail = 1 << iota 86 | HasOk 87 | HasEmulated 88 | ) 89 | 90 | const ( 91 | BoolNull = 0 92 | BoolFalse = -1 93 | BoolTrue = 1 94 | ) 95 | 96 | const ( 97 | OptProxy = "Proxy" 98 | OptApiKey = "ApiKey" 99 | OptApiSecret = "ApiSecret" 100 | OptAccCreds = "Creds" 101 | OptAccName = "AccName" 102 | OptNoTrade = "NoTrade" 103 | OptUserAgent = "UserAgent" 104 | OptReqHeaders = "ReqHeaders" 105 | OptCareMarkets = "CareMarkets" 106 | OptMarketType = "MarketType" 107 | OptContractType = "ContractType" 108 | OptTimeInForce = "TimeInForce" 109 | OptWsIntvs = "WsIntvs" // ws 订阅间隔 110 | OptRetries = "Retries" 111 | OptWsConn = "WsConn" 112 | OptAuthRefreshSecs = "AuthRefreshSecs" 113 | OptPositionMethod = "PositionMethod" 114 | OptDebugWs = "DebugWs" 115 | OptDebugApi = "DebugApi" 116 | OptApiCaches = "ApiCaches" 117 | OptFees = "Fees" 118 | OptDumpPath = "DumpPath" 119 | OptDumpBatchSize = "DumpBatchSize" 120 | OptReplayPath = "ReplayPath" 121 | OptEnv = "Env" 122 | OptWsTimeout = "WsTimeout" 123 | ) 124 | 125 | const ( 126 | PrecModeDecimalPlace = utils.PrecModeDecimalPlace // 保留小数点后位数 127 | PrecModeSignifDigits = utils.PrecModeSignifDigits // 保留有效数字位数 128 | PrecModeTickSize = utils.PrecModeTickSize // 返回给定数字的整数倍 129 | ) 130 | 131 | const ( 132 | MarketSpot = "spot" // 现货交易 133 | MarketMargin = "margin" // 保证金杠杆现货交易 margin trade 134 | MarketLinear = "linear" 135 | MarketInverse = "inverse" 136 | MarketOption = "option" // 期权 for option contracts 137 | 138 | MarketSwap = "swap" // 永续合约 for perpetual swap futures that don't have a delivery date 139 | MarketFuture = "future" // 有交割日的期货 for expiring futures contracts that have a delivery/settlement date 140 | ) 141 | 142 | const ( 143 | MarginCross = "cross" 144 | MarginIsolated = "isolated" 145 | ) 146 | 147 | const ( 148 | OdStatusOpen = "open" 149 | OdStatusPartFilled = "part_filled" 150 | OdStatusFilled = "filled" 151 | OdStatusCanceled = "canceled" 152 | OdStatusCanceling = "canceling" 153 | OdStatusRejected = "rejected" 154 | OdStatusExpired = "expired" 155 | ) 156 | 157 | // 此处订单类型全部使用币安订单类型小写 158 | const ( 159 | OdTypeMarket = "market" 160 | OdTypeLimit = "limit" 161 | OdTypeLimitMaker = "limit_maker" 162 | OdTypeStop = "stop" 163 | OdTypeStopMarket = "stop_market" 164 | OdTypeStopLoss = "stop_loss" 165 | OdTypeStopLossLimit = "stop_loss_limit" 166 | OdTypeTakeProfit = "take_profit" 167 | OdTypeTakeProfitLimit = "take_profit_limit" 168 | OdTypeTakeProfitMarket = "take_profit_market" 169 | OdTypeTrailingStopMarket = "trailing_stop_market" 170 | ) 171 | 172 | const ( 173 | OdSideBuy = "buy" 174 | OdSideSell = "sell" 175 | ) 176 | 177 | const ( 178 | PosSideLong = "long" 179 | PosSideShort = "short" 180 | PosSideBoth = "both" 181 | ) 182 | 183 | const ( 184 | TimeInForceGTC = "GTC" // Good Till Cancel 一直有效,直到被成交或取消 185 | TimeInForceIOC = "IOC" // Immediate or Cancel 无法立即成交的部分取消 186 | TimeInForceFOK = "FOK" // Fill or Kill 无法全部立即成交就撤销 187 | TimeInForceGTX = "GTX" // Good Till Crossing 无法成为挂单方就取消 188 | TimeInForceGTD = "GTD" // Good Till Date 在特定时间前有效,到期自动取消 189 | TimeInForcePO = "PO" // Post Only 190 | ) 191 | 192 | const ( 193 | MidListenKey = "listenKey" 194 | ) 195 | 196 | const ( 197 | ApiFetchTicker = "FetchTicker" 198 | ApiFetchTickers = "FetchTickers" 199 | ApiFetchTickerPrice = "FetchTickerPrice" 200 | ApiLoadLeverageBrackets = "LoadLeverageBrackets" 201 | ApiFetchCurrencies = "FetchCurrencies" 202 | ApiGetLeverage = "GetLeverage" 203 | ApiFetchOHLCV = "FetchOHLCV" 204 | ApiFetchOrderBook = "FetchOrderBook" 205 | ApiFetchOrder = "FetchOrder" 206 | ApiFetchOrders = "FetchOrders" 207 | ApiFetchBalance = "FetchBalance" 208 | ApiFetchAccountPositions = "FetchAccountPositions" 209 | ApiFetchPositions = "FetchPositions" 210 | ApiFetchOpenOrders = "FetchOpenOrders" 211 | ApiCreateOrder = "CreateOrder" 212 | ApiEditOrder = "EditOrder" 213 | ApiCancelOrder = "CancelOrder" 214 | ApiSetLeverage = "SetLeverage" 215 | ApiCalcMaintMargin = "CalcMaintMargin" 216 | ApiWatchOrderBooks = "WatchOrderBooks" 217 | ApiUnWatchOrderBooks = "UnWatchOrderBooks" 218 | ApiWatchOHLCVs = "WatchOHLCVs" 219 | ApiUnWatchOHLCVs = "UnWatchOHLCVs" 220 | ApiWatchMarkPrices = "WatchMarkPrices" 221 | ApiUnWatchMarkPrices = "UnWatchMarkPrices" 222 | ApiWatchTrades = "WatchTrades" 223 | ApiUnWatchTrades = "UnWatchTrades" 224 | ApiWatchMyTrades = "WatchMyTrades" 225 | ApiWatchBalance = "WatchBalance" 226 | ApiWatchPositions = "WatchPositions" 227 | ApiWatchAccountConfig = "WatchAccountConfig" 228 | ) 229 | 230 | var ( 231 | AllMarketTypes = map[string]struct{}{ 232 | MarketSpot: {}, 233 | MarketMargin: {}, 234 | MarketLinear: {}, 235 | MarketInverse: {}, 236 | MarketOption: {}, 237 | } 238 | AllContractTypes = map[string]struct{}{ 239 | MarketSwap: {}, 240 | MarketFuture: {}, 241 | } 242 | ) 243 | 244 | var ( 245 | exgCacheMarkets = map[string]MarketMap{} // cache markets for exchanges before expired 246 | exgCacheCurrs = map[string]CurrencyMap{} // cache currencies for exchanges before expired 247 | exgCareMarkets = map[string][]string{} // what market types was cached for exchanges 248 | exgMarketTS = map[string]int64{} // when was markets cached 249 | exgMarketExpireMins = 360 // ttl minutes for markets cache 250 | marketsLock deadlock.RWMutex // 访问缓存的读写锁 251 | LocUTC, _ = time.LoadLocation("UTC") 252 | ) 253 | -------------------------------------------------------------------------------- /readme.cn.md: -------------------------------------------------------------------------------- 1 | [English](readme.md) 2 | 3 | # BanExg - 数字货币交易所SDK 4 | 一个Go版本的数字货币交易SDK 5 | 目前支持交易所:`binance`, `china`。都实现了接口`BanExchange` 6 | 7 | [![DeepWiki问答](https://deepwiki.com/badge.svg)](https://deepwiki.com/banbox/banexg) 8 | 9 | # 特性 10 | * 多账户支持,自由切换,互不冲突 11 | * 完善的Websocket支持,订阅和取消订阅 12 | * 支持Websocket转储和回放,可用于回测 13 | * ws断线自动重连并恢复订阅 14 | 15 | # 如何使用 16 | ```go 17 | var options = map[string]interface{}{} 18 | // more option keys can be found by type `banexg.Opt...` 19 | options[banexg.OptMarketType] = banexg.MarketLinear // usd based future market 20 | exchange, err := bex.New('binance', options) 21 | 22 | // 您也可以直接实例化某个交易所对象 23 | // exchange, err := binance.NewExchange(options) 24 | 25 | // exchange is a BanExchange interface object 26 | res, err := exg.FetchOHLCV("ETH/USDT:USDT", "1d", 0, 10, nil) 27 | if err != nil { 28 | panic(err) 29 | } 30 | for _, k := range res { 31 | fmt.Printf("%v, %v %v %v %v %v\n", k.Time, k.Open, k.High, k.Low, k.Close, int(k.Volume)) 32 | } 33 | ``` 34 | 35 | # 完整初始化选项 36 | ```go 37 | // 初始化交易所对象时可以传入以下参数 38 | var options = map[string]interface{}{ 39 | // 代理服务器地址 40 | banexg.OptProxy: "http://127.0.0.1:7890", 41 | 42 | // API密钥配置方式1:直接配置单个账户 43 | banexg.OptApiKey: "your-api-key", // API Key 44 | banexg.OptApiSecret: "your-secret", // API Secret 45 | 46 | // API密钥配置方式2:配置多个账户 47 | banexg.OptAccCreds: map[string]map[string]interface{}{ 48 | "account1": { 49 | "ApiKey": "key1", 50 | "ApiSecret": "secret1", 51 | }, 52 | "account2": { 53 | "ApiKey": "key2", 54 | "ApiSecret": "secret2", 55 | }, 56 | }, 57 | banexg.OptAccName: "account1", // 设置默认账户 58 | 59 | // HTTP请求相关 60 | banexg.OptUserAgent: "Mozilla/5.0", // 自定义User-Agent 61 | banexg.OptReqHeaders: map[string]string{ // 自定义请求头 62 | "X-Custom": "value", 63 | }, 64 | 65 | // 市场类型设置 66 | banexg.OptMarketType: banexg.MarketLinear, // 设置默认市场类型:现货/合约等 67 | banexg.OptContractType: banexg.MarketSwap, // 设置合约类型:永续/交割 68 | banexg.OptTimeInForce: banexg.TimeInForceGTC, // 订单有效期类型 69 | 70 | // WebSocket相关 71 | banexg.OptWsIntvs: map[string]int{ // WebSocket订阅间隔(毫秒) 72 | "WatchOrderBooks": 100, // 订阅订单簿的间隔 73 | }, 74 | 75 | // API重试设置 76 | banexg.OptRetries: map[string]int{ // API调用重试次数 77 | "FetchOrderBook": 3, // 获取订单簿时重试3次 78 | "FetchPositions": 2, // 获取持仓时重试2次 79 | }, 80 | 81 | // API缓存设置 82 | banexg.OptApiCaches: map[string]int{ // API结果缓存时间(秒) 83 | "FetchMarkets": 3600, // 市场信息缓存1小时 84 | }, 85 | 86 | // 手续费设置 87 | banexg.OptFees: map[string]map[string]float64{ 88 | "linear": { // U本位合约手续费 89 | "maker": 0.0002, // Maker费率 90 | "taker": 0.0004, // Taker费率 91 | }, 92 | "inverse": { // 币本位合约手续费 93 | "maker": 0.0001, 94 | "taker": 0.0005, 95 | }, 96 | }, 97 | 98 | // 调试选项 99 | banexg.OptDebugWS: true, // 打印WebSocket调试信息 100 | banexg.OptDebugAPI: true, // 打印API调试信息 101 | 102 | // 数据抓取、回放 103 | banexg.OptDumpPath: "./ws_dump", // WebSocket数据保存路径 104 | banexg.OptDumpBatchSize: 1000, // 每批次保存的消息数量 105 | banexg.OptReplayPath: "./ws_replay", // 回放数据路径 106 | } 107 | 108 | // 使用参数创建交易所实例 109 | exchange, err := bex.New("binance", options) 110 | if err != nil { 111 | panic(err) 112 | } 113 | ``` 114 | 115 | 以上参数都是可选的,根据实际需要传入。一些重要说明: 116 | 117 | 1. API密钥配置支持两种方式: 118 | - 直接通过OptApiKey和OptApiSecret配置单个账户 119 | - 通过OptAccCreds配置多个账户,并用OptAccName指定默认账户 120 | 121 | 2. 市场类型(OptMarketType)可选值: 122 | - MarketSpot: 现货 123 | - MarketMargin: 保证金 124 | - MarketLinear: U本位合约 125 | - MarketInverse: 币本位合约 126 | - MarketOption: 期权 127 | 128 | 3. 合约类型(OptContractType)可选值: 129 | - MarketSwap: 永续合约 130 | - MarketFuture: 交割合约 131 | 132 | 4. 订单有效期(OptTimeInForce)可选值: 133 | - TimeInForceGTC: 成交为止 134 | - TimeInForceIOC: 立即成交或取消 135 | - TimeInForceFOK: 全部成交或取消 136 | - TimeInForceGTX: 无法成为挂单方就取消 137 | - TimeInForceGTD: 指定时间前有效 138 | - TimeInForcePO: 只做挂单方 139 | 140 | 5. API缓存和重试次数可以针对不同接口单独设置 141 | 142 | 6. 手续费可以针对不同市场类型设置不同费率 143 | ``` 144 | 145 | # API列表 146 | ```go 147 | // 加载市场信息 148 | LoadMarkets(reload bool, params map[string]interface{}) (MarketMap, *errs.Error) 149 | GetCurMarkets() MarketMap 150 | GetMarket(symbol string) (*Market, *errs.Error) 151 | MapMarket(rawID string, year int) (*Market, *errs.Error) 152 | FetchTicker(symbol string, params map[string]interface{}) (*Ticker, *errs.Error) 153 | FetchTickers(symbols []string, params map[string]interface{}) ([]*Ticker, *errs.Error) 154 | FetchTickerPrice(symbol string, params map[string]interface{}) (map[string]float64, *errs.Error) 155 | LoadLeverageBrackets(reload bool, params map[string]interface{}) *errs.Error 156 | GetLeverage(symbol string, notional float64, account string) (float64, float64) 157 | CheckSymbols(symbols ...string) ([]string, []string) 158 | Info() *ExgInfo 159 | 160 | // 获取K线、订单簿、资金费率等 161 | FetchOHLCV(symbol, timeframe string, since int64, limit int, params map[string]interface{}) ([]*Kline, *errs.Error) 162 | FetchOrderBook(symbol string, limit int, params map[string]interface{}) (*OrderBook, *errs.Error) 163 | FetchLastPrices(symbols []string, params map[string]interface{}) ([]*LastPrice, *errs.Error) 164 | FetchFundingRate(symbol string, params map[string]interface{}) (*FundingRateCur, *errs.Error) 165 | FetchFundingRates(symbols []string, params map[string]interface{}) ([]*FundingRateCur, *errs.Error) 166 | FetchFundingRateHistory(symbol string, since int64, limit int, params map[string]interface{}) ([]*FundingRate, *errs.Error) 167 | 168 | // 鉴权:获取订单、余额、仓位 169 | FetchOrder(symbol, orderId string, params map[string]interface{}) (*Order, *errs.Error) 170 | FetchOrders(symbol string, since int64, limit int, params map[string]interface{}) ([]*Order, *errs.Error) 171 | FetchBalance(params map[string]interface{}) (*Balances, *errs.Error) 172 | FetchAccountPositions(symbols []string, params map[string]interface{}) ([]*Position, *errs.Error) 173 | FetchPositions(symbols []string, params map[string]interface{}) ([]*Position, *errs.Error) 174 | FetchOpenOrders(symbol string, since int64, limit int, params map[string]interface{}) ([]*Order, *errs.Error) 175 | FetchIncomeHistory(inType string, symbol string, since int64, limit int, params map[string]interface{}) ([]*Income, *errs.Error) 176 | // 鉴权:创建、修改、取消订单 177 | CreateOrder(symbol, odType, side string, amount, price float64, params map[string]interface{}) (*Order, *errs.Error) 178 | EditOrder(symbol, orderId, side string, amount, price float64, params map[string]interface{}) (*Order, *errs.Error) 179 | CancelOrder(id string, symbol string, params map[string]interface{}) (*Order, *errs.Error) 180 | // 设置、计算手续费;设置杠杆,计算维持保证金 181 | SetFees(fees map[string]map[string]float64) 182 | CalculateFee(symbol, odType, side string, amount float64, price float64, isMaker bool, params map[string]interface{}) (*Fee, *errs.Error) 183 | SetLeverage(leverage float64, symbol string, params map[string]interface{}) (map[string]interface{}, *errs.Error) 184 | CalcMaintMargin(symbol string, cost float64) (float64, *errs.Error) 185 | Call(method string, params map[string]interface{}) (*HttpRes, *errs.Error) 186 | 187 | // websocket相关:订阅订单簿、K线、标记价格、交易流、余额、仓位、账户配置 188 | WatchOrderBooks(symbols []string, limit int, params map[string]interface{}) (chan *OrderBook, *errs.Error) 189 | UnWatchOrderBooks(symbols []string, params map[string]interface{}) *errs.Error 190 | WatchOHLCVs(jobs [][2]string, params map[string]interface{}) (chan *PairTFKline, *errs.Error) 191 | UnWatchOHLCVs(jobs [][2]string, params map[string]interface{}) *errs.Error 192 | WatchMarkPrices(symbols []string, params map[string]interface{}) (chan map[string]float64, *errs.Error) 193 | UnWatchMarkPrices(symbols []string, params map[string]interface{}) *errs.Error 194 | WatchTrades(symbols []string, params map[string]interface{}) (chan *Trade, *errs.Error) 195 | UnWatchTrades(symbols []string, params map[string]interface{}) *errs.Error 196 | WatchMyTrades(params map[string]interface{}) (chan *MyTrade, *errs.Error) 197 | WatchBalance(params map[string]interface{}) (chan *Balances, *errs.Error) 198 | WatchPositions(params map[string]interface{}) (chan []*Position, *errs.Error) 199 | WatchAccountConfig(params map[string]interface{}) (chan *AccountConfig, *errs.Error) 200 | 201 | // websocket数据抓取、回放(用于回测) 202 | SetDump(path string) *errs.Error 203 | SetReplay(path string) *errs.Error 204 | GetReplayTo() int64 205 | ReplayOne() *errs.Error 206 | ReplayAll() *errs.Error 207 | SetOnWsChan(cb FuncOnWsChan) 208 | 209 | // 精度处理 210 | PrecAmount(m *Market, amount float64) (float64, *errs.Error) 211 | PrecPrice(m *Market, price float64) (float64, *errs.Error) 212 | PrecCost(m *Market, cost float64) (float64, *errs.Error) 213 | PrecFee(m *Market, fee float64) (float64, *errs.Error) 214 | 215 | // 其他 216 | HasApi(key, market string) bool 217 | SetOnHost(cb func(n string) string) 218 | PriceOnePip(symbol string) (float64, *errs.Error) 219 | IsContract(marketType string) bool 220 | MilliSeconds() int64 221 | 222 | GetAccount(id string) (*Account, *errs.Error) 223 | SetMarketType(marketType, contractType string) *errs.Error 224 | GetExg() *Exchange 225 | Close() *errs.Error 226 | ``` 227 | 228 | # 注意 229 | ### 使用`Options`而不是直接字段赋值来初始化交易所对象 230 | 交易所对象被初始化时,一些如int的简单类型字段会有类型默认值,在`Init`方法进行设置时,无法区分当前值是用户设置的值还是默认值。 231 | 所以所有需要外部传入的配置应通过`Options`传入,然后在`Init`中读取`Options`并设置到对应字段上。 232 | 233 | ### 市场类型MarketType 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | 257 | 258 | 259 | 260 | 261 | 262 |
SpotMarginContract LinearContract InverseOption
Swap Linearfuture Linearswap Inversefuture Inverseoption linearoption inverse
Desc现货保证金U本位永续U本位到期币本位永续币本位到期U本位期权币本位期权
263 | 264 | ### 原始数据字典返回 265 | 可通过`Info`访问交易所返回的原始数据,类型是`map[string]interface{}`或`[]map[string]interface{}`。 266 | 267 | 包含的类型:Currency, ChainNetwork, Market, Ticker, Balances, Position, Order, Trade, MyTrade, FundingRate, FundingRateCur, LastPrice 268 | 269 | ### 常见参数命名调整 270 | **`MarketType`** 271 | 当前交易所的默认市场类型。可在初始化时传入`OptMarketType`设置,也可随时设置交易所的`MarketType`属性。 272 | 有效值:`MarketSpot/MarketMargin/MarketLinear/MarketInverse/MarketOption` 273 | 274 | **`ContractType`** 275 | 当前交易所合约类型,可选值`swap`永续合约,`future`有到期日的合约。 276 | 可在初始化时传入`OptContractType`设置,也可初始化后设置交易所的`ContractType`属性。 277 | 278 | ### 死锁检测 279 | 此项目默认使用了[go-deadlock](https://github.com/sasha-s/go-deadlock)库,用于检测死锁。 280 | 这可能会在高频调用一些方法时,将运行速度减慢十多倍,您可通过`deadlock.Opts.Disable = true`来禁用。 281 | 282 | # 联系我 283 | 邮箱:`anyongjin163@163.com` 284 | 微信:`phiilo_null` 285 | -------------------------------------------------------------------------------- /utils/dec_precs_test.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "github.com/shopspring/decimal" 5 | "testing" 6 | ) 7 | 8 | type DecPrecCase struct { 9 | text string 10 | precision string 11 | countMode int 12 | isRound bool 13 | padZero bool 14 | output string 15 | } 16 | 17 | func TestNumToPrecs(t *testing.T) { 18 | items := []DecPrecCase{ 19 | {"12.3456000", "100", PrecModeDecimalPlace, false, false, "12.3456"}, 20 | {"12.3456", "100", PrecModeDecimalPlace, false, false, "12.3456"}, 21 | {"12.3456", "4", PrecModeDecimalPlace, false, false, "12.3456"}, 22 | {"12.3456", "3", PrecModeDecimalPlace, false, false, "12.345"}, 23 | {"12.3456", "2", PrecModeDecimalPlace, false, false, "12.34"}, 24 | {"12.3456", "1", PrecModeDecimalPlace, false, false, "12.3"}, 25 | {"12.3456", "0", PrecModeDecimalPlace, false, false, "12"}, 26 | {"0.0000001", "8", PrecModeDecimalPlace, false, false, "0.0000001"}, 27 | {"0.00000001", "8", PrecModeDecimalPlace, false, false, "0.00000001"}, 28 | {"0.000000000", "9", PrecModeDecimalPlace, false, true, "0.000000000"}, 29 | {"0.000000001", "9", PrecModeDecimalPlace, false, true, "0.000000001"}, 30 | {"12.3456", "-1", PrecModeDecimalPlace, false, false, "10"}, 31 | {"123.456", "-1", PrecModeDecimalPlace, false, false, "120"}, 32 | {"123.456", "-2", PrecModeDecimalPlace, false, false, "100"}, 33 | {"9.99999", "-1", PrecModeDecimalPlace, false, false, "0"}, 34 | {"99.9999", "-1", PrecModeDecimalPlace, false, false, "90"}, 35 | {"99.9999", "-2", PrecModeDecimalPlace, false, false, "0"}, 36 | {"0", "0", PrecModeDecimalPlace, false, false, "0"}, 37 | {"-0.9", "0", PrecModeDecimalPlace, false, false, "0"}, 38 | {"0.000123456700", "100", PrecModeSignifDigits, false, false, "0.0001234567"}, 39 | {"0.0001234567", "100", PrecModeSignifDigits, false, false, "0.0001234567"}, 40 | {"0.0001234567", "7", PrecModeSignifDigits, false, false, "0.0001234567"}, 41 | {"0.000123456", "6", PrecModeSignifDigits, false, false, "0.000123456"}, 42 | {"0.000123456", "5", PrecModeSignifDigits, false, false, "0.00012345"}, 43 | {"0.000123456", "2", PrecModeSignifDigits, false, false, "0.00012"}, 44 | {"0.000123456", "1", PrecModeSignifDigits, false, false, "0.0001"}, 45 | {"123.0000987654", "10", PrecModeSignifDigits, false, true, "123.0000987"}, 46 | {"123.0000987654", "8", PrecModeSignifDigits, false, false, "123.00009"}, 47 | {"123.0000987654", "7", PrecModeSignifDigits, false, true, "123.0000"}, 48 | {"123.0000987654", "6", PrecModeSignifDigits, false, false, "123"}, 49 | {"123.0000987654", "5", PrecModeSignifDigits, false, true, "123.00"}, 50 | {"123.0000987654", "4", PrecModeSignifDigits, false, false, "123"}, 51 | {"123.0000987654", "4", PrecModeSignifDigits, false, true, "123.0"}, 52 | {"123.0000987654", "3", PrecModeSignifDigits, false, true, "123"}, 53 | {"123.0000987654", "2", PrecModeSignifDigits, false, false, "120"}, 54 | {"123.0000987654", "1", PrecModeSignifDigits, false, false, "100"}, 55 | {"123.0000987654", "1", PrecModeSignifDigits, false, true, "100"}, 56 | {"1234", "5", PrecModeSignifDigits, false, false, "1234"}, 57 | {"1234", "5", PrecModeSignifDigits, false, true, "1234.0"}, 58 | {"1234", "4", PrecModeSignifDigits, false, false, "1234"}, 59 | {"1234", "4", PrecModeSignifDigits, false, true, "1234"}, 60 | {"1234.69", "0", PrecModeSignifDigits, false, false, "0"}, 61 | {"1234.69", "0", PrecModeSignifDigits, false, true, "0"}, 62 | {"12.3456000", "100", PrecModeDecimalPlace, true, false, "12.3456"}, 63 | {"12.3456", "100", PrecModeDecimalPlace, true, false, "12.3456"}, 64 | {"12.3456", "4", PrecModeDecimalPlace, true, false, "12.3456"}, 65 | {"12.3456", "3", PrecModeDecimalPlace, true, false, "12.346"}, 66 | {"12.3456", "2", PrecModeDecimalPlace, true, false, "12.35"}, 67 | {"12.3456", "1", PrecModeDecimalPlace, true, false, "12.3"}, 68 | {"12.3456", "0", PrecModeDecimalPlace, true, false, "12"}, 69 | {"10000", "6", PrecModeDecimalPlace, true, false, "10000"}, 70 | {"0.00003186", "8", PrecModeDecimalPlace, true, false, "0.00003186"}, 71 | {"12.3456", "-1", PrecModeDecimalPlace, true, false, "10"}, 72 | {"123.456", "-1", PrecModeDecimalPlace, true, false, "120"}, 73 | {"123.456", "-2", PrecModeDecimalPlace, true, false, "100"}, 74 | {"9.99999", "-1", PrecModeDecimalPlace, true, false, "10"}, 75 | {"99.9999", "-1", PrecModeDecimalPlace, true, false, "100"}, 76 | {"99.9999", "-2", PrecModeDecimalPlace, true, false, "100"}, 77 | {"9.999", "3", PrecModeDecimalPlace, true, false, "9.999"}, 78 | {"9.999", "2", PrecModeDecimalPlace, true, false, "10"}, 79 | {"9.999", "2", PrecModeDecimalPlace, true, true, "10.00"}, 80 | {"99.999", "2", PrecModeDecimalPlace, true, true, "100.00"}, 81 | {"-99.999", "2", PrecModeDecimalPlace, true, true, "-100.00"}, 82 | {"0.000123456700", "100", PrecModeSignifDigits, true, false, "0.0001234567"}, 83 | {"0.0001234567", "100", PrecModeSignifDigits, true, false, "0.0001234567"}, 84 | {"0.0001234567", "7", PrecModeSignifDigits, true, false, "0.0001234567"}, 85 | {"0.000123456", "6", PrecModeSignifDigits, true, false, "0.000123456"}, 86 | {"0.000123456", "5", PrecModeSignifDigits, true, false, "0.00012346"}, 87 | {"0.000123456", "4", PrecModeSignifDigits, true, false, "0.0001235"}, 88 | {"0.00012", "2", PrecModeSignifDigits, true, false, "0.00012"}, 89 | {"0.0001", "1", PrecModeSignifDigits, true, false, "0.0001"}, 90 | {"123.0000987654", "7", PrecModeSignifDigits, true, false, "123.0001"}, 91 | {"123.0000987654", "6", PrecModeSignifDigits, true, false, "123"}, 92 | {"0.00098765", "2", PrecModeSignifDigits, true, false, "0.00099"}, 93 | {"0.00098765", "2", PrecModeSignifDigits, true, true, "0.00099"}, 94 | {"0.00098765", "1", PrecModeSignifDigits, true, false, "0.001"}, 95 | {"0.00098765", "10", PrecModeSignifDigits, true, true, "0.0009876500000"}, 96 | {"0.098765", "1", PrecModeSignifDigits, true, true, "0.1"}, 97 | {"0", "0", PrecModeSignifDigits, true, false, "0"}, 98 | {"-0.123", "0", PrecModeSignifDigits, true, false, "0"}, 99 | {"0.00000044", "5", PrecModeSignifDigits, true, false, "0.00000044"}, 100 | {"0.000123456700", "0.00012", PrecModeTickSize, true, false, "0.00012"}, 101 | {"0.0001234567", "0.00013", PrecModeTickSize, true, false, "0.00013"}, 102 | {"0.0001234567", "0.00013", PrecModeTickSize, false, false, "0"}, 103 | {"101.000123456700", "100", PrecModeTickSize, true, false, "100"}, 104 | {"0.000123456700", "100", PrecModeTickSize, true, false, "0"}, 105 | {"165", "110", PrecModeTickSize, false, false, "110"}, 106 | {"3210", "1110", PrecModeTickSize, false, false, "2220"}, 107 | {"165", "110", PrecModeTickSize, true, false, "220"}, 108 | {"0.000123456789", "0.00000012", PrecModeTickSize, true, false, "0.00012348"}, 109 | {"0.000123456789", "0.00000012", PrecModeTickSize, false, false, "0.00012336"}, 110 | {"0.000273398", "1e-7", PrecModeTickSize, true, false, "0.0002734"}, 111 | {"0.00005714", "0.00000001", PrecModeTickSize, false, false, "0.00005714"}, 112 | {"0.01", "0.0001", PrecModeTickSize, true, true, "0.0100"}, 113 | {"0.01", "0.0001", PrecModeTickSize, false, true, "0.0100"}, 114 | {"-0.000123456789", "0.00000012", PrecModeTickSize, true, false, "-0.00012348"}, 115 | {"-0.000123456789", "0.00000012", PrecModeTickSize, false, false, "-0.00012336"}, 116 | {"-165", "110", PrecModeTickSize, false, false, "-110"}, 117 | {"-165", "110", PrecModeTickSize, true, false, "-220"}, 118 | {"-1650", "1100", PrecModeTickSize, false, false, "-1100"}, 119 | {"-1650", "1100", PrecModeTickSize, true, false, "-2200"}, 120 | {"0.0006", "0.0001", PrecModeTickSize, false, false, "0.0006"}, 121 | {"-0.0006", "0.0001", PrecModeTickSize, false, false, "-0.0006"}, 122 | {"0.6", "0.2", PrecModeTickSize, false, false, "0.6"}, 123 | {"-0.6", "0.2", PrecModeTickSize, false, false, "-0.6"}, 124 | {"1.2", "0.4", PrecModeTickSize, true, false, "1.2"}, 125 | {"-1.2", "0.4", PrecModeTickSize, true, false, "-1.2"}, 126 | {"1.2", "0.02", PrecModeTickSize, true, false, "1.2"}, 127 | {"-1.2", "0.02", PrecModeTickSize, true, false, "-1.2"}, 128 | {"44", "4.4", PrecModeTickSize, true, false, "44"}, 129 | {"-44", "4.4", PrecModeTickSize, true, false, "-44"}, 130 | {"44.00000001", "4.4", PrecModeTickSize, true, false, "44"}, 131 | {"-44.00000001", "4.4", PrecModeTickSize, true, false, "-44"}, 132 | {"20", "0.00000001", PrecModeTickSize, false, false, "20"}, 133 | {"-0.123456", "5", PrecModeDecimalPlace, false, false, "-0.12345"}, 134 | {"-0.123456", "5", PrecModeDecimalPlace, true, false, "-0.12346"}, 135 | {"123", "0", PrecModeDecimalPlace, false, false, "123"}, 136 | {"123", "5", PrecModeDecimalPlace, false, false, "123"}, 137 | {"123", "5", PrecModeDecimalPlace, false, true, "123.00000"}, 138 | {"123.", "0", PrecModeDecimalPlace, false, false, "123"}, 139 | {"123.", "5", PrecModeDecimalPlace, false, true, "123.00000"}, 140 | {"0.", "0", PrecModeDecimalPlace, false, false, "0"}, 141 | {"0.", "5", PrecModeDecimalPlace, false, true, "0.00000"}, 142 | {"1.44", "1", PrecModeDecimalPlace, true, false, "1.4"}, 143 | {"1.45", "1", PrecModeDecimalPlace, true, false, "1.5"}, 144 | {"1.45", "0", PrecModeDecimalPlace, true, false, "1"}, 145 | {"5", "-1", PrecModeDecimalPlace, true, false, "10"}, 146 | {"4.999", "-1", PrecModeDecimalPlace, true, false, "0"}, 147 | {"0.0431531423", "-1", PrecModeDecimalPlace, true, false, "0"}, 148 | {"-69.3", "-1", PrecModeDecimalPlace, true, false, "-70"}, 149 | {"5001", "-4", PrecModeDecimalPlace, true, false, "10000"}, 150 | {"4999.999", "-4", PrecModeDecimalPlace, true, false, "0"}, 151 | {"69.3", "-2", PrecModeDecimalPlace, false, false, "0"}, 152 | {"-69.3", "-2", PrecModeDecimalPlace, false, false, "0"}, 153 | {"69.3", "-1", PrecModeSignifDigits, false, false, "60"}, 154 | {"-69.3", "-1", PrecModeSignifDigits, false, false, "-60"}, 155 | {"69.3", "-2", PrecModeSignifDigits, false, false, "0"}, 156 | {"1602000000000000000000", "3", PrecModeSignifDigits, false, false, "1600000000000000000000"}, 157 | {"-0.000123456789", "0.00000012", PrecModeTickSize, true, false, "-0.00012348"}, 158 | {"-0.000123456789", "0.00000012", PrecModeTickSize, false, false, "-0.00012336"}, 159 | {"-165", "110", PrecModeTickSize, false, false, "-110"}, 160 | {"-165", "110", PrecModeTickSize, true, false, "-220"}, 161 | } 162 | for _, it := range items { 163 | outText, err := DecToPrec(it.text, it.countMode, it.precision, it.isRound, it.padZero) 164 | if err != nil { 165 | panic(err) 166 | } 167 | if outText != it.output { 168 | t.Errorf("Fail %s %v %v %v %v out: %s exp: %s", it.text, it.precision, it.countMode, it.isRound, it.padZero, outText, it.output) 169 | } else { 170 | //t.Logf("Pass %s %v %v %v %v out: %s exp: %s", it.text, it.precision, it.countMode, it.isRound, it.padZero, outText, it.output) 171 | } 172 | } 173 | } 174 | 175 | func TestAdjust(t *testing.T) { 176 | cases := []struct { 177 | input string 178 | output int32 179 | }{ 180 | {"0.01", -2}, 181 | {"0.0123", -2}, 182 | {"0.1", -1}, 183 | {"0.003", -3}, 184 | {"3.125", 0}, 185 | {"30.125", 1}, 186 | {"39.125", 1}, 187 | {"3912348.125", 6}, 188 | {"-3912348.125", 6}, 189 | {"-0.0123", -2}, 190 | {"-3.125", 0}, 191 | {"-30.125", 1}, 192 | } 193 | for _, c := range cases { 194 | dec, _ := decimal.NewFromString(c.input) 195 | output := adjusted(dec) 196 | if output != c.output { 197 | t.Errorf("FAIL %s, out: %d exp: %d", c.input, output, c.output) 198 | } else { 199 | t.Logf("Pass %s, out: %d exp: %d", c.input, output, c.output) 200 | } 201 | } 202 | } 203 | -------------------------------------------------------------------------------- /binance/biz_order_create.go: -------------------------------------------------------------------------------- 1 | package binance 2 | 3 | import ( 4 | "context" 5 | "strings" 6 | 7 | "github.com/banbox/banexg" 8 | "github.com/banbox/banexg/errs" 9 | "github.com/banbox/banexg/utils" 10 | ) 11 | 12 | func isBnbOrderType(market *banexg.Market, odType string) bool { 13 | var allows []string 14 | allows = utils.GetMapVal(market.Info, "orderTypes", allows) 15 | return utils.ArrContains(allows, odType) 16 | } 17 | 18 | /* 19 | CreateOrder 提交订单到交易所 20 | 21 | :see: https://binance-docs.github.io/apidocs/spot/en/#new-order-trade 22 | 23 | :see: https://binance-docs.github.io/apidocs/spot/en/#test-new-order-trade 24 | :see: https://binance-docs.github.io/apidocs/futures/en/#new-order-trade 25 | :see: https://binance-docs.github.io/apidocs/delivery/en/#new-order-trade 26 | :see: https://binance-docs.github.io/apidocs/voptions/en/#new-order-trade 27 | :see: https://binance-docs.github.io/apidocs/spot/en/#new-order-using-sor-trade 28 | :see: https://binance-docs.github.io/apidocs/spot/en/#test-new-order-using-sor-trade 29 | :param str symbol: unified symbol of the market to create an order in 30 | :param str type: 'MARKET' or 'LIMIT' or 'STOP_LOSS' or 'STOP_LOSS_LIMIT' or 'TAKE_PROFIT' or 'TAKE_PROFIT_LIMIT' or 'STOP' 31 | :param str side: 'buy' or 'sell' 32 | :param float amount: how much of currency you want to trade in units of base currency 33 | :param float [price]: the price at which the order is to be fullfilled, in units of the quote currency, ignored in market orders 34 | :param dict [params]: extra parameters specific to the exchange API endpoint 35 | :param str [params.marginMode]: 'cross' or 'isolated', for spot margin trading 36 | :param boolean [params.sor]: *spot only* whether to use SOR(Smart Order Routing) or not, default is False 37 | :param boolean [params.test]: *spot only* whether to use the test endpoint or not, default is False 38 | :returns dict: an `order structure ` 39 | */ 40 | func (e *Binance) CreateOrder(symbol, odType, side string, amount float64, price float64, params map[string]interface{}) (*banexg.Order, *errs.Error) { 41 | args, market, err := e.LoadArgsMarket(symbol, params) 42 | if err != nil { 43 | return nil, err 44 | } 45 | marginMode := utils.PopMapVal(args, banexg.ParamMarginMode, "") 46 | sor := utils.PopMapVal(args, banexg.ParamSor, false) 47 | clientOrderId := utils.PopMapVal(args, banexg.ParamClientOrderId, "") 48 | postOnly := utils.PopMapVal(args, banexg.ParamPostOnly, false) 49 | timeInForce := utils.GetMapVal(args, banexg.ParamTimeInForce, "") 50 | if postOnly || timeInForce == banexg.TimeInForcePO || odType == banexg.OdTypeLimitMaker { 51 | if timeInForce == banexg.TimeInForceIOC || timeInForce == banexg.TimeInForceFOK { 52 | return nil, errs.NewMsg(errs.CodeParamInvalid, "postOnly orders cannot have timeInForce: %s", timeInForce) 53 | } else if odType == banexg.OdTypeMarket { 54 | return nil, errs.NewMsg(errs.CodeParamInvalid, "market orders cannot be postOnly") 55 | } 56 | postOnly = true 57 | } 58 | if market.Spot || market.Type == banexg.MarketMargin { 59 | if postOnly { 60 | odType = banexg.OdTypeLimitMaker 61 | } 62 | } else if odType == banexg.OdTypeLimitMaker { 63 | // 币安仅现货支持limit_maker 64 | odType = banexg.OdTypeLimit 65 | timeInForce = banexg.TimeInForceGTX 66 | } 67 | isMarket := odType == banexg.OdTypeMarket 68 | isLimit := odType == banexg.OdTypeLimit 69 | triggerPrice := utils.PopMapVal(args, banexg.ParamTriggerPrice, float64(0)) 70 | stopLossPrice := utils.PopMapVal(args, banexg.ParamStopLossPrice, float64(0)) 71 | if stopLossPrice == 0 { 72 | stopLossPrice = triggerPrice 73 | } 74 | takeProfitPrice := utils.PopMapVal(args, banexg.ParamTakeProfitPrice, float64(0)) 75 | trailingDelta := utils.PopMapVal(args, banexg.ParamTrailingDelta, 0) 76 | isStopLoss := stopLossPrice != float64(0) || trailingDelta != 0 77 | isTakeProfit := takeProfitPrice != float64(0) 78 | args["symbol"] = market.ID 79 | args["side"] = strings.ToUpper(side) 80 | if market.Type == banexg.MarketMargin || marginMode != "" { 81 | reduceOnly := utils.PopMapVal(args, banexg.ParamReduceOnly, false) 82 | if reduceOnly { 83 | args["sideEffectType"] = "AUTO_REPAY" 84 | } 85 | } 86 | stopPrice := float64(0) 87 | if isStopLoss { 88 | stopPrice = stopLossPrice 89 | if isMarket { 90 | odType = banexg.OdTypeStopLoss 91 | if market.Contract { 92 | odType = banexg.OdTypeStopMarket 93 | } 94 | } else if isLimit { 95 | odType = banexg.OdTypeStopLossLimit 96 | if market.Contract { 97 | odType = banexg.OdTypeStop 98 | } 99 | } 100 | } else if isTakeProfit { 101 | stopPrice = takeProfitPrice 102 | if isMarket { 103 | odType = banexg.OdTypeTakeProfit 104 | if market.Contract { 105 | odType = banexg.OdTypeTakeProfitMarket 106 | } 107 | } else if isLimit { 108 | odType = banexg.OdTypeTakeProfitLimit 109 | if market.Contract { 110 | odType = banexg.OdTypeTakeProfit 111 | } 112 | } 113 | } 114 | if marginMode == banexg.MarginIsolated { 115 | args["isIsolated"] = true 116 | } 117 | if clientOrderId == "" { 118 | brokerId := utils.GetMapVal(params, banexg.ParamBrokerId, "") 119 | clientOrderId = brokerId + utils.UUID(22) 120 | } 121 | args["newClientOrderId"] = clientOrderId 122 | odRspType := "RESULT" 123 | if market.Spot || market.Type == banexg.MarketMargin { 124 | if rspType, ok := e.newOrderRespType[odType]; ok { 125 | odRspType = rspType 126 | } 127 | } 128 | // 'ACK' for order id, 'RESULT' for full order or 'FULL' for order with fills 129 | args["newOrderRespType"] = odRspType 130 | exgOdType := strings.ToUpper(odType) 131 | if market.Option { 132 | if odType == banexg.OdTypeMarket { 133 | return nil, errs.NewMsg(errs.CodeParamInvalid, "market order is invalid for option") 134 | } 135 | } else if !isBnbOrderType(market, exgOdType) { 136 | return nil, errs.NewMsg(errs.CodeParamInvalid, "invalid order type %s for %s market", exgOdType, market.Type) 137 | } 138 | args["type"] = exgOdType 139 | timeInForceRequired, priceRequired, stopPriceRequired, quantityRequired := false, false, false, false 140 | /* 141 | # spot/margin 142 | # 143 | # LIMIT timeInForce, quantity, price 144 | # MARKET quantity or quoteOrderQty 145 | # STOP_LOSS quantity, stopPrice 146 | # STOP_LOSS_LIMIT timeInForce, quantity, price, stopPrice 147 | # TAKE_PROFIT quantity, stopPrice 148 | # TAKE_PROFIT_LIMIT timeInForce, quantity, price, stopPrice 149 | # LIMIT_MAKER quantity, price 150 | # 151 | # futures 152 | # 153 | # LIMIT timeInForce, quantity, price 154 | # MARKET quantity 155 | # STOP/TAKE_PROFIT quantity, price, stopPrice 156 | # STOP_MARKET stopPrice 157 | # TAKE_PROFIT_MARKET stopPrice 158 | # TRAILING_STOP_MARKET callbackRate 159 | */ 160 | if odType == banexg.OdTypeMarket { 161 | quantityRequired = true 162 | if market.Spot { 163 | cost := utils.PopMapVal(args, banexg.ParamCost, 0.0) 164 | if cost == 0 && price != 0 { 165 | cost = amount * price 166 | } 167 | if cost != 0 { 168 | precRes, err := e.PrecCost(market, cost) 169 | if err != nil { 170 | return nil, err 171 | } 172 | args["quoteOrderQty"] = precRes 173 | quantityRequired = false 174 | } 175 | } 176 | } else if odType == banexg.OdTypeLimit { 177 | priceRequired = true 178 | timeInForceRequired = true 179 | quantityRequired = true 180 | } else if odType == banexg.OdTypeStopLoss || odType == banexg.OdTypeTakeProfit { 181 | stopPriceRequired = true 182 | quantityRequired = true 183 | if market.Linear || market.Inverse { 184 | priceRequired = true 185 | } 186 | } else if odType == banexg.OdTypeStopLossLimit || odType == banexg.OdTypeTakeProfitLimit { 187 | quantityRequired = true 188 | stopPriceRequired = true 189 | priceRequired = true 190 | timeInForceRequired = true 191 | } else if odType == banexg.OdTypeLimitMaker { 192 | priceRequired = true 193 | quantityRequired = true 194 | } else if odType == banexg.OdTypeStop { 195 | quantityRequired = true 196 | stopPriceRequired = true 197 | priceRequired = true 198 | } else if odType == banexg.OdTypeStopMarket || odType == banexg.OdTypeTakeProfitMarket { 199 | closePosition := utils.GetMapVal(args, banexg.ParamClosePosition, false) 200 | if !closePosition { 201 | quantityRequired = true 202 | } 203 | stopPriceRequired = true 204 | } else if odType == banexg.OdTypeTrailingStopMarket { 205 | quantityRequired = true 206 | callBackRate := utils.GetMapVal(args, banexg.ParamCallbackRate, 0.0) 207 | if callBackRate == 0 { 208 | return nil, errs.NewMsg(errs.CodeParamRequired, "createOrder require callbackRate for %s order", odType) 209 | } 210 | args["callbackRate"] = callBackRate 211 | activationPrice := utils.GetMapVal(args, banexg.ParamActivationPrice, 0.0) 212 | if activationPrice > 0 { 213 | args["activationPrice"] = activationPrice 214 | } 215 | } 216 | if quantityRequired { 217 | amtStr, err := e.PrecAmount(market, amount) 218 | if err != nil { 219 | return nil, err 220 | } 221 | args["quantity"] = amtStr 222 | } 223 | if priceRequired { 224 | if price == 0 { 225 | return nil, errs.NewMsg(errs.CodeParamRequired, "createOrder require price for %s order", odType) 226 | } 227 | priceStr, err := e.PrecPrice(market, price) 228 | if err != nil { 229 | return nil, err 230 | } 231 | args["price"] = priceStr 232 | } 233 | if timeInForceRequired { 234 | if timeInForce == "" { 235 | timeInForce = e.TimeInForce 236 | } 237 | args["timeInForce"] = timeInForce 238 | } 239 | if stopPriceRequired { 240 | if market.Contract { 241 | if stopPrice == 0 { 242 | return nil, errs.NewMsg(errs.CodeParamRequired, "createOrder require stopPrice for %s order", odType) 243 | } 244 | } else if trailingDelta == 0 && stopPrice == 0 { 245 | return nil, errs.NewMsg(errs.CodeParamRequired, "createOrder require stopPrice/trailingDelta for %s order", odType) 246 | } 247 | if stopPrice != 0 { 248 | stopPriceStr, err := e.PrecPrice(market, stopPrice) 249 | if err != nil { 250 | return nil, err 251 | } 252 | args["stopPrice"] = stopPriceStr 253 | } 254 | } 255 | if timeInForce == banexg.TimeInForcePO { 256 | delete(args, banexg.ParamTimeInForce) 257 | } 258 | method := MethodPrivatePostOrder 259 | if sor { 260 | method = MethodPrivatePostSorOrder 261 | } else if market.Linear { 262 | method = MethodFapiPrivatePostOrder 263 | } else if market.Inverse { 264 | method = MethodDapiPrivatePostOrder 265 | } else if market.Type == banexg.MarketMargin || marginMode != "" { 266 | method = MethodSapiPostMarginOrder 267 | } else if market.Option { 268 | method = MethodEapiPrivatePostOrder 269 | } 270 | if market.Spot || market.Type == banexg.MarketMargin { 271 | test := utils.GetMapVal(args, banexg.ParamTest, false) 272 | if test { 273 | method += "Test" 274 | } 275 | } 276 | tryNum := utils.PopMapVal(params, banexg.ParamRetry, -1) 277 | if tryNum < 0 { 278 | tryNum = e.GetRetryNum("CreateOrder", 3) 279 | } 280 | 281 | if market.Linear { 282 | if odType == banexg.OdTypeStop || odType == banexg.OdTypeStopMarket || 283 | odType == banexg.OdTypeTakeProfit || odType == banexg.OdTypeTakeProfitMarket || 284 | odType == banexg.OdTypeTrailingStopMarket { 285 | return e.createAlgoOrder(market, args, tryNum) 286 | } 287 | } 288 | 289 | rsp := e.RequestApiRetry(context.Background(), method, args, tryNum) 290 | if rsp.Error != nil { 291 | return nil, rsp.Error 292 | } 293 | var mapSymbol = func(mid string) string { 294 | return market.Symbol 295 | } 296 | if method == MethodFapiPrivatePostOrder { 297 | return parseOrder[*FutureOrder](mapSymbol, rsp) 298 | } else if method == MethodDapiPrivatePostOrder { 299 | return parseOrder[*InverseOrder](mapSymbol, rsp) 300 | } else if method == MethodEapiPrivatePostOrder { 301 | return parseOrder[*OptionOrder](mapSymbol, rsp) 302 | } else { 303 | // spot margin sor 304 | return parseOrder[*SpotOrder](mapSymbol, rsp) 305 | } 306 | } 307 | --------------------------------------------------------------------------------