Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

FEATURE: [okx] margin feature api (part2) #1893

Merged
merged 8 commits into from
Jan 23, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
84 changes: 55 additions & 29 deletions pkg/exchange/okex/convert.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,11 +40,15 @@ func toGlobalTicker(marketTicker okexapi.MarketTicker) *types.Ticker {

func toGlobalBalance(account *okexapi.Account) types.BalanceMap {
var balanceMap = types.BalanceMap{}
for _, balanceDetail := range account.Details {
balanceMap[balanceDetail.Currency] = types.Balance{
Currency: balanceDetail.Currency,
Available: balanceDetail.CashBalance,
Locked: balanceDetail.Frozen,
for _, detail := range account.Details {

balanceMap[detail.Currency] = types.Balance{
Currency: detail.Currency,
Available: detail.Available,
Locked: detail.FrozenBalance,
Interest: detail.Interest, // accrued interest
Borrowed: detail.Liability.Abs(), // okx liability does not include the accrued interest
NetAsset: detail.Equity,
}
}
return balanceMap
Expand Down Expand Up @@ -132,7 +136,7 @@ func toLocalSideType(side types.SideType) okexapi.SideType {
return okexapi.SideType(strings.ToLower(string(side)))
}

func tradeToGlobal(trade okexapi.Trade) types.Trade {
func toGlobalTrade(trade okexapi.Trade) types.Trade {
side := toGlobalSide(trade.Side)
return types.Trade{
ID: uint64(trade.TradeId),
Expand All @@ -149,8 +153,8 @@ func tradeToGlobal(trade okexapi.Trade) types.Trade {
// 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,
IsMargin: trade.InstrumentType == okexapi.InstrumentTypeMargin,
IsFutures: trade.InstrumentType == okexapi.InstrumentTypeFutures,
IsIsolated: false,
}
}
Expand All @@ -174,7 +178,7 @@ func processMarketBuySize(o *okexapi.OrderDetail) (fixedpoint.Value, error) {
}
}

func orderDetailToGlobal(order *okexapi.OrderDetail) (*types.Order, error) {
func orderDetailToGlobalOrder(order *okexapi.OrderDetail) (*types.Order, error) {
side := toGlobalSide(order.Side)

orderType, err := toGlobalOrderType(order.OrderType)
Expand Down Expand Up @@ -226,6 +230,8 @@ func orderDetailToGlobal(order *okexapi.OrderDetail) (*types.Order, error) {
IsWorking: order.State.IsWorking(),
CreationTime: types.Time(order.CreatedTime),
UpdateTime: types.Time(order.UpdatedTime),
IsMargin: order.InstrumentType == okexapi.InstrumentTypeMargin,
IsFutures: order.InstrumentType == okexapi.InstrumentTypeFutures,
}, nil
}

Expand All @@ -245,34 +251,32 @@ func toGlobalOrderStatus(state okexapi.OrderState) (types.OrderStatus, error) {
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
var localOrderTypeMap = map[types.OrderType]okexapi.OrderType{
types.OrderTypeMarket: okexapi.OrderTypeMarket,
types.OrderTypeLimit: okexapi.OrderTypeLimit,
types.OrderTypeLimitMaker: okexapi.OrderTypePostOnly,
}

func toLocalOrderType(orderType types.OrderType) (okexapi.OrderType, error) {
if ot, ok := localOrderTypeMap[orderType]; ok {
return ot, nil
}

return "", fmt.Errorf("unknown or unsupported okex order type: %s", orderType)
}

var globalOrderTypeMap = map[okexapi.OrderType]types.OrderType{
okexapi.OrderTypeMarket: types.OrderTypeMarket,
okexapi.OrderTypeLimit: types.OrderTypeLimit,
okexapi.OrderTypePostOnly: types.OrderTypeLimitMaker,
okexapi.OrderTypeFOK: types.OrderTypeLimit,
okexapi.OrderTypeIOC: types.OrderTypeLimit,
}

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

case okexapi.OrderTypeLimit, okexapi.OrderTypeFOK, okexapi.OrderTypeIOC:
return types.OrderTypeLimit, nil

case okexapi.OrderTypePostOnly:
return types.OrderTypeLimitMaker, nil

if ot, ok := globalOrderTypeMap[orderType]; ok {
return ot, nil
}

return "", fmt.Errorf("unknown or unsupported okex order type: %s", orderType)
Expand Down Expand Up @@ -362,3 +366,25 @@ func toGlobalOrder(okexOrder *okexapi.OrderDetails) (*types.Order, error) {
IsIsolated: false,
}, nil
}

func toGlobalMarginLoan(record okexapi.MarginHistoryEntry) types.MarginLoan {
return types.MarginLoan{
Exchange: types.ExchangeOKEx,
TransactionID: uint64(record.Ts.Time().UnixMilli()),
Asset: record.Currency,
Principle: record.Amount,
Time: types.Time(record.Ts.Time()),
IsolatedSymbol: "",
}
}

func toGlobalMarginRepay(record okexapi.MarginHistoryEntry) types.MarginRepay {
return types.MarginRepay{
Exchange: types.ExchangeOKEx,
TransactionID: uint64(record.Ts.Time().UnixMilli()),
Asset: record.Currency,
Principle: record.Amount,
Time: types.Time(record.Ts.Time()),
IsolatedSymbol: "",
}
}
16 changes: 8 additions & 8 deletions pkg/exchange/okex/convert_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ func Test_orderDetailToGlobal(t *testing.T) {
)

t.Run("succeeds", func(t *testing.T) {
order, err := orderDetailToGlobal(openOrder)
order, err := orderDetailToGlobalOrder(openOrder)
assert.NoError(err)
assert.Equal(expOrder, order)
})
Expand All @@ -83,22 +83,22 @@ func Test_orderDetailToGlobal(t *testing.T) {
newExpOrder.Quantity = fixedpoint.NewFromFloat(100)
newExpOrder.Status = types.OrderStatusPartiallyFilled
newExpOrder.OriginalStatus = string(okexapi.OrderStatePartiallyFilled)
order, err := orderDetailToGlobal(&newOrder)
order, err := orderDetailToGlobalOrder(&newOrder)
assert.NoError(err)
assert.Equal(&newExpOrder, order)
})

t.Run("unexpected order status", func(t *testing.T) {
newOrder := *openOrder
newOrder.State = "xxx"
_, err := orderDetailToGlobal(&newOrder)
_, err := orderDetailToGlobalOrder(&newOrder)
assert.ErrorContains(err, "xxx")
})

t.Run("unexpected order type", func(t *testing.T) {
newOrder := *openOrder
newOrder.OrderType = "xxx"
_, err := orderDetailToGlobal(&newOrder)
_, err := orderDetailToGlobalOrder(&newOrder)
assert.ErrorContains(err, "xxx")
})

Expand All @@ -114,7 +114,7 @@ func Test_tradeToGlobal(t *testing.T) {
assert.NoError(err)

t.Run("succeeds with sell/taker", func(t *testing.T) {
assert.Equal(tradeToGlobal(res), types.Trade{
assert.Equal(toGlobalTrade(res), types.Trade{
ID: uint64(724072849),
OrderID: uint64(665951654130348158),
Exchange: types.ExchangeOKEx,
Expand All @@ -134,7 +134,7 @@ func Test_tradeToGlobal(t *testing.T) {
t.Run("succeeds with buy/taker", func(t *testing.T) {
newRes := res
newRes.Side = okexapi.SideTypeBuy
assert.Equal(tradeToGlobal(newRes), types.Trade{
assert.Equal(toGlobalTrade(newRes), types.Trade{
ID: uint64(724072849),
OrderID: uint64(665951654130348158),
Exchange: types.ExchangeOKEx,
Expand All @@ -154,7 +154,7 @@ func Test_tradeToGlobal(t *testing.T) {
t.Run("succeeds with sell/maker", func(t *testing.T) {
newRes := res
newRes.ExecutionType = okexapi.LiquidityTypeMaker
assert.Equal(tradeToGlobal(newRes), types.Trade{
assert.Equal(toGlobalTrade(newRes), types.Trade{
ID: uint64(724072849),
OrderID: uint64(665951654130348158),
Exchange: types.ExchangeOKEx,
Expand All @@ -175,7 +175,7 @@ func Test_tradeToGlobal(t *testing.T) {
newRes := res
newRes.Side = okexapi.SideTypeBuy
newRes.ExecutionType = okexapi.LiquidityTypeMaker
assert.Equal(tradeToGlobal(newRes), types.Trade{
assert.Equal(toGlobalTrade(newRes), types.Trade{
ID: uint64(724072849),
OrderID: uint64(665951654130348158),
Exchange: types.ExchangeOKEx,
Expand Down
110 changes: 89 additions & 21 deletions pkg/exchange/okex/exchange.go
Original file line number Diff line number Diff line change
Expand Up @@ -210,22 +210,28 @@ func (e *Exchange) PlatformFeeCurrency() string {
}

func (e *Exchange) QueryAccount(ctx context.Context) (*types.Account, error) {
bals, err := e.QueryAccountBalances(ctx)
accounts, err := e.queryAccountBalance(ctx)
if err != nil {
return nil, err
}

if len(accounts) == 0 {
return nil, fmt.Errorf("account balance is empty")
}

balances := toGlobalBalance(&accounts[0])
account := types.NewAccount()
account.UpdateBalances(bals)
account.UpdateBalances(balances)

// for margin account
account.MarginRatio = accounts[0].MarginRatio
account.TotalAccountValue = accounts[0].TotalEquityInUSD

return account, nil
}

func (e *Exchange) QueryAccountBalances(ctx context.Context) (types.BalanceMap, error) {
if err := queryAccountLimiter.Wait(ctx); err != nil {
return nil, fmt.Errorf("account rate limiter wait error: %w", err)
}

accountBalances, err := e.client.NewGetAccountBalanceRequest().Do(ctx)
accountBalances, err := e.queryAccountBalance(ctx)
if err != nil {
return nil, err
}
Expand All @@ -237,6 +243,14 @@ func (e *Exchange) QueryAccountBalances(ctx context.Context) (types.BalanceMap,
return toGlobalBalance(&accountBalances[0]), nil
}

func (e *Exchange) queryAccountBalance(ctx context.Context) ([]okexapi.Account, error) {
if err := queryAccountLimiter.Wait(ctx); err != nil {
return nil, fmt.Errorf("account rate limiter wait error: %w", err)
}

return e.client.NewGetAccountBalanceRequest().Do(ctx)
}

func (e *Exchange) SubmitOrder(ctx context.Context, order types.SubmitOrder) (*types.Order, error) {
orderReq := e.client.NewPlaceOrderRequest()

Expand All @@ -245,7 +259,10 @@ func (e *Exchange) SubmitOrder(ctx context.Context, order types.SubmitOrder) (*t
orderReq.Size(order.Market.FormatQuantity(order.Quantity))

if e.MarginSettings.IsMargin {
orderReq.TradeMode(okexapi.TradeModeCross)
// okx market order with trade mode cross will be rejected:
// "The corresponding product of this BTC-USDT doesn't support the tgtCcy parameter"
//
// orderReq.TradeMode(okexapi.TradeModeCross)
}

// set price field for limit orders
Expand Down Expand Up @@ -346,7 +363,7 @@ func (e *Exchange) QueryOpenOrders(ctx context.Context, symbol string) (orders [
}

for _, o := range openOrders {
o, err := orderDetailToGlobal(&o.OrderDetail)
o, err := orderDetailToGlobalOrder(&o.OrderDetail)
if err != nil {
return nil, fmt.Errorf("failed to convert order, err: %v", err)
}
Expand Down Expand Up @@ -489,7 +506,7 @@ func (e *Exchange) QueryOrderTrades(ctx context.Context, q types.OrderQuery) (tr
}

for _, trade := range response {
trades = append(trades, tradeToGlobal(trade))
trades = append(trades, toGlobalTrade(trade))
}

return trades, nil
Expand Down Expand Up @@ -546,7 +563,7 @@ func (e *Exchange) QueryClosedOrders(
}

for _, order := range res {
o, err2 := orderDetailToGlobal(&order)
o, err2 := orderDetailToGlobalOrder(&order)
if err2 != nil {
err = multierr.Append(err, err2)
continue
Expand Down Expand Up @@ -591,27 +608,78 @@ func (e *Exchange) BorrowMarginAsset(ctx context.Context, asset string, amount f
}

func (e *Exchange) QueryMarginAssetMaxBorrowable(ctx context.Context, asset string) (fixedpoint.Value, error) {
req := e.client.NewGetAccountInterestLimitsRequest()
req.Currency(asset)
req := e.client.NewGetAccountMaxLoanRequest()
req.Currency(asset).
MarginMode(okexapi.MarginModeCross)

resp, err := req.Do(ctx)
if err != nil {
return fixedpoint.Zero, err
}

log.Infof("%+v", resp)

if len(resp) == 0 || len(resp[0].Records) == 0 {
if len(resp) == 0 {
return fixedpoint.Zero, nil
}

for _, record := range resp[0].Records {
if strings.ToUpper(record.Currency) == asset {
return record.LoanQuota, nil
return resp[0].MaxLoan, nil
}

func (e *Exchange) QueryLoanHistory(ctx context.Context, asset string, startTime, endTime *time.Time) ([]types.MarginLoan, error) {
req := e.client.NewGetAccountSpotBorrowRepayHistoryRequest().Currency(asset)
if endTime != nil {
req.Before(*endTime)
}
if startTime != nil {
req.After(*startTime)
}

resp, err := req.Do(ctx)
if err != nil {
return nil, err
}

var records []types.MarginLoan
for _, r := range resp {
switch r.Type {
case okexapi.MarginEventTypeManualBorrow, okexapi.MarginEventTypeAutoBorrow:
records = append(records, toGlobalMarginLoan(r))
}
}

return fixedpoint.Zero, nil
return records, nil
}

func (e *Exchange) QueryRepayHistory(ctx context.Context, asset string, startTime, endTime *time.Time) ([]types.MarginRepay, error) {
req := e.client.NewGetAccountSpotBorrowRepayHistoryRequest().Currency(asset)
if endTime != nil {
req.Before(*endTime)
}
if startTime != nil {
req.After(*startTime)
}

resp, err := req.Do(ctx)
if err != nil {
return nil, err
}

var records []types.MarginRepay
for _, r := range resp {
switch r.Type {
case okexapi.MarginEventTypeManualRepay, okexapi.MarginEventTypeAutoRepay:
records = append(records, toGlobalMarginRepay(r))
}
}

return records, nil
}

func (e *Exchange) QueryLiquidationHistory(ctx context.Context, startTime, endTime *time.Time) ([]types.MarginLiquidation, error) {
return nil, nil
}

func (e *Exchange) QueryInterestHistory(ctx context.Context, asset string, startTime, endTime *time.Time) ([]types.MarginInterest, error) {
return nil, nil
}

/*
Expand Down Expand Up @@ -708,7 +776,7 @@ func getTrades(
}

for _, trade := range response {
trades = append(trades, tradeToGlobal(trade))
trades = append(trades, toGlobalTrade(trade))
}

tradeLen := int64(len(response))
Expand Down
Loading
Loading