first commit

This commit is contained in:
lychiyu 2024-06-26 00:59:56 +08:00
commit b9699ebe72
29 changed files with 21457 additions and 0 deletions

28
binance/binance.go Normal file
View File

@ -0,0 +1,28 @@
package binance
import (
"fmt"
"git.qtrade.icu/coin-quant/exchange"
"git.qtrade.icu/coin-quant/exchange/binance/common"
mfutures "git.qtrade.icu/coin-quant/exchange/binance/features"
)
func init() {
exchange.RegisterExchange("binance", NewBinance)
}
func NewBinance(cfg exchange.Config, cltName string) (e exchange.Exchange, err error) {
var eCfg common.BinanceConfig
err = cfg.UnmarshalKey(fmt.Sprintf("exchanges.%s", cltName), &eCfg)
if err != nil {
return
}
clientProxy := cfg.GetString("proxy")
switch eCfg.Kind {
case "futures":
e, err = mfutures.NewBinanceTrader(eCfg, cltName, clientProxy)
default:
err = fmt.Errorf("binance unsupport kind %s", &eCfg.Kind)
}
return
}

11
binance/common/config.go Normal file
View File

@ -0,0 +1,11 @@
package common
type BinanceConfig struct {
Name string
ApiKey string
SecretKey string
Passphrase string
IsTest bool
Kind string
Currency string
}

547
binance/features/binance.go Normal file
View File

@ -0,0 +1,547 @@
package features
import (
"context"
"fmt"
"git.qtrade.icu/coin-quant/exchange"
bcommon "git.qtrade.icu/coin-quant/exchange/binance/common"
. "git.qtrade.icu/coin-quant/trademodel"
gobinance "github.com/adshao/go-binance/v2"
bfutures "github.com/adshao/go-binance/v2/futures"
"github.com/gorilla/websocket"
log "github.com/sirupsen/logrus"
"net/http"
"net/url"
"sort"
"strconv"
"strings"
"sync"
"time"
)
var (
background = context.Background()
newLock sync.Mutex
)
var _ exchange.Exchange = &BinanceTrade{}
type BinanceTrade struct {
name string
api *bfutures.Client
tradeCb exchange.WatchFn
positionCb exchange.WatchFn
balanceCb exchange.WatchFn
closeCh chan bool
cancelService *bfutures.CancelAllOpenOrdersService
cancelOneService *bfutures.CancelOrderService
timeService *bfutures.ServerTimeService
klineLimit int
timeout time.Duration
wsUserListenKey string
baseCurrency string
symbols map[string]Symbol
}
func NewBinanceTrader(cfg bcommon.BinanceConfig, cltName, clientProxy string) (b *BinanceTrade, err error) {
b = new(BinanceTrade)
b.name = "binance"
if cltName == "" {
cltName = "binance"
}
b.klineLimit = 1500
b.baseCurrency = "USDT"
if cfg.Currency != "" {
b.baseCurrency = cfg.Currency
}
b.timeout = time.Second * 5
b.closeCh = make(chan bool)
newLock.Lock()
defer func() {
bfutures.UseTestnet = false
newLock.Unlock()
}()
if cfg.IsTest {
bfutures.UseTestnet = true
log.Warnf("binance trade connecting to testnet")
}
bfutures.WebsocketKeepalive = true
b.api = gobinance.NewFuturesClient(cfg.ApiKey, cfg.SecretKey)
if clientProxy != "" {
var proxyURL *url.URL
proxyURL, err = url.Parse(clientProxy)
if err != nil {
return
}
b.api.HTTPClient = &http.Client{Transport: &http.Transport{Proxy: http.ProxyURL(proxyURL)}}
websocket.DefaultDialer.Proxy = http.ProxyURL(proxyURL)
websocket.DefaultDialer.HandshakeTimeout = time.Second * 60
}
b.cancelService = b.api.NewCancelAllOpenOrdersService()
b.cancelOneService = b.api.NewCancelOrderService()
b.timeService = b.api.NewServerTimeService()
_, err = b.Symbols()
if err != nil {
return nil, err
}
// err = b.Start()
return
}
// fetchBalance different with spot
func (b *BinanceTrade) fetchBalanceAndPosition() (err error) {
ctx, cancel := context.WithTimeout(background, b.timeout)
defer cancel()
account, err := b.api.NewGetAccountService().Do(ctx)
if err != nil {
return
}
var balance Balance
balance.Balance = parseFloat(account.TotalWalletBalance)
balance.Available = parseFloat(account.TotalCrossWalletBalance)
// balance.Frozen =
if b.balanceCb != nil {
b.balanceCb(&balance)
}
if b.positionCb != nil {
var amount, profit, initMargin float64
for _, v := range account.Positions {
amount = parseFloat(v.PositionAmt)
if amount == 0 {
continue
}
var position Position
position.Symbol = v.Symbol
position.Hold = amount
position.Price = parseFloat(v.EntryPrice)
profit = parseFloat(v.UnrealizedProfit)
initMargin = parseFloat(v.PositionInitialMargin)
position.ProfitRatio = profit / initMargin
if position.Hold > 0 {
position.Type = Long
} else {
position.Type = Short
}
b.positionCb(&position)
}
}
return
}
func (b *BinanceTrade) Info() (info exchange.ExchangeInfo) {
info = exchange.ExchangeInfo{
Name: "binance_futures",
Value: "binance_futures",
Desc: "binance futures api",
KLineLimit: exchange.FetchLimit{
Limit: b.klineLimit,
},
}
return
}
func (b *BinanceTrade) Symbols() (symbols []Symbol, err error) {
ctx, cancel := context.WithTimeout(background, b.timeout)
defer cancel()
resp, err := b.api.NewExchangeInfoService().Do(ctx)
if err != nil {
return
}
symbols = make([]Symbol, len(resp.Symbols))
for i, v := range resp.Symbols {
value := Symbol{
Name: v.Symbol,
Exchange: "binance",
Symbol: v.Symbol,
Resolutions: "1m,5m,15m,30m,1h,4h,1d,1w",
Precision: v.PricePrecision,
AmountPrecision: v.QuantityPrecision,
PriceStep: 0,
AmountStep: 0,
}
for _, f := range v.Filters {
switch f["filterType"] {
case "PRICE_FILTER":
value.PriceStep = parseFloat(f["tickSize"].(string))
case "LOT_SIZE":
value.AmountStep = parseFloat(f["stepSize"].(string))
default:
}
}
symbols[i] = value
}
if len(symbols) > 0 {
symbolMap := make(map[string]Symbol)
for _, v := range symbols {
symbolMap[v.Symbol] = v
}
b.symbols = symbolMap
}
return
}
func (b *BinanceTrade) Start() (err error) {
err = b.fetchBalanceAndPosition()
if err != nil {
return
}
// watch position and order changed
err = b.startUserWS()
return
}
func (b *BinanceTrade) Stop() (err error) {
close(b.closeCh)
return
}
// KlineChan get klines
func (b *BinanceTrade) GetKline(symbol, bSize string, start, end time.Time) (data []*Candle, err error) {
var temp *Candle
ctx, cancel := context.WithTimeout(background, b.timeout)
defer cancel()
defer func() {
if err != nil && strings.Contains(err.Error(), "Too many requests") {
err = fmt.Errorf("%w, retry: %s", exchange.ErrRetry, err.Error())
}
}()
// get server time
nTime, err := b.timeService.Do(ctx)
if err != nil {
return
}
nStart := start.Unix() * 1000
nEnd := end.Unix() * 1000
klines, err := b.api.NewKlinesService().Interval(bSize).Symbol(symbol).StartTime(nStart).EndTime(nEnd).Limit(b.klineLimit).Do(ctx)
if err != nil {
return
}
sort.Slice(klines, func(i, j int) bool {
return klines[i].OpenTime < klines[j].OpenTime
})
if len(klines) == 0 {
log.Warnf("GetKline once, param: [%s]-[%s] no data", start, end)
return
}
log.Infof("GetKline once, param: [%s]-[%s], total: %d, first: %s, last: %s", start, end, len(klines), time.UnixMilli(klines[0].OpenTime), time.UnixMilli(klines[len(klines)-1].OpenTime))
data = []*Candle{}
for k, v := range klines {
temp = transCandle(v)
if k == len(klines)-1 {
// check if candle is unfinished
if v.CloseTime > nTime {
log.Infof("skip unfinished candle: %##v\n", *v)
break
}
}
data = append(data, temp)
}
return
}
func (b *BinanceTrade) handleError(typ string, cb func() error) func(error) {
return func(err error) {
log.Errorf("binance %s error:%s, call callback", typ, err.Error())
if cb != nil {
cb()
}
}
}
func (b *BinanceTrade) handleAggTradeEvent(fn exchange.WatchFn) func(evt *bfutures.WsAggTradeEvent) {
return func(evt *bfutures.WsAggTradeEvent) {
var err error
var trade Trade
trade.ID = fmt.Sprintf("%d", evt.AggregateTradeID)
trade.Amount, err = strconv.ParseFloat(evt.Quantity, 64)
if err != nil {
log.Errorf("AggTradeEvent parse amount failed: %s", evt.Quantity)
}
trade.Price, err = strconv.ParseFloat(evt.Price, 64)
if err != nil {
log.Errorf("AggTradeEvent parse amount failed: %s", evt.Quantity)
}
trade.Time = time.Unix(evt.Time/1000, (evt.Time%1000)*int64(time.Millisecond))
if fn != nil {
fn(&trade)
}
}
}
func (b *BinanceTrade) handleDepth(fn exchange.WatchFn) func(evt *bfutures.WsDepthEvent) {
return func(evt *bfutures.WsDepthEvent) {
var depth Depth
var err error
var price, amount float64
depth.UpdateTime = time.Unix(evt.TransactionTime/1000, (evt.TransactionTime%1000)*int64(time.Millisecond))
for _, v := range evt.Asks {
// depth.Sells
price, err = strconv.ParseFloat(v.Price, 64)
if err != nil {
log.Errorf("handleDepth parse price failed: %s", v.Price)
}
amount, err = strconv.ParseFloat(v.Quantity, 64)
if err != nil {
log.Errorf("handleDepth parse amount failed: %s", v.Quantity)
}
depth.Sells = append(depth.Sells, DepthInfo{Price: price, Amount: amount})
}
for _, v := range evt.Bids {
// depth.Sells
price, err = strconv.ParseFloat(v.Price, 64)
if err != nil {
log.Errorf("handleDepth parse price failed: %s", v.Price)
}
amount, err = strconv.ParseFloat(v.Quantity, 64)
if err != nil {
log.Errorf("handleDepth parse amount failed: %s", v.Quantity)
}
depth.Buys = append(depth.Buys, DepthInfo{Price: price, Amount: amount})
}
if fn != nil {
fn(&depth)
}
}
}
func (b *BinanceTrade) retry(param exchange.WatchParam, fn exchange.WatchFn) func() error {
return func() error {
// retry when error cause
select {
case <-b.closeCh:
return nil
default:
}
return b.Watch(param, fn)
}
}
func (b *BinanceTrade) Watch(param exchange.WatchParam, fn exchange.WatchFn) (err error) {
symbol := param.Param["symbol"]
var stopC chan struct{}
switch param.Type {
case exchange.WatchTypeCandle:
binSize := param.Param["bin"]
if binSize == "" {
binSize = "1m"
}
var doneC chan struct{}
finishC := make(chan struct{})
doneC, stopC, err = bfutures.WsKlineServe(symbol, binSize, processWsCandle(finishC, fn), b.handleError("watchKline", b.retry(param, fn)))
if err != nil {
log.Error("exchange emitCandle error:", err.Error())
}
go func() {
<-doneC
close(finishC)
}()
case exchange.WatchTypeDepth:
_, stopC, err = bfutures.WsPartialDepthServeWithRate(symbol, 10, 100*time.Millisecond, b.handleDepth(fn), b.handleError("depth", b.retry(param, fn)))
case exchange.WatchTypeTradeMarket:
_, stopC, err = bfutures.WsAggTradeServe(symbol, b.handleAggTradeEvent(fn), b.handleError("aggTrade", b.retry(param, fn)))
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 wathc param: %s", param.Type)
}
if err != nil {
return
}
if stopC != nil {
go func() {
<-b.closeCh
close(stopC)
}()
}
return
}
func (b *BinanceTrade) CancelOrder(old *Order) (order *Order, err error) {
orderID, err := strconv.ParseInt(old.OrderID, 10, 64)
if err != nil {
return
}
resp, err := b.cancelOneService.Symbol(old.Symbol).OrderID(orderID).Do(context.Background())
if err != nil {
return
}
price, err := strconv.ParseFloat(resp.Price, 64)
if err != nil {
panic(fmt.Sprintf("CancelOrder parse price %s error: %s", resp.Price, err.Error()))
}
amount, err := strconv.ParseFloat(resp.OrigQuantity, 64)
if err != nil {
panic(fmt.Sprintf("CancelOrder parse damount %s error: %s", resp.OrigQuantity, err.Error()))
}
order = &Order{
OrderID: strconv.FormatInt(resp.OrderID, 10),
Symbol: resp.Symbol,
Currency: resp.Symbol,
Amount: amount,
Price: price,
Status: strings.ToUpper(string(resp.Status)),
Side: strings.ToLower(string(resp.Side)),
Time: time.Unix(resp.UpdateTime/1000, 0),
}
return
}
func (b *BinanceTrade) ProcessOrder(act TradeAction) (ret *Order, err error) {
ctx, cancel := context.WithTimeout(background, b.timeout)
defer cancel()
orderType := bfutures.OrderTypeLimit
if act.Action.IsStop() {
orderType = bfutures.OrderTypeStopMarket
}
var side bfutures.SideType
if act.Action.IsLong() {
side = bfutures.SideTypeBuy
} else {
side = bfutures.SideTypeSell
}
symbol, ok := b.symbols[act.Symbol]
if ok {
price := symbol.FixPrice(act.Price)
if price != act.Price {
log.Infof("binance change order price form %f to %f", act.Price, price)
act.Price = price
}
}
sent := b.api.NewCreateOrderService().Symbol(act.Symbol)
if act.Action.IsStop() {
sent = sent.StopPrice(fmt.Sprintf("%f", act.Price))
} else {
sent = sent.Price(fmt.Sprintf("%f", act.Price))
}
if !act.Action.IsOpen() {
sent = sent.ReduceOnly(true)
}
resp, err := sent.
Quantity(fmt.Sprintf("%f", act.Amount)).
TimeInForce(bfutures.TimeInForceTypeGTC).
Type(orderType).
Side(side).
Do(ctx)
if err != nil {
return
}
ret = transCreateOrder(resp)
return
}
// TODO: cancel stop order
func (b *BinanceTrade) CancelAllOrders() (orders []*Order, err error) {
ctx, cancel := context.WithTimeout(background, b.timeout)
defer cancel()
ret, err := b.api.NewListOrdersService().Do(ctx)
if err != nil {
err = fmt.Errorf("CancelOrder failed with list: %w", err)
return
}
symbolMap := make(map[string]bool)
var st string
var ok bool
for _, v := range ret {
st = string(v.Status)
if st == OrderStatusFilled || st == OrderStatusCanceled {
continue
}
od := transOrder(v)
orders = append(orders, od)
_, ok = symbolMap[od.Symbol]
if ok {
continue
}
symbolMap[od.Symbol] = true
err = b.cancelService.Symbol(od.Symbol).Do(ctx)
if err != nil {
return nil, err
}
}
return
}
func transOrder(fo *bfutures.Order) (o *Order) {
price, err := strconv.ParseFloat(fo.Price, 64)
if err != nil {
panic(fmt.Sprintf("parse price %s error: %s", fo.Price, err.Error()))
}
amount, err := strconv.ParseFloat(fo.OrigQuantity, 64)
if err != nil {
panic(fmt.Sprintf("parse damount %s error: %s", fo.OrigQuantity, err.Error()))
}
o = &Order{
OrderID: strconv.FormatInt(fo.OrderID, 10),
Symbol: fo.Symbol,
Currency: fo.Symbol,
Amount: amount,
Price: price,
Status: strings.ToUpper(string(fo.Status)),
Side: strings.ToLower(string(fo.Side)),
Time: time.Unix(fo.Time/1000, 0),
}
return
}
func transCreateOrder(fo *bfutures.CreateOrderResponse) (o *Order) {
price, err := strconv.ParseFloat(fo.Price, 64)
if err != nil {
panic(fmt.Sprintf("parse price %s error: %s", fo.Price, err.Error()))
}
amount, err := strconv.ParseFloat(fo.OrigQuantity, 64)
if err != nil {
panic(fmt.Sprintf("parse damount %s error: %s", fo.OrigQuantity, err.Error()))
}
o = &Order{
OrderID: strconv.FormatInt(fo.OrderID, 10),
Symbol: fo.Symbol,
Currency: fo.Symbol,
Amount: amount,
Price: price,
Status: strings.ToUpper(string(fo.Status)),
Side: strings.ToLower(string(fo.Side)),
Time: time.Unix(fo.UpdateTime/1000, 0),
}
return
}
func transCandle(candle *bfutures.Kline) (ret *Candle) {
ret = &Candle{
ID: 0,
Start: candle.OpenTime / 1000,
Open: parseFloat(candle.Open),
High: parseFloat(candle.High),
Low: parseFloat(candle.Low),
Close: parseFloat(candle.Close),
Turnover: parseFloat(candle.QuoteAssetVolume),
Volume: parseFloat(candle.Volume),
Trades: candle.TradeNum,
}
return
}
func parseFloat(str string) float64 {
f, err := strconv.ParseFloat(str, 64)
if err != nil {
panic("binance parseFloat error:" + err.Error())
}
return f
}

