2023-07-21 06:55:27 +00:00
package bybit
import (
2023-07-24 09:01:34 +00:00
"context"
2023-08-15 03:43:43 +00:00
"errors"
2023-07-24 15:27:43 +00:00
"fmt"
2023-07-27 09:44:47 +00:00
"strconv"
2023-07-24 15:27:43 +00:00
"time"
2023-07-24 09:01:34 +00:00
2023-07-21 06:55:27 +00:00
"github.com/sirupsen/logrus"
2023-07-26 14:24:20 +00:00
"go.uber.org/multierr"
2023-07-24 15:27:43 +00:00
"golang.org/x/time/rate"
2023-07-21 06:55:27 +00:00
"github.com/c9s/bbgo/pkg/exchange/bybit/bybitapi"
2023-07-28 07:37:05 +00:00
v3 "github.com/c9s/bbgo/pkg/exchange/bybit/bybitapi/v3"
2023-08-09 06:05:26 +00:00
"github.com/c9s/bbgo/pkg/fixedpoint"
2023-07-21 06:55:27 +00:00
"github.com/c9s/bbgo/pkg/types"
)
2023-07-26 13:40:39 +00:00
const (
2023-07-28 07:37:05 +00:00
maxOrderIdLen = 36
defaultQueryLimit = 50
2023-08-09 03:41:04 +00:00
defaultKLineLimit = 1000
2023-07-28 07:37:05 +00:00
halfYearDuration = 6 * 30 * 24 * time . Hour
2023-07-26 13:40:39 +00:00
)
2023-07-24 15:27:43 +00:00
// https://bybit-exchange.github.io/docs/zh-TW/v5/rate-limit
// sharedRateLimiter indicates that the API belongs to the public API.
//
2023-08-10 07:05:13 +00:00
// The default order limiter apply 5 requests per second and a 5 initial bucket
// this includes QueryMarkets, QueryTicker, QueryAccountBalances, GetFeeRates
2023-07-25 13:30:49 +00:00
var (
2023-08-10 07:05:13 +00:00
sharedRateLimiter = rate . NewLimiter ( rate . Every ( time . Second / 5 ) , 5 )
2023-07-28 07:37:05 +00:00
tradeRateLimiter = rate . NewLimiter ( rate . Every ( time . Second / 5 ) , 5 )
orderRateLimiter = rate . NewLimiter ( rate . Every ( 100 * time . Millisecond ) , 10 )
closedOrderQueryLimiter = rate . NewLimiter ( rate . Every ( time . Second ) , 1 )
2023-07-24 15:27:43 +00:00
2023-07-25 13:30:49 +00:00
log = logrus . WithFields ( logrus . Fields {
"exchange" : "bybit" ,
} )
2023-08-09 06:05:26 +00:00
2023-08-09 03:41:04 +00:00
_ types . ExchangeAccountService = & Exchange { }
_ types . ExchangeMarketDataService = & Exchange { }
_ types . CustomIntervalProvider = & Exchange { }
_ types . ExchangeMinimal = & Exchange { }
_ types . ExchangeTradeService = & Exchange { }
_ types . Exchange = & Exchange { }
2023-08-15 06:18:36 +00:00
_ types . ExchangeOrderQueryService = & Exchange { }
2023-07-25 13:30:49 +00:00
)
2023-07-21 06:55:27 +00:00
type Exchange struct {
key , secret string
client * bybitapi . RestClient
2023-07-28 07:37:05 +00:00
v3client * v3 . Client
2023-07-21 06:55:27 +00:00
}
func New ( key , secret string ) ( * Exchange , error ) {
client , err := bybitapi . NewClient ( )
if err != nil {
return nil , err
}
if len ( key ) > 0 && len ( secret ) > 0 {
client . Auth ( key , secret )
}
return & Exchange {
key : key ,
// pragma: allowlist nextline secret
2023-08-07 06:55:09 +00:00
secret : secret ,
client : client ,
v3client : v3 . NewClient ( client ) ,
2023-07-21 06:55:27 +00:00
} , nil
}
func ( e * Exchange ) Name ( ) types . ExchangeName {
return types . ExchangeBybit
}
// PlatformFeeCurrency returns empty string. The platform does not support "PlatformFeeCurrency" but instead charges
// fees using the native token.
func ( e * Exchange ) PlatformFeeCurrency ( ) string {
return ""
}
2023-07-24 09:01:34 +00:00
func ( e * Exchange ) QueryMarkets ( ctx context . Context ) ( types . MarketMap , error ) {
2023-07-24 15:27:43 +00:00
if err := sharedRateLimiter . Wait ( ctx ) ; err != nil {
2023-07-27 09:44:47 +00:00
return nil , fmt . Errorf ( "markets rate limiter wait error: %w" , err )
2023-07-24 15:27:43 +00:00
}
2023-07-24 09:01:34 +00:00
instruments , err := e . client . NewGetInstrumentsInfoRequest ( ) . Do ( ctx )
if err != nil {
2023-07-26 14:24:20 +00:00
return nil , fmt . Errorf ( "failed to get instruments, err: %v" , err )
2023-07-24 09:01:34 +00:00
}
marketMap := types . MarketMap { }
for _ , s := range instruments . List {
marketMap . Add ( toGlobalMarket ( s ) )
}
return marketMap , nil
}
2023-07-24 15:27:43 +00:00
func ( e * Exchange ) QueryTicker ( ctx context . Context , symbol string ) ( * types . Ticker , error ) {
if err := sharedRateLimiter . Wait ( ctx ) ; err != nil {
2023-07-27 09:44:47 +00:00
return nil , fmt . Errorf ( "ticker order rate limiter wait error: %w" , err )
2023-07-24 15:27:43 +00:00
}
s , err := e . client . NewGetTickersRequest ( ) . Symbol ( symbol ) . DoWithResponseTime ( ctx )
if err != nil {
2023-07-27 09:44:47 +00:00
return nil , fmt . Errorf ( "failed to call ticker, symbol: %s, err: %w" , symbol , err )
2023-07-24 15:27:43 +00:00
}
if len ( s . List ) != 1 {
return nil , fmt . Errorf ( "unexpected ticker lenght, exp:1, got:%d" , len ( s . List ) )
}
ticker := toGlobalTicker ( s . List [ 0 ] , s . ClosedTime . Time ( ) )
return & ticker , nil
}
func ( e * Exchange ) QueryTickers ( ctx context . Context , symbols ... string ) ( map [ string ] types . Ticker , error ) {
tickers := map [ string ] types . Ticker { }
if len ( symbols ) > 0 {
for _ , s := range symbols {
t , err := e . QueryTicker ( ctx , s )
if err != nil {
return nil , err
}
tickers [ s ] = * t
}
return tickers , nil
}
if err := sharedRateLimiter . Wait ( ctx ) ; err != nil {
2023-07-27 09:44:47 +00:00
return nil , fmt . Errorf ( "tickers rate limiter wait error: %w" , err )
2023-07-24 15:27:43 +00:00
}
allTickers , err := e . client . NewGetTickersRequest ( ) . DoWithResponseTime ( ctx )
if err != nil {
2023-07-27 09:44:47 +00:00
return nil , fmt . Errorf ( "failed to call ticker, err: %w" , err )
2023-07-24 15:27:43 +00:00
}
for _ , s := range allTickers . List {
tickers [ s . Symbol ] = toGlobalTicker ( s , allTickers . ClosedTime . Time ( ) )
}
return tickers , nil
}
2023-07-25 13:30:49 +00:00
func ( e * Exchange ) QueryOpenOrders ( ctx context . Context , symbol string ) ( orders [ ] types . Order , err error ) {
cursor := ""
for {
req := e . client . NewGetOpenOrderRequest ( ) . Symbol ( symbol )
if len ( cursor ) != 0 {
// the default limit is 20.
req = req . Cursor ( cursor )
}
if err = tradeRateLimiter . Wait ( ctx ) ; err != nil {
2023-07-27 09:44:47 +00:00
return nil , fmt . Errorf ( "place order rate limiter wait error: %w" , err )
2023-07-25 13:30:49 +00:00
}
res , err := req . Do ( ctx )
if err != nil {
2023-07-27 09:44:47 +00:00
return nil , fmt . Errorf ( "failed to query open orders, err: %w" , err )
2023-07-25 13:30:49 +00:00
}
for _ , order := range res . List {
order , err := toGlobalOrder ( order )
if err != nil {
2023-07-26 14:24:20 +00:00
return nil , fmt . Errorf ( "failed to convert order, err: %v" , err )
2023-07-25 13:30:49 +00:00
}
orders = append ( orders , * order )
}
if len ( res . NextPageCursor ) == 0 {
break
}
cursor = res . NextPageCursor
}
return orders , nil
}
2023-07-26 13:40:39 +00:00
2023-08-15 03:43:43 +00:00
func ( e * Exchange ) QueryOrder ( ctx context . Context , q types . OrderQuery ) ( * types . Order , error ) {
if len ( q . OrderID ) == 0 && len ( q . ClientOrderID ) == 0 {
return nil , errors . New ( "one of OrderID/ClientOrderID is required parameter" )
}
if len ( q . OrderID ) != 0 && len ( q . ClientOrderID ) != 0 {
return nil , errors . New ( "only accept one parameter of OrderID/ClientOrderID" )
}
req := e . client . NewGetOrderHistoriesRequest ( )
if len ( q . Symbol ) != 0 {
req . Symbol ( q . Symbol )
}
if len ( q . OrderID ) != 0 {
req . OrderId ( q . OrderID )
}
if len ( q . ClientOrderID ) != 0 {
req . OrderLinkId ( q . ClientOrderID )
}
res , err := req . Do ( ctx )
if err != nil {
return nil , fmt . Errorf ( "failed to query order, queryConfig: %+v, err: %w" , q , err )
}
if len ( res . List ) != 1 {
return nil , fmt . Errorf ( "unexpected order length, queryConfig: %+v" , q )
}
return toGlobalOrder ( res . List [ 0 ] )
}
2023-08-15 06:18:36 +00:00
func ( e * Exchange ) QueryOrderTrades ( ctx context . Context , q types . OrderQuery ) ( trades [ ] types . Trade , err error ) {
if len ( q . ClientOrderID ) != 0 {
log . Warn ( "!!!BYBIT EXCHANGE API NOTICE!!! Bybit does not support searching for trades using OrderClientId." )
}
if len ( q . OrderID ) == 0 {
return nil , errors . New ( "orderID is required parameter" )
}
req := e . v3client . NewGetTradesRequest ( ) . OrderId ( q . OrderID )
if len ( q . Symbol ) != 0 {
req . Symbol ( q . Symbol )
}
if err := tradeRateLimiter . Wait ( ctx ) ; err != nil {
return nil , fmt . Errorf ( "trade rate limiter wait error: %w" , err )
}
response , err := req . Do ( ctx )
if err != nil {
return nil , fmt . Errorf ( "failed to query order trades, err: %w" , err )
}
var errs error
for _ , trade := range response . List {
res , err := v3ToGlobalTrade ( trade )
if err != nil {
errs = multierr . Append ( errs , err )
continue
}
trades = append ( trades , * res )
}
if errs != nil {
return nil , errs
}
return trades , nil
}
2023-07-26 13:40:39 +00:00
func ( e * Exchange ) SubmitOrder ( ctx context . Context , order types . SubmitOrder ) ( * types . Order , error ) {
if len ( order . Market . Symbol ) == 0 {
return nil , fmt . Errorf ( "order.Market.Symbol is required: %+v" , order )
}
req := e . client . NewPlaceOrderRequest ( )
2023-08-18 05:34:00 +00:00
req . Symbol ( order . Market . Symbol )
2023-07-26 13:40:39 +00:00
// set order type
orderType , err := toLocalOrderType ( order . Type )
if err != nil {
return nil , err
}
req . OrderType ( orderType )
// set side
side , err := toLocalSide ( order . Side )
if err != nil {
return nil , err
}
req . Side ( side )
// set quantity
2023-08-18 05:34:00 +00:00
orderQty := order . Quantity
// if the order is market buy, the quantity is quote coin, instead of base coin. so we need to convert it.
if order . Type == types . OrderTypeMarket && order . Side == types . SideTypeBuy {
ticker , err := e . QueryTicker ( ctx , order . Market . Symbol )
if err != nil {
return nil , err
}
orderQty = order . Quantity . Mul ( ticker . Buy )
}
req . Qty ( order . Market . FormatQuantity ( orderQty ) )
2023-07-26 13:40:39 +00:00
// set price
switch order . Type {
case types . OrderTypeLimit :
req . Price ( order . Market . FormatPrice ( order . Price ) )
}
// set timeInForce
switch order . TimeInForce {
case types . TimeInForceFOK :
req . TimeInForce ( bybitapi . TimeInForceFOK )
case types . TimeInForceIOC :
req . TimeInForce ( bybitapi . TimeInForceIOC )
default :
req . TimeInForce ( bybitapi . TimeInForceGTC )
}
// set client order id
if len ( order . ClientOrderID ) > maxOrderIdLen {
return nil , fmt . Errorf ( "unexpected length of order id, got: %d" , len ( order . ClientOrderID ) )
}
2023-08-15 03:03:21 +00:00
if len ( order . ClientOrderID ) > 0 {
req . OrderLinkId ( order . ClientOrderID )
}
2023-07-26 13:40:39 +00:00
if err := orderRateLimiter . Wait ( ctx ) ; err != nil {
2023-07-27 09:44:47 +00:00
return nil , fmt . Errorf ( "place order rate limiter wait error: %w" , err )
2023-07-26 13:40:39 +00:00
}
res , err := req . Do ( ctx )
if err != nil {
2023-07-27 09:44:47 +00:00
return nil , fmt . Errorf ( "failed to place order, order: %#v, err: %w" , order , err )
2023-07-26 13:40:39 +00:00
}
2023-08-15 03:03:21 +00:00
if len ( res . OrderId ) == 0 || ( len ( order . ClientOrderID ) != 0 && res . OrderLinkId != order . ClientOrderID ) {
2023-07-26 13:40:39 +00:00
return nil , fmt . Errorf ( "unexpected order id, resp: %#v, order: %#v" , res , order )
}
2023-08-15 03:03:21 +00:00
ordersResp , err := e . client . NewGetOpenOrderRequest ( ) . OrderId ( res . OrderId ) . Do ( ctx )
2023-07-26 13:40:39 +00:00
if err != nil {
2023-07-27 09:44:47 +00:00
return nil , fmt . Errorf ( "failed to query order by client order id: %s, err: %w" , res . OrderLinkId , err )
2023-07-26 13:40:39 +00:00
}
if len ( ordersResp . List ) != 1 {
return nil , fmt . Errorf ( "unexpected order length, client order id: %s" , res . OrderLinkId )
}
return toGlobalOrder ( ordersResp . List [ 0 ] )
}
2023-07-26 14:24:20 +00:00
func ( e * Exchange ) CancelOrders ( ctx context . Context , orders ... types . Order ) ( errs error ) {
if len ( orders ) == 0 {
return nil
}
for _ , order := range orders {
req := e . client . NewCancelOrderRequest ( )
2023-08-14 10:13:24 +00:00
reqId := ""
2023-07-26 14:24:20 +00:00
switch {
2023-08-14 10:13:24 +00:00
// use the OrderID first, then the ClientOrderID
case order . OrderID > 0 :
req . OrderId ( order . UUID )
reqId = order . UUID
2023-07-26 14:24:20 +00:00
case len ( order . ClientOrderID ) != 0 :
req . OrderLinkId ( order . ClientOrderID )
2023-08-14 10:13:24 +00:00
reqId = order . ClientOrderID
2023-07-26 14:24:20 +00:00
default :
errs = multierr . Append (
errs ,
fmt . Errorf ( "the order uuid and client order id are empty, order: %#v" , order ) ,
)
continue
}
req . Symbol ( order . Market . Symbol )
if err := orderRateLimiter . Wait ( ctx ) ; err != nil {
2023-07-27 09:44:47 +00:00
errs = multierr . Append ( errs , fmt . Errorf ( "cancel order rate limiter wait, order id: %s, error: %w" , order . ClientOrderID , err ) )
2023-07-26 14:24:20 +00:00
continue
}
res , err := req . Do ( ctx )
if err != nil {
2023-07-27 09:44:47 +00:00
errs = multierr . Append ( errs , fmt . Errorf ( "failed to cancel order id: %s, err: %w" , order . ClientOrderID , err ) )
2023-07-26 14:24:20 +00:00
continue
}
2023-08-14 10:13:24 +00:00
// sanity check
if res . OrderId != reqId && res . OrderLinkId != reqId {
errs = multierr . Append ( errs , fmt . Errorf ( "order id mismatch, exp: %s, respOrderId: %s, respOrderLinkId: %s" , reqId , res . OrderId , res . OrderLinkId ) )
2023-07-26 14:24:20 +00:00
continue
}
}
return errs
}
2023-07-27 09:44:47 +00:00
func ( e * Exchange ) QueryClosedOrders ( ctx context . Context , symbol string , since , util time . Time , lastOrderID uint64 ) ( orders [ ] types . Order , err error ) {
if ! since . IsZero ( ) || ! util . IsZero ( ) {
log . Warn ( "!!!BYBIT EXCHANGE API NOTICE!!! the since/until conditions will not be effected on SPOT account, bybit exchange does not support time-range-based query currently" )
}
2023-07-28 07:37:05 +00:00
if err := closedOrderQueryLimiter . Wait ( ctx ) ; err != nil {
2023-07-27 09:44:47 +00:00
return nil , fmt . Errorf ( "query closed order rate limiter wait error: %w" , err )
}
res , err := e . client . NewGetOrderHistoriesRequest ( ) .
Symbol ( symbol ) .
Cursor ( strconv . FormatUint ( lastOrderID , 10 ) ) .
2023-07-28 07:37:05 +00:00
Limit ( defaultQueryLimit ) .
2023-07-27 09:44:47 +00:00
Do ( ctx )
if err != nil {
return nil , fmt . Errorf ( "failed to call get order histories error: %w" , err )
}
for _ , order := range res . List {
o , err2 := toGlobalOrder ( order )
if err2 != nil {
err = multierr . Append ( err , err2 )
continue
}
if o . Status . Closed ( ) {
orders = append ( orders , * o )
}
}
if err != nil {
return nil , err
}
return types . SortOrdersAscending ( orders ) , nil
}
2023-07-28 07:37:05 +00:00
/ *
QueryTrades queries trades by time range or trade id range .
If options . StartTime is not specified , you can only query for records in the last 7 days .
If you want to query for records older than 7 days , options . StartTime is required .
It supports to query records up to 180 days .
If the orderId is null , fromTradeId is passed , and toTradeId is null , then the result is sorted by
ticketId in ascend . Otherwise , the result is sorted by ticketId in descend .
* * Here includes MakerRebate . If needed , let ' s discuss how to modify it to return in trade . * *
* * StartTime and EndTime are inclusive . * *
* * StartTime and EndTime cannot exceed 180 days . * *
* /
func ( e * Exchange ) QueryTrades ( ctx context . Context , symbol string , options * types . TradeQueryOptions ) ( trades [ ] types . Trade , err error ) {
// using v3 client, since the v5 API does not support feeCurrency.
req := e . v3client . NewGetTradesRequest ( )
req . Symbol ( symbol )
if options . StartTime != nil || options . EndTime != nil {
if options . StartTime != nil {
req . StartTime ( options . StartTime . UTC ( ) )
}
if options . EndTime != nil {
req . EndTime ( options . EndTime . UTC ( ) )
}
} else {
req . FromTradeId ( strconv . FormatUint ( options . LastTradeID , 10 ) )
}
limit := uint64 ( options . Limit )
if limit > defaultQueryLimit || limit <= 0 {
log . Debugf ( "limtit is exceeded or zero, update to %d, got: %d" , defaultQueryLimit , options . Limit )
limit = defaultQueryLimit
}
req . Limit ( limit )
if err := tradeRateLimiter . Wait ( ctx ) ; err != nil {
return nil , fmt . Errorf ( "trade rate limiter wait error: %w" , err )
}
response , err := req . Do ( ctx )
if err != nil {
return nil , fmt . Errorf ( "failed to query trades, err: %w" , err )
}
var errs error
for _ , trade := range response . List {
res , err := v3ToGlobalTrade ( trade )
if err != nil {
errs = multierr . Append ( errs , err )
continue
}
trades = append ( trades , * res )
}
if errs != nil {
return nil , errs
}
return trades , nil
}
2023-08-02 08:57:30 +00:00
2023-08-09 06:05:26 +00:00
func ( e * Exchange ) QueryAccount ( ctx context . Context ) ( * types . Account , error ) {
balanceMap , err := e . QueryAccountBalances ( ctx )
if err != nil {
return nil , err
}
acct := & types . Account {
AccountType : types . AccountTypeSpot ,
// MakerFeeRate bybit doesn't support global maker fee rate.
MakerFeeRate : fixedpoint . Zero ,
// TakerFeeRate bybit doesn't support global taker fee rate.
TakerFeeRate : fixedpoint . Zero ,
}
acct . UpdateBalances ( balanceMap )
return acct , nil
}
2023-08-09 04:50:30 +00:00
func ( e * Exchange ) QueryAccountBalances ( ctx context . Context ) ( types . BalanceMap , error ) {
if err := sharedRateLimiter . Wait ( ctx ) ; err != nil {
return nil , fmt . Errorf ( "query account balances rate limiter wait error: %w" , err )
}
req := e . client . NewGetWalletBalancesRequest ( )
accounts , err := req . Do ( ctx )
if err != nil {
return nil , err
}
return toGlobalBalanceMap ( accounts . List ) , nil
}
2023-08-09 03:41:04 +00:00
/ *
QueryKLines queries for historical klines ( also known as candles / candlesticks ) . Charts are returned in groups based
on the requested interval .
A k - line ' s start time is inclusive , but end time is not ( startTime + interval - 1 millisecond ) .
e . q . 15 m interval k line can be represented as 00 : 00 : 00.000 ~ 00 : 14 : 59.999
* /
func ( e * Exchange ) QueryKLines ( ctx context . Context , symbol string , interval types . Interval , options types . KLineQueryOptions ) ( [ ] types . KLine , error ) {
req := e . client . NewGetKLinesRequest ( ) . Symbol ( symbol )
intervalStr , err := toLocalInterval ( interval )
if err != nil {
return nil , err
}
req . Interval ( intervalStr )
limit := uint64 ( options . Limit )
if limit > defaultKLineLimit || limit <= 0 {
log . Debugf ( "limtit is exceeded or zero, update to %d, got: %d" , defaultKLineLimit , options . Limit )
limit = defaultKLineLimit
}
req . Limit ( limit )
if options . StartTime != nil {
req . StartTime ( * options . StartTime )
}
if options . EndTime != nil {
req . EndTime ( * options . EndTime )
}
if err := sharedRateLimiter . Wait ( ctx ) ; err != nil {
return nil , fmt . Errorf ( "query klines rate limiter wait error: %w" , err )
}
resp , err := req . Do ( ctx )
if err != nil {
return nil , fmt . Errorf ( "failed to call k line, err: %w" , err )
}
if resp . Category != bybitapi . CategorySpot {
return nil , fmt . Errorf ( "unexpected category: %s" , resp . Category )
}
if resp . Symbol != symbol {
return nil , fmt . Errorf ( "unexpected symbol: %s, exp: %s" , resp . Category , symbol )
}
kLines := toGlobalKLines ( symbol , interval , resp . List )
return types . SortKLinesAscending ( kLines ) , nil
}
func ( e * Exchange ) SupportedInterval ( ) map [ types . Interval ] int {
return bybitapi . SupportedIntervals
}
func ( e * Exchange ) IsSupportedInterval ( interval types . Interval ) bool {
_ , ok := bybitapi . SupportedIntervals [ interval ]
return ok
}
2023-08-10 07:11:06 +00:00
func ( e * Exchange ) GetAllFeeRates ( ctx context . Context ) ( bybitapi . FeeRates , error ) {
2023-08-10 07:05:13 +00:00
if err := sharedRateLimiter . Wait ( ctx ) ; err != nil {
return bybitapi . FeeRates { } , fmt . Errorf ( "query fee rate limiter wait error: %w" , err )
}
feeRates , err := e . client . NewGetFeeRatesRequest ( ) . Do ( ctx )
if err != nil {
return bybitapi . FeeRates { } , fmt . Errorf ( "failed to get fee rates, err: %w" , err )
}
return * feeRates , nil
}
2023-08-02 08:57:30 +00:00
func ( e * Exchange ) NewStream ( ) types . Stream {
2023-08-10 07:11:06 +00:00
return NewStream ( e . key , e . secret , e )
2023-08-02 08:57:30 +00:00
}