bbgo_origin/pkg/exchange/okex/convert.go

411 lines
11 KiB
Go
Raw Normal View History

2021-05-25 18:11:02 +00:00
package okex
2021-05-25 19:04:49 +00:00
import (
2021-05-27 06:42:14 +00:00
"fmt"
"strconv"
2021-05-25 19:04:49 +00:00
"strings"
2022-02-18 05:52:13 +00:00
"github.com/pkg/errors"
"go.uber.org/multierr"
2022-02-18 05:52:13 +00:00
2021-05-25 19:04:49 +00:00
"github.com/c9s/bbgo/pkg/exchange/okex/okexapi"
"github.com/c9s/bbgo/pkg/fixedpoint"
2022-02-18 05:52:13 +00:00
"github.com/c9s/bbgo/pkg/types"
2021-05-25 19:04:49 +00:00
)
2021-05-25 18:11:02 +00:00
func toGlobalSymbol(symbol string) string {
return strings.ReplaceAll(symbol, "-", "")
}
2022-05-19 01:48:36 +00:00
// //go:generate sh -c "echo \"package okex\nvar spotSymbolMap = map[string]string{\n\" $(curl -s -L 'https://okex.com/api/v5/public/instruments?instType=SPOT' | jq -r '.data[] | \"\\(.instId | sub(\"-\" ; \"\") | tojson ): \\( .instId | tojson),\n\"') \"\n}\" > symbols.go"
2023-08-09 07:05:26 +00:00
//
2021-05-25 18:53:55 +00:00
//go:generate go run gensymbols.go
func toLocalSymbol(symbol string) string {
2021-05-25 18:53:55 +00:00
if s, ok := spotSymbolMap[symbol]; ok {
return s
}
log.Errorf("failed to look up local symbol from %s", symbol)
return symbol
}
2021-05-25 19:04:49 +00:00
func toGlobalTicker(marketTicker okexapi.MarketTicker) *types.Ticker {
return &types.Ticker{
Time: marketTicker.Timestamp.Time(),
Volume: marketTicker.Volume24H,
Last: marketTicker.Last,
Open: marketTicker.Open24H,
High: marketTicker.High24H,
Low: marketTicker.Low24H,
Buy: marketTicker.BidPrice,
Sell: marketTicker.AskPrice,
2021-05-25 19:04:49 +00:00
}
}
2021-05-26 16:24:16 +00:00
func toGlobalBalance(account *okexapi.Account) types.BalanceMap {
2021-05-26 16:24:16 +00:00
var balanceMap = types.BalanceMap{}
for _, balanceDetail := range account.Details {
balanceMap[balanceDetail.Currency] = types.Balance{
Currency: balanceDetail.Currency,
Available: balanceDetail.CashBalance,
Locked: balanceDetail.Frozen,
2021-05-26 16:24:16 +00:00
}
}
return balanceMap
}
2021-05-27 06:42:14 +00:00
type WebsocketSubscription struct {
Channel Channel `json:"channel"`
InstrumentID string `json:"instId,omitempty"`
InstrumentType string `json:"instType,omitempty"`
2021-05-27 06:42:14 +00:00
}
2021-05-27 09:40:24 +00:00
var CandleChannels = []string{
"candle1Y",
"candle6M", "candle3M", "candle1M",
"candle1W",
"candle1D", "candle2D", "candle3D", "candle5D",
"candle12H", "candle6H", "candle4H", "candle2H", "candle1H",
"candle30m", "candle15m", "candle5m", "candle3m", "candle1m",
}
2022-05-19 01:48:36 +00:00
func convertIntervalToCandle(interval types.Interval) string {
s := interval.String()
switch s {
2021-05-27 09:40:24 +00:00
case "1h", "2h", "4h", "6h", "12h", "1d", "3d":
2022-05-19 01:48:36 +00:00
return "candle" + strings.ToUpper(s)
2021-05-27 09:40:24 +00:00
case "1m", "5m", "15m", "30m":
2022-05-19 01:48:36 +00:00
return "candle" + s
2021-05-27 09:40:24 +00:00
}
2022-05-19 01:48:36 +00:00
return "candle" + s
2021-05-27 09:40:24 +00:00
}
2021-05-27 06:42:14 +00:00
func convertSubscription(s types.Subscription) (WebsocketSubscription, error) {
// binance uses lower case symbol name,
// for kline, it's "<symbol>@kline_<interval>"
// for depth, it's "<symbol>@depth OR <symbol>@depth@100ms"
switch s.Channel {
case types.KLineChannel:
2021-05-27 09:40:24 +00:00
// Channel names are:
2021-05-27 06:42:14 +00:00
return WebsocketSubscription{
Channel: Channel(convertIntervalToCandle(s.Options.Interval)),
2021-05-27 06:42:14 +00:00
InstrumentID: toLocalSymbol(s.Symbol),
}, nil
case types.BookChannel:
2024-01-10 06:02:03 +00:00
if s.Options.Depth != types.DepthLevel400 {
return WebsocketSubscription{}, fmt.Errorf("%s depth not supported", s.Options.Depth)
}
2021-05-27 06:42:14 +00:00
return WebsocketSubscription{
Channel: ChannelBooks,
2021-05-27 06:42:14 +00:00
InstrumentID: toLocalSymbol(s.Symbol),
}, nil
case types.BookTickerChannel:
return WebsocketSubscription{
Channel: ChannelBook5,
InstrumentID: toLocalSymbol(s.Symbol),
}, nil
case types.MarketTradeChannel:
return WebsocketSubscription{
Channel: ChannelMarketTrades,
InstrumentID: toLocalSymbol(s.Symbol),
}, nil
2021-05-27 06:42:14 +00:00
}
return WebsocketSubscription{}, fmt.Errorf("unsupported public stream channel %s", s.Channel)
}
func toLocalSideType(side types.SideType) okexapi.SideType {
return okexapi.SideType(strings.ToLower(string(side)))
}
func segmentOrderDetails(orderDetails []okexapi.OrderDetails) (trades, orders []okexapi.OrderDetails) {
for _, orderDetail := range orderDetails {
if len(orderDetail.LastTradeID) > 0 {
trades = append(trades, orderDetail)
}
2021-05-28 16:27:05 +00:00
orders = append(orders, orderDetail)
}
return trades, orders
}
func toGlobalTrades(orderDetails []okexapi.OrderDetails) ([]types.Trade, error) {
var trades []types.Trade
2023-09-08 07:33:09 +00:00
var err error
for _, orderDetail := range orderDetails {
2023-09-08 07:33:09 +00:00
trade, err2 := toGlobalTrade(&orderDetail)
if err2 != nil {
err = multierr.Append(err, err2)
continue
}
2023-09-08 07:33:09 +00:00
trades = append(trades, *trade)
}
return trades, nil
}
func tradeToGlobal(trade okexapi.Trade) types.Trade {
// ** We use the bill id as the trade id, because okx uses billId to perform pagination. **
billID := trade.BillId
side := toGlobalSide(trade.Side)
return types.Trade{
ID: uint64(billID),
OrderID: uint64(trade.OrderId),
Exchange: types.ExchangeOKEx,
Price: trade.FillPrice,
Quantity: trade.FillSize,
QuoteQuantity: trade.FillPrice.Mul(trade.FillSize),
Symbol: toGlobalSymbol(trade.InstrumentId),
Side: side,
IsBuyer: side == types.SideTypeBuy,
IsMaker: trade.ExecutionType == okexapi.LiquidityTypeMaker,
Time: types.Time(trade.Timestamp),
// The fees obtained from the exchange are negative, hence they are forcibly converted to positive.
Fee: trade.Fee.Abs(),
FeeCurrency: trade.FeeCurrency,
IsMargin: false,
IsFutures: false,
IsIsolated: false,
}
}
func orderDetailToGlobal(order *okexapi.OrderDetail) (*types.Order, error) {
2024-01-11 08:41:42 +00:00
side := toGlobalSide(order.Side)
orderType, err := toGlobalOrderType(order.OrderType)
if err != nil {
return nil, err
}
timeInForce := types.TimeInForceGTC
switch order.OrderType {
case okexapi.OrderTypeFOK:
timeInForce = types.TimeInForceFOK
case okexapi.OrderTypeIOC:
timeInForce = types.TimeInForceIOC
}
orderStatus, err := toGlobalOrderStatus(order.State)
if err != nil {
return nil, err
}
return &types.Order{
SubmitOrder: types.SubmitOrder{
ClientOrderID: order.ClientOrderId,
Symbol: toGlobalSymbol(order.InstrumentID),
Side: side,
Type: orderType,
Price: order.Price,
Quantity: order.Size,
TimeInForce: timeInForce,
},
Exchange: types.ExchangeOKEx,
OrderID: uint64(order.OrderId),
UUID: strconv.FormatInt(int64(order.OrderId), 10),
Status: orderStatus,
OriginalStatus: string(order.State),
ExecutedQuantity: order.AccumulatedFillSize,
IsWorking: order.State.IsWorking(),
CreationTime: types.Time(order.CreatedTime),
UpdateTime: types.Time(order.UpdatedTime),
}, nil
}
func toGlobalOrders(orderDetails []okexapi.OrderDetails) ([]types.Order, error) {
var orders []types.Order
var err error
for _, orderDetail := range orderDetails {
o, err2 := toGlobalOrder(&orderDetail)
if err2 != nil {
err = multierr.Append(err, err2)
2023-08-23 08:16:38 +00:00
continue
}
orders = append(orders, *o)
}
return orders, err
}
func toGlobalOrderStatus(state okexapi.OrderState) (types.OrderStatus, error) {
switch state {
case okexapi.OrderStateCanceled:
return types.OrderStatusCanceled, nil
case okexapi.OrderStateLive:
return types.OrderStatusNew, nil
case okexapi.OrderStatePartiallyFilled:
return types.OrderStatusPartiallyFilled, nil
case okexapi.OrderStateFilled:
return types.OrderStatusFilled, nil
}
return "", fmt.Errorf("unknown or unsupported okex order state: %s", state)
}
func toLocalOrderType(orderType types.OrderType) (okexapi.OrderType, error) {
switch orderType {
case types.OrderTypeMarket:
return okexapi.OrderTypeMarket, nil
case types.OrderTypeLimit:
return okexapi.OrderTypeLimit, nil
case types.OrderTypeLimitMaker:
return okexapi.OrderTypePostOnly, nil
}
return "", fmt.Errorf("unknown or unsupported okex order type: %s", orderType)
}
func toGlobalOrderType(orderType okexapi.OrderType) (types.OrderType, error) {
// IOC, FOK are only allowed with limit order type, so we assume the order type is always limit order for FOK, IOC orders
switch orderType {
case okexapi.OrderTypeMarket:
return types.OrderTypeMarket, nil
2023-08-11 01:28:58 +00:00
2023-08-22 07:14:18 +00:00
case okexapi.OrderTypeLimit, okexapi.OrderTypeFOK, okexapi.OrderTypeIOC:
return types.OrderTypeLimit, nil
2023-08-11 01:28:58 +00:00
2023-08-22 07:14:18 +00:00
case okexapi.OrderTypePostOnly:
return types.OrderTypeLimitMaker, nil
2023-08-11 01:28:58 +00:00
}
return "", fmt.Errorf("unknown or unsupported okex order type: %s", orderType)
}
2022-05-03 03:14:53 +00:00
2023-10-02 04:55:30 +00:00
func toLocalInterval(interval types.Interval) (string, error) {
if _, ok := SupportedIntervals[interval]; !ok {
return "", fmt.Errorf("interval %s is not supported", interval)
}
2023-10-04 08:24:32 +00:00
in, ok := ToLocalInterval[interval]
if !ok {
return "", fmt.Errorf("interval %s is not supported, got local interval %s", interval, in)
2023-10-02 04:55:30 +00:00
}
2023-10-04 08:24:32 +00:00
return in, nil
2022-05-03 03:14:53 +00:00
}
2023-08-11 01:28:58 +00:00
2023-08-21 07:31:30 +00:00
func toGlobalSide(side okexapi.SideType) (s types.SideType) {
switch string(side) {
case "sell":
s = types.SideTypeSell
case "buy":
s = types.SideTypeBuy
}
return s
}
func toGlobalOrder(okexOrder *okexapi.OrderDetails) (*types.Order, error) {
orderID, err := strconv.ParseInt(okexOrder.OrderID, 10, 64)
if err != nil {
2023-08-21 07:31:30 +00:00
return nil, err
}
2023-08-21 07:31:30 +00:00
side := toGlobalSide(okexOrder.Side)
orderType, err := toGlobalOrderType(okexOrder.OrderType)
if err != nil {
2023-08-21 07:31:30 +00:00
return nil, err
}
timeInForce := types.TimeInForceGTC
switch okexOrder.OrderType {
case okexapi.OrderTypeFOK:
timeInForce = types.TimeInForceFOK
case okexapi.OrderTypeIOC:
2023-08-11 01:28:58 +00:00
timeInForce = types.TimeInForceIOC
}
orderStatus, err := toGlobalOrderStatus(okexOrder.State)
if err != nil {
2023-08-21 07:31:30 +00:00
return nil, err
}
isWorking := false
switch orderStatus {
case types.OrderStatusNew, types.OrderStatusPartiallyFilled:
isWorking = true
}
2023-08-21 07:31:30 +00:00
isMargin := false
2023-09-08 07:33:09 +00:00
if okexOrder.InstrumentType == okexapi.InstrumentTypeMARGIN {
2023-08-21 07:31:30 +00:00
isMargin = true
}
2023-08-11 01:28:58 +00:00
return &types.Order{
SubmitOrder: types.SubmitOrder{
ClientOrderID: okexOrder.ClientOrderID,
Symbol: toGlobalSymbol(okexOrder.InstrumentID),
Side: side,
Type: orderType,
2023-08-11 01:28:58 +00:00
Price: okexOrder.Price,
Quantity: okexOrder.Quantity,
StopPrice: fixedpoint.Zero, // not supported yet
TimeInForce: timeInForce,
2023-08-11 01:28:58 +00:00
},
Exchange: types.ExchangeOKEx,
OrderID: uint64(orderID),
Status: orderStatus,
2023-08-11 01:28:58 +00:00
ExecutedQuantity: okexOrder.FilledQuantity,
IsWorking: isWorking,
2023-08-11 01:28:58 +00:00
CreationTime: types.Time(okexOrder.CreationTime),
UpdateTime: types.Time(okexOrder.UpdateTime),
IsMargin: isMargin,
IsIsolated: false,
2023-08-11 01:28:58 +00:00
}, nil
}
2023-09-08 07:33:09 +00:00
func toGlobalTrade(orderDetail *okexapi.OrderDetails) (*types.Trade, error) {
// Should use tradeId, but okex use billId to perform pagination, so use billID as tradeID instead.
billID := orderDetail.BillID
2023-09-08 07:33:09 +00:00
orderID, err := strconv.ParseInt(orderDetail.OrderID, 10, 64)
if err != nil {
return nil, errors.Wrapf(err, "error parsing ordId value: %s", orderDetail.OrderID)
}
side := toGlobalSide(orderDetail.Side)
isMargin := false
if orderDetail.InstrumentType == okexapi.InstrumentTypeMARGIN {
isMargin = true
}
isFuture := false
if orderDetail.InstrumentType == okexapi.InstrumentTypeFutures {
isFuture = true
}
return &types.Trade{
ID: uint64(billID),
2023-09-08 07:33:09 +00:00
OrderID: uint64(orderID),
Exchange: types.ExchangeOKEx,
Price: orderDetail.LastFilledPrice,
Quantity: orderDetail.LastFilledQuantity,
QuoteQuantity: orderDetail.LastFilledPrice.Mul(orderDetail.LastFilledQuantity),
Symbol: toGlobalSymbol(orderDetail.InstrumentID),
Side: side,
IsBuyer: side == types.SideTypeBuy,
IsMaker: orderDetail.ExecutionType == "M",
Time: types.Time(orderDetail.LastFilledTime),
Fee: orderDetail.LastFilledFee,
FeeCurrency: orderDetail.LastFilledFeeCurrency,
IsMargin: isMargin,
IsFutures: isFuture,
IsIsolated: false,
}, nil
}