31
binance/features/kline.go Normal file
View File

@ -0,0 +1,31 @@
package features
import (
"git.qtrade.icu/coin-quant/exchange"
. "git.qtrade.icu/coin-quant/trademodel"
bfutures "github.com/adshao/go-binance/v2/futures"
)
func processWsCandle(doneC chan struct{}, cb exchange.WatchFn) func(event *bfutures.WsKlineEvent) {
return func(event *bfutures.WsKlineEvent) {
select {
case <-doneC:
return
default:
}
if event.Kline.IsFinal {
candle := Candle{
ID: 0,
Start: event.Kline.StartTime / 1000,
Open: parseFloat(event.Kline.Open),
High: parseFloat(event.Kline.High),
Low: parseFloat(event.Kline.Low),
Close: parseFloat(event.Kline.Close),
Turnover: parseFloat(event.Kline.QuoteVolume),
Volume: parseFloat(event.Kline.Volume),
Trades: event.Kline.TradeNum,
}
cb(&candle)
}
}
}

137
binance/features/user_ws.go Normal file
View File

@ -0,0 +1,137 @@
package features
import (
"context"
. "git.qtrade.icu/coin-quant/trademodel"
bfutures "github.com/adshao/go-binance/v2/futures"
log "github.com/sirupsen/logrus"
"strconv"
"time"
)
func (b *BinanceTrade) updateUserListenKey() {
var err error
ticker := time.NewTicker(time.Minute * 30)
Out:
for {
select {
case <-b.closeCh:
break Out
case <-ticker.C:
for i := 0; i < 10; i++ {
ctx, cancel := context.WithTimeout(background, b.timeout)
err = b.api.NewKeepaliveUserStreamService().ListenKey(b.wsUserListenKey).Do(ctx)
cancel()
if err != nil {
log.Error("update listen key failed:", err.Error())
time.Sleep(time.Minute)
continue
}
break
}
if err != nil {
log.Error("update listen key failed 10 times,just exist::", err.Error())
break Out
}
}
}
}
func (b *BinanceTrade) startUserWS() (err error) {
ctx, cancel := context.WithTimeout(background, b.timeout)
defer cancel()
listenKey, err := b.api.NewStartUserStreamService().Do(ctx)
if err != nil {
return
}
b.wsUserListenKey = listenKey
doneC, stopC, err := bfutures.WsUserDataServe(listenKey, b.handleUserData, b.handleUserDataError)
if err != nil {
return
}
go func() {
select {
case <-doneC:
case <-b.closeCh:
}
close(stopC)
}()
go b.updateUserListenKey()
return
}
func (b *BinanceTrade) handleUserData(event *bfutures.WsUserDataEvent) {
switch event.Event {
case bfutures.UserDataEventTypeAccountUpdate:
var profit, total float64
var balance Balance
for _, v := range event.AccountUpdate.Balances {
if v.Asset == b.baseCurrency {
balance.Balance = parseFloat(v.Balance)
balance.Available = parseFloat(v.CrossWalletBalance)
if b.balanceCb != nil {
b.balanceCb(&balance)
}
total = balance.Balance
}
}
var pos Position
for _, v := range event.AccountUpdate.Positions {
pos.Symbol = v.Symbol
profit = parseFloat(v.UnrealizedPnL)
if v.IsolatedWallet != "" {
total = parseFloat(v.IsolatedWallet)
}
if total > 0 {
pos.ProfitRatio = profit / total
}
pos.Price = parseFloat(v.EntryPrice)
pos.Hold = parseFloat(v.Amount)
if pos.Hold > 0 {
pos.Type = Long
} else if pos.Hold < 0 {
pos.Type = Short
}
if b.positionCb != nil {
b.positionCb(&pos)
}
}
case bfutures.UserDataEventTypeOrderTradeUpdate:
var order Order
order.OrderID = strconv.FormatInt(event.OrderTradeUpdate.ID, 10)
order.Symbol = event.OrderTradeUpdate.Symbol
order.Currency = event.OrderTradeUpdate.Symbol
order.Amount = parseFloat(event.OrderTradeUpdate.OriginalQty)
order.Filled = parseFloat(event.OrderTradeUpdate.AccumulatedFilledQty)
if order.Filled == order.Amount {
order.Price = parseFloat(event.OrderTradeUpdate.AveragePrice)
} else {
order.Price = parseFloat(event.OrderTradeUpdate.OriginalPrice)
}
order.Status = string(event.OrderTradeUpdate.Status)
order.Side = string(event.OrderTradeUpdate.Side)
order.Time = time.UnixMilli(event.OrderTradeUpdate.TradeTime)
if b.tradeCb != nil {
b.tradeCb(&order)
}
}
}
func (b *BinanceTrade) handleUserDataError(err error) {
log.Errorf("binance userdata error: %s,reconnect", err.Error())
doneC, stopC, err := bfutures.WsUserDataServe(b.wsUserListenKey, b.handleUserData, b.handleUserDataError)
if err != nil {
return
}
go func() {
select {
case <-doneC:
case <-b.closeCh:
}
close(stopC)
}()
}

31
config.go Normal file
View File

@ -0,0 +1,31 @@
package exchange
import "github.com/spf13/viper"
type Config interface {
Get(string) interface{}
GetBool(string) bool
GetInt(string) int
GetString(string) string
UnmarshalKey(string, interface{}) error
}
type ExchangeConfig struct {
Name string
ApiKey string
SecretKey string
Passphrase string
IsTest bool
}
func WrapViper(cfg *viper.Viper) Config {
return &ViperCfg{Viper: cfg}
}
type ViperCfg struct {
*viper.Viper
}
func (c *ViperCfg) UnmarshalKey(key string, rawVal interface{}) error {
return c.Viper.UnmarshalKey(key, rawVal)
}

111
exchange.go Normal file
View File

@ -0,0 +1,111 @@
package exchange
import (
"errors"
"sort"
"time"
. "git.qtrade.icu/coin-quant/trademodel"
"github.com/sirupsen/logrus"
)
const (
WatchTypeCandle = "candle"
WatchTypeDepth = "depth"
WatchTypeTradeMarket = "trade_market"
WatchTypeTrade = "trade"
WatchTypePosition = "position"
WatchTypeBalance = "balance"
)
var (
ErrRetry = errors.New("need retry")
)
type WatchParam struct {
Type string
Param map[string]string
}
func WatchCandle(symbol, binSize string) WatchParam {
return WatchParam{
Type: WatchTypeCandle,
Param: map[string]string{"symbol": symbol, "bin": binSize},
}
}
type WatchFn func(interface{})
type FetchLimit struct {
Duration string
Limit int
}
// ExchangeInfo exchange info
type ExchangeInfo struct {
Name string
Value string
Desc string
KLineLimit FetchLimit
OrderLimit FetchLimit
}
type Exchange interface {
Info() ExchangeInfo
Symbols() ([]Symbol, error)
Start() error
Stop() error
// Watch exchange event, call multiple times for different event
Watch(WatchParam, WatchFn) error
// Kline get klines
GetKline(symbol, bSize string, start, end time.Time) (data []*Candle, err error)
// for trade
ProcessOrder(act TradeAction) (ret *Order, err error)
CancelAllOrders() (orders []*Order, err error)
CancelOrder(old *Order) (orders *Order, err error)
}
func KlineChan(e Exchange, symbol, bSize string, start, end time.Time) (dataCh chan *Candle, errCh chan error) {
dataCh = make(chan *Candle, 1024*10)
errCh = make(chan error, 1)
go func() {
defer func() {
close(dataCh)
close(errCh)
}()
tStart := start
tEnd := end
var nPrevStart int64
for {
klines, err := e.GetKline(symbol, bSize, tStart, tEnd)
if err != nil {
if errors.Is(err, ErrRetry) {
time.Sleep(time.Second * 5)
continue
}
errCh <- err
return
}
sort.Slice(klines, func(i, j int) bool {
return klines[i].Start < klines[j].Start
})
for _, v := range klines {
if v.Start <= nPrevStart {
continue
}
dataCh <- v
tStart = v.Time()
}
if tStart.Sub(tEnd) >= 0 || tStart.Unix() <= nPrevStart || len(klines) == 0 {
logrus.Info("KlineChan finished: [%s]-[%s], last datatime: %s", start, end, tStart)
break
}
nPrevStart = tStart.Unix()
}
}()
return
}

