package okex import ( "bytes" "context" "crypto/hmac" "crypto/sha256" "encoding/base64" "encoding/json" "errors" "fmt" "io" "net/http" "net/url" "sort" "strconv" "strings" "sync" "time" "git.qtrade.icu/coin-quant/exchange" "git.qtrade.icu/coin-quant/exchange/okex/api/account" "git.qtrade.icu/coin-quant/exchange/okex/api/market" "git.qtrade.icu/coin-quant/exchange/okex/api/public" "git.qtrade.icu/coin-quant/exchange/okex/api/trade" "git.qtrade.icu/coin-quant/exchange/ws" . "git.qtrade.icu/coin-quant/trademodel" "github.com/gorilla/websocket" log "github.com/sirupsen/logrus" ) var ( background = context.Background() ApiAddr = "https://www.okx.com/" WSOkexPUbilc = "wss://wsaws.okx.com:8443/ws/v5/public" WSOkexPrivate = "wss://wsaws.okx.com:8443/ws/v5/private" TypeSPOT = "SPOT" //币币 TypeMARGIN = "MARGIN" // 币币杠杆 TypeSWAP = "SWAP" //永续合约 TypeFUTURES = "FUTURES" //交割合约 TypeOption = "OPTION" //期权 PosNetMode = "net_mode" PosLongShortMode = "long_short_mode" ) var _ exchange.Exchange = &OkxTrader{} func init() { exchange.RegisterExchange("okx", NewOkexExchange) } type OkxTrader struct { Name string tradeApi *trade.ClientWithResponses marketApi *market.ClientWithResponses publicApi *public.ClientWithResponses accountApi *account.ClientWithResponses tradeCb exchange.WatchFn positionCb exchange.WatchFn balanceCb exchange.WatchFn depthCb exchange.WatchFn tradeMarketCb exchange.WatchFn klineCb exchange.WatchFn closeCh chan bool cfg *OKEXConfig klineLimit int wsUser *ws.WSConn wsPublic *ws.WSConn ordersCache sync.Map stopOrdersCache sync.Map posMode string watchPublics []OPParam instType string timeout time.Duration symbols map[string]Symbol } func NewOkexExchange(cfg exchange.Config, cltName string) (e exchange.Exchange, err error) { b, err := NewOkxTrader(cfg, cltName) if err != nil { return } e = b return } func NewOkxTrader(cfg exchange.Config, cltName string) (b *OkxTrader, err error) { b = new(OkxTrader) b.Name = "okx" b.instType = "SWAP" if cltName == "" { cltName = "okx" } b.symbols = make(map[string]Symbol) b.klineLimit = 100 b.timeout = time.Second * 10 var okxCfg OKEXConfig err = cfg.UnmarshalKey(fmt.Sprintf("exchanges.%s", cltName), &okxCfg) if err != nil { return nil, err } b.cfg = &okxCfg // isDebug := cfg.GetBool(fmt.Sprintf("exchanges.%s.debug", cltName)) if b.cfg.TdMode == "" { b.cfg.TdMode = "isolated" } log.Infof("okex %s, tdMode: %s, isTest: %t", cltName, b.cfg.TdMode, b.cfg.IsTest) b.closeCh = make(chan bool) b.tradeApi, err = trade.NewClientWithResponses(ApiAddr) if err != nil { return } b.marketApi, err = market.NewClientWithResponses(ApiAddr) if err != nil { return } b.publicApi, err = public.NewClientWithResponses(ApiAddr) if err != nil { return } b.accountApi, err = account.NewClientWithResponses(ApiAddr) if err != nil { return } clientProxy := cfg.GetString("proxy") if clientProxy != "" { var proxyURL *url.URL proxyURL, err = url.Parse(clientProxy) if err != nil { return } clt := b.tradeApi.ClientInterface.(*trade.Client).Client.(*http.Client) *clt = http.Client{Transport: &http.Transport{Proxy: http.ProxyURL(proxyURL)}} clt = b.marketApi.ClientInterface.(*market.Client).Client.(*http.Client) *clt = http.Client{Transport: &http.Transport{Proxy: http.ProxyURL(proxyURL)}} clt = b.publicApi.ClientInterface.(*public.Client).Client.(*http.Client) *clt = http.Client{Transport: &http.Transport{Proxy: http.ProxyURL(proxyURL)}} clt = b.accountApi.ClientInterface.(*account.Client).Client.(*http.Client) *clt = http.Client{Transport: &http.Transport{Proxy: http.ProxyURL(proxyURL)}} websocket.DefaultDialer.Proxy = http.ProxyURL(proxyURL) websocket.DefaultDialer.HandshakeTimeout = time.Second * 60 } _, err = b.Symbols() if err != nil { return nil, err } if b.cfg.ApiKey != "" { err = b.getAccountConfig() } return } func (b *OkxTrader) Info() exchange.ExchangeInfo { info := exchange.ExchangeInfo{ Name: "okx", Value: "okx", Desc: "okx api", KLineLimit: exchange.FetchLimit{ Limit: b.klineLimit, }, } return info } func (b *OkxTrader) SetInstType(instType string) { b.instType = instType } func (b *OkxTrader) auth(ctx context.Context, req *http.Request) (err error) { var temp []byte if req.Method != "GET" { temp, err = io.ReadAll(req.Body) if err != nil { return } req.Body.Close() buf := bytes.NewBuffer(temp) req.Body = io.NopCloser(buf) } else { if req.URL.RawQuery != "" { temp = []byte(fmt.Sprintf("?%s", req.URL.RawQuery)) } } var signStr string tmStr := time.Now().UTC().Format("2006-01-02T15:04:05.000Z") signStr = fmt.Sprintf("%s%s%s%s", tmStr, req.Method, req.URL.Path, string(temp)) h := hmac.New(sha256.New, []byte(b.cfg.SecretKey)) h.Write([]byte(signStr)) ret := h.Sum(nil) n := base64.StdEncoding.EncodedLen(len(ret)) dst := make([]byte, n) base64.StdEncoding.Encode(dst, ret) sign := string(dst) req.Header.Set("Content-Type", "application/json") req.Header.Set("OK-ACCESS-KEY", b.cfg.ApiKey) req.Header.Set("OK-ACCESS-SIGN", sign) req.Header.Set("OK-ACCESS-TIMESTAMP", tmStr) req.Header.Set("OK-ACCESS-PASSPHRASE", b.cfg.Passphrase) return } func (b *OkxTrader) Start() (err error) { fmt.Println("start okx") err = b.runPublic() if err != nil { return } err = b.runPrivate() if err != nil { return } fmt.Println("start okx finished") return } func (b *OkxTrader) Stop() (err error) { b.wsPublic.Close() b.wsUser.Close() close(b.closeCh) return } func (b *OkxTrader) customReq(ctx context.Context, req *http.Request) error { if b.cfg.IsTest { req.Header.Set("x-simulated-trading", "1") } return nil } func (b *OkxTrader) getAccountConfig() (err error) { ctx, cancel := context.WithTimeout(background, b.timeout) defer cancel() resp, err := b.accountApi.GetApiV5AccountConfigWithResponse(ctx, b.auth, b.customReq) if err != nil { return err } var accountCfg AccountConfig err = json.Unmarshal(resp.Body, &accountCfg) if err != nil { return err } if accountCfg.Code != "0" { err = fmt.Errorf("[%s]%s", accountCfg.Code, accountCfg.Msg) return } // long_short_mode:双向持仓 net_mode:单向持仓 // 仅适用交割/永续 b.posMode = accountCfg.Data[0].PosMode log.Infof("posMode: %s", b.posMode) if b.posMode != PosNetMode { log.Warnf("account posmode is %s, stop order will failed if no position", b.posMode) } return nil } // KlineChan get klines func (b *OkxTrader) GetKline(symbol, bSize string, start, end time.Time) (data []*Candle, err error) { // fmt.Println("GetKline:", symbol, bSize, start, end) nStart := start.Unix() * 1000 nEnd := end.UnixMilli() tempEnd := nEnd var resp *market.GetApiV5MarketHistoryCandlesResponse var startStr, endStr string ctx, cancel := context.WithTimeout(background, time.Second*3) startStr = strconv.FormatInt(nStart, 10) tempEnd = nStart + 100*60*1000 if tempEnd > nEnd { tempEnd = nEnd } endStr = strconv.FormatInt(tempEnd, 10) var params = market.GetApiV5MarketHistoryCandlesParams{InstId: symbol, Bar: &bSize, Before: &startStr, After: &endStr} resp, err = b.marketApi.GetApiV5MarketHistoryCandlesWithResponse(ctx, ¶ms, b.customReq) cancel() if err != nil { return } data, err = parseCandles(resp) if err != nil { if strings.Contains(err.Error(), "Requests too frequent.") { err = fmt.Errorf("requests too frequent %w", exchange.ErrRetry) } return } sort.Slice(data, func(i, j int) bool { return data[i].Start < data[j].Start }) return } func (b *OkxTrader) Watch(param exchange.WatchParam, fn exchange.WatchFn) (err error) { symbol := param.Param["symbol"] log.Info("okex watch:", param) switch param.Type { case exchange.WatchTypeCandle: var p = OPParam{ OP: "subscribe", Args: []interface{}{ OPArg{Channel: "candle1m", InstType: b.instType, InstID: symbol}, }, } b.klineCb = fn b.watchPublics = append(b.watchPublics, p) err = b.wsPublic.WriteMsg(p) case exchange.WatchTypeDepth: var p = OPParam{ OP: "subscribe", Args: []interface{}{ OPArg{Channel: "books5", InstType: b.instType, InstID: symbol}, }, } b.depthCb = fn b.watchPublics = append(b.watchPublics, p) err = b.wsPublic.WriteMsg(p) case exchange.WatchTypeTradeMarket: var p = OPParam{ OP: "subscribe", Args: []interface{}{ OPArg{Channel: "trades", InstType: b.instType, InstID: symbol}, }, } b.tradeMarketCb = fn b.watchPublics = append(b.watchPublics, p) err = b.wsPublic.WriteMsg(p) case exchange.WatchTypeTrade: b.tradeCb = fn case exchange.WatchTypePosition: b.positionCb = fn err = b.fetchBalanceAndPosition() case exchange.WatchTypeBalance: b.balanceCb = fn err = b.fetchBalanceAndPosition() default: err = fmt.Errorf("unknown wath param: %s", param.Type) } return } func (b *OkxTrader) fetchBalanceAndPosition() (err error) { err = b.fetchBalance() if err != nil { return err } err = b.fetchPosition() return } func (b *OkxTrader) fetchPosition() (err error) { ctx, cancel := context.WithTimeout(background, b.timeout) defer cancel() var params = account.GetApiV5AccountPositionsParams{InstType: &b.instType} resp, err := b.accountApi.GetApiV5AccountPositionsWithResponse(ctx, ¶ms, b.auth, b.customReq) if err != nil { return err } if b.positionCb == nil { return } var data AccountPositionResp err = json.Unmarshal(resp.Body, &data) if err != nil { return err } for _, v := range data.Data { var pos Position pos.Hold = parseFloat(v.Pos) pos.Price = parseFloat(v.AvgPx) pos.Symbol = v.InstID if pos.Hold > 0 { pos.Type = Long } else { pos.Type = Short } pos.ProfitRatio = parseFloat(v.UplRatio) if pos.Hold == 0 { log.Warnf("fetch position return 0: %s", string(resp.Body)) continue } b.positionCb(&pos) } return } func (b *OkxTrader) fetchBalance() (err error) { ctx, cancel := context.WithTimeout(background, b.timeout) defer cancel() var ccy = "USDT" var param = account.GetApiV5AccountBalanceParams{Ccy: &ccy} resp, err := b.accountApi.GetApiV5AccountBalanceWithResponse(ctx, ¶m, b.auth, b.customReq) if err != nil { return err } if b.balanceCb == nil { return } var balance AccountBalanceResp err = json.Unmarshal(resp.Body, &balance) if err != nil { return err } for _, v := range balance.Data { for _, d := range v.Details { if d.Ccy == ccy { var bal Balance bal.Available = parseFloat(d.AvailBal) bal.Balance = parseFloat(d.CashBal) bal.Currency = ccy bal.Frozen = parseFloat(d.OrdFrozen) b.balanceCb(&bal) break } } } return } func (b *OkxTrader) processStopOrder(act TradeAction) (ret *Order, err error) { ctx, cancel := context.WithTimeout(background, b.timeout) defer cancel() var side, posSide string // open: side = posSide, close: side!=posSide if act.Action.IsLong() { side = "buy" posSide = "short" } else { side = "sell" posSide = "long" } reduceOnly := true var orderPx = "-1" triggerPx := fmt.Sprintf("%f", act.Price) // PostApiV5TradeOrderAlgoJSONBody defines parameters for PostApiV5TradeOrderAlgo. params := trade.PostApiV5TradeOrderAlgoJSONBody{ // 非必填
保证金币种,如:USDT
仅适用于单币种保证金模式下的全仓杠杆订单 // Ccy *string `json:"ccy,omitempty"` // 必填
产品ID,如:`BTC-USDT` InstId: act.Symbol, // 必填
订单类型。
`conditional`:单向止盈止损
`oco`:双向止盈止损
`trigger`:计划委托
`iceberg`:冰山委托
`twap`:时间加权委托 OrdType: "conditional", // 非必填
委托价格
委托价格为-1时,执行市价委托
适用于`计划委托` OrderPx: &orderPx, // 可选
持仓方向
在双向持仓模式下必填,且仅可选择 `long` 或 `short` PosSide: &posSide, // 非必填
挂单限制价
适用于`冰山委托`和`时间加权委托` // PxLimit *string `json:"pxLimit,omitempty"` // 非必填
距离盘口的比例价距
适用于`冰山委托`和`时间加权委托` // PxSpread *string `json:"pxSpread,omitempty"` // 非必填
距离盘口的比例
pxVar和pxSpread只能传入一个
适用于`冰山委托`和`时间加权委托` // PxVar *string `json:"pxVar,omitempty"` // 非必填
是否只减仓,`true` 或 `false`,默认`false` // 仅适用于币币杠杆,以及买卖模式下的交割/永续 ReduceOnly: &reduceOnly, // 必填
订单方向。买:`buy` 卖:`sell` Side: side, // 非必填
止损委托价,如果填写此参数,必须填写止损触发价
委托价格为-1时,执行市价止损
适用于`止盈止损委托` SlOrdPx: &orderPx, // 非必填
止损触发价,如果填写此参数,必须填写止损委托价
适用于`止盈止损委托` SlTriggerPx: &triggerPx, // 必填
委托数量 Sz: fmt.Sprintf("%d", int(act.Amount)), // 非必填
单笔数量
适用于`冰山委托`和`时间加权委托` // SzLimit *string `json:"szLimit,omitempty"` // 必填
交易模式
保证金模式:`isolated`:逐仓 ;`cross`
全仓非保证金模式:`cash`:非保证金 TdMode: b.cfg.TdMode, // 非必填
市价单委托数量的类型
交易货币:`base_ccy`
计价货币:`quote_ccy`
仅适用于币币订单 // TgtCcy *string `json:"tgtCcy,omitempty"` // 非必填
挂单限制价
适用于`时间加权委托` // TimeInterval *string `json:"timeInterval,omitempty"` // 非必填
止盈委托价,如果填写此参数,必须填写止盈触发价
委托价格为-1时,执行市价止盈
适用于`止盈止损委托` // TpOrdPx , // 非必填
止盈触发价,如果填写此参数,必须填写止盈委托价
适用于`止盈止损委托` // TpTriggerPx *string `json:"tpTriggerPx,omitempty"` // 非必填
计划委托触发价格
适用于`计划委托` // TriggerPx *string `json:"triggerPx,omitempty"` } if b.posMode == PosNetMode { params.PosSide = nil } resp, err := b.tradeApi.PostApiV5TradeOrderAlgoWithResponse(ctx, params, b.auth, b.customReq) if err != nil { return } orders, err := parsePostAlgoOrders(act.Symbol, "open", side, act.Price, act.Amount, resp.Body) if err != nil { return } if len(orders) != 1 { err = fmt.Errorf("orders len not match: %#v", orders) log.Warnf(err.Error()) return } ret = orders[0] ret.Remark = "stop" return } func (b *OkxTrader) CancelOrder(old *Order) (order *Order, err error) { _, ok := b.ordersCache.Load(old.OrderID) if ok { order, err = b.cancelNormalOrder(old) if err != nil { return } b.ordersCache.Delete(old.OrderID) } _, ok = b.stopOrdersCache.Load(old.OrderID) if ok { order, err = b.cancelAlgoOrder(old) b.stopOrdersCache.Delete(old.OrderID) } return } func (b *OkxTrader) cancelNormalOrder(old *Order) (order *Order, err error) { ctx, cancel := context.WithTimeout(background, b.timeout) defer cancel() var body trade.PostApiV5TradeCancelOrderJSONRequestBody body.InstId = old.Symbol body.OrdId = &old.OrderID cancelResp, err := b.tradeApi.PostApiV5TradeCancelOrderWithResponse(ctx, body, b.auth, b.customReq) if err != nil { return } temp := OKEXOrder{} err = json.Unmarshal(cancelResp.Body, &temp) if err != nil { return } if temp.Code != "0" { err = errors.New(string(cancelResp.Body)) } order = old if len(temp.Data) > 0 { order.OrderID = temp.Data[0].OrdID } return } func (b *OkxTrader) cancelAlgoOrder(old *Order) (order *Order, err error) { ctx, cancel := context.WithTimeout(background, time.Second*2) defer cancel() var body = make(trade.PostApiV5TradeCancelAlgosJSONBody, 1) body[0] = trade.CancelAlgoOrder{AlgoId: old.OrderID, InstId: old.Symbol} cancelResp, err := b.tradeApi.PostApiV5TradeCancelAlgosWithResponse(ctx, body, b.auth, b.customReq) if err != nil { return } temp := OKEXAlgoOrder{} err = json.Unmarshal(cancelResp.Body, &temp) if err != nil { return } if temp.Code != "0" { err = errors.New(string(cancelResp.Body)) } order = old if len(temp.Data) > 0 { order.OrderID = temp.Data[0].AlgoID } return } func (b *OkxTrader) ProcessOrder(act TradeAction) (ret *Order, err error) { symbol, ok := b.symbols[act.Symbol] if ok { price := symbol.FixPrice(act.Price) if price != act.Price { log.Infof("okx change order price form %f to %f", act.Price, price) act.Price = price } } if act.Action.IsStop() { // if no position: // stopOrder will fail when posMode = long_short_mode // stopOrder will success when posMode =net_mode ret, err = b.processStopOrder(act) if err != nil { return } b.stopOrdersCache.Store(ret.OrderID, ret) return } ctx, cancel := context.WithTimeout(background, b.timeout) defer cancel() var side, posSide, px string if act.Action.IsLong() { side = "buy" if act.Action.IsOpen() { posSide = "long" } else { posSide = "short" } } else { side = "sell" if act.Action.IsOpen() { posSide = "short" } else { posSide = "long" } } ordType := "limit" tag := "trade" px = fmt.Sprintf("%f", act.Price) params := trade.PostApiV5TradeOrderJSONRequestBody{ //ClOrdId *string `json:"clOrdId,omitempty"` // 必填
产品ID,如:`BTC-USDT` InstId: act.Symbol, // 必填
订单类型。
市价单:`market`
限价单:`limit`
只做maker单:`post_only`
全部成交或立即取消:`fok`
立即成交并取消剩余:`ioc`
市价委托立即成交并取消剩余:`optimal_limit_ioc`(仅适用交割、永续) OrdType: ordType, // 可选
持仓方向
在双向持仓模式下必填,且仅可选择 `long` 或 `short` PosSide: &posSide, // 可选
委托价格
仅适用于`limit`、`post_only`、`fok`、`ioc`类型的订单 Px: &px, // 非必填
是否只减仓,`true` 或 `false`,默认`false`
仅适用于币币杠杆订单 // ReduceOnly: &reduceOnly, // 必填
订单方向。买:`buy` 卖:`sell` Side: side, // 必填
委托数量 Sz: fmt.Sprintf("%d", int(act.Amount)), // 非必填
订单标签
字母(区分大小写)与数字的组合,可以是纯字母、纯数字,且长度在1-8位之间。 Tag: &tag, // 必填
交易模式
保证金模式:`isolated`:逐仓 ;`cross`
全仓非保证金模式:`cash`:非保证金 TdMode: b.cfg.TdMode, // 非必填
市价单委托数量的类型
交易货币:`base_ccy`
计价货币:`quote_ccy`
仅适用于币币订单 // TgtCcy *string `json:"tgtCcy,omitempty"` } if b.posMode == PosNetMode { params.PosSide = nil } resp, err := b.tradeApi.PostApiV5TradeOrderWithResponse(ctx, params, b.auth, b.customReq) if err != nil { return } orders, err := parsePostOrders(act.Symbol, "open", side, act.Price, act.Amount, resp.Body) if err != nil { return } if len(orders) != 1 { err = fmt.Errorf("orders len not match: %#v", orders) log.Warnf(err.Error()) return } ret = orders[0] b.ordersCache.Store(ret.OrderID, ret) return } func (b *OkxTrader) cancelAllNormal() (orders []*Order, err error) { ctx, cancel := context.WithTimeout(background, b.timeout) defer cancel() instType := b.instType var params = trade.GetApiV5TradeOrdersPendingParams{ // InstId: &b.symbol, InstType: &instType, } resp, err := b.tradeApi.GetApiV5TradeOrdersPendingWithResponse(ctx, ¶ms, b.auth, b.customReq) if err != nil { return } var orderResp CancelNormalResp err = json.Unmarshal(resp.Body, &orderResp) if err != nil { return } if orderResp.Code != "0" { err = errors.New(string(resp.Body)) return } if len(orderResp.Data) == 0 { return } var body trade.PostApiV5TradeCancelBatchOrdersJSONRequestBody for _, v := range orderResp.Data { temp := v.OrdID body = append(body, trade.CancelBatchOrder{ InstId: v.InstID, OrdId: &temp, }) } cancelResp, err := b.tradeApi.PostApiV5TradeCancelBatchOrdersWithResponse(ctx, body, b.auth, b.customReq) if err != nil { return } temp := OKEXOrder{} err = json.Unmarshal(cancelResp.Body, &temp) if err != nil { return } if temp.Code != "0" { err = errors.New(string(cancelResp.Body)) } return } func (b *OkxTrader) cancelAllAlgo() (orders []*Order, err error) { ctx, cancel := context.WithTimeout(background, b.timeout) defer cancel() instType := b.instType var params = trade.GetApiV5TradeOrdersAlgoPendingParams{ OrdType: "conditional", // InstId: &b.symbol, InstType: &instType, } resp, err := b.tradeApi.GetApiV5TradeOrdersAlgoPendingWithResponse(ctx, ¶ms, b.auth, b.customReq) if err != nil { return } var orderResp CancelAlgoResp err = json.Unmarshal(resp.Body, &orderResp) if err != nil { return } if orderResp.Code != "0" { err = errors.New(string(resp.Body)) return } if len(orderResp.Data) == 0 { return } var body trade.PostApiV5TradeCancelAlgosJSONRequestBody for _, v := range orderResp.Data { body = append(body, trade.CancelAlgoOrder{ InstId: v.InstID, AlgoId: v.AlgoID, }) } cancelResp, err := b.tradeApi.PostApiV5TradeCancelAlgosWithResponse(ctx, body, b.auth, b.customReq) if err != nil { return } temp := OKEXAlgoOrder{} err = json.Unmarshal(cancelResp.Body, &temp) if err != nil { return } if temp.Code != "0" { err = errors.New(string(cancelResp.Body)) } return } func (b *OkxTrader) CancelAllOrders() (orders []*Order, err error) { temp, err := b.cancelAllNormal() if err != nil { return } orders, err = b.cancelAllAlgo() if err != nil { return } orders = append(temp, orders...) return } func (b *OkxTrader) Symbols() (symbols []Symbol, err error) { ctx, cancel := context.WithTimeout(background, b.timeout) defer cancel() resp, err := b.publicApi.GetApiV5PublicInstrumentsWithResponse(ctx, &public.GetApiV5PublicInstrumentsParams{InstType: b.instType}, b.customReq) if err != nil { return } var instruments InstrumentResp err = json.Unmarshal(resp.Body, &instruments) if instruments.Code != "0" { err = errors.New(string(resp.Body)) return } var value, amountPrecision float64 symbols = make([]Symbol, len(instruments.Data)) for i, v := range instruments.Data { value, err = strconv.ParseFloat(v.TickSz, 64) if err != nil { return } amountPrecision, err = strconv.ParseFloat(v.LotSz, 64) if err != nil { return } symbolInfo := Symbol{ Name: v.InstID, Exchange: "okx", Symbol: v.InstID, Resolutions: "1m,5m,15m,30m,1h,4h,1d,1w", Precision: int(float64(1) / value), AmountPrecision: int(float64(1) / amountPrecision), PriceStep: value, AmountStep: 0, } value, err = strconv.ParseFloat(v.MinSz, 64) if err != nil { return } symbolInfo.AmountStep = value symbols[i] = symbolInfo } if len(symbols) > 0 { symbolMap := make(map[string]Symbol) for _, v := range symbols { symbolMap[v.Symbol] = v } b.symbols = symbolMap } return }