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"
|
2021-05-27 18:21:35 +00:00
|
|
|
"strconv"
|
2021-05-25 19:04:49 +00:00
|
|
|
"strings"
|
|
|
|
|
|
|
|
"github.com/c9s/bbgo/pkg/exchange/okex/okexapi"
|
2022-02-03 04:55:25 +00:00
|
|
|
"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, "-", "")
|
|
|
|
}
|
2021-05-25 18:37:28 +00:00
|
|
|
|
2024-05-16 07:29:47 +00:00
|
|
|
//go:generate sh -c "echo \"package okex\nvar spotSymbolMap = map[string]string{\n\" $(curl -s -L 'https://www.okx.com/api/v5/public/instruments?instType=SPOT' | jq -r '.data[] | \"\\(.instId | sub(\"-\" ; \"\") | tojson ): \\( .instId | tojson),\n\"') \"\n}\" > symbols.go"
|
2021-05-25 18:53:55 +00:00
|
|
|
//go:generate go run gensymbols.go
|
2021-05-25 18:37:28 +00:00
|
|
|
func toLocalSymbol(symbol string) string {
|
2021-05-25 18:53:55 +00:00
|
|
|
if s, ok := spotSymbolMap[symbol]; ok {
|
2021-05-25 18:37:28 +00:00
|
|
|
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(),
|
2022-02-03 04:55:25 +00:00
|
|
|
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
|
|
|
|
2021-05-27 17:14:11 +00:00
|
|
|
func toGlobalBalance(account *okexapi.Account) types.BalanceMap {
|
2021-05-26 16:24:16 +00:00
|
|
|
var balanceMap = types.BalanceMap{}
|
2021-05-27 17:14:11 +00:00
|
|
|
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 {
|
2024-01-08 07:05:32 +00:00
|
|
|
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) {
|
|
|
|
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{
|
2024-01-08 07:05:32 +00:00
|
|
|
Channel: Channel(convertIntervalToCandle(s.Options.Interval)),
|
2021-05-27 06:42:14 +00:00
|
|
|
InstrumentID: toLocalSymbol(s.Symbol),
|
|
|
|
}, nil
|
|
|
|
|
|
|
|
case types.BookChannel:
|
2024-06-03 08:07:47 +00:00
|
|
|
ch := ChannelBooks
|
|
|
|
|
|
|
|
switch s.Options.Depth {
|
|
|
|
case types.DepthLevelFull:
|
|
|
|
ch = ChannelBooks
|
|
|
|
|
|
|
|
case types.DepthLevelMedium:
|
|
|
|
ch = ChannelBooks50
|
|
|
|
|
|
|
|
case types.DepthLevel50:
|
|
|
|
ch = ChannelBooks50
|
|
|
|
|
|
|
|
case types.DepthLevel5:
|
|
|
|
ch = ChannelBooks5
|
|
|
|
|
|
|
|
case types.DepthLevel1:
|
|
|
|
ch = ChannelBooks1
|
2024-01-10 06:02:03 +00:00
|
|
|
}
|
|
|
|
|
2021-05-27 06:42:14 +00:00
|
|
|
return WebsocketSubscription{
|
2024-06-03 08:07:47 +00:00
|
|
|
Channel: ch,
|
2021-05-27 06:42:14 +00:00
|
|
|
InstrumentID: toLocalSymbol(s.Symbol),
|
|
|
|
}, nil
|
2021-12-22 16:42:25 +00:00
|
|
|
case types.BookTickerChannel:
|
|
|
|
return WebsocketSubscription{
|
2024-06-03 08:07:47 +00:00
|
|
|
Channel: ChannelBooks5,
|
2024-01-08 07:05:32 +00:00
|
|
|
InstrumentID: toLocalSymbol(s.Symbol),
|
|
|
|
}, nil
|
|
|
|
case types.MarketTradeChannel:
|
|
|
|
return WebsocketSubscription{
|
|
|
|
Channel: ChannelMarketTrades,
|
2021-12-22 16:42:25 +00:00
|
|
|
InstrumentID: toLocalSymbol(s.Symbol),
|
|
|
|
}, nil
|
2021-05-27 06:42:14 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return WebsocketSubscription{}, fmt.Errorf("unsupported public stream channel %s", s.Channel)
|
|
|
|
}
|
2021-05-27 18:21:35 +00:00
|
|
|
|
2021-05-27 18:45:09 +00:00
|
|
|
func toLocalSideType(side types.SideType) okexapi.SideType {
|
|
|
|
return okexapi.SideType(strings.ToLower(string(side)))
|
|
|
|
}
|
|
|
|
|
2024-01-15 09:10:14 +00:00
|
|
|
func tradeToGlobal(trade okexapi.Trade) types.Trade {
|
|
|
|
side := toGlobalSide(trade.Side)
|
|
|
|
return types.Trade{
|
2024-02-07 17:37:35 +00:00
|
|
|
ID: uint64(trade.TradeId),
|
2024-01-15 09:10:14 +00:00
|
|
|
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,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-01-16 07:29:12 +00:00
|
|
|
func processMarketBuySize(o *okexapi.OrderDetail) (fixedpoint.Value, error) {
|
|
|
|
switch o.State {
|
|
|
|
case okexapi.OrderStateLive, okexapi.OrderStateCanceled:
|
|
|
|
return fixedpoint.Zero, nil
|
|
|
|
|
|
|
|
case okexapi.OrderStatePartiallyFilled:
|
|
|
|
if o.FillPrice.IsZero() {
|
|
|
|
return fixedpoint.Zero, fmt.Errorf("fillPrice for a partialFilled should not be zero")
|
|
|
|
}
|
|
|
|
return o.Size.Div(o.FillPrice), nil
|
|
|
|
|
|
|
|
case okexapi.OrderStateFilled:
|
|
|
|
return o.AccumulatedFillSize, nil
|
|
|
|
|
|
|
|
default:
|
|
|
|
return fixedpoint.Zero, fmt.Errorf("unexpected status: %s", o.State)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-01-12 07:09:45 +00:00
|
|
|
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
|
|
|
|
}
|
|
|
|
|
2024-01-16 07:29:12 +00:00
|
|
|
size := order.Size
|
|
|
|
if order.Side == okexapi.SideTypeBuy &&
|
|
|
|
order.OrderType == okexapi.OrderTypeMarket &&
|
|
|
|
order.TargetCurrency == okexapi.TargetCurrencyQuote {
|
|
|
|
|
|
|
|
size, err = processMarketBuySize(order)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-01-11 08:41:42 +00:00
|
|
|
return &types.Order{
|
|
|
|
SubmitOrder: types.SubmitOrder{
|
|
|
|
ClientOrderID: order.ClientOrderId,
|
|
|
|
Symbol: toGlobalSymbol(order.InstrumentID),
|
|
|
|
Side: side,
|
|
|
|
Type: orderType,
|
|
|
|
Price: order.Price,
|
2024-01-16 07:29:12 +00:00
|
|
|
Quantity: size,
|
|
|
|
AveragePrice: order.AvgPrice,
|
2024-01-11 08:41:42 +00:00
|
|
|
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
|
|
|
|
}
|
|
|
|
|
2023-08-15 06:26:27 +00:00
|
|
|
func toGlobalOrderStatus(state okexapi.OrderState) (types.OrderStatus, error) {
|
2021-05-27 18:21:35 +00:00
|
|
|
switch state {
|
|
|
|
case okexapi.OrderStateCanceled:
|
2023-08-15 06:26:27 +00:00
|
|
|
return types.OrderStatusCanceled, nil
|
2021-05-27 18:21:35 +00:00
|
|
|
case okexapi.OrderStateLive:
|
2023-08-15 06:26:27 +00:00
|
|
|
return types.OrderStatusNew, nil
|
2021-05-27 18:21:35 +00:00
|
|
|
case okexapi.OrderStatePartiallyFilled:
|
2023-08-15 06:26:27 +00:00
|
|
|
return types.OrderStatusPartiallyFilled, nil
|
2021-05-27 18:21:35 +00:00
|
|
|
case okexapi.OrderStateFilled:
|
2023-08-15 06:26:27 +00:00
|
|
|
return types.OrderStatusFilled, nil
|
2021-05-27 18:21:35 +00:00
|
|
|
|
|
|
|
}
|
|
|
|
|
2023-08-15 06:26:27 +00:00
|
|
|
return "", fmt.Errorf("unknown or unsupported okex order state: %s", state)
|
2021-05-27 18:21:35 +00:00
|
|
|
}
|
|
|
|
|
2021-05-27 18:45:09 +00:00
|
|
|
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)
|
|
|
|
}
|
|
|
|
|
2023-08-15 06:26:27 +00:00
|
|
|
func toGlobalOrderType(orderType okexapi.OrderType) (types.OrderType, error) {
|
2023-08-23 07:44:51 +00:00
|
|
|
// IOC, FOK are only allowed with limit order type, so we assume the order type is always limit order for FOK, IOC orders
|
2021-05-27 18:21:35 +00:00
|
|
|
switch orderType {
|
2023-08-15 06:26:27 +00:00
|
|
|
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:
|
2023-08-15 06:26:27 +00:00
|
|
|
return types.OrderTypeLimit, nil
|
2023-08-11 01:28:58 +00:00
|
|
|
|
2023-08-22 07:14:18 +00:00
|
|
|
case okexapi.OrderTypePostOnly:
|
2023-08-15 06:26:27 +00:00
|
|
|
return types.OrderTypeLimitMaker, nil
|
2023-08-11 01:28:58 +00:00
|
|
|
|
2021-05-27 18:21:35 +00:00
|
|
|
}
|
2023-08-15 06:26:27 +00:00
|
|
|
|
|
|
|
return "", fmt.Errorf("unknown or unsupported okex order type: %s", orderType)
|
2021-05-27 18:21:35 +00:00
|
|
|
}
|
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 06:23:13 +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) {
|
2023-08-15 06:26:27 +00:00
|
|
|
|
|
|
|
orderID, err := strconv.ParseInt(okexOrder.OrderID, 10, 64)
|
|
|
|
if err != nil {
|
2023-08-21 07:31:30 +00:00
|
|
|
return nil, err
|
2023-08-15 06:26:27 +00:00
|
|
|
}
|
|
|
|
|
2023-08-21 07:31:30 +00:00
|
|
|
side := toGlobalSide(okexOrder.Side)
|
2023-08-15 06:26:27 +00:00
|
|
|
|
|
|
|
orderType, err := toGlobalOrderType(okexOrder.OrderType)
|
|
|
|
if err != nil {
|
2023-08-21 07:31:30 +00:00
|
|
|
return nil, err
|
2023-08-15 06:26:27 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
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
|
|
|
|
}
|
2023-08-15 06:26:27 +00:00
|
|
|
|
|
|
|
orderStatus, err := toGlobalOrderStatus(okexOrder.State)
|
|
|
|
if err != nil {
|
2023-08-21 07:31:30 +00:00
|
|
|
return nil, err
|
2023-08-15 06:26:27 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
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,
|
2023-08-15 06:26:27 +00:00
|
|
|
Symbol: toGlobalSymbol(okexOrder.InstrumentID),
|
|
|
|
Side: side,
|
|
|
|
Type: orderType,
|
2023-08-11 01:28:58 +00:00
|
|
|
Price: okexOrder.Price,
|
2023-08-15 06:26:27 +00:00
|
|
|
Quantity: okexOrder.Quantity,
|
|
|
|
StopPrice: fixedpoint.Zero, // not supported yet
|
|
|
|
TimeInForce: timeInForce,
|
2023-08-11 01:28:58 +00:00
|
|
|
},
|
|
|
|
Exchange: types.ExchangeOKEx,
|
2023-08-15 06:26:27 +00:00
|
|
|
OrderID: uint64(orderID),
|
|
|
|
Status: orderStatus,
|
2023-08-11 01:28:58 +00:00
|
|
|
ExecutedQuantity: okexOrder.FilledQuantity,
|
2023-08-15 06:26:27 +00:00
|
|
|
IsWorking: isWorking,
|
2023-08-11 01:28:58 +00:00
|
|
|
CreationTime: types.Time(okexOrder.CreationTime),
|
|
|
|
UpdateTime: types.Time(okexOrder.UpdateTime),
|
|
|
|
IsMargin: isMargin,
|
2023-08-15 06:26:27 +00:00
|
|
|
IsIsolated: false,
|
2023-08-11 01:28:58 +00:00
|
|
|
}, nil
|
|
|
|
}
|