42
go.mod Normal file
View File

@ -0,0 +1,42 @@
module git.qtrade.icu/coin-quant/exchange
go 1.22.0
require (
git.qtrade.icu/coin-quant/trademodel v0.0.0-20240625151548-cef4b6fc28b9
github.com/adshao/go-binance/v2 v2.4.5
github.com/deepmap/oapi-codegen v1.10.1
github.com/getkin/kin-openapi v0.94.0
github.com/gorilla/websocket v1.5.0
github.com/mitchellh/mapstructure v1.5.0
github.com/sirupsen/logrus v1.9.0
github.com/spf13/viper v1.11.0
)
require (
github.com/bitly/go-simplejson v0.5.0 // indirect
github.com/fsnotify/fsnotify v1.5.4 // indirect
github.com/ghodss/yaml v1.0.0 // indirect
github.com/go-openapi/jsonpointer v0.19.5 // indirect
github.com/go-openapi/swag v0.21.1 // indirect
github.com/google/uuid v1.3.0 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/magiconair/properties v1.8.6 // indirect
github.com/mailru/easyjson v0.7.7 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/pelletier/go-toml v1.9.5 // indirect
github.com/pelletier/go-toml/v2 v2.0.0-beta.8 // indirect
github.com/spf13/afero v1.8.2 // indirect
github.com/spf13/cast v1.4.1 // indirect
github.com/spf13/jwalterweatherman v1.1.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/subosito/gotenv v1.2.0 // indirect
golang.org/x/sys v0.2.0 // indirect
golang.org/x/text v0.4.0 // indirect
gopkg.in/ini.v1 v1.66.4 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

610
go.sum Normal file
View File

@ -0,0 +1,610 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU=
cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
cloud.google.com/go v0.44.3/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc=
cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0=
cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To=
cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4=
cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M=
cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc=
cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk=
cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs=
cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc=
cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY=
cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI=
cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk=
cloud.google.com/go v0.75.0/go.mod h1:VGuuCn7PG0dwsd5XPVm2Mm3wlh3EL55/79EKB6hlPTY=
cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE=
cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc=
cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg=
cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc=
cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ=
cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk=
cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw=
cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA=
cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU=
cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos=
cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk=
cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs=
cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3fOKtUw0Xmo=
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
git.qtrade.icu/coin-quant/trademodel v0.0.0-20240625151548-cef4b6fc28b9 h1:9T1u+MzfbG9jZU1wzDtmBoOwN1m/fRX0iX7NbLwAHgU=
git.qtrade.icu/coin-quant/trademodel v0.0.0-20240625151548-cef4b6fc28b9/go.mod h1:SZnI+IqcRlKVcDSS++NIgthZX4GG1OU4UG+RDrSOD34=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
github.com/adshao/go-binance/v2 v2.4.5 h1:V3KpolmS9a7TLVECSrl2gYm+GGBSxhVk9ILaxvOTOVw=
github.com/adshao/go-binance/v2 v2.4.5/go.mod h1:41Up2dG4NfMXpCldrDPETEtiOq+pHoGsFZ73xGgaumo=
github.com/bitly/go-simplejson v0.5.0 h1:6IH+V8/tVMab511d5bn4M7EwGXZf9Hj6i2xSwkNEM+Y=
github.com/bitly/go-simplejson v0.5.0/go.mod h1:cXHtHw4XUPsvGaxgjIAn8PhEWG9NfngEKAMDJEczWVA=
github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 h1:DDGfHa7BWjL4YnC6+E63dPcxHo2sUxDIu8g3QgEJdRY=
github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/cyberdelia/templates v0.0.0-20141128023046-ca7fffd4298c/go.mod h1:GyV+0YP4qX0UQ7r2MoYZ+AvYDp12OF5yg4q8rGnyNh4=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn47hh8kt6rqSlvmrXFAc=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.0-20210816181553-5444fa50b93d/go.mod h1:tmAIfUFEirG/Y8jhZ9M+h36obRZAk/1fcSpXwAVlfqE=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1/go.mod h1:hyedUtir6IdtD/7lIxGeCxkaw7y45JueMRL4DIyJDKs=
github.com/deepmap/oapi-codegen v1.10.1 h1:xybuJUR6D8l7P+LAuxOm5SD7nTlFKHWvOPl31q+DDVs=
github.com/deepmap/oapi-codegen v1.10.1/go.mod h1:TvVmDQlUkFli9gFij/gtW1o+tFBr4qCHyv2zG+R0YZY=
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po=
github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/fsnotify/fsnotify v1.5.4 h1:jRbGcIw6P2Meqdwuo0H1p6JVLbL5DHKAKlYndzMwVZI=
github.com/fsnotify/fsnotify v1.5.4/go.mod h1:OVB6XrOHzAwXMpEM7uPOzcehqUV2UqJxmVXmkdnm1bU=
github.com/getkin/kin-openapi v0.94.0 h1:bAxg2vxgnHHHoeefVdmGbR+oxtJlcv5HsJJa3qmAHuo=
github.com/getkin/kin-openapi v0.94.0/go.mod h1:LWZfzOd7PRy8GJ1dJ6mCU6tNdSfOwRac1BUPam4aw6Q=
github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
github.com/gin-gonic/gin v1.7.7/go.mod h1:axIBovoeJpVj8S3BwE0uPMTeReE4+AfFtqpqaZ1qq1U=
github.com/go-chi/chi/v5 v5.0.7/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-openapi/jsonpointer v0.19.5 h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUeOFYEICuY=
github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk=
github.com/go-openapi/swag v0.21.1 h1:wm0rhTb5z7qpJRHBdPOMuY4QjVUMbF6/kwoYeRAOrKU=
github.com/go-openapi/swag v0.21.1/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ=
github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8=
github.com/go-playground/locales v0.14.0/go.mod h1:sawfccIbzZTqEDETgFXqTho0QybSa7l++s0DH+LDiLs=
github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA=
github.com/go-playground/universal-translator v0.18.0/go.mod h1:UvRDBj+xPUEGrFYl+lu/H90nyDXpg0fqeB/AQUGNTVA=
github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4=
github.com/go-playground/validator/v10 v10.10.1/go.mod h1:i+3WkQ1FvaUjjxh1kSvIA4dMGDBiPU55YFDl0WbKdWU=
github.com/goccy/go-json v0.9.6/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/golangci/lint-1 v0.0.0-20181222135242-d2cdd8c08219/go.mod h1:/X8TswGSh1pIozq4ZwCfxS0WA5JGXguxk94ar/4c87Y=
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g=
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/labstack/echo/v4 v4.7.2/go.mod h1:xkCDAdFCIf8jsFQ5NnbK7oqaF/yU1A1X20Ltm0OvSks=
github.com/labstack/gommon v0.3.1/go.mod h1:uW6kP17uPlLJsD3ijUYn3/M5bAxtlZhMI6m3MFxTMTM=
github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=
github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY=
github.com/lestrrat-go/backoff/v2 v2.0.8/go.mod h1:rHP/q/r9aT27n24JQLa7JhSQZCKBBOiM/uP402WwN8Y=
github.com/lestrrat-go/blackmagic v1.0.0/go.mod h1:TNgH//0vYSs8VXDCfkZLgIrVTTXQELZffUV0tz3MtdQ=
github.com/lestrrat-go/blackmagic v1.0.1/go.mod h1:UrEqBzIR2U6CnzVyUtfM6oZNMt/7O7Vohk2J0OGSAtU=
github.com/lestrrat-go/httpcc v1.0.1/go.mod h1:qiltp3Mt56+55GPVCbTdM9MlqhvzyuL6W/NMDA8vA5E=
github.com/lestrrat-go/iter v1.0.1/go.mod h1:zIdgO1mRKhn8l9vrZJZz9TUMMFbQbLeTsbqPDrJ/OJc=
github.com/lestrrat-go/iter v1.0.2/go.mod h1:Momfcq3AnRlRjI5b5O8/G5/BvpzrhoFTZcn06fEOPt4=
github.com/lestrrat-go/jwx v1.2.23/go.mod h1:sAXjRwzSvCN6soO4RLoWWm1bVPpb8iOuv0IYfH8OWd8=
github.com/lestrrat-go/option v1.0.0/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I=
github.com/magiconair/properties v1.8.6 h1:5ibWZ6iY0NctNGWo87LalDlEZ6R41TqbbDamhfG/Qzo=
github.com/magiconair/properties v1.8.6/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60=
github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
github.com/matryer/moq v0.2.7/go.mod h1:kITsx543GOENm48TUAQyJ9+SAvFSr7iGQXPoth/VUBk=
github.com/mattn/go-colorable v0.1.11/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8=
github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
github.com/pelletier/go-toml/v2 v2.0.0-beta.8 h1:dy81yyLYJDwMTifq24Oi/IslOslRrDSb3jwDggjz3Z0=
github.com/pelletier/go-toml/v2 v2.0.0-beta.8/go.mod h1:r9LEWfGN8R5k0VXJ+0BkIe7MYkRdwZOjgMj2KwnJFUo=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8=
github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE=
github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0=
github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/spf13/afero v1.8.2 h1:xehSyVa0YnHWsJ49JFljMpg1HX19V6NDZ1fkm1Xznbo=
github.com/spf13/afero v1.8.2/go.mod h1:CtAatgMJh6bJEIs48Ay/FOnkljP3WeGUG0MC1RfAqwo=
github.com/spf13/cast v1.4.1 h1:s0hze+J0196ZfEMTs80N7UlFt0BDuQ7Q+JDnHiMWKdA=
github.com/spf13/cast v1.4.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk=
github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/viper v1.11.0 h1:7OX/1FS6n7jHD1zGrZTM7WtY13ZELRyosK4k93oPr44=
github.com/spf13/viper v1.11.0/go.mod h1:djo0X/bA5+tYVoCn+C7cAYJGcVn/qYLFTG8gdUsX7Zk=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s=
github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw=
github.com/ugorji/go v1.2.7/go.mod h1:nF9osbDWLy6bDVv/Rtoh6QgnvNDpmCalQV5urGCCS6M=
github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=
github.com/ugorji/go/codec v1.2.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95kRgeqEY=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasttemplate v1.2.1/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.0.0-20220214200702-86341886e292/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.0.0-20220411220226-7b82a4e95df4/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek=
golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=
golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs=
golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3/go.mod h1:3p9vT2HGsQu2K1YbXdKPJLVgG5VJdoTa1poYQBtP1AY=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220418201149-a630d4f3e7a2/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211103235746-7861aae1554b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.2.0 h1:ljd4t30dBnAvMZaQCevtY0xLLD0A+bRZXbgLMLU1F/A=
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.4.0 h1:BrVqGRd7+k1DiOgtnFvAkoQEWQvBc25ouMJM6429SFg=
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20201208040808-7e3f01d25324/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20220411224347-583f2d630306/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8=
golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE=
golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
golang.org/x/tools v0.1.10/go.mod h1:Uh6Zz+xoGYZom868N8YTex3t7RhtHDBrE8Gzo9bV56E=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20220411194840-2f41105eb62f/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM=
google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc=
google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg=
google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE=
google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=
google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA=
google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U=
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA=
google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20210108203827-ffc7fda8c3d7/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20210226172003-ab064af71705/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60=
google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk=
google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8=
google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4=
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/ini.v1 v1.66.4 h1:SsAcf+mM7mRZo2nJNGt8mZCjG8ZRaNGMURJw7BsIST4=
gopkg.in/ini.v1 v1.66.4/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=

6
include/all.go Normal file
View File

@ -0,0 +1,6 @@
package include
import (
_ "git.qtrade.icu/coin-quant/exchange/binance"
_ "git.qtrade.icu/coin-quant/exchange/okex"
)

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,25 @@
package market
import (
"context"
"strconv"
"testing"
"time"
)
func TestCandles(t *testing.T) {
ApiAddr := "https://www.okex.com/"
api, err := NewClientWithResponses(ApiAddr)
tEnd := time.Now().Add(-time.Hour * 24 * 365)
tStart := tEnd.Add(-time.Hour)
startStr := strconv.FormatInt((tStart.Unix() * 1000), 10)
endStr := strconv.FormatInt((tEnd.Unix() * 1000), 10)
bSize := "1m"
var params = GetApiV5MarketHistoryCandlesParams{InstId: "ETH-USDT-SWAP", Bar: &bSize, After: &endStr, Before: &startStr}
resp, err := api.GetApiV5MarketHistoryCandlesWithResponse(context.Background(), &params)
if err != nil {
t.Fatal(err.Error())
}
t.Log(string(resp.Body), resp.JSON200)
}

File diff suppressed because it is too large Load Diff

3877
okex/api/trade/trade.gen.go Normal file

File diff suppressed because it is too large Load Diff

290
okex/def.go Normal file
View File

@ -0,0 +1,290 @@
package okex
type OKEXConfig struct {
Name string
ApiKey string
SecretKey string
Passphrase string
TdMode string
IsTest bool
}
type AlgoOrder struct {
ActualPx string `json:"actualPx"`
ActualSide string `json:"actualSide"`
ActualSz string `json:"actualSz"`
AlgoID string `json:"algoId"`
CTime string `json:"cTime"`
Ccy string `json:"ccy"`
InstID string `json:"instId"`
InstType string `json:"instType"`
Lever string `json:"lever"`
NotionalUsd string `json:"notionalUsd"`
OrdID string `json:"ordId"`
OrdPx string `json:"ordPx"`
OrdType string `json:"ordType"`
PosSide string `json:"posSide"`
ReduceOnly string `json:"reduceOnly"`
Side string `json:"side"`
SlOrdPx string `json:"slOrdPx"`
SlTriggerPx string `json:"slTriggerPx"`
State string `json:"state"`
Sz string `json:"sz"`
TdMode string `json:"tdMode"`
TgtCcy string `json:"tgtCcy"`
TpOrdPx string `json:"tpOrdPx"`
TpTriggerPx string `json:"tpTriggerPx"`
TriggerPx string `json:"triggerPx"`
TriggerTime string `json:"triggerTime"`
}
type OrderNormal struct {
AccFillSz string `json:"accFillSz"`
AmendResult string `json:"amendResult"`
AvgPx string `json:"avgPx"`
CTime string `json:"cTime"`
Category string `json:"category"`
Ccy string `json:"ccy"`
ClOrdID string `json:"clOrdId"`
Code string `json:"code"`
ExecType string `json:"execType"`
Fee string `json:"fee"`
FeeCcy string `json:"feeCcy"`
FillFee string `json:"fillFee"`
FillFeeCcy string `json:"fillFeeCcy"`
FillNotionalUsd string `json:"fillNotionalUsd"`
FillPx string `json:"fillPx"`
FillSz string `json:"fillSz"`
FillTime string `json:"fillTime"`
InstID string `json:"instId"`
InstType string `json:"instType"`
Lever string `json:"lever"`
Msg string `json:"msg"`
NotionalUsd string `json:"notionalUsd"`
OrdID string `json:"ordId"`
OrdType string `json:"ordType"`
Pnl string `json:"pnl"`
PosSide string `json:"posSide"`
Px string `json:"px"`
Rebate string `json:"rebate"`
RebateCcy string `json:"rebateCcy"`
ReduceOnly string `json:"reduceOnly"`
ReqID string `json:"reqId"`
Side string `json:"side"`
SlOrdPx string `json:"slOrdPx"`
SlTriggerPx string `json:"slTriggerPx"`
SlTriggerPxType string `json:"slTriggerPxType"`
Source string `json:"source"`
State string `json:"state"`
Sz string `json:"sz"`
Tag string `json:"tag"`
TdMode string `json:"tdMode"`
TgtCcy string `json:"tgtCcy"`
TpOrdPx string `json:"tpOrdPx"`
TpTriggerPx string `json:"tpTriggerPx"`
TpTriggerPxType string `json:"tpTriggerPxType"`
TradeID string `json:"tradeId"`
UTime string `json:"uTime"`
}
type CandleResp struct {
Code string `json:"code"`
Msg string `json:"msg"`
Data [][9]string `json:"data"`
}
type OKEXOrder struct {
Code string `json:"code"`
Msg string `json:"msg"`
Data []struct {
ClOrdID string `json:"clOrdId"`
OrdID string `json:"ordId"`
Tag string `json:"tag"`
SCode string `json:"sCode"`
SMsg string `json:"sMsg"`
} `json:"data"`
}
type OKEXAlgoOrder struct {
Code string `json:"code"`
Msg string `json:"msg"`
Data []struct {
AlgoID string `json:"algoId"`
SCode string `json:"sCode"`
SMsg string `json:"sMsg"`
} `json:"data"`
}
type InstrumentResp struct {
Code string `json:"code"`
Msg string `json:"msg"`
Data []Instrument `json:"data"`
}
type Instrument struct {
InstType string `json:"instType"` // 产品类型
InstID string `json:"instId"` // 产品id 如 BTC-USD-SWAP
Uly string `json:"uly"` // 标的指数,如 BTC-USD仅适用于交割/永续/期权
Category string `json:"category"` // 手续费档位,每个交易产品属于哪个档位手续费
BaseCcy string `json:"baseCcy"` // 交易货币币种,如 BTC-USDT 中的 BTC ,仅适用于币币
QuoteCcy string `json:"quoteCcy"` // 计价货币币种,如 BTC-USDT 中的USDT ,仅适用于币币
SettleCcy string `json:"settleCcy"` // 盈亏结算和保证金币种,如 BTC 仅适用于交割/永续/期权
CtVal string `json:"ctVal"` // 合约面值,仅适用于交割/永续/期权
CtMult string `json:"ctMult"` // 合约乘数,仅适用于交割/永续/期权
CtValCcy string `json:"ctValCcy"` // 合约面值计价币种,仅适用于交割/永续/期权
OptType string `json:"optType"` // 期权类型C或P 仅适用于期权
Stk string `json:"stk"` // 行权价格,仅适用于期权
ListTime string `json:"listTime"` // 上线日期 Unix时间戳的毫秒数格式如 1597026383085
ExpTime string `json:"expTime"` // 交割/行权日期,仅适用于交割 和 期权 Unix时间戳的毫秒数格式如 1597026383085
Lever string `json:"lever"` // 该instId支持的最大杠杆倍数不适用于币币、期权
TickSz string `json:"tickSz"` // 下单价格精度,如 0.0001
LotSz string `json:"lotSz"` // 下单数量精度,如 BTC-USDT-SWAP1
MinSz string `json:"minSz"` // 最小下单数量
CtType string `json:"ctType"` // linear正向合约 inverse反向合约 仅适用于交割/永续
Alias string `json:"alias"` // 合约日期别名 this_week本周 next_week次周 quarter季度 next_quarter次季度 仅适用于交割
State string `json:"state"` // 产品状态 live交易中 suspend暂停中 preopen预上线settlement资金费结算
}
type AccountConfig struct {
Code string `json:"code"`
Data []struct {
AcctLv string `json:"acctLv"`
AutoLoan bool `json:"autoLoan"`
CtIsoMode string `json:"ctIsoMode"`
GreeksType string `json:"greeksType"`
Level string `json:"level"`
LevelTmp string `json:"levelTmp"`
MgnIsoMode string `json:"mgnIsoMode"`
PosMode string `json:"posMode"`
SpotOffsetType string `json:"spotOffsetType"`
UID string `json:"uid"`
Label string `json:"label"`
RoleType string `json:"roleType"`
TraderInsts []any `json:"traderInsts"`
OpAuth string `json:"opAuth"`
IP string `json:"ip"`
} `json:"data"`
Msg string `json:"msg"`
}
type AccountPosition struct {
Adl string `json:"adl"`
AvailPos string `json:"availPos"`
AvgPx string `json:"avgPx"`
CTime string `json:"cTime"`
Ccy string `json:"ccy"`
DeltaBS string `json:"deltaBS"`
DeltaPA string `json:"deltaPA"`
GammaBS string `json:"gammaBS"`
GammaPA string `json:"gammaPA"`
Imr string `json:"imr"`
InstID string `json:"instId"`
InstType string `json:"instType"`
Interest string `json:"interest"`
Last string `json:"last"`
UsdPx string `json:"usdPx"`
Lever string `json:"lever"`
Liab string `json:"liab"`
LiabCcy string `json:"liabCcy"`
LiqPx string `json:"liqPx"`
MarkPx string `json:"markPx"`
Margin string `json:"margin"`
MgnMode string `json:"mgnMode"`
MgnRatio string `json:"mgnRatio"`
Mmr string `json:"mmr"`
NotionalUsd string `json:"notionalUsd"`
OptVal string `json:"optVal"`
PTime string `json:"pTime"`
Pos string `json:"pos"`
PosCcy string `json:"posCcy"`
PosID string `json:"posId"`
PosSide string `json:"posSide"`
SpotInUseAmt string `json:"spotInUseAmt"`
SpotInUseCcy string `json:"spotInUseCcy"`
ThetaBS string `json:"thetaBS"`
ThetaPA string `json:"thetaPA"`
TradeID string `json:"tradeId"`
BizRefID string `json:"bizRefId"`
BizRefType string `json:"bizRefType"`
QuoteBal string `json:"quoteBal"`
BaseBal string `json:"baseBal"`
BaseBorrowed string `json:"baseBorrowed"`
BaseInterest string `json:"baseInterest"`
QuoteBorrowed string `json:"quoteBorrowed"`
QuoteInterest string `json:"quoteInterest"`
UTime string `json:"uTime"`
Upl string `json:"upl"`
UplRatio string `json:"uplRatio"`
VegaBS string `json:"vegaBS"`
VegaPA string `json:"vegaPA"`
CloseOrderAlgo []struct {
AlgoID string `json:"algoId"`
SlTriggerPx string `json:"slTriggerPx"`
SlTriggerPxType string `json:"slTriggerPxType"`
TpTriggerPx string `json:"tpTriggerPx"`
TpTriggerPxType string `json:"tpTriggerPxType"`
CloseFraction string `json:"closeFraction"`
} `json:"closeOrderAlgo"`
}
type AccountPositionResp struct {
Code string `json:"code"`
Msg string `json:"msg"`
Data []AccountPosition `json:"data"`
}
type AccountBalance struct {
AdjEq string `json:"adjEq"`
Details []struct {
AvailBal string `json:"availBal"`
AvailEq string `json:"availEq"`
CashBal string `json:"cashBal"`
Ccy string `json:"ccy"`
CrossLiab string `json:"crossLiab"`
DisEq string `json:"disEq"`
Eq string `json:"eq"`
EqUsd string `json:"eqUsd"`
FrozenBal string `json:"frozenBal"`
Interest string `json:"interest"`
IsoEq string `json:"isoEq"`
IsoLiab string `json:"isoLiab"`
IsoUpl string `json:"isoUpl"`
Liab string `json:"liab"`
MaxLoan string `json:"maxLoan"`
MgnRatio string `json:"mgnRatio"`
NotionalLever string `json:"notionalLever"`
OrdFrozen string `json:"ordFrozen"`
Twap string `json:"twap"`
UTime string `json:"uTime"`
Upl string `json:"upl"`
UplLiab string `json:"uplLiab"`
StgyEq string `json:"stgyEq"`
SpotInUseAmt string `json:"spotInUseAmt"`
} `json:"details"`
Imr string `json:"imr"`
IsoEq string `json:"isoEq"`
MgnRatio string `json:"mgnRatio"`
Mmr string `json:"mmr"`
NotionalUsd string `json:"notionalUsd"`
OrdFroz string `json:"ordFroz"`
TotalEq string `json:"totalEq"`
UTime string `json:"uTime"`
}
type AccountBalanceResp struct {
Code string `json:"code"`
Data []AccountBalance `json:"data"`
Msg string `json:"msg"`
}
type CancelNormalResp struct {
Code string `json:"code"`
Msg string `json:"msg"`
Data []OrderNormal `json:"data"`
}
type CancelAlgoResp struct {
Code string `json:"code"`
Msg string `json:"msg"`
Data []AlgoOrder `json:"data"`
}

1390
okex/docs/account.json Normal file

File diff suppressed because it is too large Load Diff

804
okex/docs/market.json Normal file
View File

@ -0,0 +1,804 @@
{
"openapi": "3.0.1",
"info": {
"title": "REST API",
"version": "v5",
"description": "# 使用说明 \n <b>接口只会请求模拟环境</b><br><br>*Parameters* 面板中点击`Try it out`按钮,编辑请求参数,点击`Execute`按钮发送请求。*Responses* 面板中查看请求结果。<br>"
},
"tags": [
{
"name": "Market Data",
"description": "行情数据"
}
],
"paths": {
"/api/v5/market/tickers": {
"get": {
"tags": [
"Market Data"
],
"summary": "获取所有产品行情信息",
"parameters": [
{
"name": "instType",
"in": "query",
"description": "产品类型<br>`SPOT`:币币<br>`SWAP`:永续合约<br>`FUTURES`:交割合约<br>`OPTION`:期权",
"required": true,
"schema": {
"type": "string"
},
"example": "SPOT"
},
{
"name": "uly",
"in": "query",
"description": "合约标的指数<br>仅适用于`交割/永续/期权`,如 `BTC-USD`",
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "成功",
"content": {
"application/json": {
"schema": {
"type": "object",
"example": {
"code": "0",
"msg": "",
"data": [
{
"instType": "SWAP",
"instId": "LTC-USD-SWAP",
"last": "9999.99",
"lastSz": "0.1",
"askPx": "9999.99",
"askSz": "11",
"bidPx": "8888.88",
"bidSz": "5",
"open24h": "9000",
"high24h": "10000",
"low24h": "8888.88",
"volCcy24h": "2222",
"vol24h": "2222",
"sodUtc0": "0.1",
"sodUtc8": "0.1",
"ts": "1597026383085"
}
]
}
}
}
}
}
}
}
},
"/api/v5/market/ticker": {
"get": {
"tags": [
"Market Data"
],
"summary": "获取单个产品行情信息",
"parameters": [
{
"name": "instId",
"in": "query",
"description": "产品ID`BTC-USD-SWAP`",
"required": true,
"schema": {
"type": "string"
},
"example": "BTC-USD-SWAP"
}
],
"responses": {
"200": {
"description": "成功",
"content": {
"application/json": {
"schema": {
"type": "object",
"example": {
"code": "0",
"msg": "",
"data": [
{
"instType": "SWAP",
"instId": "BTC-USD-SWAP",
"last": "56956.1",
"lastSz": "3",
"askPx": "56959.1",
"askSz": "10582",
"bidPx": "56959",
"bidSz": "4552",
"open24h": "55926",
"high24h": "57641.1",
"low24h": "54570.1",
"volCcy24h": "81137.755",
"vol24h": "46258703",
"ts": "1620289117764",
"sodUtc0": "55926",
"sodUtc8": "55926"
}
]
}
}
}
}
}
}
}
},
"/api/v5/market/index-tickers": {
"get": {
"tags": [
"Market Data"
],
"summary": "获取指数行情",
"parameters": [
{
"name": "instId",
"in": "query",
"description": "指数,如:`BTC-USD`<br>`instId`和`quoteCcy`必须填写一个",
"schema": {
"type": "string"
},
"example": "BTC-USD"
},
{
"name": "quoteCcy",
"in": "query",
"description": "指数计价单位<br>目前只有`USD`/`USDT`/`BTC`为计价单位的指数",
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "成功",
"content": {
"application/json": {
"schema": {
"type": "object",
"example": {
"code": "0",
"msg": "",
"data": [
{
"instId": "BTC-USDT",
"idxPx": "0.1",
"high24h": "0.5",
"low24h": "0.1",
"open24h": "0.1",
"sodUtc0": "0.1",
"sodUtc8": "0.1",
"ts": "1597026383085"
}
]
}
}
}
}
}
}
}
},
"/api/v5/market/books": {
"get": {
"tags": [
"Market Data"
],
"summary": "获取产品深度",
"parameters": [
{
"name": "instId",
"in": "query",
"description": "产品ID`BTC-USDT`",
"schema": {
"type": "string"
},
"required": true,
"example": "BTC-USDT"
},
{
"name": "sz",
"in": "query",
"description": "深度档位数量<br>最大值可传400即买卖深度共800条<br>不填写此参数默认返回1档深度数据",
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "成功",
"content": {
"application/json": {
"schema": {
"type": "object",
"example": {
"code": "0",
"msg": "",
"data": [
{
"asks": [
[
"41006.8",
"0.60038921",
"0",
"1"
]
],
"bids": [
[
"41006.3",
"0.30178218",
"0",
"2"
]
],
"ts": "1629966436396"
}
]
}
}
}
}
}
}
}
},
"/api/v5/market/candles": {
"get": {
"tags": [
"Market Data"
],
"summary": "获取所有交易产品K线数据",
"parameters": [
{
"name": "instId",
"in": "query",
"description": "产品ID`BTC-USDT`",
"schema": {
"type": "string"
},
"required": true,
"example": "BTC-USDT"
},
{
"name": "bar",
"in": "query",
"description": "时间粒度,默认值`1m`<br>如 [1m/3m/5m/15m/30m/1H/2H/4H/6H/12H/1D/1W/1M/3M/6M/1Y]",
"schema": {
"type": "string"
}
},
{
"name": "after",
"in": "query",
"description": "请求此时间戳之前(更旧的数据)的分页内容,传的值为对应接口的`ts`",
"schema": {
"type": "string"
}
},
{
"name": "before",
"in": "query",
"description": "请求此时间戳之后(更新的数据)的分页内容,传的值为对应接口的`ts`",
"schema": {
"type": "string"
}
},
{
"name": "limit",
"in": "query",
"description": "分页返回的结果集数量最大为100不填默认返回100条",
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "成功",
"content": {
"application/json": {
"schema": {
"type": "object",
"example": {
"code": "0",
"msg": "",
"data": [
[
"1597026383085",
"3.721",
"3.743",
"3.677",
"3.708",
"8422410",
"22698348.04828491"
],
[
"1597026383085",
"3.731",
"3.799",
"3.494",
"3.72",
"24912403",
"67632347.24399722"
]
]
}
}
}
}
}
}
}
},
"/api/v5/market/history-candles": {
"get": {
"tags": [
"Market Data"
],
"summary": "获取交易产品历史K线数据仅主流币",
"parameters": [
{
"name": "instId",
"in": "query",
"description": "产品ID`BTC-USDT`",
"schema": {
"type": "string"
},
"required": true,
"example": "BTC-USDT"
},
{
"name": "bar",
"in": "query",
"description": "时间粒度,默认值`1m`<br>如 [1m/3m/5m/15m/30m/1H/2H/4H/6H/12H/1D/1W/1M/3M/6M/1Y]",
"schema": {
"type": "string"
}
},
{
"name": "after",
"in": "query",
"description": "请求此时间戳之前(更旧的数据)的分页内容,传的值为对应接口的`ts`",
"schema": {
"type": "string"
}
},
{
"name": "before",
"in": "query",
"description": "请求此时间戳之后(更新的数据)的分页内容,传的值为对应接口的`ts`",
"schema": {
"type": "string"
}
},
{
"name": "limit",
"in": "query",
"description": "分页返回的结果集数量最大为100不填默认返回100条",
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "成功",
"content": {
"application/json": {
"schema": {
"type": "object",
"example": {
"code": "0",
"msg": "",
"data": [
[
"1597026383085",
"3.721",
"3.743",
"3.677",
"3.708",
"8422410",
"22698348.04828491"
],
[
"1597026383085",
"3.731",
"3.799",
"3.494",
"3.72",
"24912403",
"67632347.24399722"
]
]
}
}
}
}
}
}
}
},
"/api/v5/market/index-candles": {
"get": {
"tags": [
"Market Data"
],
"summary": "获取指数K线数据",
"parameters": [
{
"name": "instId",
"in": "query",
"description": "现货指数,如:`BTC-USD`",
"schema": {
"type": "string"
},
"required": true,
"example": "BTC-USDT"
},
{
"name": "bar",
"in": "query",
"description": "时间粒度,默认值`1m`<br>如 [1m/3m/5m/15m/30m/1H/2H/4H/6H/12H/1D/1W/1M/3M/6M/1Y]",
"schema": {
"type": "string"
}
},
{
"name": "after",
"in": "query",
"description": "请求此时间戳之前(更旧的数据)的分页内容,传的值为对应接口的`ts`",
"schema": {
"type": "string"
}
},
{
"name": "before",
"in": "query",
"description": "请求此时间戳之后(更新的数据)的分页内容,传的值为对应接口的`ts`",
"schema": {
"type": "string"
}
},
{
"name": "limit",
"in": "query",
"description": "分页返回的结果集数量最大为100不填默认返回100条",
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "成功",
"content": {
"application/json": {
"schema": {
"type": "object",
"example": {
"code": "0",
"msg": "",
"data": [
[
"1597026383085",
"3.721",
"3.743",
"3.677",
"3.708",
"8422410",
"22698348.04828491"
],
[
"1597026383085",
"3.731",
"3.799",
"3.494",
"3.72",
"24912403",
"67632347.24399722"
]
]
}
}
}
}
}
}
}
},
"/api/v5/market/mark-price-candles": {
"get": {
"tags": [
"Market Data"
],
"summary": "获取标记价格K线数据",
"parameters": [
{
"name": "instId",
"in": "query",
"description": "现货指数,如:`BTC-USD-SWAP`",
"schema": {
"type": "string"
},
"required": true,
"example": "BTC-USD-SWAP"
},
{
"name": "bar",
"in": "query",
"description": "时间粒度,默认值`1m`<br>如 [1m/3m/5m/15m/30m/1H/2H/4H/6H/12H/1D/1W/1M/3M/6M/1Y]",
"schema": {
"type": "string"
}
},
{
"name": "after",
"in": "query",
"description": "请求此时间戳之前(更旧的数据)的分页内容,传的值为对应接口的`ts`",
"schema": {
"type": "string"
}
},
{
"name": "before",
"in": "query",
"description": "请求此时间戳之后(更新的数据)的分页内容,传的值为对应接口的`ts`",
"schema": {
"type": "string"
}
},
{
"name": "limit",
"in": "query",
"description": "分页返回的结果集数量最大为100不填默认返回100条",
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "成功",
"content": {
"application/json": {
"schema": {
"type": "object",
"example": {
"code": "0",
"msg": "",
"data": [
[
"1597026383085",
"3.721",
"3.743",
"3.677",
"3.708"
],
[
"1597026383085",
"3.731",
"3.799",
"3.494",
"3.72"
]
]
}
}
}
}
}
}
}
},
"/api/v5/market/trades": {
"get": {
"tags": [
"Market Data"
],
"summary": "获取交易产品公共成交数据",
"parameters": [
{
"name": "instId",
"in": "query",
"description": "产品ID`BTC-USDT`",
"schema": {
"type": "string"
},
"required": true,
"example": "BTC-USDT"
},
{
"name": "limit",
"in": "query",
"description": "分页返回的结果集数量最大为500不填默认返回100条",
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "成功",
"content": {
"application/json": {
"schema": {
"type": "object",
"example": {
"code": "0",
"msg": "",
"data": [
{
"instId": "BTC-USDT",
"tradeId": "9",
"px": "0.016",
"sz": "50",
"side": "buy",
"ts": "1597026383085"
},
{
"instId": "BTC-USDT",
"tradeId": "9",
"px": "0.016",
"sz": "50",
"side": "buy",
"ts": "1597026383085"
}
]
}
}
}
}
}
}
}
},
"/api/v5/market/platform-24-volume": {
"get": {
"tags": [
"Market Data"
],
"summary": "获取平台24小时总成交量",
"responses": {
"200": {
"description": "成功",
"content": {
"application/json": {
"schema": {
"type": "object",
"example": {
"code": "0",
"msg": "",
"data": [
{
"volUsd": "2222",
"volCny": "14220.8"
}
]
}
}
}
}
}
}
}
},
"/api/v5/market/open-oracle": {
"get": {
"tags": [
"Market Data"
],
"summary": "Oracle上链交易数据",
"responses": {
"200": {
"description": "成功",
"content": {
"application/json": {
"schema": {
"type": "object",
"example": {
"code": "0",
"msg": "",
"data": {
"messages": [
"0x00000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000060d98cc000000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000000000000000000000000000000000081a06ed800000000000000000000000000000000000000000000000000000000000000006707269636573000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000034254430000000000000000000000000000000000000000000000000000000000"
],
"prices": {
"BTC": "34796.4"
},
"signatures": [
"0xa8124d0dd7a6cd46aafc752272d2e67b09f0abb0f759c55712cf0c100e5ed6ad25853d97fd691c47539eac08d7e7b0ce3f6d1e8f6fa15850d8099718d37af376000000000000000000000000000000000000000000000000000000000000001c"
],
"timestamp": "1624870080"
}
}
}
}
}
}
}
}
},
"/api/v5/market/index-components": {
"get": {
"tags": [
"Market Data"
],
"summary": "获取指数成分数据",
"parameters": [
{
"name": "index",
"in": "query",
"description": "指数,如:`BTC-USDT`",
"required": true,
"schema": {
"type": "string"
},
"example": "BTC-USDT"
}
],
"responses": {
"200": {
"description": "成功",
"content": {
"application/json": {
"schema": {
"type": "object",
"example": {
"code": "0",
"msg": "",
"data": {
"components": [
{
"symbol": "BTC/USDT",
"symPx": "52733.2",
"wgt": "0.250",
"cnvPx": "52733.2",
"exch": "OKEx"
},
{
"symbol": "BTC/USDT",
"symPx": "52739.87000000",
"wgt": "0.250",
"cnvPx": "52739.87000000",
"exch": "Binance"
},
{
"symbol": "BTC/USDT",
"symPx": "52729.1",
"wgt": "0.250",
"cnvPx": "52729.1",
"exch": "Huobi"
},
{
"symbol": "BTC/USDT",
"symPx": "52739.47929397",
"wgt": "0.250",
"cnvPx": "52739.47929397",
"exch": "Poloniex"
}
],
"last": "52735.4123234925",
"index": "BTC-USDT",
"ts": "1630985335599"
}
}
}
}
}
}
}
}
}
},
"components": {}
}

1125
okex/docs/public.json Normal file

File diff suppressed because it is too large Load Diff

2015
okex/docs/trade.json Normal file

File diff suppressed because it is too large Load Diff

4
okex/gen.sh Normal file
View File

@ -0,0 +1,4 @@
oapi-codegen -package trade -alias-types -generate types,client,spec docs/trade.json > api/trade/trade.gen.go
oapi-codegen -package account -alias-types -generate types,client,spec docs/account.json > api/account/account.gen.go
oapi-codegen -package market -alias-types -generate types,client,spec docs/market.json > api/market/market.gen.go
oapi-codegen -package public -alias-types -generate types,client,spec docs/public.json > api/public/public.gen.go

849
okex/okex.go Normal file
View File

@ -0,0 +1,849 @@
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, &params, 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, &params, 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, &param, 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{
// 非必填<br>保证金币种USDT<br>仅适用于单币种保证金模式下的全仓杠杆订单
// Ccy *string `json:"ccy,omitempty"`
// 必填<br>产品ID`BTC-USDT`
InstId: act.Symbol,
// 必填<br>订单类型。<br>`conditional`:单向止盈止损<br>`oco`:双向止盈止损<br>`trigger`:计划委托<br>`iceberg`:冰山委托<br>`twap`:时间加权委托
OrdType: "conditional",
// 非必填<br>委托价格<br>委托价格为-1时执行市价委托<br>适用于`计划委托`
OrderPx: &orderPx,
// 可选<br>持仓方向<br>在双向持仓模式下必填,且仅可选择 `long` 或 `short`
PosSide: &posSide,
// 非必填<br>挂单限制价<br>适用于`冰山委托`和`时间加权委托`
// PxLimit *string `json:"pxLimit,omitempty"`
// 非必填<br>距离盘口的比例价距<br>适用于`冰山委托`和`时间加权委托`
// PxSpread *string `json:"pxSpread,omitempty"`
// 非必填<br>距离盘口的比例<br>pxVar和pxSpread只能传入一个<br>适用于`冰山委托`和`时间加权委托`
// PxVar *string `json:"pxVar,omitempty"`
// 非必填<br>是否只减仓,`true` 或 `false`,默认`false`
// 仅适用于币币杠杆,以及买卖模式下的交割/永续
ReduceOnly: &reduceOnly,
// 必填<br>订单方向。买:`buy` 卖:`sell`
Side: side,
// 非必填<br>止损委托价,如果填写此参数,必须填写止损触发价<br>委托价格为-1时执行市价止损<br>适用于`止盈止损委托`
SlOrdPx: &orderPx,
// 非必填<br>止损触发价,如果填写此参数,必须填写止损委托价<br>适用于`止盈止损委托`
SlTriggerPx: &triggerPx,
// 必填<br>委托数量
Sz: fmt.Sprintf("%d", int(act.Amount)),
// 非必填<br>单笔数量<br>适用于`冰山委托`和`时间加权委托`
// SzLimit *string `json:"szLimit,omitempty"`
// 必填<br>交易模式<br>保证金模式:`isolated`:逐仓 `cross`<br>全仓非保证金模式:`cash`:非保证金
TdMode: b.cfg.TdMode,
// 非必填<br>市价单委托数量的类型<br>交易货币:`base_ccy`<br>计价货币:`quote_ccy`<br>仅适用于币币订单
// TgtCcy *string `json:"tgtCcy,omitempty"`
// 非必填<br>挂单限制价<br>适用于`时间加权委托`
// TimeInterval *string `json:"timeInterval,omitempty"`
// 非必填<br>止盈委托价,如果填写此参数,必须填写止盈触发价<br>委托价格为-1时执行市价止盈<br>适用于`止盈止损委托`
// TpOrdPx ,
// 非必填<br>止盈触发价,如果填写此参数,必须填写止盈委托价<br>适用于`止盈止损委托`
// TpTriggerPx *string `json:"tpTriggerPx,omitempty"`
// 非必填<br>计划委托触发价格<br>适用于`计划委托`
// 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"`
// 必填<br>产品ID`BTC-USDT`
InstId: act.Symbol,
// 必填<br>订单类型。<br>市价单:`market`<br>限价单:`limit`<br>只做maker单`post_only`<br>全部成交或立即取消:`fok`<br>立即成交并取消剩余:`ioc`<br>市价委托立即成交并取消剩余:`optimal_limit_ioc`(仅适用交割、永续)
OrdType: ordType,
// 可选<br>持仓方向<br>在双向持仓模式下必填,且仅可选择 `long` 或 `short`
PosSide: &posSide,
// 可选<br>委托价格<br>仅适用于`limit`、`post_only`、`fok`、`ioc`类型的订单
Px: &px,
// 非必填<br>是否只减仓,`true` 或 `false`,默认`false`<br>仅适用于币币杠杆订单
// ReduceOnly: &reduceOnly,
// 必填<br>订单方向。买:`buy` 卖:`sell`
Side: side,
// 必填<br>委托数量
Sz: fmt.Sprintf("%d", int(act.Amount)),
// 非必填<br>订单标签<br>字母区分大小写与数字的组合可以是纯字母、纯数字且长度在1-8位之间。
Tag: &tag,
// 必填<br>交易模式<br>保证金模式:`isolated`:逐仓 `cross`<br>全仓非保证金模式:`cash`:非保证金
TdMode: b.cfg.TdMode,
// 非必填<br>市价单委托数量的类型<br>交易货币:`base_ccy`<br>计价货币:`quote_ccy`<br>仅适用于币币订单
// 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, &params, 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, &params, 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
}

156
okex/okex_test.go Normal file
View File

@ -0,0 +1,156 @@
package okex
import (
"fmt"
"testing"
"time"
"git.qtrade.icu/coin-quant/exchange"
"git.qtrade.icu/coin-quant/trademodel"
log "github.com/sirupsen/logrus"
"github.com/spf13/viper"
)
var (
testClt *OkxTrader
)
func getTestClt() *OkxTrader {
cfgPath := "../test/test.yaml"
cfg := viper.New()
cfg.SetConfigFile(cfgPath)
err := cfg.ReadInConfig()
if err != nil {
log.Fatal("ReadInConfig failed:" + err.Error())
}
testClt, err = NewOkxTrader(exchange.WrapViper(cfg), "okx")
if err != nil {
log.Fatal("create client failed:" + err.Error())
}
testClt.Start()
return testClt
}
func TestMain(m *testing.M) {
testClt = getTestClt()
m.Run()
}
func TestSymbols(t *testing.T) {
symbols, err := testClt.Symbols()
if err != nil {
t.Fatal(err.Error())
}
for _, v := range symbols {
t.Log(v.Symbol, v.Precision, v.PriceStep, v.AmountPrecision, v.AmountStep)
}
}
func TestKline(t *testing.T) {
// start, _ := time.Parse("2006-01-02 15:04:05", "2023-01-01 00:00:00")
start := time.Now().Add(time.Minute * -5)
end := start.Add(time.Hour + time.Second)
datas, err := testClt.GetKline("BTC-USDT-SWAP", "1m", start, end)
if err != nil {
t.Fatal(err)
}
for _, v := range datas {
t.Log(v)
}
if len(datas) != 60 {
t.Fatal("Kline resp not match:", len(datas))
}
}
func TestOrder(t *testing.T) {
order, err := testClt.ProcessOrder(trademodel.TradeAction{
Symbol: "APT-USDT-SWAP",
Action: trademodel.OpenShort,
Amount: 1,
Price: 20,
Time: time.Now(),
})
if err != nil {
t.Fatal(err.Error())
}
t.Log(*order)
time.Sleep(time.Second)
_, err = testClt.CancelAllOrders()
if err != nil {
t.Fatal(err.Error())
}
}
func TestStopOrder(t *testing.T) {
order, err := testClt.ProcessOrder(trademodel.TradeAction{
Symbol: "APT-USDT-SWAP",
Action: trademodel.StopShort,
Amount: 1,
Price: 17,
Time: time.Now(),
})
if err != nil {
t.Fatal(err.Error())
}
t.Log(*order)
time.Sleep(time.Second)
_, err = testClt.CancelAllOrders()
if err != nil {
t.Fatal(err.Error())
}
}
func TestCancelAllOrder(t *testing.T) {
_, err := testClt.CancelAllOrders()
if err != nil {
t.Fatal(err.Error())
}
}
func TestCancelOrder(t *testing.T) {
act := trademodel.TradeAction{
Action: trademodel.OpenLong,
Amount: 1,
Price: 2000,
Time: time.Now(),
}
order, err := testClt.ProcessOrder(act)
if err != nil {
t.Fatal(err.Error())
}
t.Log(*order)
time.Sleep(time.Second * 5)
ret, err := testClt.CancelOrder(order)
if err != nil {
t.Fatal(err.Error())
}
t.Log("ret:", *ret)
}
func TestCancelStopOrder(t *testing.T) {
act := trademodel.TradeAction{
Action: trademodel.StopLong,
Amount: 1,
Price: 2000,
Time: time.Now(),
}
order, err := testClt.ProcessOrder(act)
if err != nil {
t.Fatal(err.Error())
}
t.Log(*order)
time.Sleep(time.Second * 5)
ret, err := testClt.CancelOrder(order)
if err != nil {
t.Fatal(err.Error())
}
t.Log("ret:", *ret)
}
func TestDepth(t *testing.T) {
param := exchange.WatchParam{Type: exchange.WatchTypeDepth, Param: map[string]string{"symbol": "ETH-USDT-SWAP", "name": "depth"}}
testClt.Watch(param, func(data interface{}) {
fmt.Println(data)
})
time.Sleep(time.Second * 10)
}

359
okex/okex_ws.go Normal file
View File

@ -0,0 +1,359 @@
package okex
import (
"crypto/hmac"
"crypto/sha256"
"encoding/base64"
"errors"
"fmt"
"github.com/bitly/go-simplejson"
"strconv"
"time"
"git.qtrade.icu/coin-quant/exchange/ws"
. "git.qtrade.icu/coin-quant/trademodel"
"github.com/mitchellh/mapstructure"
log "github.com/sirupsen/logrus"
)
func (b *OkxTrader) runPrivate() (err error) {
b.wsUser, err = ws.NewWSConn(WSOkexPrivate, func(ws *ws.WSConn) error {
login := OPParam{
OP: "login",
Args: []interface{}{NewLoginArg(b.cfg.ApiKey, b.cfg.Passphrase, b.cfg.SecretKey)},
}
return ws.WriteMsg(login)
}, b.parseUserMsg)
return
}
type LoginArg struct {
ApiKey string `json:"apiKey"`
Passphrase string `json:"passphrase"`
Timestamp int64 `json:"timestamp"`
Sign string `json:"sign"`
}
func NewLoginArg(apiKey, pass, secret string) *LoginArg {
a := new(LoginArg)
a.ApiKey = apiKey
a.Passphrase = pass
t := time.Now()
a.Timestamp = t.Unix()
src := fmt.Sprintf("%dGET/users/self/verify", a.Timestamp)
h := hmac.New(sha256.New, []byte(secret))
h.Write([]byte(src))
ret := h.Sum(nil)
n := base64.StdEncoding.EncodedLen(len(ret))
dst := make([]byte, n)
base64.StdEncoding.Encode(dst, ret)
a.Sign = string(dst)
return a
}
type OPArg struct {
Channel string `json:"channel"`
InstType string `json:"instType"`
Uly string `json:"uly,omitempty"`
InstID string `json:"instId,omitempty"`
}
type OPParam struct {
OP string `json:"op"`
Args []interface{} `json:"args"`
}
func (b *OkxTrader) getSymbolSub() (p OPParam) {
p.OP = "subscribe"
p.Args = append(p.Args, OPArg{Channel: "orders", InstType: "ANY"})
p.Args = append(p.Args, OPArg{Channel: "orders-algo", InstType: "ANY"})
p.Args = append(p.Args, OPArg{Channel: "algo-advance", InstType: "ANY"})
p.Args = append(p.Args, OPArg{Channel: "positions", InstType: "ANY"})
return
}
// {"arg":{"channel":"trades","instId":"BSV-USD-210924"},"data":[{"instId":"BSV-USD-210924","tradeId":"1957771","px":"166.5","sz":"22","side":"sell","ts":"1621862533713"}]}
// {"arg":{"channel":"books","instId":"BSV-USD-210924"},"action":"update","data":[{"asks":[["167.4","54","0","1"]],"bids":[["164.81","0","0","0"],["164.69","79","0","1"]],"ts":"1621862579059","checksum":71082836}]}
// books5 {"arg":{"channel":"books5","instId":"BSV-USD-210924"},"data":[{"asks":[["166.35","20","0","1"],["166.4","135","0","2"],["166.42","86","0","1"],["166.45","310","0","1"],["166.46","61","0","2"]],"bids":[["166.14","33","0","1"],["166.07","106","0","1"],["166.05","2","0","1"],["166.04","97","0","1"],["165.98","20","0","1"]],"instId":"BSV-USD-210924","ts":"1621862688397"}]}
func (b *OkxTrader) parseUserMsg(message []byte) (err error) {
var sj *simplejson.Json
var evt string
var channel string
sj, err = simplejson.NewJson(message)
if err != nil {
log.Warnf("parse json error:%s", string(message))
return
}
evtValue, ok := sj.CheckGet("event")
if ok {
evt, err = evtValue.String()
if err != nil {
log.Warnf("login error:%s %s", string(message), err.Error())
return
}
switch evt {
case "error":
log.Errorf("recv error: %s", string(message))
return
case "login":
var code string
code, err = sj.Get("code").String()
if code != "0" {
log.Warnf("login error:%s %s", string(message), err.Error())
return
}
param := b.getSymbolSub()
err = b.wsUser.WriteMsg(param)
if err != nil {
return
}
default:
}
return
}
arg, ok := sj.CheckGet("arg")
if !ok {
return
}
channelValue, ok := arg.CheckGet("channel")
if !ok {
return
}
channel, err = channelValue.String()
if err != nil {
log.Warnf("channelValue %v is not string error:%s", channelValue, err.Error())
return
}
switch channel {
case "orders":
var orders []OrderNormal
orders, err = parseOkexOrder(sj.Get("data"))
if err != nil {
log.Warnf("parseOkexOrder error:%s, %s", orders, err.Error())
return
}
for _, v := range orders {
o := v.GetOrder()
if o == nil {
continue
}
o.Status = OrderStatusFilled
b.ordersCache.Delete(v.OrdID)
b.stopOrdersCache.Delete(v.OrdID)
if b.tradeCb != nil {
b.tradeCb(o)
}
}
case "orders-algo":
// 算法单最终还是会生成一个普通单子
var algoOrders []AlgoOrder
algoOrders, err = parseOkexAlgoOrder(sj.Get("data"))
if err != nil {
log.Warnf("parseOkexAlgoOrder error:%s, %s", algoOrders, err.Error())
return
}
for _, v := range algoOrders {
if v.State == "filled" {
b.stopOrdersCache.Delete(v.AlgoID)
}
}
case "algo-advance":
case "positions":
var pos []OKEXPos
pos, err = parseOkexPos(sj.Get("data"))
if err != nil {
log.Warnf("parseOkexPos error:%s, %s", pos, err.Error())
return
}
for _, v := range pos {
t := v.GetPos()
if t == nil {
continue
}
if b.positionCb != nil {
b.positionCb(t)
}
}
default:
}
return
}
func (o *OrderNormal) GetOrder() (ret *Order) {
if o.State != "filled" {
return
}
var side = transSide(o.Side)
ret = &Order{
OrderID: o.OrdID,
Symbol: o.InstID,
// Currency:o.
Amount: parseFloat(o.Sz),
Price: parseFloat(o.AvgPx),
Status: o.State,
Side: side,
Time: parseTime(o.FillTime),
}
return
}
func transSide(oSide string) (side string) {
switch oSide {
case "buy":
side = "long"
case "sell":
side = "short"
default:
side = oSide
}
return
}
func parseTime(v string) time.Time {
n, err := strconv.ParseInt(v, 10, 64)
if err != nil {
log.Errorf("parseTime failed: %s", err.Error())
return time.Now()
}
return time.Unix(n/1000, (n%1000)*int64(time.Millisecond))
}
// {"accFillSz":"0","amendResult":"","avgPx":"0","cTime":"1639669567849","category":"normal","ccy":"","clOrdId":"","code":"0","execType":"","fee":"0","feeCcy":"USDT","fillFee":"0","fillFeeCcy":"","fillNotionalUsd":"","fillPx":"","fillSz":"0","fillTime":"","instId":"SOL-USDT-SWAP","instType":"SWAP","lever":"10","msg":"","notionalUsd":"185.666","ordId":"391737792421838866","ordType":"limit","pnl":"0","posSide":"short","px":"200","rebate":"0","rebateCcy":"USDT","reduceOnly":"false","reqId":"","side":"sell","slOrdPx":"","slTriggerPx":"","slTriggerPxType":"last","source":"","state":"live","sz":"1","tag":"","tdMode":"isolated","tgtCcy":"","tpOrdPx":"","tpTriggerPx":"","tpTriggerPxType":"last","tradeId":"","uTime":"1639669567849"}
func parseOkexOrder(sj *simplejson.Json) (orders []OrderNormal, err error) {
arr, err := sj.Array()
if err != nil {
return
}
var ret map[string]interface{}
var ok bool
for _, v := range arr {
ret, ok = v.(map[string]interface{})
if !ok {
err = errors.New("parseOrderBook error")
return
}
var o OrderNormal
err = mapstructure.Decode(ret, &o)
if err != nil {
return
}
orders = append(orders, o)
}
return
}
func parseOkexPos(sj *simplejson.Json) (trades []OKEXPos, err error) {
arr, err := sj.Array()
if err != nil {
return
}
var ret map[string]interface{}
var ok bool
for _, v := range arr {
ret, ok = v.(map[string]interface{})
if !ok {
err = errors.New("parseOkexTrades error")
return
}
var t OKEXPos
err = mapstructure.Decode(ret, &t)
if err != nil {
return
}
trades = append(trades, t)
}
return
}
type OKEXPos struct {
Adl string `json:"adl"`
AvailPos string `json:"availPos"`
AvgPx string `json:"avgPx"`
CTime string `json:"cTime"`
Ccy string `json:"ccy"`
DeltaBS string `json:"deltaBS"`
DeltaPA string `json:"deltaPA"`
GammaBS string `json:"gammaBS"`
GammaPA string `json:"gammaPA"`
Imr string `json:"imr"`
InstID string `json:"instId"`
InstType string `json:"instType"`
Interest string `json:"interest"`
Last string `json:"last"`
Lever string `json:"lever"`
Liab string `json:"liab"`
LiabCcy string `json:"liabCcy"`
LiqPx string `json:"liqPx"`
MarkPx string `json:"markPx"`
Margin string `json:"margin"`
MgnMode string `json:"mgnMode"`
MgnRatio string `json:"mgnRatio"`
Mmr string `json:"mmr"`
NotionalUsd string `json:"notionalUsd"`
OptVal string `json:"optVal"`
PTime string `json:"pTime"`
Pos string `json:"pos"`
PosCcy string `json:"posCcy"`
PosID string `json:"posId"`
PosSide string `json:"posSide"`
ThetaBS string `json:"thetaBS"`
ThetaPA string `json:"thetaPA"`
TradeID string `json:"tradeId"`
UTime string `json:"uTime"`
Upl string `json:"upl"`
UplRatio string `json:"uplRatio"`
VegaBS string `json:"vegaBS"`
VegaPA string `json:"vegaPA"`
}
func (ot *OKEXPos) GetPos() (pos *Position) {
var typ int
hold := parseFloat(ot.Pos)
if ot.PosSide == "long" {
typ = Long
} else if ot.PosSide == "short" {
typ = Short
hold = 0 - hold
} else if ot.PosSide == "net" {
if hold > 0 {
typ = Long
} else {
typ = Short
}
}
price := parseFloat(ot.AvgPx)
pos = &Position{
Symbol: ot.InstID,
Type: typ,
Hold: hold,
Price: price,
ProfitRatio: parseFloat(ot.Upl),
}
return
}
func parseOkexAlgoOrder(sj *simplejson.Json) (orders []AlgoOrder, err error) {
arr, err := sj.Array()
if err != nil {
return
}
var ret map[string]interface{}
var ok bool
for _, v := range arr {
ret, ok = v.(map[string]interface{})
if !ok {
err = errors.New("parseOrderBook error")
return
}
var o AlgoOrder
err = mapstructure.Decode(ret, &o)
if err != nil {
return
}
orders = append(orders, o)
}
return
}

312
okex/okex_ws_public.go Normal file
View File

@ -0,0 +1,312 @@
package okex
import (
"errors"
"fmt"
"github.com/bitly/go-simplejson"
"strconv"
"time"
"git.qtrade.icu/coin-quant/exchange/ws"
log "github.com/sirupsen/logrus"
. "git.qtrade.icu/coin-quant/trademodel"
)
func (b *OkxTrader) runPublic() (err error) {
b.wsPublic, err = ws.NewWSConn(WSOkexPUbilc, func(ws *ws.WSConn) error {
// watch when reconnect
for _, v := range b.watchPublics {
err1 := ws.WriteMsg(v)
if err1 != nil {
return err1
}
}
return nil
}, b.parsePublicMsg)
return
}
func (b *OkxTrader) parsePublicMsg(message []byte) (err error) {
sj, err := simplejson.NewJson(message)
if err != nil {
log.Warnf("parse json error:%s", string(message))
return
}
_, ok := sj.CheckGet("event")
if ok {
return
}
arg, ok := sj.CheckGet("arg")
if !ok {
return
}
channelValue, ok := arg.CheckGet("channel")
if !ok {
return
}
symbol, err := arg.Get("instId").String()
if err != nil {
log.Warnf("okex public ws message instId not found %s, %s", string(message), err.Error())
}
_ = symbol
channel, err := channelValue.String()
if err != nil {
log.Warnf("channelValue %v is not string error:%s", channelValue, err.Error())
return
}
switch channel {
case "books5":
value, ok := sj.CheckGet("data")
if !ok {
return
}
if b.depthCb == nil {
return
}
var depths5 []*Depth
depths5, err = parseOkexBooks5(value)
if err != nil {
log.Warnf("parseOkexBooks5 failed:%s, %s", string(message), err.Error())
return
}
for _, v := range depths5 {
b.depthCb(v)
}
case "trades":
value, ok := sj.CheckGet("data")
if !ok {
return
}
if b.tradeMarketCb == nil {
return
}
var trades []*Trade
trades, err = parseOkexTrade(value)
if err != nil {
log.Warnf("parseOkexTrade failed:%s, %s", string(message), err.Error())
return
}
for _, v := range trades {
b.tradeMarketCb(v)
}
case "candle1m":
value, ok := sj.CheckGet("data")
if !ok {
return
}
// fmt.Println("candle1m:", string(message))
var candles []*Candle
candles, err = parseWsCandle(value)
if err != nil {
log.Warnf("parseWsCandle failed:%s, %s", string(message), err.Error())
return
}
if len(candles) == 0 {
return
} else if len(candles) != 1 {
log.Warnf("parseWsCandle candles len warn :%d, %v", len(candles), err)
}
if b.klineCb == nil {
return
}
b.klineCb(candles[0])
default:
}
return
}
// books5 {"arg":{"channel":"books5","instId":"BSV-USD-210924"},"data":[{"asks":[["166.35","20","0","1"],["166.4","135","0","2"],["166.42","86","0","1"],["166.45","310","0","1"],["166.46","61","0","2"]],"bids":[["166.14","33","0","1"],["166.07","106","0","1"],["166.05","2","0","1"],["166.04","97","0","1"],["165.98","20","0","1"]],"instId":"BSV-USD-210924","ts":"1621862688397"}]}
func parseOkexBooks5(sj *simplejson.Json) (depths []*Depth, err error) {
arr, err := sj.Array()
if err != nil {
err = fmt.Errorf("parseOkexBooks5 not array: %w", err)
return
}
var obj, asks, bids *simplejson.Json
var askInfo, bidInfo []DepthInfo
var tsStr string
var nTs int64
for k := range arr {
obj = sj.GetIndex(k)
asks = obj.Get("asks")
bids = obj.Get("bids")
if asks == nil || bids == nil {
err = errors.New("data error")
return
}
askInfo, err = parseOrderBook(asks)
if err != nil {
return
}
bidInfo, err = parseOrderBook(bids)
if err != nil {
return
}
tsStr, err = obj.Get("ts").String()
if err != nil {
return
}
dp := Depth{Buys: bidInfo, Sells: askInfo}
nTs, err = strconv.ParseInt(tsStr, 10, 64)
if err != nil {
return
}
dp.UpdateTime = time.Unix(nTs/1000, (nTs%1000)*int64(time.Millisecond))
depths = append(depths, &dp)
}
return
}
func parseOrderBook(sj *simplejson.Json) (datas []DepthInfo, err error) {
arr, err := sj.Array()
if err != nil {
return
}
var ret []interface{}
var ok bool
for _, v := range arr {
ret, ok = v.([]interface{})
if !ok {
err = errors.New("parseOrderBook error")
return
}
if len(ret) != 4 {
err = errors.New("parseOrderBook len error")
return
}
di := DepthInfo{Price: getFloat(ret[0]), Amount: getFloat(ret[1])}
datas = append(datas, di)
}
return
}
func parseOkexTrade(sj *simplejson.Json) (trades []*Trade, err error) {
arr, err := sj.Array()
if err != nil {
return
}
// {"instId":"BSV-USD-210924","tradeId":"1957771","px":"166.5","sz":"22","side":"sell","ts":"1621862533713"}
var temp map[string]interface{}
var ok bool
for _, v := range arr {
temp, ok = v.(map[string]interface{})
if !ok {
log.Warnf("data error:%#v", temp)
continue
}
var t Trade
t.Remark = getStr(temp, "instId")
t.ID = getStr(temp, "tradeId")
t.Side = getStr(temp, "side")
t.Price = getStrFloat(temp, "px")
t.Amount = getStrFloat(temp, "sz")
nTs := getStrTs(temp, "ts")
t.Time = time.Unix(nTs/1000, int64(time.Millisecond)*(nTs%1000))
trades = append(trades, &t)
}
return
}
func getStrTs(dict map[string]interface{}, key string) (nTs int64) {
str := getStr(dict, key)
if str == "" {
log.Warnf("getStrTs of %s empty", key)
return
}
nTs, err := strconv.ParseInt(str, 10, 64)
if err != nil {
log.Warnf("getStrTs of %s parse int err: %s", key, err.Error())
return
}
return nTs
// t = time.Unix(nTs/1000, (nTs%1000)*int64(time.Millisecond))
return
}
func getStrFloat(dict map[string]interface{}, key string) float64 {
str := getStr(dict, key)
if str == "" {
log.Warnf("getStrFloat of %s empty", key)
return 0
}
f, err := strconv.ParseFloat(str, 64)
if err != nil {
log.Warnf("getStrFloat of %s failed: %s", key, err.Error())
return 0
}
return f
}
func getStr(dict map[string]interface{}, key string) string {
v, ok := dict[key]
if !ok {
log.Warnf("getStr of %s empty", key)
return ""
}
str, ok := v.(string)
if !ok {
log.Warnf("getStr of %s type error: %#v", key, v)
return ""
}
return str
}
func getFloat(v interface{}) float64 {
str, ok := v.(string)
if !ok {
return 0
}
f, err := strconv.ParseFloat(str, 64)
if err != nil {
log.Warnf("getFloat %#v failed: %s", v, err.Error())
return 0
}
return f
}
func parseWsCandle(sj *simplejson.Json) (ret []*Candle, err error) {
arr, err := sj.Array()
if err != nil {
return
}
// {"instId":"BSV-USD-210924","tradeId":"1957771","px":"166.5","sz":"22","side":"sell","ts":"1621862533713"}
var values []interface{}
var ok bool
var nTs int64
for _, v := range arr {
values, ok = v.([]interface{})
if !ok {
log.Warnf("transWsCandle data error:%#v", values)
continue
}
if len(values) != 9 {
log.Warnf("transWsCandle data len error:%#v", values)
continue
}
nTs, err = strconv.ParseInt(values[0].(string), 10, 64)
if err != nil {
panic(fmt.Sprintf("trans candle error: %#v", values))
return
}
// unfinished kline
if values[8].(string) == "0" {
continue
}
temp := Candle{
ID: 0,
Start: nTs / 1000,
Open: parseFloat(values[1].(string)),
High: parseFloat(values[2].(string)),
Low: parseFloat(values[3].(string)),
Close: parseFloat(values[4].(string)),
Volume: parseFloat(values[5].(string)),
Turnover: parseFloat(values[7].(string)),
}
ret = append(ret, &temp)
}
return
}

124
okex/tool.go Normal file
View File

@ -0,0 +1,124 @@
package okex
import (
"encoding/json"
"errors"
"fmt"
"git.qtrade.icu/coin-quant/exchange/okex/api/market"
. "git.qtrade.icu/coin-quant/trademodel"
"strconv"
"time"
)
func transCandle(values [9]string) (ret *Candle) {
nTs, err := strconv.ParseInt(values[0], 10, 64)
if err != nil {
panic(fmt.Sprintf("trans candle error: %#v", values))
return nil
}
ret = &Candle{
ID: 0,
Start: nTs / 1000,
Open: parseFloat(values[1]),
High: parseFloat(values[2]),
Low: parseFloat(values[3]),
Close: parseFloat(values[4]),
Volume: parseFloat(values[5]),
Turnover: parseFloat(values[7]),
}
return
}
func parseFloat(str string) float64 {
if str == "" {
return 0
}
f, err := strconv.ParseFloat(str, 64)
if err != nil {
panic("okex parseFloat error:" + err.Error())
}
return f
}
func parseCandles(resp *market.GetApiV5MarketHistoryCandlesResponse) (candles []*Candle, err error) {
var candleResp CandleResp
err = json.Unmarshal(resp.Body, &candleResp)
if err != nil {
return
}
if candleResp.Code != "0" {
err = errors.New(string(resp.Body))
return
}
for _, v := range candleResp.Data {
// unfinished candle
if v[8] == "0" {
continue
}
temp := transCandle(v)
candles = append(candles, temp)
}
return
}
func parsePostOrders(symbol, status, side string, amount, price float64, body []byte) (ret []*Order, err error) {
temp := OKEXOrder{}
err = json.Unmarshal(body, &temp)
if err != nil {
return
}
if temp.Code != "0" {
err = fmt.Errorf("error resp: %s", string(body))
return
}
for _, v := range temp.Data {
if v.SCode != "0" {
err = fmt.Errorf("%s %s", v.SCode, v.SMsg)
return
}
temp := &Order{
OrderID: v.OrdID,
Symbol: symbol,
// Currency
Side: side,
Status: status,
Price: price,
Amount: amount,
Time: time.Now(),
}
ret = append(ret, temp)
}
return
}
func parsePostAlgoOrders(symbol, status, side string, amount, price float64, body []byte) (ret []*Order, err error) {
temp := OKEXAlgoOrder{}
err = json.Unmarshal(body, &temp)
if err != nil {
return
}
if temp.Code != "0" {
err = fmt.Errorf("error resp: %s", string(body))
return
}
for _, v := range temp.Data {
if v.SCode != "0" {
err = fmt.Errorf("%s %s", v.SCode, v.SMsg)
return
}
temp := &Order{
OrderID: v.AlgoID,
Symbol: symbol,
// Currency
Side: side,
Status: status,
Price: price,
Amount: amount,
Time: time.Now(),
}
ret = append(ret, temp)
}
return
}

49
pool.go Normal file
View File

@ -0,0 +1,49 @@
package exchange
import (
"fmt"
"github.com/spf13/viper"
"sync"
)
var (
exchangeFactory = map[string]NewExchangeFn{}
exchangeMutex sync.Mutex
exchanges = map[string]Exchange{}
)
type NewExchangeFn func(cfg Config, cltName string) (t Exchange, err error)
func RegisterExchange(name string, fn NewExchangeFn) {
exchangeFactory[name] = fn
}
func NewExchange(name string, cfg Config, cltName string) (ex Exchange, err error) {
exchangeMutex.Lock()
defer exchangeMutex.Unlock()
if cfg.GetBool("share_exchange") {
v, ok := exchanges[cltName]
if ok {
ex = v
return
}
defer func() {
if err == nil {
exchanges[cltName] = ex
}
}()
}
fn, ok := exchangeFactory[name]
if !ok {
err = fmt.Errorf("no such exchange %s", name)
return
}
ex, err = fn(cfg, cltName)
return
}
func NewExchangeViper(name, cltName string) (ex Exchange, err error) {
cfg := WrapViper(viper.GetViper())
return NewExchange(name, cfg, cltName)
}

204
ws/ws.go Normal file
View File

@ -0,0 +1,204 @@
package ws
import (
"bytes"
"fmt"
"net/url"
"sync"
"time"
"github.com/gorilla/websocket"
log "github.com/sirupsen/logrus"
)
var (
pongMsg = []byte("pong")
)
type WSInitFn func(ws *WSConn) error
type MessageFn func(message []byte) error
type PingFn func(ws *WSConn) error
type PongFn func(message []byte) bool
func defaultPingFn(ws *WSConn) error {
return ws.WriteText("ping")
}
func defaultPongFn(message []byte) bool {
return bytes.Equal(pongMsg, message)
}
type WSConn struct {
addr string
ws *websocket.Conn
initFn WSInitFn
messageFn MessageFn
pingFn PingFn
pongFn PongFn
closeCh chan int
writeMuetx sync.Mutex
wg sync.WaitGroup
}
func NewWSConnWithPingPong(addr string, initFn WSInitFn, messageFn MessageFn, ping PingFn, pong PongFn) (conn *WSConn, err error) {
conn = new(WSConn)
conn.addr = addr
conn.initFn = initFn
conn.messageFn = messageFn
conn.closeCh = make(chan int, 1)
conn.pingFn = ping
conn.pongFn = pong
err = conn.connect()
return
}
func NewWSConn(addr string, initFn WSInitFn, messageFn MessageFn) (conn *WSConn, err error) {
conn = new(WSConn)
conn.addr = addr
conn.initFn = initFn
conn.pingFn = defaultPingFn
conn.pongFn = defaultPongFn
conn.messageFn = messageFn
conn.closeCh = make(chan int, 1)
err = conn.connect()
if err != nil {
return
}
return
}
func (conn *WSConn) SetPingPongFn(ping PingFn, pong PongFn) {
conn.pingFn = ping
conn.pongFn = pong
}
func (conn *WSConn) Close() (err error) {
close(conn.closeCh)
conn.wg.Wait()
return
}
func (conn *WSConn) WriteText(value string) (err error) {
conn.writeMuetx.Lock()
if conn.ws != nil {
err = conn.ws.WriteMessage(websocket.TextMessage, []byte(value))
} else {
log.Warnf("WriteText ignore conn of %s not init", conn.addr)
}
conn.writeMuetx.Unlock()
return
}
func (conn *WSConn) WriteMsg(value interface{}) (err error) {
conn.writeMuetx.Lock()
if conn.ws != nil {
err = conn.ws.WriteJSON(value)
} else {
log.Warnf("WriteMsg ignore conn of %s not init", conn.addr)
}
conn.writeMuetx.Unlock()
return
}
func (conn *WSConn) connect() (err error) {
u, err := url.Parse(conn.addr)
if err != nil {
return
}
conn.ws, _, err = websocket.DefaultDialer.Dial(u.String(), nil)
if err != nil {
err = fmt.Errorf("connect to %s failed: %w", conn.addr, err)
return
}
if conn.initFn != nil {
err = conn.initFn(conn)
if err != nil {
conn.ws.Close()
return
}
}
go conn.loop()
return
}
func (conn *WSConn) loop() {
ws := conn.ws
ch := make(chan []byte, 1024)
needReconn := make(chan bool, 1)
go conn.readLoop(ws, ch, needReconn)
var msg []byte
var err error
var lastMsgTime time.Time
ticker := time.NewTicker(time.Second * 5)
conn.wg.Add(1)
defer conn.wg.Done()
var ok bool
defer ticker.Stop()
Out:
for {
select {
case msg, ok = <-ch:
if !ok {
break Out
}
lastMsgTime = time.Now()
if conn.pongFn != nil && conn.pongFn(msg) {
continue
}
err = conn.messageFn(msg)
if err != nil {
break Out
}
case <-ticker.C:
dur := time.Since(lastMsgTime)
if dur > time.Second*5 {
if conn.pingFn != nil {
err1 := conn.pingFn(conn)
if err1 != nil {
log.Errorf("ws pingFn failed: %s", err1.Error())
}
}
}
case <-conn.closeCh:
return
}
}
reConn := <-needReconn
if reConn {
for i := 0; i != 100; i++ {
err = conn.connect()
if err == nil {
break
}
log.Errorf("ws reconnect %d to failed: %s", i, err.Error())
time.Sleep(time.Second)
}
}
}
func (conn *WSConn) readLoop(ws *websocket.Conn, ch chan []byte, needConn chan bool) {
defer func() {
ws.Close()
close(ch)
close(needConn)
}()
var message []byte
var err error
for {
select {
case <-conn.closeCh:
needConn <- false
return
default:
}
_, message, err = ws.ReadMessage()
if err != nil {
log.Printf("%s ws read error: %s", conn.addr, err.Error())
needConn <- true
return
}
ch <- message
}
}

53
ws/ws_test.go Normal file
View File

@ -0,0 +1,53 @@
package ws
import (
"fmt"
"testing"
"time"
"github.com/gorilla/websocket"
)
func TestWsConn(t *testing.T) {
initFn := func(ws *WSConn) error {
var p = map[string]interface{}{
"op": "subscribe",
"args": []interface{}{
map[string]interface{}{"channel": "trades", "instType": "SWAP", "instId": "BTC-USDT-SWAP"},
},
}
return ws.WriteMsg(p)
}
messageFn := func(msg []byte) error {
fmt.Println("msg:", string(msg))
return nil
}
conn, err := NewWSConn("wss://wsaws.okx.com:8443/ws/v5/public", initFn, messageFn)
if err != nil {
t.Fatal(err.Error())
}
time.Sleep(time.Second * 5)
conn.Close()
}
func TestWsPing(t *testing.T) {
initFn := func(ws *WSConn) error {
return nil
}
messageFn := func(msg []byte) error {
fmt.Println("msg:", string(msg))
return nil
}
pongFn := func(msg []byte) bool {
fmt.Println("recv pong msg:", string(msg))
return true
}
conn, err := NewWSConn("wss://wsaws.okx.com:8443/ws/v5/public", initFn, messageFn)
if err != nil {
t.Fatal(err.Error())
}
conn.SetPingPongFn(defaultPingFn, pongFn)
conn.ws.WriteMessage(websocket.TextMessage, []byte("ping"))
time.Sleep(time.Second * 15)
conn.Close()
}