Binance,Okx: Add Leverage, MarginType, Positions and CollateralMode support (#1220)

* init

* surprise train commit

* basic distinctions

* the terms of binance are confusing

* renames and introduction of allocatedMargin

* add new margin funcs

* pulling out wires

* implement proper getposition stuff

* bad coding day

* investigate order manager next

* a broken mess, but a progressing one

* finally completes some usdtmargined stuff

* coinMfutures eludes me

* expand to okx

* imports fix

* completes okx wrapper implementations

* cleans and polishes before rpc implementations

* rpc setup, order manager features, exch features

* more rpc, collateral and margin things

* mini test

* looking at rpc response, expansion of features

* reorganising before the storm

* changing how futures requests work

* cleanup and tests of cli usage

* remove silly client side logic

* cleanup

* collateral package, typo fix, margin err, rpc derive

* uses convert.StringToFloat ONLY ON STRUCTS FROM THIS PR

* fix binance order history bug

* niteroos

* adds new funcs to exchange standards testing

* more post merge fixes

* fix binance

* replace simepletimeformat

* fix for merge

* merge fixes

* micro fixes

* order side now required for leverage

* fix up the rest

* global -> portfolio collateral

* Update exchanges/collateral/collateral_test.go

Co-authored-by: Adrian Gallagher <adrian.gallagher@thrasher.io>

* adds fields and todos

* rm field redundancy

* lint fix oopsie daisy

* fixes panic, expands error and cli explanations (sorry shaz)

* ensures casing is appropriate for underlying

* Adds a shiny TODO

---------

Co-authored-by: Adrian Gallagher <adrian.gallagher@thrasher.io>
This commit is contained in:
Scott
2023-09-26 16:16:31 +10:00
committed by GitHub
parent a2ae99ed7f
commit 5f2f6f884b
67 changed files with 11558 additions and 4475 deletions

View File

@@ -284,6 +284,7 @@ var (
errMissingValidGreeksType = errors.New("missing valid greeks type")
errMissingIsolatedMarginTradingSetting = errors.New("missing isolated margin trading setting, isolated margin trading settings automatic:Auto transfers autonomy:Manual transfers")
errInvalidOrderSide = errors.New("invalid order side")
errOrderSideRequired = errors.New("order side required")
errInvalidCounterParties = errors.New("missing counter parties")
errInvalidLegs = errors.New("no legs are provided")
errMissingRFQIDANDClientSuppliedRFQID = errors.New("missing rfq id or client supplied rfq id")
@@ -338,6 +339,7 @@ var (
errInvalidProtocolType = errors.New("invalid protocol type, only 'staking' and 'defi' allowed")
errExceedLimit = errors.New("limit exceeded")
errOnlyThreeMonthsSupported = errors.New("only three months of trade data retrieval supported")
errOnlyOneResponseExpected = errors.New("one response item expected")
errNoInstrumentFound = errors.New("no instrument found")
)
@@ -1859,9 +1861,9 @@ func (ok *Okx) GetConvertHistory(ctx context.Context, before, after time.Time, l
/********************************** Account endpoints ***************************************************/
// GetNonZeroBalances retrieves a list of assets (with non-zero balance), remaining balance, and available amount in the trading account.
// AccountBalance retrieves a list of assets (with non-zero balance), remaining balance, and available amount in the trading account.
// Interest-free quota and discount rates are public data and not displayed on the account interface.
func (ok *Okx) GetNonZeroBalances(ctx context.Context, currency string) ([]Account, error) {
func (ok *Okx) AccountBalance(ctx context.Context, currency string) ([]Account, error) {
var resp []Account
params := url.Values{}
if currency != "" {
@@ -2010,26 +2012,12 @@ func (ok *Okx) SetPositionMode(ctx context.Context, positionMode string) (string
return "", errNoValidResponseFromServer
}
// SetLeverage sets a leverage setting for instrument id.
func (ok *Okx) SetLeverage(ctx context.Context, arg SetLeverageInput) (*SetLeverageResponse, error) {
// SetLeverageRate sets a leverage setting for instrument id.
func (ok *Okx) SetLeverageRate(ctx context.Context, arg SetLeverageInput) (*SetLeverageResponse, error) {
if arg.InstrumentID == "" && arg.Currency == "" {
return nil, errors.New("either instrument id or currency is required")
}
if arg.Leverage < 0 {
return nil, errors.New("missing leverage")
}
if arg.InstrumentID == "" && arg.MarginMode == TradeModeIsolated {
return nil, errors.New("only can be cross if ccy is passed")
}
if arg.MarginMode != TradeModeCross && arg.MarginMode != TradeModeIsolated {
return nil, errors.New("only applicable to \"isolated\" margin mode of FUTURES/SWAP allowed")
}
arg.PositionSide = strings.ToLower(arg.PositionSide)
if arg.PositionSide != positionSideLong &&
arg.PositionSide != positionSideShort &&
arg.MarginMode == "isolated" {
return nil, errors.New("\"long\" \"short\" Only applicable to isolated margin mode of FUTURES/SWAP")
}
var resp []SetLeverageResponse
err := ok.SendHTTPRequest(ctx, exchange.RestSpot, setLeverageEPL, http.MethodPost, accountSetLeverage, &arg, &resp, true)
if err != nil {
@@ -2107,7 +2095,7 @@ func (ok *Okx) IncreaseDecreaseMargin(ctx context.Context, arg IncreaseDecreaseM
return nil, errors.New("missing valid amount")
}
var resp []IncreaseDecreaseMargin
err := ok.SendHTTPRequest(ctx, exchange.RestSpot, increaseOrDecreaseMarginEPL, http.MethodGet, accountPositionMarginBalance, &arg, &resp, true)
err := ok.SendHTTPRequest(ctx, exchange.RestSpot, increaseOrDecreaseMarginEPL, http.MethodPost, accountPositionMarginBalance, &arg, &resp, true)
if err != nil {
return nil, err
}
@@ -2117,8 +2105,8 @@ func (ok *Okx) IncreaseDecreaseMargin(ctx context.Context, arg IncreaseDecreaseM
return nil, errNoValidResponseFromServer
}
// GetLeverage retrieves leverage data for different instrument id or margin mode.
func (ok *Okx) GetLeverage(ctx context.Context, instrumentID, marginMode string) ([]LeverageResponse, error) {
// GetLeverageRate retrieves leverage data for different instrument id or margin mode.
func (ok *Okx) GetLeverageRate(ctx context.Context, instrumentID, marginMode string) ([]LeverageResponse, error) {
params := url.Values{}
if instrumentID != "" {
params.Set("instId", instrumentID)
@@ -3461,10 +3449,8 @@ func (ok *Okx) GetFundingRateHistory(ctx context.Context, instrumentID string, b
if !after.IsZero() {
params.Set("after", strconv.FormatInt(after.UnixMilli(), 10))
}
if limit > 0 && limit <= 100 {
if limit > 0 {
params.Set("limit", strconv.FormatInt(limit, 10))
} else if limit > 100 {
return nil, errLimitValueExceedsMaxOf100
}
var resp []FundingRateResponse
return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, getFundingRateHistoryEPL, http.MethodGet, common.EncodeURLValues(publicFundingRateHistory, params), nil, &resp, false)

File diff suppressed because it is too large Load Diff

View File

@@ -1237,72 +1237,72 @@ type Account struct {
// AccountDetail account detail information.
type AccountDetail struct {
AvailableBalance okxNumericalValue `json:"availBal"`
AvailableEquity okxNumericalValue `json:"availEq"`
CashBalance okxNumericalValue `json:"cashBal"` // Cash Balance
Currency string `json:"ccy"`
CrossLiab okxNumericalValue `json:"crossLiab"`
DiscountEquity okxNumericalValue `json:"disEq"`
EquityOfCurrency okxNumericalValue `json:"eq"`
EquityUsd okxNumericalValue `json:"eqUsd"`
FrozenBalance okxNumericalValue `json:"frozenBal"`
Interest okxNumericalValue `json:"interest"`
IsoEquity okxNumericalValue `json:"isoEq"`
IsolatedLiabilities okxNumericalValue `json:"isoLiab"`
IsoUpl okxNumericalValue `json:"isoUpl"` // Isolated unrealized profit and loss of the currency applicable to Single-currency margin and Multi-currency margin and Portfolio margin
LiabilitiesOfCurrency okxNumericalValue `json:"liab"`
MaxLoan okxNumericalValue `json:"maxLoan"`
MarginRatio okxNumericalValue `json:"mgnRatio"` // Equity of the currency
NotionalLever okxNumericalValue `json:"notionalLever"` // Leverage of the currency applicable to Single-currency margin
OpenOrdersMarginFrozen okxNumericalValue `json:"ordFrozen"`
Twap okxNumericalValue `json:"twap"`
UpdateTime okxUnixMilliTime `json:"uTime"`
UnrealizedProfit okxNumericalValue `json:"upl"`
UnrealizedCurrencyLiabilities okxNumericalValue `json:"uplLiab"`
StrategyEquity okxNumericalValue `json:"stgyEq"` // strategy equity
TotalEquity okxNumericalValue `json:"totalEq"` // Total equity in USD level
AvailableBalance convert.StringToFloat64 `json:"availBal"`
AvailableEquity convert.StringToFloat64 `json:"availEq"`
CashBalance convert.StringToFloat64 `json:"cashBal"` // Cash Balance
Currency string `json:"ccy"`
CrossLiab convert.StringToFloat64 `json:"crossLiab"`
DiscountEquity convert.StringToFloat64 `json:"disEq"`
EquityOfCurrency convert.StringToFloat64 `json:"eq"`
EquityUsd convert.StringToFloat64 `json:"eqUsd"`
FrozenBalance convert.StringToFloat64 `json:"frozenBal"`
Interest convert.StringToFloat64 `json:"interest"`
IsoEquity convert.StringToFloat64 `json:"isoEq"`
IsolatedLiabilities convert.StringToFloat64 `json:"isoLiab"`
IsoUpl convert.StringToFloat64 `json:"isoUpl"` // Isolated unrealized profit and loss of the currency applicable to Single-currency margin and Multi-currency margin and Portfolio margin
LiabilitiesOfCurrency convert.StringToFloat64 `json:"liab"`
MaxLoan convert.StringToFloat64 `json:"maxLoan"`
MarginRatio convert.StringToFloat64 `json:"mgnRatio"` // Equity of the currency
NotionalLever convert.StringToFloat64 `json:"notionalLever"` // Leverage of the currency applicable to Single-currency margin
OpenOrdersMarginFrozen convert.StringToFloat64 `json:"ordFrozen"`
Twap convert.StringToFloat64 `json:"twap"`
UpdateTime okxUnixMilliTime `json:"uTime"`
UnrealizedProfit convert.StringToFloat64 `json:"upl"`
UnrealizedCurrencyLiabilities convert.StringToFloat64 `json:"uplLiab"`
StrategyEquity convert.StringToFloat64 `json:"stgyEq"` // strategy equity
TotalEquity convert.StringToFloat64 `json:"totalEq"` // Total equity in USD level. Appears unused
}
// AccountPosition account position.
type AccountPosition struct {
AutoDeleveraging string `json:"adl"` // Auto-deleveraging (ADL) indicator Divided into 5 levels, from 1 to 5, the smaller the number, the weaker the adl intensity.
AvailablePosition string `json:"availPos"` // Position that can be closed Only applicable to MARGIN, FUTURES/SWAP in the long-short mode, OPTION in Simple and isolated OPTION in margin Account.
AveragePrice string `json:"avgPx"`
CreationTime okxUnixMilliTime `json:"cTime"`
Currency string `json:"ccy"`
DeltaBS string `json:"deltaBS"` // deltaBlack-Scholes Greeks in dollars,only applicable to OPTION
DeltaPA string `json:"deltaPA"` // deltaGreeks in coins,only applicable to OPTION
GammaBS string `json:"gammaBS"` // gammaBlack-Scholes Greeks in dollars,only applicable to OPTION
GammaPA string `json:"gammaPA"` // gammaGreeks in coins,only applicable to OPTION
InitialMarginRequirement string `json:"imr"` // Initial margin requirement, only applicable to cross.
InstrumentID string `json:"instId"`
InstrumentType string `json:"instType"`
Interest string `json:"interest"`
USDPrice string `json:"usdPx"`
LastTradePrice string `json:"last"`
Leverage string `json:"lever"` // Leverage, not applicable to OPTION seller
Liabilities string `json:"liab"` // Liabilities, only applicable to MARGIN.
LiabilitiesCurrency string `json:"liabCcy"` // Liabilities currency, only applicable to MARGIN.
LiquidationPrice string `json:"liqPx"` // Estimated liquidation price Not applicable to OPTION
MarkPx string `json:"markPx"`
Margin string `json:"margin"`
MgnMode string `json:"mgnMode"`
MgnRatio string `json:"mgnRatio"`
MaintenanceMarginRequirement string `json:"mmr"` // Maintenance margin requirement in USD level Applicable to Multi-currency margin and Portfolio margin
NotionalUsd string `json:"notionalUsd"` // Quality of Positions -- usd
OptionValue string `json:"optVal"` // Option Value, only application to position.
QuantityOfPosition string `json:"pos"` // Quantity of positions,In the mode of autonomous transfer from position to position, after the deposit is transferred, a position with pos of 0 will be generated
PositionCurrency string `json:"posCcy"`
PositionID string `json:"posId"`
PositionSide string `json:"posSide"`
ThetaBS string `json:"thetaBS"` // thetaBlack-Scholes Greeks in dollars,only applicable to OPTION
ThetaPA string `json:"thetaPA"` // thetaGreeks in coins,only applicable to OPTION
TradeID string `json:"tradeId"`
UpdatedTime okxUnixMilliTime `json:"uTime"` // Latest time position was adjusted,
Upl float64 `json:"upl,string,omitempty"` // Unrealized profit and loss
UPLRatio float64 `json:"uplRatio,string,omitempty"` // Unrealized profit and loss ratio
VegaBS string `json:"vegaBS"` // vegaBlack-Scholes Greeks in dollars,only applicable to OPTION
VegaPA string `json:"vegaPA"` // vegaGreeks in coins,only applicable to OPTION
AutoDeleveraging string `json:"adl"` // Auto-deleveraging (ADL) indicator Divided into 5 levels, from 1 to 5, the smaller the number, the weaker the adl intensity.
AvailablePosition string `json:"availPos"` // Position that can be closed Only applicable to MARGIN, FUTURES/SWAP in the long-short mode, OPTION in Simple and isolated OPTION in margin Account.
AveragePrice convert.StringToFloat64 `json:"avgPx"`
CreationTime okxUnixMilliTime `json:"cTime"`
Currency string `json:"ccy"`
DeltaBS string `json:"deltaBS"` // deltaBlack-Scholes Greeks in dollars,only applicable to OPTION
DeltaPA string `json:"deltaPA"` // deltaGreeks in coins,only applicable to OPTION
GammaBS string `json:"gammaBS"` // gammaBlack-Scholes Greeks in dollars,only applicable to OPTION
GammaPA string `json:"gammaPA"` // gammaGreeks in coins,only applicable to OPTION
InitialMarginRequirement convert.StringToFloat64 `json:"imr"` // Initial margin requirement, only applicable to cross.
InstrumentID string `json:"instId"`
InstrumentType asset.Item `json:"instType"`
Interest convert.StringToFloat64 `json:"interest"`
USDPrice convert.StringToFloat64 `json:"usdPx"`
LastTradePrice convert.StringToFloat64 `json:"last"`
Leverage convert.StringToFloat64 `json:"lever"` // Leverage, not applicable to OPTION seller
Liabilities string `json:"liab"` // Liabilities, only applicable to MARGIN.
LiabilitiesCurrency string `json:"liabCcy"` // Liabilities currency, only applicable to MARGIN.
LiquidationPrice convert.StringToFloat64 `json:"liqPx"` // Estimated liquidation price Not applicable to OPTION
MarkPrice convert.StringToFloat64 `json:"markPx"`
Margin convert.StringToFloat64 `json:"margin"`
MarginMode string `json:"mgnMode"`
MarginRatio convert.StringToFloat64 `json:"mgnRatio"`
MaintenanceMarginRequirement convert.StringToFloat64 `json:"mmr"` // Maintenance margin requirement in USD level Applicable to Multi-currency margin and Portfolio margin
NotionalUsd convert.StringToFloat64 `json:"notionalUsd"` // Quality of Positions -- usd
OptionValue convert.StringToFloat64 `json:"optVal"` // Option Value, only application to position.
QuantityOfPosition convert.StringToFloat64 `json:"pos"` // Quantity of positions,In the mode of autonomous transfer from position to position, after the deposit is transferred, a position with pos of 0 will be generated
PositionCurrency string `json:"posCcy"`
PositionID string `json:"posId"`
PositionSide string `json:"posSide"`
ThetaBS string `json:"thetaBS"` // thetaBlack-Scholes Greeks in dollars,only applicable to OPTION
ThetaPA string `json:"thetaPA"` // thetaGreeks in coins,only applicable to OPTION
TradeID string `json:"tradeId"`
UpdatedTime okxUnixMilliTime `json:"uTime"` // Latest time position was adjusted,
UPNL convert.StringToFloat64 `json:"upl"` // Unrealized profit and loss
UPLRatio convert.StringToFloat64 `json:"uplRatio"` // Unrealized profit and loss ratio
VegaBS string `json:"vegaBS"` // vegaBlack-Scholes Greeks in dollars,only applicable to OPTION
VegaPA string `json:"vegaPA"` // vegaGreeks in coins,only applicable to OPTION
// PushTime added feature in the websocket push data.
@@ -1421,19 +1421,19 @@ type PositionMode struct {
// SetLeverageInput represents set leverage request input
type SetLeverageInput struct {
Leverage int `json:"lever,string"` // set leverage for isolated
MarginMode string `json:"mgnMode"` // Margin Mode "cross" and "isolated"
InstrumentID string `json:"instId,omitempty"` // Optional:
Currency string `json:"ccy,omitempty"` // Optional:
PositionSide string `json:"posSide,omitempty"`
Leverage float64 `json:"lever,string"` // set leverage for isolated
MarginMode string `json:"mgnMode"` // Margin Mode "cross" and "isolated"
InstrumentID string `json:"instId,omitempty"` // Optional:
Currency string `json:"ccy,omitempty"` // Optional:
PositionSide string `json:"posSide,omitempty"`
}
// SetLeverageResponse represents set leverage response
type SetLeverageResponse struct {
Leverage string `json:"lever"`
MarginMode string `json:"mgnMode"` // Margin Mode "cross" and "isolated"
InstrumentID string `json:"instId"`
PositionSide string `json:"posSide"` // "long", "short", and "net"
Leverage okxNumericalValue `json:"lever"`
MarginMode string `json:"mgnMode"` // Margin Mode "cross" and "isolated"
InstrumentID string `json:"instId"`
PositionSide string `json:"posSide"` // "long", "short", and "net"
}
// MaximumBuyAndSell get maximum buy , sell amount or open amount
@@ -1464,20 +1464,20 @@ type IncreaseDecreaseMarginInput struct {
// IncreaseDecreaseMargin represents increase or decrease the margin of the isolated position response
type IncreaseDecreaseMargin struct {
Amt string `json:"amt"`
Ccy string `json:"ccy"`
InstrumentID string `json:"instId"`
Leverage string `json:"leverage"`
PosSide string `json:"posSide"`
Type string `json:"type"`
Amount okxNumericalValue `json:"amt"`
Ccy string `json:"ccy"`
InstrumentID string `json:"instId"`
Leverage okxNumericalValue `json:"leverage"`
PosSide string `json:"posSide"`
Type string `json:"type"`
}
// LeverageResponse instrument id leverage response.
type LeverageResponse struct {
InstrumentID string `json:"instId"`
MarginMode string `json:"mgnMode"`
PositionSide string `json:"posSide"`
Leverage uint `json:"lever,string"`
InstrumentID string `json:"instId"`
MarginMode string `json:"mgnMode"`
PositionSide string `json:"posSide"`
Leverage okxNumericalValue `json:"lever"`
}
// MaximumLoanInstrument represents maximum loan of an instrument id.

View File

@@ -11,15 +11,18 @@ import (
"sync"
"time"
"github.com/shopspring/decimal"
"github.com/thrasher-corp/gocryptotrader/common"
"github.com/thrasher-corp/gocryptotrader/config"
"github.com/thrasher-corp/gocryptotrader/currency"
exchange "github.com/thrasher-corp/gocryptotrader/exchanges"
"github.com/thrasher-corp/gocryptotrader/exchanges/account"
"github.com/thrasher-corp/gocryptotrader/exchanges/asset"
"github.com/thrasher-corp/gocryptotrader/exchanges/collateral"
"github.com/thrasher-corp/gocryptotrader/exchanges/deposit"
"github.com/thrasher-corp/gocryptotrader/exchanges/fundingrate"
"github.com/thrasher-corp/gocryptotrader/exchanges/kline"
"github.com/thrasher-corp/gocryptotrader/exchanges/margin"
"github.com/thrasher-corp/gocryptotrader/exchanges/order"
"github.com/thrasher-corp/gocryptotrader/exchanges/orderbook"
"github.com/thrasher-corp/gocryptotrader/exchanges/protocol"
@@ -68,11 +71,13 @@ func (ok *Okx) SetDefaults() {
ok.API.CredentialsValidator.RequiresKey = true
ok.API.CredentialsValidator.RequiresSecret = true
ok.API.CredentialsValidator.RequiresClientID = true
pairFormat := &currency.PairFormat{
cpf := &currency.PairFormat{
Delimiter: currency.DashDelimiter,
Uppercase: true,
}
err := ok.SetGlobalPairsManager(pairFormat, pairFormat, asset.Spot, asset.Futures, asset.PerpetualSwap, asset.Options, asset.Margin)
err := ok.SetGlobalPairsManager(cpf, cpf, asset.Spot, asset.Futures, asset.PerpetualSwap, asset.Options, asset.Margin)
if err != nil {
log.Errorln(log.ExchangeSys, err)
}
@@ -80,8 +85,9 @@ func (ok *Okx) SetDefaults() {
// Fill out the capabilities/features that the exchange supports
ok.Features = exchange.Features{
Supports: exchange.FeaturesSupported{
REST: true,
Websocket: true,
REST: true,
Websocket: true,
MaximumOrderHistory: kline.OneDay.Duration() * 90,
RESTCapabilities: protocol.Features{
TickerFetching: true,
OrderbookFetching: true,
@@ -123,6 +129,9 @@ func (ok *Okx) SetDefaults() {
},
WithdrawPermissions: exchange.AutoWithdrawCrypto,
FuturesCapabilities: exchange.FuturesCapabilities{
Positions: true,
Leverage: true,
CollateralMode: true,
FundingRates: true,
MaximumFundingRateHistory: kline.ThreeMonth.Duration(),
FundingRateFrequency: kline.EightHour.Duration(),
@@ -360,14 +369,14 @@ func (ok *Okx) UpdateOrderExecutionLimits(ctx context.Context, a asset.Item) err
// UpdateTicker updates and returns the ticker for a currency pair
func (ok *Okx) UpdateTicker(ctx context.Context, p currency.Pair, a asset.Item) (*ticker.Price, error) {
format, err := ok.GetPairFormat(a, false)
pairFormat, err := ok.GetPairFormat(a, true)
if err != nil {
return nil, err
}
if p.IsEmpty() {
return nil, currency.ErrCurrencyPairEmpty
}
instrumentID := format.Format(p)
instrumentID := pairFormat.Format(p)
if !ok.SupportsAsset(a) {
return nil, fmt.Errorf("%w: %v", asset.ErrNotSupported, a)
}
@@ -502,14 +511,14 @@ func (ok *Okx) UpdateOrderbook(ctx context.Context, pair currency.Pair, assetTyp
return nil, err
}
var instrumentID string
format, err := ok.GetPairFormat(assetType, false)
pairFormat, err := ok.GetPairFormat(assetType, true)
if err != nil {
return nil, err
}
if !pair.IsPopulated() {
return nil, errIncompleteCurrencyPair
}
instrumentID = format.Format(pair)
instrumentID = pairFormat.Format(pair)
orderbookNew, err = ok.GetOrderBookDepth(ctx, instrumentID, 400)
if err != nil {
return book, err
@@ -552,7 +561,7 @@ func (ok *Okx) UpdateAccountInfo(ctx context.Context, assetType asset.Item) (acc
if !ok.SupportsAsset(assetType) {
return info, fmt.Errorf("%w: %v", asset.ErrNotSupported, assetType)
}
accountBalances, err := ok.GetNonZeroBalances(ctx, "")
accountBalances, err := ok.AccountBalance(ctx, "")
if err != nil {
return info, err
}
@@ -660,15 +669,14 @@ func (ok *Okx) GetWithdrawalsHistory(ctx context.Context, c currency.Code, _ ass
// GetRecentTrades returns the most recent trades for a currency and asset
func (ok *Okx) GetRecentTrades(ctx context.Context, p currency.Pair, assetType asset.Item) ([]trade.Data, error) {
format, err := ok.GetPairFormat(assetType, false)
pairFormat, err := ok.GetPairFormat(assetType, true)
if err != nil {
return nil, err
}
if p.IsEmpty() {
return nil, currency.ErrCurrencyPairEmpty
}
instrumentID := format.Format(p)
instrumentID := pairFormat.Format(p)
tradeData, err := ok.GetTrades(ctx, instrumentID, 1000)
if err != nil {
return nil, err
@@ -708,7 +716,7 @@ func (ok *Okx) GetHistoricTrades(ctx context.Context, p currency.Pair, assetType
return nil, errOnlyThreeMonthsSupported
}
const limit = 100
format, err := ok.GetPairFormat(assetType, false)
pairFormat, err := ok.GetPairFormat(assetType, true)
if err != nil {
return nil, err
}
@@ -716,7 +724,7 @@ func (ok *Okx) GetHistoricTrades(ctx context.Context, p currency.Pair, assetType
return nil, currency.ErrCurrencyPairEmpty
}
var resp []trade.Data
instrumentID := format.Format(p)
instrumentID := pairFormat.Format(p)
tradeIDEnd := ""
allTrades:
for {
@@ -774,17 +782,17 @@ func (ok *Okx) SubmitOrder(ctx context.Context, s *order.Submit) (*order.SubmitR
if s.Amount <= 0 {
return nil, fmt.Errorf("amount, or size (sz) of quantity to buy or sell hast to be greater than zero ")
}
format, err := ok.GetPairFormat(s.AssetType, false)
pairFormat, err := ok.GetPairFormat(s.AssetType, true)
if err != nil {
return nil, err
}
if s.Pair.IsEmpty() {
return nil, currency.ErrCurrencyPairEmpty
}
instrumentID := format.Format(s.Pair)
var tradeMode string
if s.AssetType != asset.Margin {
tradeMode = "cash"
instrumentID := pairFormat.Format(s.Pair)
tradeMode := ok.marginTypeToString(s.MarginType)
if s.Leverage != 0 && s.Leverage != 1 {
return nil, fmt.Errorf("%w received '%v'", order.ErrSubmitLeverageNotSupported, s.Leverage)
}
var sideType string
if s.Side.IsLong() {
@@ -846,6 +854,17 @@ func (ok *Okx) SubmitOrder(ctx context.Context, s *order.Submit) (*order.SubmitR
return s.DeriveSubmitResponse(placeOrderResponse.OrderID)
}
func (ok *Okx) marginTypeToString(m margin.Type) string {
switch m {
case margin.Isolated:
return "isolated"
case margin.Multi:
return "cross"
default:
return "cash"
}
}
// ModifyOrder will allow of changing orderbook placement and limit to market conversion
func (ok *Okx) ModifyOrder(ctx context.Context, action *order.Modify) (*order.ModifyResponse, error) {
if err := action.Validate(); err != nil {
@@ -855,14 +874,14 @@ func (ok *Okx) ModifyOrder(ctx context.Context, action *order.Modify) (*order.Mo
if math.Trunc(action.Amount) != action.Amount {
return nil, errors.New("okx contract amount can not be decimal")
}
format, err := ok.GetPairFormat(action.AssetType, false)
pairFormat, err := ok.GetPairFormat(action.AssetType, true)
if err != nil {
return nil, err
}
if action.Pair.IsEmpty() {
return nil, currency.ErrCurrencyPairEmpty
}
instrumentID := format.Format(action.Pair)
instrumentID := pairFormat.Format(action.Pair)
if err != nil {
return nil, err
}
@@ -891,14 +910,14 @@ func (ok *Okx) CancelOrder(ctx context.Context, ord *order.Cancel) error {
if !ok.SupportsAsset(ord.AssetType) {
return fmt.Errorf("%w: %v", asset.ErrNotSupported, ord.AssetType)
}
format, err := ok.GetPairFormat(ord.AssetType, false)
pairFormat, err := ok.GetPairFormat(ord.AssetType, true)
if err != nil {
return err
}
if ord.Pair.IsEmpty() {
return currency.ErrCurrencyPairEmpty
}
instrumentID := format.Format(ord.Pair)
instrumentID := pairFormat.Format(ord.Pair)
req := CancelOrderRequestParam{
InstrumentID: instrumentID,
OrderID: ord.OrderID,
@@ -921,7 +940,6 @@ func (ok *Okx) CancelBatchOrders(ctx context.Context, o []order.Cancel) (*order.
}
cancelOrderParams := make([]CancelOrderRequestParam, len(o))
var err error
var format currency.PairFormat
for x := range o {
ord := o[x]
err = ord.Validate(ord.StandardCancel())
@@ -931,16 +949,17 @@ func (ok *Okx) CancelBatchOrders(ctx context.Context, o []order.Cancel) (*order.
if !ok.SupportsAsset(ord.AssetType) {
return nil, fmt.Errorf("%w: %v", asset.ErrNotSupported, ord.AssetType)
}
format, err = ok.GetPairFormat(ord.AssetType, true)
var instrumentID string
var pairFormat currency.PairFormat
pairFormat, err = ok.GetPairFormat(ord.AssetType, true)
if err != nil {
return nil, err
}
if !ord.Pair.IsPopulated() {
return nil, errIncompleteCurrencyPair
}
instrumentID = format.Format(ord.Pair)
instrumentID = pairFormat.Format(ord.Pair)
if err != nil {
return nil, err
}
@@ -1078,12 +1097,17 @@ func (ok *Okx) GetOrderInfo(ctx context.Context, orderID string, pair currency.P
return nil, err
}
format, err := ok.GetPairFormat(assetType, false)
pairFormat, err := ok.GetPairFormat(assetType, false)
if err != nil {
return nil, err
}
instrumentID := format.Format(pair)
if !pair.IsPopulated() {
return nil, errIncompleteCurrencyPair
}
instrumentID := pairFormat.Format(pair)
if !ok.SupportsAsset(assetType) {
return nil, fmt.Errorf("%w: %v", asset.ErrNotSupported, assetType)
}
orderDetail, err := ok.GetOrderDetail(ctx, &OrderDetailRequestParam{
InstrumentID: instrumentID,
OrderID: orderID,
@@ -1712,3 +1736,386 @@ func (ok *Okx) GetFundingRates(ctx context.Context, r *fundingrate.RatesRequest)
func (ok *Okx) IsPerpetualFutureCurrency(a asset.Item, _ currency.Pair) (bool, error) {
return a == asset.PerpetualSwap, nil
}
// SetMarginType sets the default margin type for when opening a new position
// okx allows this to be set with an order, however this sets a default
func (ok *Okx) SetMarginType(_ context.Context, _ asset.Item, _ currency.Pair, _ margin.Type) error {
return fmt.Errorf("%w margin type is set per order", common.ErrFunctionNotSupported)
}
// SetCollateralMode sets the collateral type for your account
func (ok *Okx) SetCollateralMode(_ context.Context, _ asset.Item, _ collateral.Mode) error {
return fmt.Errorf("%w must be set via website", common.ErrFunctionNotSupported)
}
// GetCollateralMode returns the collateral type for your account
func (ok *Okx) GetCollateralMode(ctx context.Context, item asset.Item) (collateral.Mode, error) {
if !ok.SupportsAsset(item) {
return 0, fmt.Errorf("%w: %v", asset.ErrNotSupported, item)
}
cfg, err := ok.GetAccountConfiguration(ctx)
if err != nil {
return 0, err
}
switch cfg[0].AccountLevel {
case 1:
if item != asset.Spot {
return 0, fmt.Errorf("%w %v", asset.ErrNotSupported, item)
}
fallthrough
case 2:
return collateral.SingleMode, nil
case 3:
return collateral.MultiMode, nil
case 4:
return collateral.PortfolioMode, nil
default:
return collateral.UnknownMode, fmt.Errorf("%w %v", order.ErrCollateralInvalid, cfg[0].AccountLevel)
}
}
// ChangePositionMargin will modify a position/currencies margin parameters
func (ok *Okx) ChangePositionMargin(ctx context.Context, req *margin.PositionChangeRequest) (*margin.PositionChangeResponse, error) {
if req == nil {
return nil, fmt.Errorf("%w PositionChangeRequest", common.ErrNilPointer)
}
if !ok.SupportsAsset(req.Asset) {
return nil, fmt.Errorf("%w: %v", asset.ErrNotSupported, req.Asset)
}
if req.NewAllocatedMargin == 0 {
return nil, fmt.Errorf("%w %v %v", margin.ErrNewAllocatedMarginRequired, req.Asset, req.Pair)
}
if req.OriginalAllocatedMargin == 0 {
return nil, margin.ErrOriginalPositionMarginRequired
}
if req.MarginType != margin.Isolated {
return nil, fmt.Errorf("%w %v", margin.ErrMarginTypeUnsupported, req.MarginType)
}
pairFormat, err := ok.GetPairFormat(req.Asset, true)
if err != nil {
return nil, err
}
fPair := req.Pair.Format(pairFormat)
marginType := "add"
amt := req.NewAllocatedMargin - req.OriginalAllocatedMargin
if req.NewAllocatedMargin < req.OriginalAllocatedMargin {
marginType = "reduce"
amt = req.OriginalAllocatedMargin - req.NewAllocatedMargin
}
if req.MarginSide == "" {
req.MarginSide = "net"
}
r := IncreaseDecreaseMarginInput{
InstrumentID: fPair.String(),
PositionSide: req.MarginSide,
Type: marginType,
Amount: amt,
}
if req.Asset == asset.Margin {
r.Currency = req.Pair.Base.Item.Symbol
}
resp, err := ok.IncreaseDecreaseMargin(ctx, r)
if err != nil {
return nil, err
}
return &margin.PositionChangeResponse{
Exchange: ok.Name,
Pair: req.Pair,
Asset: req.Asset,
AllocatedMargin: resp.Amount.Float64(),
MarginType: req.MarginType,
}, nil
}
// GetFuturesPositionSummary returns position summary details for an active position
func (ok *Okx) GetFuturesPositionSummary(ctx context.Context, req *order.PositionSummaryRequest) (*order.PositionSummary, error) {
if req == nil {
return nil, fmt.Errorf("%w PositionSummaryRequest", common.ErrNilPointer)
}
if req.CalculateOffline {
return nil, common.ErrCannotCalculateOffline
}
if !ok.SupportsAsset(req.Asset) || !req.Asset.IsFutures() {
return nil, fmt.Errorf("%w %v", asset.ErrNotSupported, req.Asset)
}
fPair, err := ok.FormatExchangeCurrency(req.Pair, req.Asset)
if err != nil {
return nil, err
}
instrumentType := ok.GetInstrumentTypeFromAssetItem(req.Asset)
positionSummaries, err := ok.GetPositions(ctx, instrumentType, fPair.String(), "")
if err != nil {
return nil, err
}
var positionSummary *AccountPosition
for i := range positionSummaries {
if positionSummaries[i].QuantityOfPosition.Float64() <= 0 {
continue
}
positionSummary = &positionSummaries[i]
break
}
if positionSummary == nil {
return nil, fmt.Errorf("%w, received '%v', no positions found", errOnlyOneResponseExpected, len(positionSummaries))
}
marginMode := margin.Isolated
if positionSummary.MarginMode == "cross" {
marginMode = margin.Multi
}
acc, err := ok.AccountBalance(ctx, "")
if err != nil {
return nil, err
}
if len(acc) != 1 {
return nil, fmt.Errorf("%w, received '%v'", errOnlyOneResponseExpected, len(acc))
}
var (
freeCollateral, totalCollateral, equityOfCurrency, frozenBalance,
availableEquity, cashBalance, discountEquity,
equityUSD, totalEquity, isolatedEquity, isolatedLiabilities,
isolatedUnrealisedProfit, notionalLeverage,
strategyEquity decimal.Decimal
)
for i := range acc[0].Details {
if acc[0].Details[i].Currency != positionSummary.Currency {
continue
}
freeCollateral = acc[0].Details[i].AvailableBalance.Decimal()
frozenBalance = acc[0].Details[i].FrozenBalance.Decimal()
totalCollateral = freeCollateral.Add(frozenBalance)
equityOfCurrency = acc[0].Details[i].EquityOfCurrency.Decimal()
availableEquity = acc[0].Details[i].AvailableEquity.Decimal()
cashBalance = acc[0].Details[i].CashBalance.Decimal()
discountEquity = acc[0].Details[i].DiscountEquity.Decimal()
equityUSD = acc[0].Details[i].EquityUsd.Decimal()
totalEquity = acc[0].Details[i].TotalEquity.Decimal()
isolatedEquity = acc[0].Details[i].IsoEquity.Decimal()
isolatedLiabilities = acc[0].Details[i].IsolatedLiabilities.Decimal()
isolatedUnrealisedProfit = acc[0].Details[i].IsoUpl.Decimal()
notionalLeverage = acc[0].Details[i].NotionalLever.Decimal()
strategyEquity = acc[0].Details[i].StrategyEquity.Decimal()
break
}
collateralMode, err := ok.GetCollateralMode(ctx, req.Asset)
if err != nil {
return nil, err
}
return &order.PositionSummary{
Pair: req.Pair,
Asset: req.Asset,
MarginType: marginMode,
CollateralMode: collateralMode,
Currency: currency.NewCode(positionSummary.Currency),
AvailableEquity: availableEquity,
CashBalance: cashBalance,
DiscountEquity: discountEquity,
EquityUSD: equityUSD,
IsolatedEquity: isolatedEquity,
IsolatedLiabilities: isolatedLiabilities,
IsolatedUPL: isolatedUnrealisedProfit,
NotionalLeverage: notionalLeverage,
TotalEquity: totalEquity,
StrategyEquity: strategyEquity,
IsolatedMargin: positionSummary.Margin.Decimal(),
NotionalSize: positionSummary.NotionalUsd.Decimal(),
Leverage: positionSummary.Leverage.Decimal(),
MaintenanceMarginRequirement: positionSummary.MaintenanceMarginRequirement.Decimal(),
InitialMarginRequirement: positionSummary.InitialMarginRequirement.Decimal(),
EstimatedLiquidationPrice: positionSummary.LiquidationPrice.Decimal(),
CollateralUsed: positionSummary.Margin.Decimal(),
MarkPrice: positionSummary.MarkPrice.Decimal(),
CurrentSize: positionSummary.QuantityOfPosition.Decimal(), // TODO: add field(s) for contract amount vs quote amount
AverageOpenPrice: positionSummary.AveragePrice.Decimal(),
PositionPNL: positionSummary.UPNL.Decimal(),
MaintenanceMarginFraction: positionSummary.MarginRatio.Decimal(),
FreeCollateral: freeCollateral,
TotalCollateral: totalCollateral,
FrozenBalance: frozenBalance,
EquityOfCurrency: equityOfCurrency,
}, nil
}
// GetFuturesPositionOrders returns the orders for futures positions
func (ok *Okx) GetFuturesPositionOrders(ctx context.Context, req *order.PositionsRequest) ([]order.PositionResponse, error) {
if req == nil {
return nil, fmt.Errorf("%w PositionSummaryRequest", common.ErrNilPointer)
}
if !ok.SupportsAsset(req.Asset) || !req.Asset.IsFutures() {
return nil, fmt.Errorf("%w %v", asset.ErrNotSupported, req.Asset)
}
if time.Since(req.StartDate) > ok.Features.Supports.MaximumOrderHistory {
if req.RespectOrderHistoryLimits {
req.StartDate = time.Now().Add(-ok.Features.Supports.MaximumOrderHistory)
} else {
return nil, fmt.Errorf("%w max lookup %v", order.ErrOrderHistoryTooLarge, time.Now().Add(-ok.Features.Supports.MaximumOrderHistory))
}
}
if err := common.StartEndTimeCheck(req.StartDate, req.EndDate); err != nil {
return nil, err
}
resp := make([]order.PositionResponse, len(req.Pairs))
for i := range req.Pairs {
fPair, err := ok.FormatExchangeCurrency(req.Pairs[i], req.Asset)
if err != nil {
return nil, err
}
instrumentType := ok.GetInstrumentTypeFromAssetItem(req.Asset)
resp[i] = order.PositionResponse{
Pair: req.Pairs[i],
Asset: req.Asset,
}
var positions []OrderDetail
historyRequest := &OrderHistoryRequestParams{
OrderListRequestParams: OrderListRequestParams{
InstrumentType: instrumentType,
InstrumentID: fPair.String(),
Start: req.StartDate,
End: req.EndDate,
},
}
if time.Since(req.StartDate) <= time.Hour*24*7 {
positions, err = ok.Get7DayOrderHistory(ctx, historyRequest)
} else {
positions, err = ok.Get3MonthOrderHistory(ctx, historyRequest)
}
if err != nil {
return nil, err
}
for j := range positions {
if req.Pairs[i].String() != positions[j].InstrumentID {
continue
}
var orderStatus order.Status
orderStatus, err = order.StringToOrderStatus(strings.ToUpper(positions[j].State))
if err != nil {
log.Errorf(log.ExchangeSys, "%s %v", ok.Name, err)
}
orderSide := positions[j].Side
var oType order.Type
oType, err = ok.OrderTypeFromString(positions[j].OrderType)
if err != nil {
return nil, err
}
orderAmount := positions[j].Size
if positions[j].QuantityType == "quote_ccy" {
// Size is quote amount.
orderAmount /= positions[j].AveragePrice
}
remainingAmount := float64(0)
if orderStatus != order.Filled {
remainingAmount = orderAmount.Float64() - positions[j].AccumulatedFillSize.Float64()
}
resp[i].Orders = append(resp[i].Orders, order.Detail{
Price: positions[j].Price.Float64(),
AverageExecutedPrice: positions[j].AveragePrice.Float64(),
Amount: orderAmount.Float64(), // TODO: add field(s) for contract amount vs quote amount
ExecutedAmount: positions[j].AccumulatedFillSize.Float64(),
RemainingAmount: remainingAmount,
Fee: positions[j].TransactionFee.Float64(),
FeeAsset: currency.NewCode(positions[j].FeeCurrency),
Exchange: ok.Name,
OrderID: positions[j].OrderID,
ClientOrderID: positions[j].ClientSupplierOrderID,
Type: oType,
Side: orderSide,
Status: orderStatus,
AssetType: req.Asset,
Date: positions[j].CreationTime,
LastUpdated: positions[j].UpdateTime,
Pair: req.Pairs[i],
Cost: positions[j].AveragePrice.Float64() * positions[j].AccumulatedFillSize.Float64(),
CostAsset: currency.NewCode(positions[j].RebateCurrency),
})
}
}
return resp, nil
}
// SetLeverage sets the account's initial leverage for the asset type and pair
func (ok *Okx) SetLeverage(ctx context.Context, item asset.Item, pair currency.Pair, marginType margin.Type, amount float64, orderSide order.Side) error {
posSide := "net"
switch item {
case asset.Futures, asset.PerpetualSwap:
if marginType == margin.Isolated {
switch {
case orderSide == order.UnknownSide:
return errOrderSideRequired
case orderSide.IsLong():
posSide = "long"
case orderSide.IsShort():
posSide = "short"
default:
return fmt.Errorf("%w %v requires long/short", errInvalidOrderSide, orderSide)
}
}
fallthrough
case asset.Margin, asset.Options:
instrumentID, err := ok.FormatSymbol(pair, item)
if err != nil {
return err
}
marginMode := ok.marginTypeToString(marginType)
_, err = ok.SetLeverageRate(ctx, SetLeverageInput{
Leverage: amount,
MarginMode: marginMode,
InstrumentID: instrumentID,
PositionSide: posSide,
})
return err
default:
return fmt.Errorf("%w %v", asset.ErrNotSupported, item)
}
}
// GetLeverage gets the account's initial leverage for the asset type and pair
func (ok *Okx) GetLeverage(ctx context.Context, item asset.Item, pair currency.Pair, marginType margin.Type, orderSide order.Side) (float64, error) {
var inspectLeverage bool
switch item {
case asset.Futures, asset.PerpetualSwap:
if marginType == margin.Isolated {
switch {
case orderSide == order.UnknownSide:
return 0, errOrderSideRequired
case orderSide.IsLong(), orderSide.IsShort():
inspectLeverage = true
default:
return 0, fmt.Errorf("%w %v requires long/short", errInvalidOrderSide, orderSide)
}
}
fallthrough
case asset.Margin, asset.Options:
instrumentID, err := ok.FormatSymbol(pair, item)
if err != nil {
return -1, err
}
marginMode := ok.marginTypeToString(marginType)
lev, err := ok.GetLeverageRate(ctx, instrumentID, marginMode)
if err != nil {
return -1, err
}
if len(lev) == 0 {
return -1, fmt.Errorf("%w %v %v %s", order.ErrPositionNotFound, item, pair, marginType)
}
if inspectLeverage {
for i := range lev {
if lev[i].PositionSide == orderSide.Lower() {
return lev[i].Leverage.Float64(), nil
}
}
}
// leverage is the same across positions
return lev[0].Leverage.Float64(), nil
default:
return -1, fmt.Errorf("%w %v", asset.ErrNotSupported, item)
}
}