bbgo_origin/pkg/exchange/okex/exchange.go

342 lines
8.4 KiB
Go
Raw Permalink Normal View History

2021-05-22 19:34:40 +00:00
package okex
import (
2021-05-25 18:11:02 +00:00
"context"
"math"
"strconv"
"time"
2021-05-25 18:11:02 +00:00
2021-05-27 19:05:59 +00:00
"github.com/pkg/errors"
2021-05-22 19:34:40 +00:00
"github.com/sirupsen/logrus"
2022-05-03 03:14:53 +00:00
"golang.org/x/time/rate"
2022-01-10 17:36:19 +00:00
"github.com/c9s/bbgo/pkg/exchange/okex/okexapi"
"github.com/c9s/bbgo/pkg/fixedpoint"
2022-05-03 03:14:53 +00:00
"github.com/c9s/bbgo/pkg/types"
2021-05-22 19:34:40 +00:00
)
2022-05-03 04:11:50 +00:00
var marketDataLimiter = rate.NewLimiter(rate.Every(time.Second/10), 1)
2022-05-03 03:14:53 +00:00
2023-05-17 05:45:38 +00:00
const ID = "okex"
// PlatformToken is the platform currency of OKEx, pre-allocate static string here
const PlatformToken = "OKB"
2021-05-22 19:34:40 +00:00
var log = logrus.WithFields(logrus.Fields{
2023-05-17 05:45:38 +00:00
"exchange": ID,
2021-05-22 19:34:40 +00:00
})
type Exchange struct {
2021-05-25 18:11:02 +00:00
key, secret, passphrase string
client *okexapi.RestClient
}
func New(key, secret, passphrase string) *Exchange {
client := okexapi.NewClient()
if len(key) > 0 && len(secret) > 0 {
client.Auth(key, secret, passphrase)
}
2021-05-25 18:11:02 +00:00
return &Exchange{
2023-05-17 05:45:38 +00:00
key: key,
2021-05-25 18:11:02 +00:00
secret: secret,
passphrase: passphrase,
2021-05-26 17:07:25 +00:00
client: client,
2021-05-25 18:11:02 +00:00
}
}
func (e *Exchange) Name() types.ExchangeName {
return types.ExchangeOKEx
}
func (e *Exchange) QueryMarkets(ctx context.Context) (types.MarketMap, error) {
instruments, err := e.client.PublicDataService.NewGetInstrumentsRequest().
InstrumentType(okexapi.InstrumentTypeSpot).
Do(ctx)
if err != nil {
return nil, err
}
markets := types.MarketMap{}
for _, instrument := range instruments {
symbol := toGlobalSymbol(instrument.InstrumentID)
market := types.Market{
Symbol: symbol,
LocalSymbol: instrument.InstrumentID,
2021-05-25 18:11:02 +00:00
QuoteCurrency: instrument.QuoteCurrency,
BaseCurrency: instrument.BaseCurrency,
// convert tick size OKEx to precision
PricePrecision: int(-math.Log10(instrument.TickSize.Float64())),
VolumePrecision: int(-math.Log10(instrument.LotSize.Float64())),
// TickSize: OKEx's price tick, for BTC-USDT it's "0.1"
TickSize: instrument.TickSize,
2021-05-25 18:11:02 +00:00
// Quantity step size, for BTC-USDT, it's "0.00000001"
StepSize: instrument.LotSize,
2021-05-25 18:11:02 +00:00
// for BTC-USDT, it's "0.00001"
MinQuantity: instrument.MinSize,
2021-05-25 18:11:02 +00:00
// OKEx does not offer minimal notional, use 1 USD here.
MinNotional: fixedpoint.One,
MinAmount: fixedpoint.One,
2021-05-25 18:11:02 +00:00
}
markets[symbol] = market
}
return markets, nil
}
func (e *Exchange) QueryTicker(ctx context.Context, symbol string) (*types.Ticker, error) {
2021-05-25 18:44:03 +00:00
symbol = toLocalSymbol(symbol)
marketTicker, err := e.client.MarketTicker(symbol)
if err != nil {
return nil, err
}
2021-05-25 19:04:49 +00:00
return toGlobalTicker(*marketTicker), nil
2021-05-25 18:11:02 +00:00
}
2021-05-25 19:04:49 +00:00
func (e *Exchange) QueryTickers(ctx context.Context, symbols ...string) (map[string]types.Ticker, error) {
marketTickers, err := e.client.MarketTickers(okexapi.InstrumentTypeSpot)
if err != nil {
return nil, err
}
tickers := make(map[string]types.Ticker)
for _, marketTicker := range marketTickers {
symbol := toGlobalSymbol(marketTicker.InstrumentID)
ticker := toGlobalTicker(marketTicker)
tickers[symbol] = *ticker
}
if len(symbols) == 0 {
return tickers, nil
}
2021-05-27 07:11:44 +00:00
selectedTickers := make(map[string]types.Ticker, len(symbols))
2021-05-25 19:04:49 +00:00
for _, symbol := range symbols {
if ticker, ok := tickers[symbol]; ok {
selectedTickers[symbol] = ticker
}
}
return selectedTickers, nil
2021-05-25 18:11:02 +00:00
}
func (e *Exchange) PlatformFeeCurrency() string {
2023-05-17 05:45:38 +00:00
return PlatformToken
2021-05-22 19:34:40 +00:00
}
func (e *Exchange) QueryAccount(ctx context.Context) (*types.Account, error) {
accountBalance, err := e.client.AccountBalances()
2021-05-26 16:24:16 +00:00
if err != nil {
return nil, err
}
var account = types.Account{
AccountType: "SPOT",
}
var balanceMap = toGlobalBalance(accountBalance)
2021-05-26 16:24:16 +00:00
account.UpdateBalances(balanceMap)
return &account, nil
}
func (e *Exchange) QueryAccountBalances(ctx context.Context) (types.BalanceMap, error) {
accountBalances, err := e.client.AccountBalances()
2021-05-26 16:24:16 +00:00
if err != nil {
return nil, err
}
var balanceMap = toGlobalBalance(accountBalances)
2021-05-26 16:24:16 +00:00
return balanceMap, nil
}
func (e *Exchange) SubmitOrder(ctx context.Context, order types.SubmitOrder) (*types.Order, error) {
orderReq := e.client.TradeService.NewPlaceOrderRequest()
orderType, err := toLocalOrderType(order.Type)
if err != nil {
return nil, err
}
orderReq.InstrumentID(toLocalSymbol(order.Symbol))
orderReq.Side(toLocalSideType(order.Side))
if order.Market.Symbol != "" {
orderReq.Quantity(order.Market.FormatQuantity(order.Quantity))
} else {
// TODO report error
orderReq.Quantity(order.Quantity.FormatString(8))
}
// set price field for limit orders
switch order.Type {
case types.OrderTypeStopLimit, types.OrderTypeLimit:
2022-01-10 17:36:19 +00:00
if order.Market.Symbol != "" {
orderReq.Price(order.Market.FormatPrice(order.Price))
} else {
// TODO report error
orderReq.Price(order.Price.FormatString(8))
}
}
switch order.TimeInForce {
case "FOK":
orderReq.OrderType(okexapi.OrderTypeFOK)
case "IOC":
orderReq.OrderType(okexapi.OrderTypeIOC)
default:
orderReq.OrderType(orderType)
}
orderHead, err := orderReq.Do(ctx)
if err != nil {
return nil, err
}
orderID, err := strconv.ParseInt(orderHead.OrderID, 10, 64)
if err != nil {
return nil, err
}
return &types.Order{
SubmitOrder: order,
Exchange: types.ExchangeOKEx,
OrderID: uint64(orderID),
Status: types.OrderStatusNew,
ExecutedQuantity: fixedpoint.Zero,
IsWorking: true,
CreationTime: types.Time(time.Now()),
UpdateTime: types.Time(time.Now()),
IsMargin: false,
IsIsolated: false,
}, nil
// TODO: move this to batch place orders interface
/*
batchReq := e.client.TradeService.NewBatchPlaceOrderRequest()
batchReq.Add(reqs...)
orderHeads, err := batchReq.Do(ctx)
if err != nil {
return nil, err
}
for idx, orderHead := range orderHeads {
orderID, err := strconv.ParseInt(orderHead.OrderID, 10, 64)
if err != nil {
return createdOrder, err
}
submitOrder := order[idx]
createdOrder = append(createdOrder, types.Order{
SubmitOrder: submitOrder,
Exchange: types.ExchangeOKEx,
OrderID: uint64(orderID),
Status: types.OrderStatusNew,
ExecutedQuantity: fixedpoint.Zero,
IsWorking: true,
CreationTime: types.Time(time.Now()),
UpdateTime: types.Time(time.Now()),
IsMargin: false,
IsIsolated: false,
})
}
*/
}
func (e *Exchange) QueryOpenOrders(ctx context.Context, symbol string) (orders []types.Order, err error) {
instrumentID := toLocalSymbol(symbol)
req := e.client.TradeService.NewGetPendingOrderRequest().InstrumentType(okexapi.InstrumentTypeSpot).InstrumentID(instrumentID)
orderDetails, err := req.Do(ctx)
if err != nil {
return orders, err
}
orders, err = toGlobalOrders(orderDetails)
return orders, err
}
func (e *Exchange) CancelOrders(ctx context.Context, orders ...types.Order) error {
2021-05-28 15:34:21 +00:00
if len(orders) == 0 {
return nil
}
var reqs []*okexapi.CancelOrderRequest
for _, order := range orders {
2021-05-27 19:05:59 +00:00
if len(order.Symbol) == 0 {
return errors.New("symbol is required for canceling an okex order")
}
req := e.client.TradeService.NewCancelOrderRequest()
2021-05-27 19:05:59 +00:00
req.InstrumentID(toLocalSymbol(order.Symbol))
req.OrderID(strconv.FormatUint(order.OrderID, 10))
2021-05-27 19:05:59 +00:00
if len(order.ClientOrderID) > 0 {
req.ClientOrderID(order.ClientOrderID)
}
reqs = append(reqs, req)
}
batchReq := e.client.TradeService.NewBatchCancelOrderRequest()
batchReq.Add(reqs...)
_, err := batchReq.Do(ctx)
return err
}
2021-05-26 16:35:51 +00:00
func (e *Exchange) NewStream() types.Stream {
2021-05-26 17:07:25 +00:00
return NewStream(e.client)
2021-05-26 16:35:51 +00:00
}
func (e *Exchange) QueryKLines(ctx context.Context, symbol string, interval types.Interval, options types.KLineQueryOptions) ([]types.KLine, error) {
if err := marketDataLimiter.Wait(ctx); err != nil {
2022-05-03 03:14:53 +00:00
return nil, err
}
intervalParam := toLocalInterval(interval.String())
req := e.client.MarketDataService.NewCandlesticksRequest(toLocalSymbol(symbol))
2022-05-03 03:14:53 +00:00
req.Bar(intervalParam)
if options.StartTime != nil {
2022-05-03 03:14:53 +00:00
req.After(options.StartTime.Unix())
}
if options.EndTime != nil {
2022-05-03 03:14:53 +00:00
req.Before(options.EndTime.Unix())
}
candles, err := req.Do(ctx)
if err != nil {
return nil, err
}
var klines []types.KLine
for _, candle := range candles {
klines = append(klines, types.KLine{
Exchange: types.ExchangeOKEx,
Symbol: symbol,
Interval: interval,
Open: candle.Open,
High: candle.High,
Low: candle.Low,
Close: candle.Close,
Closed: true,
Volume: candle.Volume,
QuoteVolume: candle.VolumeInCurrency,
StartTime: types.Time(candle.Time),
EndTime: types.Time(candle.Time.Add(interval.Duration() - time.Millisecond)),
})
}
return klines, nil
2021-05-26 16:35:51 +00:00
}