2021-05-22 19:34:40 +00:00
package okex
import (
2021-05-25 18:11:02 +00:00
"context"
2023-09-08 07:33:09 +00:00
"fmt"
2024-02-20 07:14:09 +00:00
"regexp"
2021-05-27 18:45:09 +00:00
"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"
2023-09-08 07:33:09 +00:00
"go.uber.org/multierr"
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"
2022-02-03 04:55:25 +00:00
"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
)
2023-09-08 07:33:09 +00:00
var (
2024-02-20 07:14:09 +00:00
// clientOrderIdRegex combine of case-sensitive alphanumerics, all numbers, or all letters of up to 32 characters.
clientOrderIdRegex = regexp . MustCompile ( "^[a-zA-Z0-9]{0,32}$" )
2024-02-20 03:26:07 +00:00
// Rate Limit: 20 requests per 2 seconds, Rate limit rule: IP + instrumentType.
// Currently, calls are not made very frequently, so only IP is considered.
queryMarketLimiter = rate . NewLimiter ( rate . Every ( 100 * time . Millisecond ) , 1 )
// Rate Limit: 20 requests per 2 seconds, Rate limit rule: IP
queryTickerLimiter = rate . NewLimiter ( rate . Every ( 100 * time . Millisecond ) , 1 )
// Rate Limit: 20 requests per 2 seconds, Rate limit rule: IP
queryTickersLimiter = rate . NewLimiter ( rate . Every ( 100 * time . Millisecond ) , 1 )
// Rate Limit: 10 requests per 2 seconds, Rate limit rule: UserID
queryAccountLimiter = rate . NewLimiter ( rate . Every ( 200 * time . Millisecond ) , 1 )
// Rate Limit: 60 requests per 2 seconds, Rate limit rule (except Options): UserID + Instrument ID.
// TODO: support UserID + Instrument ID
placeOrderLimiter = rate . NewLimiter ( rate . Every ( 33 * time . Millisecond ) , 1 )
// Rate Limit: 60 requests per 2 seconds, Rate limit rule (except Options): UserID + Instrument ID
// TODO: support UserID + Instrument ID
batchCancelOrderLimiter = rate . NewLimiter ( rate . Every ( 33 * time . Millisecond ) , 1 )
// Rate Limit: 60 requests per 2 seconds, Rate limit rule: UserID
queryOpenOrderLimiter = rate . NewLimiter ( rate . Every ( 33 * time . Millisecond ) , 1 )
// Rate Limit: 20 requests per 2 seconds, Rate limit rule: UserID
queryClosedOrderRateLimiter = rate . NewLimiter ( rate . Every ( 100 * time . Millisecond ) , 1 )
// Rate Limit: 10 requests per 2 seconds, Rate limit rule: UserID
queryTradeLimiter = rate . NewLimiter ( rate . Every ( 200 * time . Millisecond ) , 1 )
// Rate Limit: 40 requests per 2 seconds, Rate limit rule: IP
queryKLineLimiter = rate . NewLimiter ( rate . Every ( 50 * time . Millisecond ) , 1 )
2023-09-08 07:33:09 +00:00
)
2022-05-03 03:14:53 +00:00
2024-01-11 08:41:42 +00:00
const (
ID = "okex"
2023-05-17 05:45:38 +00:00
2024-01-11 08:41:42 +00:00
// PlatformToken is the platform currency of OKEx, pre-allocate static string here
PlatformToken = "OKB"
2021-05-25 18:13:59 +00:00
2023-09-18 07:55:58 +00:00
defaultQueryLimit = 100
2024-01-12 07:09:45 +00:00
maxHistoricalDataQueryPeriod = 90 * 24 * time . Hour
2024-03-14 09:17:41 +00:00
threeDaysHistoricalPeriod = 3 * 24 * time . Hour
2023-09-18 07:55:58 +00:00
)
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
} )
2023-08-22 09:23:16 +00:00
var ErrSymbolRequired = errors . New ( "symbol is a required parameter" )
2023-08-21 07:31:30 +00:00
2021-05-22 19:34:40 +00:00
type Exchange struct {
2024-10-21 07:09:24 +00:00
key , secret , passphrase , brokerId string
2021-05-25 18:11:02 +00:00
2024-03-14 09:17:41 +00:00
client * okexapi . RestClient
timeNowFunc func ( ) time . Time
2021-05-25 18:11:02 +00:00
}
2024-10-21 07:09:24 +00:00
type Option func ( exchange * Exchange )
func WithBrokerId ( id string ) Option {
return func ( exchange * Exchange ) {
exchange . brokerId = id
}
}
func New ( key , secret , passphrase string , opts ... Option ) * Exchange {
2023-09-06 13:21:13 +00:00
client := okexapi . NewClient ( )
2021-12-08 15:27:01 +00:00
if len ( key ) > 0 && len ( secret ) > 0 {
client . Auth ( key , secret , passphrase )
}
2021-05-25 18:11:02 +00:00
2024-10-21 07:09:24 +00:00
ex := & Exchange {
2024-03-14 09:17:41 +00:00
key : key ,
secret : secret ,
passphrase : passphrase ,
client : client ,
timeNowFunc : time . Now ,
2023-09-07 03:25:12 +00:00
}
2024-10-21 07:09:24 +00:00
for _ , o := range opts {
o ( ex )
}
return ex
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 ) {
2024-01-09 03:56:10 +00:00
if err := queryMarketLimiter . Wait ( ctx ) ; err != nil {
return nil , fmt . Errorf ( "markets rate limiter wait error: %w" , err )
}
2021-05-25 18:11:02 +00:00
2024-01-09 03:56:10 +00:00
instruments , err := e . client . NewGetInstrumentsInfoRequest ( ) . Do ( ctx )
2021-05-25 18:11:02 +00:00
if err != nil {
return nil , err
}
markets := types . MarketMap { }
for _ , instrument := range instruments {
symbol := toGlobalSymbol ( instrument . InstrumentID )
market := types . Market {
2024-02-23 09:04:03 +00:00
Exchange : types . ExchangeOKEx ,
2021-05-25 18:13:59 +00:00
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
2024-01-09 03:56:10 +00:00
PricePrecision : instrument . TickSize . NumFractionalDigits ( ) ,
VolumePrecision : instrument . LotSize . NumFractionalDigits ( ) ,
2021-05-25 18:11:02 +00:00
// TickSize: OKEx's price tick, for BTC-USDT it's "0.1"
2022-02-03 04:55:25 +00:00
TickSize : instrument . TickSize ,
2021-05-25 18:11:02 +00:00
// Quantity step size, for BTC-USDT, it's "0.00000001"
2022-02-03 04:55:25 +00:00
StepSize : instrument . LotSize ,
2021-05-25 18:11:02 +00:00
// for BTC-USDT, it's "0.00001"
2022-02-03 04:55:25 +00:00
MinQuantity : instrument . MinSize ,
2021-05-25 18:11:02 +00:00
// OKEx does not offer minimal notional, use 1 USD here.
2022-02-03 04:55:25 +00:00
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 ) {
2024-01-09 05:57:19 +00:00
if err := queryTickerLimiter . Wait ( ctx ) ; err != nil {
return nil , fmt . Errorf ( "ticker rate limiter wait error: %w" , err )
}
2021-05-25 18:44:03 +00:00
2024-01-09 05:57:19 +00:00
symbol = toLocalSymbol ( symbol )
marketTicker , err := e . client . NewGetTickerRequest ( ) . InstId ( symbol ) . Do ( ctx )
2021-05-25 18:44:03 +00:00
if err != nil {
return nil , err
}
2024-01-09 05:57:19 +00:00
if len ( marketTicker ) != 1 {
return nil , fmt . Errorf ( "unexpected length of %s market ticker, got: %v" , symbol , marketTicker )
}
return toGlobalTicker ( marketTicker [ 0 ] ) , 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 ) {
2024-01-09 05:57:19 +00:00
if err := queryTickersLimiter . Wait ( ctx ) ; err != nil {
return nil , fmt . Errorf ( "tickers rate limiter wait error: %w" , err )
}
marketTickers , err := e . client . NewGetTickersRequest ( ) . Do ( ctx )
2021-05-25 19:04:49 +00:00
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
}
2021-05-25 19:05:54 +00:00
func ( e * Exchange ) QueryAccount ( ctx context . Context ) ( * types . Account , error ) {
2024-01-09 06:23:39 +00:00
bals , err := e . QueryAccountBalances ( ctx )
2021-05-26 16:24:16 +00:00
if err != nil {
return nil , err
}
2024-01-09 06:23:39 +00:00
account := types . NewAccount ( )
account . UpdateBalances ( bals )
return account , nil
2021-05-25 19:05:54 +00:00
}
func ( e * Exchange ) QueryAccountBalances ( ctx context . Context ) ( types . BalanceMap , error ) {
2024-01-09 06:23:39 +00:00
if err := queryAccountLimiter . Wait ( ctx ) ; err != nil {
return nil , fmt . Errorf ( "account rate limiter wait error: %w" , err )
}
accountBalances , err := e . client . NewGetAccountInfoRequest ( ) . Do ( ctx )
2021-05-26 16:24:16 +00:00
if err != nil {
return nil , err
}
2024-01-09 06:23:39 +00:00
if len ( accountBalances ) != 1 {
return nil , fmt . Errorf ( "unexpected length of balances: %v" , accountBalances )
}
return toGlobalBalance ( & accountBalances [ 0 ] ) , nil
2021-05-25 19:05:54 +00:00
}
2022-09-09 10:41:06 +00:00
func ( e * Exchange ) SubmitOrder ( ctx context . Context , order types . SubmitOrder ) ( * types . Order , error ) {
2023-09-06 11:14:21 +00:00
orderReq := e . client . NewPlaceOrderRequest ( )
2021-05-27 18:45:09 +00:00
2022-09-09 10:41:06 +00:00
orderReq . InstrumentID ( toLocalSymbol ( order . Symbol ) )
orderReq . Side ( toLocalSideType ( order . Side ) )
2024-01-10 02:31:27 +00:00
orderReq . Size ( order . Market . FormatQuantity ( order . Quantity ) )
2021-05-27 18:45:09 +00:00
2022-09-09 10:41:06 +00:00
// set price field for limit orders
switch order . Type {
2024-02-20 10:20:07 +00:00
case types . OrderTypeStopLimit , types . OrderTypeLimit , types . OrderTypeLimitMaker :
2024-01-10 02:31:27 +00:00
orderReq . Price ( order . Market . FormatPrice ( order . Price ) )
case types . OrderTypeMarket :
// Because our order.Quantity unit is base coin, so we indicate the target currency to Base.
if order . Side == types . SideTypeBuy {
orderReq . Size ( order . Market . FormatQuantity ( order . Quantity ) )
orderReq . TargetCurrency ( okexapi . TargetCurrencyBase )
2021-05-27 18:45:09 +00:00
} else {
2024-01-10 02:31:27 +00:00
orderReq . Size ( order . Market . FormatQuantity ( order . Quantity ) )
orderReq . TargetCurrency ( okexapi . TargetCurrencyQuote )
2021-05-27 18:45:09 +00:00
}
2022-09-09 10:41:06 +00:00
}
2021-05-27 18:45:09 +00:00
2024-01-10 02:31:27 +00:00
orderType , err := toLocalOrderType ( order . Type )
if err != nil {
return nil , err
}
2022-09-09 10:41:06 +00:00
switch order . TimeInForce {
2024-01-10 02:31:27 +00:00
case types . TimeInForceFOK :
2022-09-09 10:41:06 +00:00
orderReq . OrderType ( okexapi . OrderTypeFOK )
2024-01-10 02:31:27 +00:00
case types . TimeInForceIOC :
2022-09-09 10:41:06 +00:00
orderReq . OrderType ( okexapi . OrderTypeIOC )
default :
orderReq . OrderType ( orderType )
}
2021-05-27 18:45:09 +00:00
2024-01-10 02:31:27 +00:00
if err := placeOrderLimiter . Wait ( ctx ) ; err != nil {
return nil , fmt . Errorf ( "place order rate limiter wait error: %w" , err )
}
2024-02-20 07:14:09 +00:00
if len ( order . ClientOrderID ) > 0 {
if ok := clientOrderIdRegex . MatchString ( order . ClientOrderID ) ; ! ok {
return nil , fmt . Errorf ( "client order id should be case-sensitive alphanumerics, all numbers, or all letters of up to 32 characters: %s" , order . ClientOrderID )
}
orderReq . ClientOrderID ( order . ClientOrderID )
2021-05-27 18:45:09 +00:00
}
2024-10-21 07:09:24 +00:00
if len ( e . brokerId ) != 0 {
orderReq . Tag ( e . brokerId )
}
2024-03-13 06:54:29 +00:00
timeNow := time . Now ( )
2024-01-10 02:31:27 +00:00
orders , err := orderReq . Do ( ctx )
2021-05-27 18:45:09 +00:00
if err != nil {
return nil , err
}
2024-01-10 02:31:27 +00:00
if len ( orders ) != 1 {
return nil , fmt . Errorf ( "unexpected length of order response: %v" , orders )
}
2024-03-13 06:54:29 +00:00
orderID , err := strconv . ParseUint ( orders [ 0 ] . OrderID , 10 , 64 )
2024-01-10 02:31:27 +00:00
if err != nil {
2024-03-13 06:54:29 +00:00
return nil , fmt . Errorf ( "failed to parse response order id: %w" , err )
2024-01-10 02:31:27 +00:00
}
2024-03-13 06:54:29 +00:00
return & types . Order {
SubmitOrder : order ,
Exchange : types . ExchangeOKEx ,
OrderID : orderID ,
Status : types . OrderStatusNew ,
ExecutedQuantity : fixedpoint . Zero ,
IsWorking : true ,
CreationTime : types . Time ( timeNow ) ,
UpdateTime : types . Time ( timeNow ) ,
} , nil
2022-09-09 10:41:06 +00:00
// TODO: move this to batch place orders interface
/ *
batchReq := e . client . TradeService . NewBatchPlaceOrderRequest ( )
batchReq . Add ( reqs ... )
orderHeads , err := batchReq . Do ( ctx )
2021-05-27 18:45:09 +00:00
if err != nil {
2022-09-09 10:41:06 +00:00
return nil , err
2021-05-27 18:45:09 +00:00
}
2022-09-09 10:41:06 +00:00
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 ,
} )
}
* /
2021-05-25 19:05:54 +00:00
}
2024-01-11 08:41:42 +00:00
// QueryOpenOrders retrieves the pending orders. The data returned is ordered by createdTime, and we utilized the
// `After` parameter to acquire all orders.
2021-05-25 19:05:54 +00:00
func ( e * Exchange ) QueryOpenOrders ( ctx context . Context , symbol string ) ( orders [ ] types . Order , err error ) {
2021-05-27 18:45:09 +00:00
instrumentID := toLocalSymbol ( symbol )
2024-01-11 08:41:42 +00:00
nextCursor := int64 ( 0 )
for {
if err := queryOpenOrderLimiter . Wait ( ctx ) ; err != nil {
return nil , fmt . Errorf ( "query open orders rate limiter wait error: %w" , err )
}
req := e . client . NewGetOpenOrdersRequest ( ) .
InstrumentID ( instrumentID ) .
After ( strconv . FormatInt ( nextCursor , 10 ) )
openOrders , err := req . Do ( ctx )
if err != nil {
return nil , fmt . Errorf ( "failed to query open orders: %w" , err )
}
for _ , o := range openOrders {
2024-01-12 07:09:45 +00:00
o , err := orderDetailToGlobal ( & o . OrderDetail )
2024-01-11 08:41:42 +00:00
if err != nil {
return nil , fmt . Errorf ( "failed to convert order, err: %v" , err )
}
orders = append ( orders , * o )
}
orderLen := len ( openOrders )
// a defensive programming to ensure the length of order response is expected.
if orderLen > defaultQueryLimit {
return nil , fmt . Errorf ( "unexpected open orders length %d" , orderLen )
}
if orderLen < defaultQueryLimit {
break
}
nextCursor = int64 ( openOrders [ orderLen - 1 ] . OrderId )
2021-05-27 18:45:09 +00:00
}
return orders , err
2021-05-25 19:05:54 +00:00
}
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
}
2021-05-27 18:45:09 +00:00
var reqs [ ] * okexapi . CancelOrderRequest
for _ , order := range orders {
2021-05-27 19:05:59 +00:00
if len ( order . Symbol ) == 0 {
2023-08-21 07:31:30 +00:00
return ErrSymbolRequired
2021-05-27 19:05:59 +00:00
}
2023-09-06 11:14:21 +00:00
req := e . client . NewCancelOrderRequest ( )
2021-05-27 19:05:59 +00:00
req . InstrumentID ( toLocalSymbol ( order . Symbol ) )
2021-05-27 18:45:09 +00:00
req . OrderID ( strconv . FormatUint ( order . OrderID , 10 ) )
2021-05-27 19:05:59 +00:00
if len ( order . ClientOrderID ) > 0 {
2024-02-20 07:14:09 +00:00
if ok := clientOrderIdRegex . MatchString ( order . ClientOrderID ) ; ! ok {
return fmt . Errorf ( "client order id should be case-sensitive alphanumerics, all numbers, or all letters of up to 32 characters: %s" , order . ClientOrderID )
2024-01-10 08:39:52 +00:00
}
2021-05-27 19:05:59 +00:00
req . ClientOrderID ( order . ClientOrderID )
}
2021-05-27 18:45:09 +00:00
reqs = append ( reqs , req )
}
2024-01-10 08:39:52 +00:00
if err := batchCancelOrderLimiter . Wait ( ctx ) ; err != nil {
return fmt . Errorf ( "batch cancel order rate limiter wait error: %w" , err )
}
2023-09-06 11:14:21 +00:00
batchReq := e . client . NewBatchCancelOrderRequest ( )
2021-05-27 18:45:09 +00:00
batchReq . Add ( reqs ... )
_ , err := batchReq . Do ( ctx )
return err
2021-05-25 19:05:54 +00:00
}
2021-05-26 16:35:51 +00:00
func ( e * Exchange ) NewStream ( ) types . Stream {
2024-01-17 03:34:15 +00:00
return NewStream ( e . client , e )
2021-05-26 16:35:51 +00:00
}
2024-02-23 09:04:03 +00:00
func ( e * Exchange ) QueryKLines (
ctx context . Context , symbol string , interval types . Interval , options types . KLineQueryOptions ,
) ( [ ] types . KLine , error ) {
2024-01-29 09:09:19 +00:00
if err := queryKLineLimiter . Wait ( ctx ) ; err != nil {
return nil , fmt . Errorf ( "query k line rate limiter wait error: %w" , err )
2022-05-03 03:14:53 +00:00
}
2023-10-02 04:55:30 +00:00
intervalParam , err := toLocalInterval ( interval )
if err != nil {
2024-01-29 09:09:19 +00:00
return nil , fmt . Errorf ( "failed to get interval: %w" , err )
2023-10-02 04:55:30 +00:00
}
2022-05-03 03:14:53 +00:00
2024-01-29 09:09:19 +00:00
req := e . client . NewGetCandlesRequest ( ) . InstrumentID ( toLocalSymbol ( symbol ) )
2022-05-03 03:14:53 +00:00
req . Bar ( intervalParam )
2021-05-28 12:51:10 +00:00
if options . StartTime != nil {
2024-01-29 09:09:19 +00:00
req . After ( * options . StartTime )
2021-05-28 12:51:10 +00:00
}
if options . EndTime != nil {
2024-01-29 09:09:19 +00:00
req . Before ( * options . EndTime )
2021-05-28 12:51:10 +00:00
}
candles , err := req . Do ( ctx )
if err != nil {
return nil , err
}
var klines [ ] types . KLine
for _ , candle := range candles {
2024-01-29 09:09:19 +00:00
klines = append ( klines , kLineToGlobal ( candle , interval , symbol ) )
2021-05-28 12:51:10 +00:00
}
return klines , nil
2021-05-26 16:35:51 +00:00
}
2023-08-09 07:05:26 +00:00
func ( e * Exchange ) QueryOrder ( ctx context . Context , q types . OrderQuery ) ( * types . Order , error ) {
if len ( q . Symbol ) == 0 {
2023-08-21 07:31:30 +00:00
return nil , ErrSymbolRequired
2023-08-09 07:05:26 +00:00
}
if len ( q . OrderID ) == 0 && len ( q . ClientOrderID ) == 0 {
2023-08-21 07:31:30 +00:00
return nil , errors . New ( "okex.QueryOrder: OrderId or ClientOrderId is required parameter" )
2023-08-09 07:05:26 +00:00
}
2023-09-06 11:14:21 +00:00
req := e . client . NewGetOrderDetailsRequest ( )
2023-09-18 07:55:58 +00:00
req . InstrumentID ( toLocalSymbol ( q . Symbol ) ) .
2023-08-09 07:05:26 +00:00
OrderID ( q . OrderID ) .
ClientOrderID ( q . ClientOrderID )
var order * okexapi . OrderDetails
2023-08-11 01:28:58 +00:00
order , err := req . Do ( ctx )
2023-08-09 07:05:26 +00:00
if err != nil {
return nil , err
}
2023-08-21 07:31:30 +00:00
return toGlobalOrder ( order )
2023-08-09 07:05:26 +00:00
}
2023-09-08 07:33:09 +00:00
2024-01-15 09:10:14 +00:00
// QueryOrderTrades quires order trades can query trades in last 3 months.
func ( e * Exchange ) QueryOrderTrades ( ctx context . Context , q types . OrderQuery ) ( trades [ ] types . Trade , err error ) {
2023-09-08 07:33:09 +00:00
if len ( q . ClientOrderID ) != 0 {
log . Warn ( "!!!OKEX EXCHANGE API NOTICE!!! Okex does not support searching for trades using OrderClientId." )
}
2023-09-18 07:55:58 +00:00
req := e . client . NewGetTransactionHistoryRequest ( )
2023-09-08 07:33:09 +00:00
if len ( q . Symbol ) != 0 {
2023-09-18 07:55:58 +00:00
req . InstrumentID ( toLocalSymbol ( q . Symbol ) )
2023-09-08 07:33:09 +00:00
}
if len ( q . OrderID ) != 0 {
req . OrderID ( q . OrderID )
}
2024-01-15 09:10:14 +00:00
if err := queryTradeLimiter . Wait ( ctx ) ; err != nil {
return nil , fmt . Errorf ( "order trade rate limiter wait error: %w" , err )
2023-09-08 07:33:09 +00:00
}
response , err := req . Do ( ctx )
if err != nil {
return nil , fmt . Errorf ( "failed to query order trades, err: %w" , err )
}
for _ , trade := range response {
2024-01-15 09:10:14 +00:00
trades = append ( trades , tradeToGlobal ( trade ) )
2023-09-08 07:33:09 +00:00
}
2023-09-18 07:55:58 +00:00
return trades , nil
}
/ *
QueryClosedOrders can query closed orders in last 3 months , there are no time interval limitations , as long as until >= since .
Please Use lastOrderID as cursor , only return orders later than that order , that order is not included .
2024-01-12 07:09:45 +00:00
If you want to query all orders within a large time range ( e . g . total orders > 100 ) , we recommend using batch . ClosedOrderBatchQuery .
* * since and until are inclusive , you can include the lastTradeId as well . * *
2023-09-18 07:55:58 +00:00
* /
2024-02-23 09:04:03 +00:00
func ( e * Exchange ) QueryClosedOrders (
ctx context . Context , symbol string , since , until time . Time , lastOrderID uint64 ,
) ( orders [ ] types . Order , err error ) {
2023-09-18 07:55:58 +00:00
if symbol == "" {
return nil , ErrSymbolRequired
}
2024-01-12 07:09:45 +00:00
newSince := since
now := time . Now ( )
if time . Since ( newSince ) > maxHistoricalDataQueryPeriod {
newSince = now . Add ( - maxHistoricalDataQueryPeriod )
log . Warnf ( "!!!OKX EXCHANGE API NOTICE!!! The closed order API cannot query data beyond 90 days from the current date, update %s -> %s" , since , newSince )
}
if until . Before ( newSince ) {
log . Warnf ( "!!!OKX EXCHANGE API NOTICE!!! The 'until' comes before 'since', update until to now(%s -> %s)." , until , now )
until = now
}
if until . Sub ( newSince ) > maxHistoricalDataQueryPeriod {
return nil , fmt . Errorf ( "the start time %s and end time %s cannot exceed 90 days" , newSince , until )
2023-09-18 07:55:58 +00:00
}
2024-01-12 07:09:45 +00:00
if err := queryClosedOrderRateLimiter . Wait ( ctx ) ; err != nil {
return nil , fmt . Errorf ( "query closed order rate limiter wait error: %w" , err )
2023-09-18 07:55:58 +00:00
}
res , err := e . client . NewGetOrderHistoryRequest ( ) .
InstrumentID ( toLocalSymbol ( symbol ) ) .
StartTime ( since ) .
EndTime ( until ) .
Limit ( defaultQueryLimit ) .
2024-01-12 07:09:45 +00:00
Before ( strconv . FormatUint ( lastOrderID , 10 ) ) .
2023-09-18 07:55:58 +00:00
Do ( ctx )
if err != nil {
return nil , fmt . Errorf ( "failed to call get order histories error: %w" , err )
}
for _ , order := range res {
2024-01-12 07:09:45 +00:00
o , err2 := orderDetailToGlobal ( & order )
2023-09-18 07:55:58 +00:00
if err2 != nil {
err = multierr . Append ( err , err2 )
continue
}
orders = append ( orders , * o )
}
if err != nil {
return nil , err
}
2023-09-08 07:33:09 +00:00
2023-09-18 07:55:58 +00:00
return types . SortOrdersAscending ( orders ) , nil
}
/ *
QueryTrades can query trades in last 3 months , there are no time interval limitations , as long as end_time >= start_time .
2024-01-15 09:10:14 +00:00
okx does not provide an API to query by trade ID , so we use the bill ID to do it . The trades result is ordered by timestamp .
REMARK : If your start time is 90 days earlier , we will update it to now - 90 days .
* * StartTime and EndTime are inclusive . * *
* * StartTime and EndTime cannot exceed 90 days . * *
* * StartTime , EndTime , FromTradeId can be used together . * *
If you want to query all trades within a large time range ( e . g . total orders > 100 ) , we recommend using batch . TradeBatchQuery .
2024-02-07 17:37:35 +00:00
We don ' t support the last trade id as a filter because okx supports bill ID only .
2023-09-18 07:55:58 +00:00
* /
2024-01-15 09:10:14 +00:00
func ( e * Exchange ) QueryTrades ( ctx context . Context , symbol string , options * types . TradeQueryOptions ) ( trades [ ] types . Trade , err error ) {
2023-09-18 07:55:58 +00:00
if symbol == "" {
return nil , ErrSymbolRequired
}
2024-01-15 09:10:14 +00:00
limit := options . Limit
2023-09-18 07:55:58 +00:00
if limit > defaultQueryLimit || limit <= 0 {
2024-01-15 09:10:14 +00:00
log . Infof ( "limit is exceeded default limit %d or zero, got: %d, use default limit" , defaultQueryLimit , limit )
2024-02-07 17:37:35 +00:00
limit = defaultQueryLimit
2023-09-18 07:55:58 +00:00
}
2024-03-14 09:17:41 +00:00
timeNow := e . timeNowFunc ( )
newStartTime := timeNow . Add ( - threeDaysHistoricalPeriod )
2024-01-15 09:10:14 +00:00
if options . StartTime != nil {
newStartTime = * options . StartTime
2024-03-14 09:17:41 +00:00
if timeNow . Sub ( newStartTime ) > maxHistoricalDataQueryPeriod {
newStartTime = timeNow . Add ( - maxHistoricalDataQueryPeriod )
2024-01-15 09:10:14 +00:00
log . Warnf ( "!!!OKX EXCHANGE API NOTICE!!! The trade API cannot query data beyond 90 days from the current date, update %s -> %s" , * options . StartTime , newStartTime )
}
2023-09-18 07:55:58 +00:00
}
2024-03-14 09:17:41 +00:00
endTime := timeNow
2024-01-15 09:10:14 +00:00
if options . EndTime != nil {
if options . EndTime . Before ( newStartTime ) {
return nil , fmt . Errorf ( "end time %s before start %s" , * options . EndTime , newStartTime )
2023-09-18 07:55:58 +00:00
}
2024-01-15 09:10:14 +00:00
if options . EndTime . Sub ( newStartTime ) > maxHistoricalDataQueryPeriod {
return nil , fmt . Errorf ( "start time %s and end time %s cannot greater than 90 days" , newStartTime , options . EndTime )
2023-09-18 07:55:58 +00:00
}
2024-03-14 09:17:41 +00:00
endTime = * options . EndTime
2024-01-15 09:10:14 +00:00
}
2023-09-18 07:55:58 +00:00
2024-02-07 17:37:35 +00:00
if options . LastTradeID != 0 {
// we don't support the last trade id as a filter because okx supports bill ID only.
// we don't have any more fields (types.Trade) to store it.
log . Infof ( "Last trade id not supported on QueryTrades" )
2023-09-18 07:55:58 +00:00
}
2024-03-14 09:17:41 +00:00
if timeNow . Sub ( newStartTime ) <= threeDaysHistoricalPeriod {
c := e . client . NewGetThreeDaysTransactionHistoryRequest ( ) .
InstrumentID ( toLocalSymbol ( symbol ) ) .
StartTime ( newStartTime ) .
EndTime ( endTime ) .
Limit ( uint64 ( limit ) )
return getTrades ( ctx , limit , func ( ctx context . Context , billId string ) ( [ ] okexapi . Trade , error ) {
c . Before ( billId )
return c . Do ( ctx )
} )
}
2024-01-15 09:10:14 +00:00
2024-03-14 09:17:41 +00:00
c := e . client . NewGetTransactionHistoryRequest ( ) .
InstrumentID ( toLocalSymbol ( symbol ) ) .
StartTime ( newStartTime ) .
EndTime ( endTime ) .
Limit ( uint64 ( limit ) )
return getTrades ( ctx , limit , func ( ctx context . Context , billId string ) ( [ ] okexapi . Trade , error ) {
c . Before ( billId )
return c . Do ( ctx )
} )
}
func getTrades ( ctx context . Context , limit int64 , doFunc func ( ctx context . Context , billId string ) ( [ ] okexapi . Trade , error ) ) ( trades [ ] types . Trade , err error ) {
billId := "0"
for {
response , err := doFunc ( ctx , billId )
2024-02-07 17:37:35 +00:00
if err != nil {
return nil , fmt . Errorf ( "failed to query trades, err: %w" , err )
}
for _ , trade := range response {
trades = append ( trades , tradeToGlobal ( trade ) )
}
tradeLen := int64 ( len ( response ) )
// a defensive programming to ensure the length of order response is expected.
if tradeLen > limit {
return nil , fmt . Errorf ( "unexpected trade length %d" , tradeLen )
}
if tradeLen < limit {
break
}
// use Before filter to get all data.
2024-03-14 09:17:41 +00:00
billId = response [ tradeLen - 1 ] . BillId . String ( )
2024-01-15 09:10:14 +00:00
}
2023-09-08 07:33:09 +00:00
return trades , nil
}
2023-10-02 04:55:30 +00:00
func ( e * Exchange ) SupportedInterval ( ) map [ types . Interval ] int {
return SupportedIntervals
}
func ( e * Exchange ) IsSupportedInterval ( interval types . Interval ) bool {
_ , ok := SupportedIntervals [ interval ]
return ok
}