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

@@ -51,6 +51,7 @@ const (
binary = "binary"
perpetualContract = "perpetualcontract"
perpetualSwap = "perpetualswap"
swap = "swap"
futures = "futures"
deliveryFutures = "delivery"
upsideProfitContract = "upsideprofitcontract"
@@ -190,7 +191,7 @@ func New(input string) (Item, error) {
return Binary, nil
case perpetualContract:
return PerpetualContract, nil
case perpetualSwap:
case perpetualSwap, swap:
return PerpetualSwap, nil
case futures:
return Futures, nil

View File

@@ -911,6 +911,9 @@ func (b *Binance) SendAuthHTTPRequest(ctx context.Context, ePath exchange.URL, m
return errors.New(errCap.Message)
}
}
if result == nil {
return nil
}
return json.Unmarshal(interim, result)
}

View File

@@ -89,7 +89,7 @@ func (b *Binance) GetFuturesOrderbook(ctx context.Context, symbol currency.Pair,
params := url.Values{}
params.Set("symbol", symbolValue)
if limit > 0 && limit <= 1000 {
if limit > 0 {
params.Set("limit", strconv.FormatInt(limit, 10))
}
@@ -156,7 +156,7 @@ func (b *Binance) GetFuturesPublicTrades(ctx context.Context, symbol currency.Pa
return resp, err
}
params.Set("symbol", symbolValue)
if limit > 0 && limit <= 1000 {
if limit > 0 {
params.Set("limit", strconv.FormatInt(limit, 10))
}
return resp, b.SendHTTPRequest(ctx, exchange.RestCoinMargined, cfuturesRecentTrades+params.Encode(), cFuturesDefaultRate, &resp)
@@ -174,7 +174,7 @@ func (b *Binance) GetFuturesHistoricalTrades(ctx context.Context, symbol currenc
if fromID != "" {
params.Set("fromID", fromID)
}
if limit > 0 && limit < 1000 {
if limit > 0 {
params.Set("limit", strconv.FormatInt(limit, 10))
}
return resp, b.SendAuthHTTPRequest(ctx, exchange.RestCoinMargined, http.MethodGet, cfuturesHistoricalTrades, params, cFuturesHistoricalTradesRate, &resp)
@@ -189,7 +189,7 @@ func (b *Binance) GetPastPublicTrades(ctx context.Context, symbol currency.Pair,
return resp, err
}
params.Set("symbol", symbolValue)
if limit > 0 && limit <= 1000 {
if limit > 0 {
params.Set("limit", strconv.FormatInt(limit, 10))
}
if fromID != 0 {
@@ -207,7 +207,7 @@ func (b *Binance) GetFuturesAggregatedTradesList(ctx context.Context, symbol cur
return resp, err
}
params.Set("symbol", symbolValue)
if limit > 0 && limit <= 1000 {
if limit > 0 {
params.Set("limit", strconv.FormatInt(limit, 10))
}
if fromID != 0 {
@@ -246,7 +246,7 @@ func (b *Binance) GetFuturesKlineData(ctx context.Context, symbol currency.Pair,
}
params.Set("symbol", symbolValue)
}
if limit > 0 && limit <= 1500 {
if limit > 0 {
params.Set("limit", strconv.FormatInt(limit, 10))
}
if !common.StringDataCompare(validFuturesIntervals, interval) {
@@ -365,7 +365,7 @@ func (b *Binance) GetContinuousKlineData(ctx context.Context, pair, contractType
return nil, errors.New("invalid contractType")
}
params.Set("contractType", contractType)
if limit > 0 && limit <= 1500 {
if limit > 0 {
params.Set("limit", strconv.FormatInt(limit, 10))
}
if !common.StringDataCompare(validFuturesIntervals, interval) {
@@ -480,7 +480,7 @@ func (b *Binance) GetContinuousKlineData(ctx context.Context, pair, contractType
func (b *Binance) GetIndexPriceKlines(ctx context.Context, pair, interval string, limit int64, startTime, endTime time.Time) ([]FuturesCandleStick, error) {
params := url.Values{}
params.Set("pair", pair)
if limit > 0 && limit <= 1500 {
if limit > 0 {
params.Set("limit", strconv.FormatInt(limit, 10))
}
if !common.StringDataCompare(validFuturesIntervals, interval) {
@@ -599,7 +599,7 @@ func (b *Binance) GetMarkPriceKline(ctx context.Context, symbol currency.Pair, i
}
params := url.Values{}
params.Set("symbol", symbolValue)
if limit > 0 && limit <= 1500 {
if limit > 0 {
params.Set("limit", strconv.FormatInt(limit, 10))
}
if !common.StringDataCompare(validFuturesIntervals, interval) {
@@ -755,7 +755,7 @@ func (b *Binance) FuturesGetFundingHistory(ctx context.Context, symbol currency.
}
params.Set("symbol", symbolValue)
}
if limit > 0 && limit < 1000 {
if limit > 0 {
params.Set("limit", strconv.FormatInt(limit, 10))
}
if !startTime.IsZero() && !endTime.IsZero() {
@@ -833,7 +833,7 @@ func (b *Binance) GetOpenInterestStats(ctx context.Context, pair, contractType,
return resp, errors.New("invalid period")
}
params.Set("period", period)
if limit > 0 && limit <= 1000 {
if limit > 0 {
params.Set("limit", strconv.FormatInt(limit, 10))
}
if !startTime.IsZero() && !endTime.IsZero() {
@@ -855,7 +855,7 @@ func (b *Binance) GetTraderFuturesAccountRatio(ctx context.Context, pair, period
return resp, errors.New("invalid period")
}
params.Set("period", period)
if limit > 0 && limit <= 1000 {
if limit > 0 {
params.Set("limit", strconv.FormatInt(limit, 10))
}
if !startTime.IsZero() && !endTime.IsZero() {
@@ -877,7 +877,7 @@ func (b *Binance) GetTraderFuturesPositionsRatio(ctx context.Context, pair, peri
return resp, errors.New("invalid period")
}
params.Set("period", period)
if limit > 0 && limit <= 1000 {
if limit > 0 {
params.Set("limit", strconv.FormatInt(limit, 10))
}
if !startTime.IsZero() && !endTime.IsZero() {
@@ -899,7 +899,7 @@ func (b *Binance) GetMarketRatio(ctx context.Context, pair, period string, limit
return resp, errors.New("invalid period")
}
params.Set("period", period)
if limit > 0 && limit <= 1000 {
if limit > 0 {
params.Set("limit", strconv.FormatInt(limit, 10))
}
if !startTime.IsZero() && !endTime.IsZero() {
@@ -921,7 +921,7 @@ func (b *Binance) GetFuturesTakerVolume(ctx context.Context, pair, contractType,
return resp, errors.New("invalid contractType")
}
params.Set("contractType", contractType)
if limit > 0 && limit <= 1000 {
if limit > 0 {
params.Set("limit", strconv.FormatInt(limit, 10))
}
if !common.StringDataCompare(validFuturesIntervals, period) {
@@ -947,7 +947,7 @@ func (b *Binance) GetFuturesBasisData(ctx context.Context, pair, contractType, p
return resp, errors.New("invalid contractType")
}
params.Set("contractType", contractType)
if limit > 0 && limit <= 1000 {
if limit > 0 {
params.Set("limit", strconv.FormatInt(limit, 10))
}
if !common.StringDataCompare(validFuturesIntervals, period) {
@@ -1198,7 +1198,7 @@ func (b *Binance) GetFuturesAllOpenOrders(ctx context.Context, symbol currency.P
}
// GetAllFuturesOrders gets all orders active cancelled or filled
func (b *Binance) GetAllFuturesOrders(ctx context.Context, symbol currency.Pair, pair string, startTime, endTime time.Time, orderID, limit int64) ([]FuturesOrderData, error) {
func (b *Binance) GetAllFuturesOrders(ctx context.Context, symbol, pair currency.Pair, startTime, endTime time.Time, orderID, limit int64) ([]FuturesOrderData, error) {
var resp []FuturesOrderData
params := url.Values{}
rateLimit := cFuturesPairOrdersRate
@@ -1210,13 +1210,13 @@ func (b *Binance) GetAllFuturesOrders(ctx context.Context, symbol currency.Pair,
}
params.Set("symbol", symbolValue)
}
if pair != "" {
params.Set("pair", pair)
if !pair.IsEmpty() {
params.Set("pair", pair.String())
}
if orderID != 0 {
params.Set("orderID", strconv.FormatInt(orderID, 10))
}
if limit > 0 && limit <= 100 {
if limit > 0 {
params.Set("limit", strconv.FormatInt(limit, 10))
}
if !startTime.IsZero() && !endTime.IsZero() {
@@ -1242,7 +1242,7 @@ func (b *Binance) GetFuturesAccountInfo(ctx context.Context) (FuturesAccountInfo
}
// FuturesChangeInitialLeverage changes initial leverage for the account
func (b *Binance) FuturesChangeInitialLeverage(ctx context.Context, symbol currency.Pair, leverage int64) (FuturesLeverageData, error) {
func (b *Binance) FuturesChangeInitialLeverage(ctx context.Context, symbol currency.Pair, leverage float64) (FuturesLeverageData, error) {
var resp FuturesLeverageData
params := url.Values{}
symbolValue, err := b.FormatSymbol(symbol, asset.CoinMarginedFutures)
@@ -1253,7 +1253,7 @@ func (b *Binance) FuturesChangeInitialLeverage(ctx context.Context, symbol curre
if leverage < 1 || leverage > 125 {
return resp, errors.New("invalid leverage")
}
params.Set("leverage", strconv.FormatInt(leverage, 10))
params.Set("leverage", strconv.FormatFloat(leverage, 'f', -1, 64))
return resp, b.SendAuthHTTPRequest(ctx, exchange.RestCoinMargined, http.MethodPost, cfuturesChangeInitialLeverage, params, cFuturesDefaultRate, &resp)
}
@@ -1274,18 +1274,20 @@ func (b *Binance) FuturesChangeMarginType(ctx context.Context, symbol currency.P
}
// ModifyIsolatedPositionMargin changes margin for an isolated position
func (b *Binance) ModifyIsolatedPositionMargin(ctx context.Context, symbol currency.Pair, positionSide, changeType string, amount float64) (GenericAuthResponse, error) {
var resp GenericAuthResponse
func (b *Binance) ModifyIsolatedPositionMargin(ctx context.Context, symbol currency.Pair, positionSide, changeType string, amount float64) (FuturesMarginUpdatedResponse, error) {
var resp FuturesMarginUpdatedResponse
params := url.Values{}
symbolValue, err := b.FormatSymbol(symbol, asset.CoinMarginedFutures)
if err != nil {
return resp, err
}
params.Set("symbol", symbolValue)
if !common.StringDataCompare(validPositionSide, positionSide) {
return resp, errors.New("invalid positionSide")
if positionSide != "" {
if !common.StringDataCompare(validPositionSide, positionSide) {
return resp, errors.New("invalid positionSide")
}
params.Set("positionSide", positionSide)
}
params.Set("positionSide", positionSide)
cType, ok := validMarginChange[changeType]
if !ok {
return resp, errors.New("invalid changeType")
@@ -1323,15 +1325,19 @@ func (b *Binance) FuturesMarginChangeHistory(ctx context.Context, symbol currenc
}
// FuturesPositionsInfo gets futures positions info
// "pair" for coinmarginedfutures in GCT terms is the pair base
// eg ADAUSD_PERP the "pair" parameter is ADAUSD
func (b *Binance) FuturesPositionsInfo(ctx context.Context, marginAsset, pair string) ([]FuturesPositionInformation, error) {
var resp []FuturesPositionInformation
params := url.Values{}
if marginAsset != "" {
params.Set("marginAsset", marginAsset)
}
if pair != "" {
params.Set("pair", pair)
}
return resp, b.SendAuthHTTPRequest(ctx, exchange.RestCoinMargined, http.MethodGet, cfuturesPositionInfo, params, cFuturesDefaultRate, &resp)
}

View File

@@ -28,6 +28,7 @@ func TestMain(m *testing.M) {
if err != nil {
log.Fatal("Binance Setup() init error", err)
}
binanceConfig.API.AuthenticatedSupport = true
binanceConfig.API.Credentials.Key = apiKey
binanceConfig.API.Credentials.Secret = apiSecret

View File

@@ -15,8 +15,10 @@ import (
"github.com/thrasher-corp/gocryptotrader/currency"
exchange "github.com/thrasher-corp/gocryptotrader/exchanges"
"github.com/thrasher-corp/gocryptotrader/exchanges/asset"
"github.com/thrasher-corp/gocryptotrader/exchanges/collateral"
"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/sharedtestvalues"
"github.com/thrasher-corp/gocryptotrader/portfolio/withdraw"
@@ -56,7 +58,7 @@ func getTime() (start, end time.Time) {
}
tn := time.Now()
offset := time.Hour * 24 * 30
offset := time.Hour * 24 * 6
return tn.Add(-offset), tn
}
@@ -71,6 +73,7 @@ func TestStart(t *testing.T) {
if err != nil {
t.Fatal(err)
}
testWg.Wait()
}
@@ -216,7 +219,7 @@ func TestUFuturesOrderbook(t *testing.T) {
func TestURecentTrades(t *testing.T) {
t.Parallel()
_, err := b.URecentTrades(context.Background(), currency.NewPair(currency.BTC, currency.USDT), "", 5)
_, err := b.URecentTrades(context.Background(), currency.NewPair(currency.BTC, currency.USDT), "", 1000)
if err != nil {
t.Error(err)
}
@@ -972,7 +975,7 @@ func TestGetFuturesAllOpenOrders(t *testing.T) {
func TestGetAllFuturesOrders(t *testing.T) {
t.Parallel()
sharedtestvalues.SkipTestIfCredentialsUnset(t, b)
_, err := b.GetAllFuturesOrders(context.Background(), currency.NewPairWithDelimiter("BTCUSD", "PERP", "_"), "", time.Time{}, time.Time{}, 0, 2)
_, err := b.GetAllFuturesOrders(context.Background(), currency.NewPairWithDelimiter("BTCUSD", "PERP", "_"), currency.EMPTYPAIR, time.Time{}, time.Time{}, 0, 2)
if err != nil {
t.Error(err)
}
@@ -1035,7 +1038,7 @@ func TestFuturesMarginChangeHistory(t *testing.T) {
func TestFuturesPositionsInfo(t *testing.T) {
t.Parallel()
sharedtestvalues.SkipTestIfCredentialsUnset(t, b)
_, err := b.FuturesPositionsInfo(context.Background(), "BTCUSD_PERP", "")
_, err := b.FuturesPositionsInfo(context.Background(), "BTCUSD", "")
if err != nil {
t.Error(err)
}
@@ -1106,8 +1109,8 @@ func TestGetExchangeInfo(t *testing.T) {
}
if mockTests {
serverTime := time.Date(2022, 2, 25, 3, 50, 40, int(601*time.Millisecond), time.UTC)
if !info.Servertime.Equal(serverTime) {
t.Errorf("Expected %v, got %v", serverTime, info.Servertime)
if !info.ServerTime.Equal(serverTime) {
t.Errorf("Expected %v, got %v", serverTime, info.ServerTime)
}
}
}
@@ -2378,17 +2381,23 @@ func TestBinance_FormatExchangeKlineInterval(t *testing.T) {
func TestGetRecentTrades(t *testing.T) {
t.Parallel()
bAssets := b.GetAssetTypes(false)
for i := range bAssets {
cps, err := b.GetAvailablePairs(bAssets[i])
if err != nil {
t.Error(err)
}
_, err = b.GetRecentTrades(context.Background(),
cps[0], bAssets[i])
if err != nil {
t.Error(err)
}
pair := currency.NewPair(currency.BTC, currency.USDT)
_, err := b.GetRecentTrades(context.Background(),
pair, asset.Spot)
if err != nil {
t.Error(err)
}
_, err = b.GetRecentTrades(context.Background(),
pair, asset.USDTMarginedFutures)
if err != nil {
t.Error(err)
}
pair.Base = currency.NewCode("BTCUSD")
pair.Quote = currency.PERP
_, err = b.GetRecentTrades(context.Background(),
pair, asset.CoinMarginedFutures)
if err != nil {
t.Error(err)
}
}
@@ -2728,7 +2737,6 @@ func TestFetchSpotExchangeLimits(t *testing.T) {
func TestUpdateOrderExecutionLimits(t *testing.T) {
t.Parallel()
tests := map[asset.Item]currency.Pair{
asset.Spot: currency.NewPair(currency.BTC, currency.USDT),
asset.Margin: currency.NewPair(currency.ETH, currency.BTC),
@@ -2939,6 +2947,229 @@ func TestGetUserMarginInterestHistory(t *testing.T) {
}
}
func TestSetAssetsMode(t *testing.T) {
t.Parallel()
sharedtestvalues.SkipTestIfCredentialsUnset(t, b)
is, err := b.GetAssetsMode(context.Background())
if !errors.Is(err, nil) {
t.Errorf("received '%v', expected '%v'", err, nil)
}
err = b.SetAssetsMode(context.Background(), !is)
if !errors.Is(err, nil) {
t.Errorf("received '%v', expected '%v'", err, nil)
}
err = b.SetAssetsMode(context.Background(), is)
if !errors.Is(err, nil) {
t.Errorf("received '%v', expected '%v'", err, nil)
}
}
func TestGetAssetsMode(t *testing.T) {
t.Parallel()
sharedtestvalues.SkipTestIfCredentialsUnset(t, b)
_, err := b.GetAssetsMode(context.Background())
if !errors.Is(err, nil) {
t.Errorf("received '%v', expected '%v'", err, nil)
}
}
func TestGetCollateralMode(t *testing.T) {
t.Parallel()
sharedtestvalues.SkipTestIfCredentialsUnset(t, b, canManipulateRealOrders)
_, err := b.GetCollateralMode(context.Background(), asset.Spot)
if !errors.Is(err, asset.ErrNotSupported) {
t.Errorf("received '%v', expected '%v'", err, asset.ErrNotSupported)
}
_, err = b.GetCollateralMode(context.Background(), asset.CoinMarginedFutures)
if !errors.Is(err, asset.ErrNotSupported) {
t.Errorf("received '%v', expected '%v'", err, asset.ErrNotSupported)
}
_, err = b.GetCollateralMode(context.Background(), asset.USDTMarginedFutures)
if !errors.Is(err, nil) {
t.Errorf("received '%v', expected '%v'", err, nil)
}
}
func TestSetCollateralMode(t *testing.T) {
t.Parallel()
sharedtestvalues.SkipTestIfCredentialsUnset(t, b, canManipulateRealOrders)
err := b.SetCollateralMode(context.Background(), asset.Spot, collateral.SingleMode)
if !errors.Is(err, asset.ErrNotSupported) {
t.Errorf("received '%v', expected '%v'", err, asset.ErrNotSupported)
}
err = b.SetCollateralMode(context.Background(), asset.CoinMarginedFutures, collateral.SingleMode)
if !errors.Is(err, asset.ErrNotSupported) {
t.Errorf("received '%v', expected '%v'", err, asset.ErrNotSupported)
}
err = b.SetCollateralMode(context.Background(), asset.USDTMarginedFutures, collateral.MultiMode)
if !errors.Is(err, nil) {
t.Errorf("received '%v', expected '%v'", err, nil)
}
err = b.SetCollateralMode(context.Background(), asset.USDTMarginedFutures, collateral.PortfolioMode)
if !errors.Is(err, order.ErrCollateralInvalid) {
t.Errorf("received '%v', expected '%v'", err, order.ErrCollateralInvalid)
}
}
func TestChangePositionMargin(t *testing.T) {
t.Parallel()
sharedtestvalues.SkipTestIfCredentialsUnset(t, b, canManipulateRealOrders)
_, err := b.ChangePositionMargin(context.Background(), &margin.PositionChangeRequest{
Pair: currency.NewBTCUSDT(),
Asset: asset.USDTMarginedFutures,
MarginType: margin.Isolated,
OriginalAllocatedMargin: 1337,
NewAllocatedMargin: 1333337,
})
if err != nil {
t.Error(err)
}
}
func TestGetPositionSummary(t *testing.T) {
t.Parallel()
sharedtestvalues.SkipTestIfCredentialsUnset(t, b)
bb := currency.NewBTCUSDT()
_, err := b.GetFuturesPositionSummary(context.Background(), &order.PositionSummaryRequest{
Asset: asset.USDTMarginedFutures,
Pair: bb,
})
if err != nil {
t.Error(err)
}
bb.Quote = currency.BUSD
_, err = b.GetFuturesPositionSummary(context.Background(), &order.PositionSummaryRequest{
Asset: asset.USDTMarginedFutures,
Pair: bb,
})
if err != nil {
t.Error(err)
}
p, err := currency.NewPairFromString("BTCUSD_PERP")
if err != nil {
t.Fatal(err)
}
bb.Quote = currency.USD
_, err = b.GetFuturesPositionSummary(context.Background(), &order.PositionSummaryRequest{
Asset: asset.CoinMarginedFutures,
Pair: p,
UnderlyingPair: bb,
})
if err != nil {
t.Error(err)
}
_, err = b.GetFuturesPositionSummary(context.Background(), &order.PositionSummaryRequest{
Asset: asset.Spot,
Pair: p,
UnderlyingPair: bb,
})
if !errors.Is(err, asset.ErrNotSupported) {
t.Error(err)
}
}
func TestGetFuturesPositionOrders(t *testing.T) {
t.Parallel()
sharedtestvalues.SkipTestIfCredentialsUnset(t, b)
_, err := b.GetFuturesPositionOrders(context.Background(), &order.PositionsRequest{
Asset: asset.USDTMarginedFutures,
Pairs: []currency.Pair{currency.NewBTCUSDT()},
StartDate: time.Now().Add(-time.Hour * 24 * 70),
RespectOrderHistoryLimits: true,
})
if err != nil {
t.Error(err)
}
p, err := currency.NewPairFromString("ADAUSD_PERP")
if err != nil {
t.Fatal(err)
}
_, err = b.GetFuturesPositionOrders(context.Background(), &order.PositionsRequest{
Asset: asset.CoinMarginedFutures,
Pairs: []currency.Pair{p},
StartDate: time.Now().Add(time.Hour * 24 * -70),
RespectOrderHistoryLimits: true,
})
if err != nil {
t.Error(err)
}
}
func TestSetMarginType(t *testing.T) {
t.Parallel()
sharedtestvalues.SkipTestIfCredentialsUnset(t, b, canManipulateRealOrders)
err := b.SetMarginType(context.Background(), asset.USDTMarginedFutures, currency.NewPair(currency.BTC, currency.USDT), margin.Isolated)
if !errors.Is(err, nil) {
t.Error(err)
}
p, err := currency.NewPairFromString("BTCUSD_PERP")
if err != nil {
t.Fatal(err)
}
err = b.SetMarginType(context.Background(), asset.CoinMarginedFutures, p, margin.Isolated)
if !errors.Is(err, nil) {
t.Error(err)
}
err = b.SetMarginType(context.Background(), asset.Spot, currency.NewPair(currency.BTC, currency.USDT), margin.Isolated)
if !errors.Is(err, asset.ErrNotSupported) {
t.Error(err)
}
}
func TestGetLeverage(t *testing.T) {
t.Parallel()
sharedtestvalues.SkipTestIfCredentialsUnset(t, b)
_, err := b.GetLeverage(context.Background(), asset.USDTMarginedFutures, currency.NewBTCUSDT(), 0, order.UnknownSide)
if err != nil {
t.Error(err)
}
p, err := currency.NewPairFromString("BTCUSD_PERP")
if err != nil {
t.Fatal(err)
}
_, err = b.GetLeverage(context.Background(), asset.CoinMarginedFutures, p, 0, order.UnknownSide)
if err != nil {
t.Error(err)
}
_, err = b.GetLeverage(context.Background(), asset.Spot, currency.NewBTCUSDT(), 0, order.UnknownSide)
if !errors.Is(err, asset.ErrNotSupported) {
t.Error(err)
}
}
func TestSetLeverage(t *testing.T) {
t.Parallel()
sharedtestvalues.SkipTestIfCredentialsUnset(t, b, canManipulateRealOrders)
err := b.SetLeverage(context.Background(), asset.USDTMarginedFutures, currency.NewBTCUSDT(), margin.Multi, 5, order.UnknownSide)
if err != nil {
t.Error(err)
}
p, err := currency.NewPairFromString("BTCUSD_PERP")
if err != nil {
t.Fatal(err)
}
err = b.SetLeverage(context.Background(), asset.CoinMarginedFutures, p, margin.Multi, 5, order.UnknownSide)
if err != nil {
t.Error(err)
}
err = b.SetLeverage(context.Background(), asset.Spot, p, margin.Multi, 5, order.UnknownSide)
if !errors.Is(err, asset.ErrNotSupported) {
t.Error(err)
}
}
func TestGetCryptoLoansIncomeHistory(t *testing.T) {
t.Parallel()
sharedtestvalues.SkipTestIfCredentialsUnset(t, b)

View File

@@ -43,7 +43,7 @@ type ExchangeInfo struct {
Code int `json:"code"`
Msg string `json:"msg"`
Timezone string `json:"timezone"`
Servertime time.Time `json:"serverTime"`
ServerTime time.Time `json:"serverTime"`
RateLimits []struct {
RateLimitType string `json:"rateLimitType"`
Interval string `json:"interval"`

View File

@@ -57,11 +57,13 @@ const (
ufuturesModifyMargin = "/fapi/v1/positionMargin"
ufuturesMarginChangeHistory = "/fapi/v1/positionMargin/history"
ufuturesPositionInfo = "/fapi/v2/positionRisk"
ufuturesCommissionRate = "/fapi/v1/commissionRate"
ufuturesAccountTradeList = "/fapi/v1/userTrades"
ufuturesIncomeHistory = "/fapi/v1/income"
ufuturesNotionalBracket = "/fapi/v1/leverageBracket"
ufuturesUsersForceOrders = "/fapi/v1/forceOrders"
ufuturesADLQuantile = "/fapi/v1/adlQuantile"
uFuturesMultiAssetsMargin = "/fapi/v1/multiAssetsMargin"
)
// UServerTime gets the server time
@@ -168,7 +170,7 @@ func (b *Binance) URecentTrades(ctx context.Context, symbol currency.Pair, fromI
if fromID != "" {
params.Set("fromID", fromID)
}
if limit > 0 && limit < 1000 {
if limit > 0 {
params.Set("limit", strconv.FormatInt(limit, 10))
}
return resp, b.SendHTTPRequest(ctx, exchange.RestUSDTMargined, ufuturesRecentTrades+params.Encode(), uFuturesDefaultRate, &resp)
@@ -186,7 +188,7 @@ func (b *Binance) UFuturesHistoricalTrades(ctx context.Context, symbol currency.
if fromID != "" {
params.Set("fromID", fromID)
}
if limit > 0 && limit < 1000 {
if limit > 0 {
params.Set("limit", strconv.FormatInt(limit, 10))
}
return resp, b.SendAuthHTTPRequest(ctx, exchange.RestUSDTMargined, http.MethodGet, ufuturesHistoricalTrades, params, uFuturesHistoricalTradesRate, &resp)
@@ -204,7 +206,7 @@ func (b *Binance) UCompressedTrades(ctx context.Context, symbol currency.Pair, f
if fromID != "" {
params.Set("fromID", fromID)
}
if limit > 0 && limit < 1000 {
if limit > 0 {
params.Set("limit", strconv.FormatInt(limit, 10))
}
if !startTime.IsZero() && !endTime.IsZero() {
@@ -229,7 +231,7 @@ func (b *Binance) UKlineData(ctx context.Context, symbol currency.Pair, interval
return nil, errors.New("invalid interval")
}
params.Set("interval", interval)
if limit > 0 && limit <= 1500 {
if limit > 0 {
params.Set("limit", strconv.FormatInt(limit, 10))
}
if !startTime.IsZero() && !endTime.IsZero() {
@@ -489,7 +491,7 @@ func (b *Binance) UOpenInterestStats(ctx context.Context, symbol currency.Pair,
return resp, errors.New("invalid period")
}
params.Set("period", period)
if limit > 0 && limit < 1000 {
if limit > 0 {
params.Set("limit", strconv.FormatInt(limit, 10))
}
if !startTime.IsZero() && !endTime.IsZero() {
@@ -515,7 +517,7 @@ func (b *Binance) UTopAcccountsLongShortRatio(ctx context.Context, symbol curren
return resp, errors.New("invalid period")
}
params.Set("period", period)
if limit > 0 && limit < 500 {
if limit > 0 {
params.Set("limit", strconv.FormatInt(limit, 10))
}
if !startTime.IsZero() && !endTime.IsZero() {
@@ -541,7 +543,7 @@ func (b *Binance) UTopPostionsLongShortRatio(ctx context.Context, symbol currenc
return resp, errors.New("invalid period")
}
params.Set("period", period)
if limit > 0 && limit < 500 {
if limit > 0 {
params.Set("limit", strconv.FormatInt(limit, 10))
}
if !startTime.IsZero() && !endTime.IsZero() {
@@ -567,7 +569,7 @@ func (b *Binance) UGlobalLongShortRatio(ctx context.Context, symbol currency.Pai
return resp, errors.New("invalid period")
}
params.Set("period", period)
if limit > 0 && limit < 500 {
if limit > 0 {
params.Set("limit", strconv.FormatInt(limit, 10))
}
if !startTime.IsZero() && !endTime.IsZero() {
@@ -593,7 +595,7 @@ func (b *Binance) UTakerBuySellVol(ctx context.Context, symbol currency.Pair, pe
return resp, errors.New("invalid period")
}
params.Set("period", period)
if limit > 0 && limit < 500 {
if limit > 0 {
params.Set("limit", strconv.FormatInt(limit, 10))
}
if !startTime.IsZero() && !endTime.IsZero() {
@@ -859,14 +861,13 @@ func (b *Binance) UAllAccountOrders(ctx context.Context, symbol currency.Pair, o
if orderID != 0 {
params.Set("orderId", strconv.FormatInt(orderID, 10))
}
if limit > 0 && limit < 1000 {
if limit > 0 {
params.Set("limit", strconv.FormatInt(limit, 10))
}
if !startTime.IsZero() && !endTime.IsZero() {
if startTime.After(endTime) {
return resp, errors.New("startTime cannot be after endTime")
}
if !startTime.IsZero() {
params.Set("startTime", strconv.FormatInt(startTime.UnixMilli(), 10))
}
if !endTime.IsZero() {
params.Set("endTime", strconv.FormatInt(endTime.UnixMilli(), 10))
}
return resp, b.SendAuthHTTPRequest(ctx, exchange.RestUSDTMargined, http.MethodGet, ufuturesAllOrders, params, uFuturesGetAllOrdersRate, &resp)
@@ -885,7 +886,7 @@ func (b *Binance) UAccountInformationV2(ctx context.Context) (UAccountInformatio
}
// UChangeInitialLeverageRequest sends a request to change account's initial leverage
func (b *Binance) UChangeInitialLeverageRequest(ctx context.Context, symbol currency.Pair, leverage int64) (UChangeInitialLeverage, error) {
func (b *Binance) UChangeInitialLeverageRequest(ctx context.Context, symbol currency.Pair, leverage float64) (UChangeInitialLeverage, error) {
var resp UChangeInitialLeverage
params := url.Values{}
symbolValue, err := b.FormatSymbol(symbol, asset.USDTMarginedFutures)
@@ -896,7 +897,7 @@ func (b *Binance) UChangeInitialLeverageRequest(ctx context.Context, symbol curr
if leverage < 1 || leverage > 125 {
return resp, errors.New("invalid leverage")
}
params.Set("leverage", strconv.FormatInt(leverage, 10))
params.Set("leverage", strconv.FormatFloat(leverage, 'f', -1, 64))
return resp, b.SendAuthHTTPRequest(ctx, exchange.RestUSDTMargined, http.MethodPost, ufuturesChangeInitialLeverage, params, uFuturesDefaultRate, &resp)
}
@@ -924,16 +925,14 @@ func (b *Binance) UModifyIsolatedPositionMarginReq(ctx context.Context, symbol c
return resp, err
}
params.Set("symbol", symbolValue)
if positionSide != "" {
if !common.StringDataCompare(validPositionSide, positionSide) {
return resp, errors.New("invalid margin changeType")
}
}
cType, ok := validMarginChange[changeType]
if !ok {
return resp, errors.New("invalid margin changeType")
}
params.Set("type", strconv.FormatInt(cType, 10))
if positionSide != "" {
params.Set("positionSide", positionSide)
}
params.Set("amount", strconv.FormatFloat(amount, 'f', -1, 64))
return resp, b.SendAuthHTTPRequest(ctx, exchange.RestUSDTMargined, http.MethodPost, ufuturesModifyMargin, params, uFuturesDefaultRate, &resp)
}
@@ -952,7 +951,7 @@ func (b *Binance) UPositionMarginChangeHistory(ctx context.Context, symbol curre
return resp, errors.New("invalid margin changeType")
}
params.Set("type", strconv.FormatInt(cType, 10))
if limit > 0 && limit < 500 {
if limit > 0 {
params.Set("limit", strconv.FormatInt(limit, 10))
}
if !startTime.IsZero() && !endTime.IsZero() {
@@ -979,6 +978,20 @@ func (b *Binance) UPositionsInfoV2(ctx context.Context, symbol currency.Pair) ([
return resp, b.SendAuthHTTPRequest(ctx, exchange.RestUSDTMargined, http.MethodGet, ufuturesPositionInfo, params, uFuturesDefaultRate, &resp)
}
// UGetCommissionRates returns the commission rates for USDTMarginedFutures
func (b *Binance) UGetCommissionRates(ctx context.Context, symbol currency.Pair) ([]UPositionInformationV2, error) {
var resp []UPositionInformationV2
params := url.Values{}
if !symbol.IsEmpty() {
symbolValue, err := b.FormatSymbol(symbol, asset.USDTMarginedFutures)
if err != nil {
return resp, err
}
params.Set("symbol", symbolValue)
}
return resp, b.SendAuthHTTPRequest(ctx, exchange.RestUSDTMargined, http.MethodGet, ufuturesCommissionRate, params, uFuturesDefaultRate, &resp)
}
// UAccountTradesHistory gets account's trade history data for USDTMarginedFutures
func (b *Binance) UAccountTradesHistory(ctx context.Context, symbol currency.Pair, fromID string, limit int64, startTime, endTime time.Time) ([]UAccountTradeHistory, error) {
var resp []UAccountTradeHistory
@@ -991,7 +1004,7 @@ func (b *Binance) UAccountTradesHistory(ctx context.Context, symbol currency.Pai
if fromID != "" {
params.Set("fromID", fromID)
}
if limit > 0 && limit < 1000 {
if limit > 0 {
params.Set("limit", strconv.FormatInt(limit, 10))
}
if !startTime.IsZero() && !endTime.IsZero() {
@@ -1019,7 +1032,7 @@ func (b *Binance) UAccountIncomeHistory(ctx context.Context, symbol currency.Pai
}
params.Set("incomeType", incomeType)
}
if limit > 0 && limit < 1000 {
if limit > 0 {
params.Set("limit", strconv.FormatInt(limit, 10))
}
if !startTime.IsZero() && !endTime.IsZero() {
@@ -1079,7 +1092,7 @@ func (b *Binance) UAccountForcedOrders(ctx context.Context, symbol currency.Pair
}
params.Set("autoCloseType", autoCloseType)
}
if limit > 0 && limit < 1000 {
if limit > 0 {
params.Set("limit", strconv.FormatInt(limit, 10))
}
if !startTime.IsZero() && !endTime.IsZero() {
@@ -1140,3 +1153,19 @@ func (b *Binance) FetchUSDTMarginExchangeLimits(ctx context.Context) ([]order.Mi
}
return limits, nil
}
// SetAssetsMode sets the current asset margin type, true for multi, false for single
func (b *Binance) SetAssetsMode(ctx context.Context, multiMargin bool) error {
params := url.Values{
"multiAssetsMargin": {strconv.FormatBool(multiMargin)},
}
return b.SendAuthHTTPRequest(ctx, exchange.RestUSDTMargined, http.MethodPost, uFuturesMultiAssetsMargin, params, uFuturesDefaultRate, nil)
}
// GetAssetsMode returns the current asset margin type, true for multi, false for single
func (b *Binance) GetAssetsMode(ctx context.Context) (bool, error) {
var result struct {
MultiAssetsMargin bool `json:"multiAssetsMargin"`
}
return result.MultiAssetsMargin, b.SendAuthHTTPRequest(ctx, exchange.RestUSDTMargined, http.MethodGet, uFuturesMultiAssetsMargin, nil, uFuturesDefaultRate, &result)
}

View File

@@ -17,9 +17,11 @@ import (
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"
@@ -120,8 +122,9 @@ func (b *Binance) SetDefaults() {
}
b.Features = exchange.Features{
Supports: exchange.FeaturesSupported{
REST: true,
Websocket: true,
REST: true,
Websocket: true,
MaximumOrderHistory: kline.OneDay.Duration() * 7,
RESTCapabilities: protocol.Features{
TickerBatching: true,
TickerFetching: true,
@@ -165,6 +168,9 @@ func (b *Binance) SetDefaults() {
Intervals: true,
},
FuturesCapabilities: exchange.FuturesCapabilities{
Positions: true,
Leverage: true,
CollateralMode: true,
FundingRates: true,
FundingRateFrequency: kline.EightHour.Duration(),
},
@@ -972,6 +978,9 @@ func (b *Binance) SubmitOrder(ctx context.Context, s *order.Submit) (*order.Subm
var orderID string
status := order.New
var trades []order.TradeHistory
if s.Leverage != 0 && s.Leverage != 1 {
return nil, fmt.Errorf("%w received '%v'", order.ErrSubmitLeverageNotSupported, s.Leverage)
}
switch s.AssetType {
case asset.Spot, asset.Margin:
var sideType string
@@ -1618,7 +1627,7 @@ func (b *Binance) GetOrderHistory(ctx context.Context, req *order.MultiOrderRequ
return nil, fmt.Errorf("can only fetch orders 30 days out")
}
orderHistory, err = b.GetAllFuturesOrders(ctx,
req.Pairs[i], "", req.StartTime, req.EndTime, 0, 0)
req.Pairs[i], currency.EMPTYPAIR, req.StartTime, req.EndTime, 0, 0)
if err != nil {
return nil, err
}
@@ -1628,7 +1637,7 @@ func (b *Binance) GetOrderHistory(ctx context.Context, req *order.MultiOrderRequ
return nil, err
}
orderHistory, err = b.GetAllFuturesOrders(ctx,
req.Pairs[i], "", time.Time{}, time.Time{}, fromID, 0)
req.Pairs[i], currency.EMPTYPAIR, time.Time{}, time.Time{}, fromID, 0)
if err != nil {
return nil, err
}
@@ -2051,7 +2060,7 @@ func (b *Binance) GetServerTime(ctx context.Context, ai asset.Item) (time.Time,
if err != nil {
return time.Time{}, err
}
return info.Servertime, nil
return info.ServerTime, nil
case asset.CoinMarginedFutures:
info, err := b.FuturesExchangeInfo(ctx)
if err != nil {
@@ -2260,3 +2269,568 @@ func (b *Binance) IsPerpetualFutureCurrency(a asset.Item, cp currency.Pair) (boo
}
return false, nil
}
// SetCollateralMode sets the account's collateral mode for the asset type
func (b *Binance) SetCollateralMode(ctx context.Context, a asset.Item, collateralMode collateral.Mode) error {
if a != asset.USDTMarginedFutures {
return fmt.Errorf("%w %v", asset.ErrNotSupported, a)
}
if collateralMode != collateral.MultiMode && collateralMode != collateral.SingleMode {
return fmt.Errorf("%w %v", order.ErrCollateralInvalid, collateralMode)
}
return b.SetAssetsMode(ctx, collateralMode == collateral.MultiMode)
}
// GetCollateralMode returns the account's collateral mode for the asset type
func (b *Binance) GetCollateralMode(ctx context.Context, a asset.Item) (collateral.Mode, error) {
if a != asset.USDTMarginedFutures {
return collateral.UnknownMode, fmt.Errorf("%w %v", asset.ErrNotSupported, a)
}
isMulti, err := b.GetAssetsMode(ctx)
if err != nil {
return collateral.UnknownMode, err
}
if isMulti {
return collateral.MultiMode, nil
}
return collateral.SingleMode, nil
}
// SetMarginType sets the default margin type for when opening a new position
func (b *Binance) SetMarginType(ctx context.Context, item asset.Item, pair currency.Pair, tp margin.Type) error {
if item != asset.USDTMarginedFutures && item != asset.CoinMarginedFutures {
return fmt.Errorf("%w %v", asset.ErrNotSupported, item)
}
if !tp.Valid() {
return fmt.Errorf("%w %v", margin.ErrInvalidMarginType, tp)
}
mt, err := b.marginTypeToString(tp)
if err != nil {
return err
}
switch item {
case asset.CoinMarginedFutures:
_, err = b.FuturesChangeMarginType(ctx, pair, mt)
case asset.USDTMarginedFutures:
err = b.UChangeInitialMarginType(ctx, pair, mt)
}
if err != nil {
return err
}
return nil
}
// ChangePositionMargin will modify a position/currencies margin parameters
func (b *Binance) ChangePositionMargin(ctx context.Context, req *margin.PositionChangeRequest) (*margin.PositionChangeResponse, error) {
if req == nil {
return nil, fmt.Errorf("%w PositionChangeRequest", common.ErrNilPointer)
}
if req.Asset != asset.USDTMarginedFutures && req.Asset != asset.CoinMarginedFutures {
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, fmt.Errorf("%w %v %v", margin.ErrOriginalPositionMarginRequired, req.Asset, req.Pair)
}
if req.MarginType == margin.Multi {
return nil, fmt.Errorf("%w %v %v", margin.ErrMarginTypeUnsupported, req.Asset, req.Pair)
}
marginType := "add"
if req.NewAllocatedMargin < req.OriginalAllocatedMargin {
marginType = "reduce"
}
var side string
if req.MarginSide != "" {
side = req.MarginSide
}
var err error
switch req.Asset {
case asset.CoinMarginedFutures:
_, err = b.ModifyIsolatedPositionMargin(ctx, req.Pair, side, marginType, req.NewAllocatedMargin)
case asset.USDTMarginedFutures:
_, err = b.UModifyIsolatedPositionMarginReq(ctx, req.Pair, side, marginType, req.NewAllocatedMargin)
}
if err != nil {
return nil, err
}
return &margin.PositionChangeResponse{
Exchange: b.Name,
Pair: req.Pair,
Asset: req.Asset,
MarginType: req.MarginType,
AllocatedMargin: req.NewAllocatedMargin,
}, nil
}
// marginTypeToString converts the GCT margin type to Binance's string
func (b *Binance) marginTypeToString(mt margin.Type) (string, error) {
switch mt {
case margin.Isolated:
return margin.Isolated.Upper(), nil
case margin.Multi:
return "CROSSED", nil
}
return "", fmt.Errorf("%w %v", margin.ErrInvalidMarginType, mt)
}
// GetFuturesPositionSummary returns the account's position summary for the asset type and pair
// it can be used to calculate potential positions
func (b *Binance) GetFuturesPositionSummary(ctx context.Context, req *order.PositionSummaryRequest) (*order.PositionSummary, error) {
if req == nil {
return nil, fmt.Errorf("%w GetFuturesPositionSummary", common.ErrNilPointer)
}
if req.CalculateOffline {
return nil, common.ErrCannotCalculateOffline
}
fPair, err := b.FormatExchangeCurrency(req.Pair, req.Asset)
if err != nil {
return nil, err
}
switch req.Asset {
case asset.USDTMarginedFutures:
ai, err := b.UAccountInformationV2(ctx)
if err != nil {
return nil, err
}
collateralMode := collateral.SingleMode
if ai.MultiAssetsMargin {
collateralMode = collateral.MultiMode
}
var accountPosition *UPosition
var leverage, maintenanceMargin, initialMargin,
liquidationPrice, markPrice, positionSize,
collateralTotal, collateralUsed, collateralAvailable,
pnl, openPrice, isolatedMargin float64
for i := range ai.Positions {
if ai.Positions[i].Symbol != fPair.String() {
continue
}
accountPosition = &ai.Positions[i]
break
}
if accountPosition == nil {
return nil, fmt.Errorf("%w %v %v position info", currency.ErrCurrencyNotFound, req.Asset, req.Pair)
}
var usdtAsset, busdAsset *UAsset
for i := range ai.Assets {
if usdtAsset != nil && busdAsset != nil {
break
}
if strings.EqualFold(ai.Assets[i].Asset, currency.USDT.Item.Symbol) {
usdtAsset = &ai.Assets[i]
continue
}
if strings.EqualFold(ai.Assets[i].Asset, currency.BUSD.Item.Symbol) {
busdAsset = &ai.Assets[i]
}
}
if usdtAsset == nil && busdAsset == nil {
return nil, fmt.Errorf("%w %v %v asset info", currency.ErrCurrencyNotFound, req.Asset, req.Pair)
}
leverage = accountPosition.Leverage
openPrice = accountPosition.EntryPrice
maintenanceMargin = accountPosition.MaintenanceMargin
initialMargin = accountPosition.PositionInitialMargin
marginType := margin.Multi
if accountPosition.Isolated {
marginType = margin.Isolated
}
var c currency.Code
if collateralMode == collateral.SingleMode {
var collateralAsset *UAsset
if strings.Contains(accountPosition.Symbol, usdtAsset.Asset) {
collateralAsset = usdtAsset
} else if strings.Contains(accountPosition.Symbol, busdAsset.Asset) {
collateralAsset = busdAsset
}
collateralTotal = collateralAsset.WalletBalance
collateralAvailable = collateralAsset.AvailableBalance
pnl = collateralAsset.UnrealizedProfit
c = currency.NewCode(collateralAsset.Asset)
if marginType == margin.Multi {
isolatedMargin = collateralAsset.CrossUnPnl
collateralUsed = collateralTotal + isolatedMargin
} else {
isolatedMargin = accountPosition.IsolatedWallet
collateralUsed = isolatedMargin
}
} else if collateralMode == collateral.MultiMode {
collateralTotal = ai.TotalWalletBalance
collateralUsed = ai.TotalWalletBalance - ai.AvailableBalance
collateralAvailable = ai.AvailableBalance
pnl = accountPosition.UnrealisedProfit
}
var maintenanceMarginFraction decimal.Decimal
if collateralTotal != 0 {
maintenanceMarginFraction = decimal.NewFromFloat(maintenanceMargin).Div(decimal.NewFromFloat(collateralTotal)).Mul(decimal.NewFromInt32(100))
}
// binance so fun, some prices exclusively here
positionsInfo, err := b.UPositionsInfoV2(ctx, fPair)
if err != nil {
return nil, err
}
var relevantPosition *UPositionInformationV2
for i := range positionsInfo {
if positionsInfo[i].Symbol != fPair.String() {
continue
}
relevantPosition = &positionsInfo[i]
}
if relevantPosition == nil {
return nil, fmt.Errorf("%w %v %v", order.ErrNoPositionsFound, req.Asset, req.Pair)
}
return &order.PositionSummary{
Pair: req.Pair,
Asset: req.Asset,
MarginType: marginType,
CollateralMode: collateralMode,
Currency: c,
IsolatedMargin: decimal.NewFromFloat(isolatedMargin),
Leverage: decimal.NewFromFloat(leverage),
MaintenanceMarginRequirement: decimal.NewFromFloat(maintenanceMargin),
InitialMarginRequirement: decimal.NewFromFloat(initialMargin),
EstimatedLiquidationPrice: decimal.NewFromFloat(liquidationPrice),
CollateralUsed: decimal.NewFromFloat(collateralUsed),
MarkPrice: decimal.NewFromFloat(markPrice),
CurrentSize: decimal.NewFromFloat(positionSize),
AverageOpenPrice: decimal.NewFromFloat(openPrice),
PositionPNL: decimal.NewFromFloat(pnl),
MaintenanceMarginFraction: maintenanceMarginFraction,
FreeCollateral: decimal.NewFromFloat(collateralAvailable),
TotalCollateral: decimal.NewFromFloat(collateralTotal),
NotionalSize: decimal.NewFromFloat(positionSize).Mul(decimal.NewFromFloat(markPrice)),
}, nil
case asset.CoinMarginedFutures:
ai, err := b.GetFuturesAccountInfo(ctx)
if err != nil {
return nil, err
}
collateralMode := collateral.SingleMode
var leverage, maintenanceMargin, initialMargin,
liquidationPrice, markPrice, positionSize,
collateralTotal, collateralUsed, collateralAvailable,
pnl, openPrice, isolatedMargin float64
var accountPosition *FuturesAccountInformationPosition
for i := range ai.Positions {
if ai.Positions[i].Symbol != fPair.String() {
continue
}
accountPosition = &ai.Positions[i]
break
}
if accountPosition == nil {
return nil, fmt.Errorf("%w %v %v position info", currency.ErrCurrencyNotFound, req.Asset, req.Pair)
}
var accountAsset *FuturesAccountAsset
for i := range ai.Assets {
// TODO: utilise contract data to discern the underlying currency
// instead of having a user provide it
if ai.Assets[i].Asset != req.UnderlyingPair.Base.Upper().String() {
continue
}
accountAsset = &ai.Assets[i]
break
}
if accountAsset == nil {
return nil, fmt.Errorf("could not get asset info: %w %v %v, please verify underlying pair: '%v'", currency.ErrCurrencyNotFound, req.Asset, req.Pair, req.UnderlyingPair)
}
leverage = accountPosition.Leverage
openPrice = accountPosition.EntryPrice
maintenanceMargin = accountPosition.MaintenanceMargin
initialMargin = accountPosition.PositionInitialMargin
marginType := margin.Multi
if accountPosition.Isolated {
marginType = margin.Isolated
}
collateralTotal = accountAsset.WalletBalance
frozenBalance := decimal.NewFromFloat(accountAsset.WalletBalance).Sub(decimal.NewFromFloat(accountAsset.AvailableBalance))
collateralAvailable = accountAsset.AvailableBalance
pnl = accountAsset.UnrealizedProfit
if marginType == margin.Multi {
isolatedMargin = accountAsset.CrossUnPNL
collateralUsed = collateralTotal + isolatedMargin
} else {
isolatedMargin = accountPosition.IsolatedWallet
collateralUsed = isolatedMargin
}
// binance so fun, some prices exclusively here
positionsInfo, err := b.FuturesPositionsInfo(ctx, "", req.Pair.Base.String())
if err != nil {
return nil, err
}
if len(positionsInfo) == 0 {
return nil, fmt.Errorf("%w %v", order.ErrNoPositionsFound, fPair)
}
var relevantPosition *FuturesPositionInformation
for i := range positionsInfo {
if positionsInfo[i].Symbol != fPair.String() {
continue
}
relevantPosition = &positionsInfo[i]
}
if relevantPosition == nil {
return nil, fmt.Errorf("%w %v %v", order.ErrNoPositionsFound, req.Asset, req.Pair)
}
liquidationPrice = relevantPosition.LiquidationPrice
markPrice = relevantPosition.MarkPrice
positionSize = relevantPosition.PositionAmount
var mmf, tc decimal.Decimal
if collateralTotal != 0 {
tc = decimal.NewFromFloat(collateralTotal)
mmf = decimal.NewFromFloat(maintenanceMargin).Div(tc).Mul(decimal.NewFromInt(100))
}
return &order.PositionSummary{
Pair: req.Pair,
Asset: req.Asset,
MarginType: marginType,
CollateralMode: collateralMode,
Currency: currency.NewCode(accountAsset.Asset),
IsolatedMargin: decimal.NewFromFloat(isolatedMargin),
NotionalSize: decimal.NewFromFloat(positionSize).Mul(decimal.NewFromFloat(markPrice)),
Leverage: decimal.NewFromFloat(leverage),
MaintenanceMarginRequirement: decimal.NewFromFloat(maintenanceMargin),
InitialMarginRequirement: decimal.NewFromFloat(initialMargin),
EstimatedLiquidationPrice: decimal.NewFromFloat(liquidationPrice),
CollateralUsed: decimal.NewFromFloat(collateralUsed),
MarkPrice: decimal.NewFromFloat(markPrice),
CurrentSize: decimal.NewFromFloat(positionSize),
AverageOpenPrice: decimal.NewFromFloat(openPrice),
PositionPNL: decimal.NewFromFloat(pnl),
MaintenanceMarginFraction: mmf,
FreeCollateral: decimal.NewFromFloat(collateralAvailable),
TotalCollateral: tc,
FrozenBalance: frozenBalance,
}, nil
default:
return nil, fmt.Errorf("%w %v", asset.ErrNotSupported, req.Asset)
}
}
// GetFuturesPositionOrders returns the orders for futures positions
func (b *Binance) GetFuturesPositionOrders(ctx context.Context, req *order.PositionsRequest) ([]order.PositionResponse, error) {
if req == nil {
return nil, fmt.Errorf("%w GetFuturesPositionOrders", common.ErrNilPointer)
}
if len(req.Pairs) == 0 {
return nil, currency.ErrCurrencyPairsEmpty
}
if time.Since(req.StartDate) > b.Features.Supports.MaximumOrderHistory+time.Hour {
if req.RespectOrderHistoryLimits {
req.StartDate = time.Now().Add(-b.Features.Supports.MaximumOrderHistory)
} else {
return nil, fmt.Errorf("%w max lookup %v", order.ErrOrderHistoryTooLarge, time.Now().Add(-b.Features.Supports.MaximumOrderHistory))
}
}
if req.EndDate.IsZero() {
req.EndDate = time.Now()
}
var resp []order.PositionResponse
sd := req.StartDate
switch req.Asset {
case asset.USDTMarginedFutures:
var orderLimit = 1000
for x := range req.Pairs {
fPair, err := b.FormatExchangeCurrency(req.Pairs[x], req.Asset)
if err != nil {
return nil, err
}
result, err := b.UPositionsInfoV2(ctx, fPair)
if err != nil {
return nil, err
}
for y := range result {
currencyPosition := order.PositionResponse{
Asset: req.Asset,
Pair: req.Pairs[x],
}
for {
var orders []UFuturesOrderData
orders, err = b.UAllAccountOrders(ctx, fPair, 0, int64(orderLimit), sd, req.EndDate)
if err != nil {
return nil, err
}
for i := range orders {
if orders[i].Time.After(req.EndDate) {
continue
}
orderVars := compatibleOrderVars(orders[i].Side, orders[i].Status, orders[i].OrderType)
var mt margin.Type
mt, err = margin.StringToMarginType(result[y].MarginType)
if err != nil {
if !errors.Is(err, margin.ErrInvalidMarginType) {
return nil, err
}
}
currencyPosition.Orders = append(currencyPosition.Orders, order.Detail{
ReduceOnly: orders[i].ClosePosition,
Price: orders[i].Price,
Amount: orders[i].ExecutedQty,
TriggerPrice: orders[i].ActivatePrice,
AverageExecutedPrice: orders[i].AvgPrice,
ExecutedAmount: orders[i].ExecutedQty,
RemainingAmount: orders[i].OrigQty - orders[i].ExecutedQty,
CostAsset: req.Pairs[x].Quote,
Leverage: result[y].Leverage,
Exchange: b.Name,
OrderID: strconv.FormatInt(orders[i].OrderID, 10),
ClientOrderID: orders[i].ClientOrderID,
Type: orderVars.OrderType,
Side: orderVars.Side,
Status: orderVars.Status,
AssetType: asset.USDTMarginedFutures,
Date: orders[i].Time,
LastUpdated: orders[i].UpdateTime,
Pair: req.Pairs[x],
MarginType: mt,
})
}
if len(orders) < orderLimit {
break
}
sd = currencyPosition.Orders[len(currencyPosition.Orders)-1].Date
}
resp = append(resp, currencyPosition)
}
}
case asset.CoinMarginedFutures:
var orderLimit = 100
for x := range req.Pairs {
fPair, err := b.FormatExchangeCurrency(req.Pairs[x], req.Asset)
if err != nil {
return nil, err
}
// "pair" for coinmarginedfutures is the pair.Base
// eg ADAUSD_PERP the pair is ADAUSD
result, err := b.FuturesPositionsInfo(ctx, "", fPair.Base.String())
if err != nil {
return nil, err
}
currencyPosition := order.PositionResponse{
Asset: req.Asset,
Pair: req.Pairs[x],
}
for y := range result {
if result[y].PositionAmount == 0 {
continue
}
for {
var orders []FuturesOrderData
orders, err = b.GetAllFuturesOrders(ctx, fPair, currency.EMPTYPAIR, sd, req.EndDate, 0, int64(orderLimit))
if err != nil {
return nil, err
}
for i := range orders {
if orders[i].Time.After(req.EndDate) {
continue
}
var orderPair currency.Pair
orderPair, err = currency.NewPairFromString(orders[i].Pair)
if err != nil {
return nil, err
}
orderVars := compatibleOrderVars(orders[i].Side, orders[i].Status, orders[i].OrderType)
var mt margin.Type
mt, err = margin.StringToMarginType(result[y].MarginType)
if err != nil {
if !errors.Is(err, margin.ErrInvalidMarginType) {
return nil, err
}
}
currencyPosition.Orders = append(currencyPosition.Orders, order.Detail{
ReduceOnly: orders[i].ClosePosition,
Price: orders[i].Price,
Amount: orders[i].ExecutedQty,
TriggerPrice: orders[i].ActivatePrice,
AverageExecutedPrice: orders[i].AvgPrice,
ExecutedAmount: orders[i].ExecutedQty,
RemainingAmount: orders[i].OrigQty - orders[i].ExecutedQty,
Leverage: result[y].Leverage,
CostAsset: orderPair.Base,
Exchange: b.Name,
OrderID: strconv.FormatInt(orders[i].OrderID, 10),
ClientOrderID: orders[i].ClientOrderID,
Type: orderVars.OrderType,
Side: orderVars.Side,
Status: orderVars.Status,
AssetType: asset.CoinMarginedFutures,
Date: orders[i].Time,
LastUpdated: orders[i].UpdateTime,
Pair: req.Pairs[x],
MarginType: mt,
})
}
if len(orders) < orderLimit {
break
}
sd = currencyPosition.Orders[len(currencyPosition.Orders)-1].Date
}
resp = append(resp, currencyPosition)
}
}
default:
return nil, fmt.Errorf("%w %v", asset.ErrNotSupported, req.Asset)
}
return resp, nil
}
// SetLeverage sets the account's initial leverage for the asset type and pair
func (b *Binance) SetLeverage(ctx context.Context, item asset.Item, pair currency.Pair, _ margin.Type, amount float64, _ order.Side) error {
switch item {
case asset.USDTMarginedFutures:
_, err := b.UChangeInitialLeverageRequest(ctx, pair, amount)
return err
case asset.CoinMarginedFutures:
_, err := b.FuturesChangeInitialLeverage(ctx, pair, amount)
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 (b *Binance) GetLeverage(ctx context.Context, item asset.Item, pair currency.Pair, _ margin.Type, _ order.Side) (float64, error) {
if pair.IsEmpty() {
return -1, currency.ErrCurrencyPairEmpty
}
switch item {
case asset.USDTMarginedFutures:
resp, err := b.UPositionsInfoV2(ctx, pair)
if err != nil {
return -1, err
}
if len(resp) == 0 {
return -1, fmt.Errorf("%w %v %v", order.ErrPositionNotFound, item, pair)
}
// leverage is the same across positions
return resp[0].Leverage, nil
case asset.CoinMarginedFutures:
resp, err := b.FuturesPositionsInfo(ctx, "", pair.Base.String())
if err != nil {
return -1, err
}
if len(resp) == 0 {
return -1, fmt.Errorf("%w %v %v", order.ErrPositionNotFound, item, pair)
}
// leverage is the same across positions
return resp[0].Leverage, nil
default:
return -1, fmt.Errorf("%w %v", asset.ErrNotSupported, item)
}
}

View File

@@ -359,12 +359,12 @@ type FuturesAccountBalanceData struct {
UpdateTime int64 `json:"updateTime"`
}
// FuturesAccountInformationPositions holds account position data
type FuturesAccountInformationPositions struct {
// FuturesAccountInformationPosition holds account position data
type FuturesAccountInformationPosition struct {
Symbol string `json:"symbol"`
Amount float64 `json:"positionAmt,string"`
InitialMargin float64 `json:"initialMargin,string"`
MaintMargin float64 `json:"maintMargin,string"`
MaintenanceMargin float64 `json:"maintMargin,string"`
UnrealizedProfit float64 `json:"unrealizedProfit,string"`
PositionInitialMargin float64 `json:"positionInitialMargin,string"`
OpenOrderInitialMargin float64 `json:"openOrderInitialMargin,string"`
@@ -380,26 +380,29 @@ type FuturesAccountInformationPositions struct {
// FuturesAccountInformation stores account information for futures account
type FuturesAccountInformation struct {
Assets []struct {
Asset string `json:"asset"`
WalletBalance float64 `json:"walletBalance,string"`
UnrealizedProfit float64 `json:"unrealizedProfit,string"`
MarginBalance float64 `json:"marginBalance,string"`
MaintMargin float64 `json:"maintMargin,string"`
InitialMargin float64 `json:"initialMargin,string"`
PositionInitialMargin float64 `json:"positionInitialMargin,string"`
OpenOrderInitialMargin float64 `json:"openOrderInitialMargin,string"`
MaxWithdrawAmount float64 `json:"maxWithdrawAmount,string"`
CrossWalletBalance float64 `json:"crossWalletBalance,string"`
CrossUnPNL float64 `json:"crossUnPnl,string"`
AvailableBalance float64 `json:"availableBalance,string"`
} `json:"assets"`
Positions []FuturesAccountInformationPositions `json:"positions"`
CanDeposit bool `json:"canDeposit"`
CanTrade bool `json:"canTrade"`
CanWithdraw bool `json:"canWithdraw"`
FeeTier int64 `json:"feeTier"`
UpdateTime time.Time `json:"updateTime"`
Assets []FuturesAccountAsset `json:"assets"`
Positions []FuturesAccountInformationPosition `json:"positions"`
CanDeposit bool `json:"canDeposit"`
CanTrade bool `json:"canTrade"`
CanWithdraw bool `json:"canWithdraw"`
FeeTier int64 `json:"feeTier"`
UpdateTime time.Time `json:"updateTime"`
}
// FuturesAccountAsset holds account asset information
type FuturesAccountAsset struct {
Asset string `json:"asset"`
WalletBalance float64 `json:"walletBalance,string"`
UnrealizedProfit float64 `json:"unrealizedProfit,string"`
MarginBalance float64 `json:"marginBalance,string"`
MaintenanceMargin float64 `json:"maintMargin,string"`
InitialMargin float64 `json:"initialMargin,string"`
PositionInitialMargin float64 `json:"positionInitialMargin,string"`
OpenOrderInitialMargin float64 `json:"openOrderInitialMargin,string"`
MaxWithdrawAmount float64 `json:"maxWithdrawAmount,string"`
CrossWalletBalance float64 `json:"crossWalletBalance,string"`
CrossUnPNL float64 `json:"crossUnPnl,string"`
AvailableBalance float64 `json:"availableBalance,string"`
}
// GenericAuthResponse is a general data response for a post auth request
@@ -408,6 +411,13 @@ type GenericAuthResponse struct {
Msg string `json:"msg"`
}
// FuturesMarginUpdatedResponse stores margin update response data
type FuturesMarginUpdatedResponse struct {
Amount float64 `json:"amount"`
Type int `json:"type"`
GenericAuthResponse
}
// FuturesLeverageData stores leverage data for futures
type FuturesLeverageData struct {
Leverage int64 `json:"leverage"`
@@ -435,18 +445,21 @@ type GetPositionMarginChangeHistoryData struct {
// FuturesPositionInformation stores futures position info
type FuturesPositionInformation struct {
Symbol string `json:"symbol"`
PositionAmount float64 `json:"positionAmt,string"`
EntryPrice float64 `json:"entryPrice,string"`
MarkPrice float64 `json:"markPrice,string"`
UnrealizedProfit float64 `json:"unRealizedProfit,string"`
LiquidationPrice float64 `json:"liquidation,string"`
Leverage int64 `json:"leverage"`
MaxQty float64 `json:"maxQty"`
MarginType string `json:"marginType"`
IsolatedMargin float64 `json:"isolatedMargin,string"`
IsAutoAddMargin bool `json:"isAutoAddMargin"`
PositionSide string `json:"positionSide"`
Symbol string `json:"symbol"`
PositionAmount float64 `json:"positionAmt,string"`
EntryPrice float64 `json:"entryPrice,string"`
MarkPrice float64 `json:"markPrice,string"`
UnRealizedProfit float64 `json:"unRealizedProfit,string"`
LiquidationPrice float64 `json:"liquidationPrice,string"`
Leverage float64 `json:"leverage,string"`
MaxQty float64 `json:"maxQty,string"`
MarginType string `json:"marginType"`
IsolatedMargin float64 `json:"isolatedMargin,string"`
IsAutoAddMargin bool `json:"isAutoAddMargin,string"`
PositionSide string `json:"positionSide"`
NotionalValue float64 `json:"notionalValue,string"`
IsolatedWallet float64 `json:"isolatedWallet,string"`
UpdateTime binanceTime `json:"updateTime"`
}
// FuturesAccountTradeList stores account trade list data

View File

@@ -92,6 +92,8 @@ const (
cFuturesAccountInformationRate
cFuturesOrderbookTickerAllRate
cFuturesOrdersDefaultRate
uFuturesMultiAssetMarginRate
uFuturesSetMultiAssetMarginRate
)
// RateLimit implements the request.Limiter interface
@@ -211,6 +213,10 @@ func (r *RateLimit) Limit(ctx context.Context, f request.EndpointLimit) error {
limiter, tokens = r.CFuturesRate, 20
case cFuturesDefaultRate:
limiter, tokens = r.CFuturesRate, 1
case uFuturesMultiAssetMarginRate:
limiter, tokens = r.UFuturesRate, 30
case uFuturesSetMultiAssetMarginRate:
limiter, tokens = r.UFuturesRate, 1
default:
limiter, tokens = r.SpotRate, 1
}

View File

@@ -53,7 +53,7 @@ func (a *ExchangeInfo) UnmarshalJSON(data []byte) error {
if err := json.Unmarshal(data, &aux); err != nil {
return err
}
a.Servertime = aux.Servertime.Time()
a.ServerTime = aux.Servertime.Time()
return nil
}
@@ -458,8 +458,8 @@ func (u *UFuturesSymbolInfo) UnmarshalJSON(data []byte) error {
}
// UnmarshalJSON deserialises the JSON info, including the timestamp
func (a *FuturesAccountInformationPositions) UnmarshalJSON(data []byte) error {
type Alias FuturesAccountInformationPositions
func (a *FuturesAccountInformationPosition) UnmarshalJSON(data []byte) error {
type Alias FuturesAccountInformationPosition
aux := &struct {
UpdateTime binanceTime `json:"updateTime"`

View File

@@ -251,49 +251,61 @@ type UAccountBalanceV2Data struct {
// UAccountInformationV2Data stores account info for ufutures
type UAccountInformationV2Data struct {
FeeTier int64 `json:"feeTier"`
CanTrade bool `json:"canTrade"`
CanDeposit bool `json:"canDeposit"`
CanWithdraw bool `json:"canWithdraw"`
UpdateTime int64 `json:"updateTime"`
TotalInitialMargin float64 `json:"totalInitialMargin,string"`
TotalMaintenance float64 `json:"totalMaintMargin,string"`
TotalWalletBalance float64 `json:"totalWalletBalance,string"`
TotalUnrealizedProfit float64 `json:"totalUnrealizedProfit,string"`
TotalMarginBalance float64 `json:"totalMarginBalance,string"`
TotalPositionInitialMargin float64 `json:"totalPositionInitialMargin,string"`
TotalOpenOrderInitialMargin float64 `json:"totalOpenOrderInitialMargin,string"`
TotalCrossWalletBalance float64 `json:"totalCrossWalletBalance,string"`
TotalCrossUnrealizedPNL float64 `json:"totalCrossUnPnl,string"`
AvailableBalance float64 `json:"availableBalance,string"`
MaxWithdrawAmount float64 `json:"maxWithdrawAmount,string"`
Assets []struct {
Asset string `json:"asset"`
WalletBalance float64 `json:"walletBalance,string"`
UnrealizedProfit float64 `json:"unrealizedProfit,string"`
MarginBalance float64 `json:"marginBalance,string"`
MaintMargin float64 `json:"maintMargin,string"`
InitialMargin float64 `json:"initialMargin,string"`
PositionInitialMargin float64 `json:"positionInitialMargin,string"`
OpenOrderInitialMargin float64 `json:"openOrderInitialMargin,string"`
CrossWalletBalance float64 `json:"crossWalletBalance,string"`
CrossUnPnl float64 `json:"crossUnPnl,string"`
AvailableBalance float64 `json:"availableBalance,string"`
MaxWithdrawAmount float64 `json:"maxWithdrawAmount,string"`
} `json:"assets"`
Positions []struct {
Symbol string `json:"symbol"`
InitialMargin float64 `json:"initialMargin,string"`
MaintenanceMargin float64 `json:"maintMargin,string"`
UnrealizedProfit float64 `json:"unrealizedProfit,string"`
PositionInitialMargin float64 `json:"positionInitialMargin,string"`
OpenOrderInitialMargin float64 `json:"openOrderInitialMargin,string"`
Leverage float64 `json:"leverage,string"`
Isolated bool `json:"isolated"`
EntryPrice float64 `json:"entryPrice,string"`
MaxNotional float64 `json:"maxNotional,string"`
PositionSide string `json:"positionSide"`
} `json:"positions"`
FeeTier int64 `json:"feeTier"`
CanTrade bool `json:"canTrade"`
CanDeposit bool `json:"canDeposit"`
CanWithdraw bool `json:"canWithdraw"`
UpdateTime int64 `json:"updateTime"`
MultiAssetsMargin bool `json:"multiAssetsMargin"`
TotalInitialMargin float64 `json:"totalInitialMargin,string"`
TotalMaintenanceMargin float64 `json:"totalMaintMargin,string"`
TotalWalletBalance float64 `json:"totalWalletBalance,string"`
TotalUnrealizedProfit float64 `json:"totalUnrealizedProfit,string"`
TotalMarginBalance float64 `json:"totalMarginBalance,string"`
TotalPositionInitialMargin float64 `json:"totalPositionInitialMargin,string"`
TotalOpenOrderInitialMargin float64 `json:"totalOpenOrderInitialMargin,string"`
TotalCrossWalletBalance float64 `json:"totalCrossWalletBalance,string"`
TotalCrossUnrealizedPNL float64 `json:"totalCrossUnPnl,string"`
AvailableBalance float64 `json:"availableBalance,string"`
MaxWithdrawAmount float64 `json:"maxWithdrawAmount,string"`
Assets []UAsset `json:"assets"`
Positions []UPosition `json:"positions"`
}
// UAsset holds account asset information
type UAsset struct {
Asset string `json:"asset"`
WalletBalance float64 `json:"walletBalance,string"`
UnrealizedProfit float64 `json:"unrealizedProfit,string"`
MarginBalance float64 `json:"marginBalance,string"`
MaintenanceMargin float64 `json:"maintMargin,string"`
InitialMargin float64 `json:"initialMargin,string"`
PositionInitialMargin float64 `json:"positionInitialMargin,string"`
OpenOrderInitialMargin float64 `json:"openOrderInitialMargin,string"`
CrossWalletBalance float64 `json:"crossWalletBalance,string"`
CrossUnPnl float64 `json:"crossUnPnl,string"`
AvailableBalance float64 `json:"availableBalance,string"`
MaxWithdrawAmount float64 `json:"maxWithdrawAmount,string"`
}
// UPosition holds account position information
type UPosition struct {
Symbol string `json:"symbol"`
InitialMargin float64 `json:"initialMargin,string"`
MaintenanceMargin float64 `json:"maintMargin,string"`
UnrealisedProfit float64 `json:"unrealizedProfit,string"`
PositionInitialMargin float64 `json:"positionInitialMargin,string"`
OpenOrderInitialMargin float64 `json:"openOrderInitialMargin,string"`
Leverage float64 `json:"leverage,string"`
Isolated bool `json:"isolated"`
IsolatedWallet float64 `json:"isolatedWallet,string"`
EntryPrice float64 `json:"entryPrice,string"`
MaxNotional float64 `json:"maxNotional,string"`
BidNotional float64 `json:"bidNotional,string"`
AskNotional float64 `json:"askNotional,string"`
PositionSide string `json:"positionSide"`
PositionAmount float64 `json:"positionAmt,string"`
UpdateTime binanceTime `json:"updateTime"`
}
// UChangeInitialLeverage stores leverage change data
@@ -321,18 +333,21 @@ type UPositionMarginChangeHistoryData struct {
// UPositionInformationV2 stores positions data
type UPositionInformationV2 struct {
EntryPrice float64 `json:"entryPrice,string"`
MarginType string `json:"marginType"`
AutoAddMarginEnabled bool `json:"isAutoAddMargin,string"`
IsolatedMargin float64 `json:"isolatedMargin,string"`
Leverage float64 `json:"leverage,string"`
LiquidationPrice float64 `json:"liquidationPrice,string"`
MarkPrice float64 `json:"markPrice,string"`
MaxNotionalValue float64 `json:"maxNotionalValue,string"`
PositionAmount float64 `json:"positionAmt,string"`
Symbol string `json:"symbol"`
UnrealizedProfit float64 `json:"unrealizedProfit,string"`
PositionSide string `json:"positionSide"`
Symbol string `json:"symbol"`
PositionAmount float64 `json:"positionAmt,string"`
EntryPrice float64 `json:"entryPrice,string"`
MarkPrice float64 `json:"markPrice,string"`
UnrealizedProfit float64 `json:"unrealizedProfit,string"`
LiquidationPrice float64 `json:"liquidationPrice,string"`
Leverage float64 `json:"leverage,string"`
MaxNotionalValue float64 `json:"maxNotionalValue,string"`
MarginType string `json:"marginType"`
IsAutoAddMargin bool `json:"isAutoAddMargin,string"`
PositionSide string `json:"positionSide"`
Notional float64 `json:"notional,string"`
IsolatedWallet float64 `json:"isolatedWallet,string"`
IsolatedMargin float64 `json:"isolatedMargin,string"`
UpdateTime binanceTime `json:"updateTime"`
}
// UAccountTradeHistory stores trade data for the users account

View File

@@ -174,7 +174,7 @@ func (by *Bybit) GetTrades(ctx context.Context, symbol string, limit int64) ([]T
params.Set("symbol", symbol)
strLimit := "60" // default limit
if limit > 0 && limit < 60 {
if limit > 0 {
strLimit = strconv.FormatInt(limit, 10)
}
params.Set("limit", strLimit)

View File

@@ -131,7 +131,7 @@ func (by *Bybit) GetFuturesKlineData(ctx context.Context, symbol currency.Pair,
}
params.Set("symbol", symbolValue)
if limit > 0 && limit <= 200 {
if limit > 0 {
params.Set("limit", strconv.FormatInt(limit, 10))
}
if !common.StringDataCompare(validFuturesIntervals, interval) {
@@ -178,7 +178,7 @@ func (by *Bybit) GetPublicTrades(ctx context.Context, symbol currency.Pair, limi
return resp.Data, err
}
params.Set("symbol", symbolValue)
if limit > 0 && limit <= 1000 {
if limit > 0 {
params.Set("limit", strconv.FormatInt(limit, 10))
}
@@ -208,7 +208,7 @@ func (by *Bybit) GetMarkPriceKline(ctx context.Context, symbol currency.Pair, in
return resp.Data, err
}
params.Set("symbol", symbolValue)
if limit > 0 && limit <= 200 {
if limit > 0 {
params.Set("limit", strconv.FormatInt(limit, 10))
}
if !common.StringDataCompare(validFuturesIntervals, interval) {
@@ -237,7 +237,7 @@ func (by *Bybit) GetIndexPriceKline(ctx context.Context, symbol currency.Pair, i
return resp.Data, err
}
params.Set("symbol", symbolValue)
if limit > 0 && limit <= 200 {
if limit > 0 {
params.Set("limit", strconv.FormatInt(limit, 10))
}
if !common.StringDataCompare(validFuturesIntervals, interval) {
@@ -266,7 +266,7 @@ func (by *Bybit) GetPremiumIndexPriceKline(ctx context.Context, symbol currency.
return resp.Data, err
}
params.Set("symbol", symbolValue)
if limit > 0 && limit <= 200 {
if limit > 0 {
params.Set("limit", strconv.FormatInt(limit, 10))
}
if !common.StringDataCompare(validFuturesIntervals, interval) {
@@ -295,7 +295,7 @@ func (by *Bybit) GetOpenInterest(ctx context.Context, symbol currency.Pair, peri
return resp.Data, err
}
params.Set("symbol", symbolValue)
if limit > 0 && limit <= 200 {
if limit > 0 {
params.Set("limit", strconv.FormatInt(limit, 10))
}
if !common.StringDataCompare(validFuturesPeriods, period) {
@@ -320,7 +320,7 @@ func (by *Bybit) GetLatestBigDeal(ctx context.Context, symbol currency.Pair, lim
return resp.Data, err
}
params.Set("symbol", symbolValue)
if limit > 0 && limit <= 1000 {
if limit > 0 {
params.Set("limit", strconv.FormatInt(limit, 10))
}
@@ -341,7 +341,7 @@ func (by *Bybit) GetAccountRatio(ctx context.Context, symbol currency.Pair, peri
return resp.Data, err
}
params.Set("symbol", symbolValue)
if limit > 0 && limit <= 500 {
if limit > 0 {
params.Set("limit", strconv.FormatInt(limit, 10))
}
if !common.StringDataCompare(validFuturesPeriods, period) {
@@ -491,7 +491,7 @@ func (by *Bybit) GetActiveCoinFuturesOrders(ctx context.Context, symbol currency
if direction != "" {
params.Set("direction", direction)
}
if limit > 0 && limit <= 50 {
if limit > 0 {
params.Set("limit", strconv.FormatInt(limit, 10))
}
if cursor != "" {
@@ -709,7 +709,7 @@ func (by *Bybit) GetConditionalCoinFuturesOrders(ctx context.Context, symbol cur
if direction != "" {
params.Set("direction", direction)
}
if limit > 0 && limit <= 50 {
if limit > 0 {
params.Set("limit", strconv.FormatInt(limit, 10))
}
if cursor != "" {
@@ -999,7 +999,7 @@ func (by *Bybit) GetCoinTradeRecords(ctx context.Context, symbol currency.Pair,
if page != 0 {
params.Set("page", strconv.FormatInt(page, 10))
}
if limit > 0 && limit <= 200 {
if limit > 0 {
params.Set("limit", strconv.FormatInt(limit, 10))
}
@@ -1036,7 +1036,7 @@ func (by *Bybit) GetClosedCoinTrades(ctx context.Context, symbol currency.Pair,
if page > 0 && page <= 50 {
params.Set("page", strconv.FormatInt(page, 10))
}
if limit > 0 && limit <= 50 {
if limit > 0 {
params.Set("limit", strconv.FormatInt(limit, 10))
}
@@ -1247,7 +1247,7 @@ func (by *Bybit) GetWalletFundRecords(ctx context.Context, startDate, endDate, c
if page != 0 {
params.Set("page", strconv.FormatInt(page, 10))
}
if limit > 0 && limit <= 50 {
if limit > 0 {
params.Set("limit", strconv.FormatInt(limit, 10))
}
@@ -1280,7 +1280,7 @@ func (by *Bybit) GetWalletWithdrawalRecords(ctx context.Context, startDate, endD
if page != 0 {
params.Set("page", strconv.FormatInt(page, 10))
}
if limit > 0 && limit <= 50 {
if limit > 0 {
params.Set("limit", strconv.FormatInt(limit, 10))
}
@@ -1303,7 +1303,7 @@ func (by *Bybit) GetAssetExchangeRecords(ctx context.Context, direction string,
if from != 0 {
params.Set("from", strconv.FormatInt(from, 10))
}
if limit > 0 && limit <= 50 {
if limit > 0 {
params.Set("limit", strconv.FormatInt(limit, 10))
}

View File

@@ -122,7 +122,7 @@ func (by *Bybit) GetActiveFuturesOrders(ctx context.Context, symbol currency.Pai
if direction != "" {
params.Set("direction", direction)
}
if limit > 0 && limit <= 50 {
if limit > 0 {
params.Set("limit", strconv.FormatInt(limit, 10))
}
if cursor != "" {
@@ -345,7 +345,7 @@ func (by *Bybit) GetConditionalFuturesOrders(ctx context.Context, symbol currenc
if direction != "" {
params.Set("direction", direction)
}
if limit > 0 && limit <= 50 {
if limit > 0 {
params.Set("limit", strconv.FormatInt(limit, 10))
}
if cursor != "" {
@@ -583,8 +583,8 @@ func (by *Bybit) SetTradingAndStop(ctx context.Context, positionMode int64, symb
return resp.Result, by.SendAuthHTTPRequest(ctx, exchange.RestFutures, http.MethodPost, futuresSetTradingStop, params, nil, &resp, futuresSetTradingStopRate)
}
// SetLeverage sets leverage
func (by *Bybit) SetLeverage(ctx context.Context, symbol currency.Pair, buyLeverage, sellLeverage float64) (float64, error) {
// SetLeverageLevel sets leverage
func (by *Bybit) SetLeverageLevel(ctx context.Context, symbol currency.Pair, buyLeverage, sellLeverage float64) (float64, error) {
resp := struct {
Result float64 `json:"result"`
Error
@@ -685,7 +685,7 @@ func (by *Bybit) GetTradeRecords(ctx context.Context, symbol currency.Pair, orde
if page != 0 {
params.Set("page", strconv.FormatInt(page, 10))
}
if limit > 0 && limit <= 200 {
if limit > 0 {
params.Set("limit", strconv.FormatInt(limit, 10))
}
@@ -721,7 +721,7 @@ func (by *Bybit) GetClosedTrades(ctx context.Context, symbol currency.Pair, exec
if page > 0 && page <= 50 {
params.Set("page", strconv.FormatInt(page, 10))
}
if limit > 0 && limit <= 50 {
if limit > 0 {
params.Set("limit", strconv.FormatInt(limit, 10))
}

View File

@@ -1870,7 +1870,7 @@ func TestSetLeverage(t *testing.T) {
t.Fatal(err)
}
_, err = b.SetLeverage(context.Background(), pair, 10, 10)
_, err = b.SetLeverageLevel(context.Background(), pair, 10, 10)
if err != nil {
t.Error(err)
}

View File

@@ -72,7 +72,7 @@ func (by *Bybit) GetUSDTFuturesKlineData(ctx context.Context, symbol currency.Pa
}
params.Set("symbol", symbolValue)
if limit > 0 && limit <= 200 {
if limit > 0 {
params.Set("limit", strconv.FormatInt(limit, 10))
}
if !common.StringDataCompare(validFuturesIntervals, interval) {
@@ -101,7 +101,7 @@ func (by *Bybit) GetUSDTPublicTrades(ctx context.Context, symbol currency.Pair,
return resp.Data, err
}
params.Set("symbol", symbolValue)
if limit > 0 && limit <= 1000 {
if limit > 0 {
params.Set("limit", strconv.FormatInt(limit, 10))
}
@@ -122,7 +122,7 @@ func (by *Bybit) GetUSDTMarkPriceKline(ctx context.Context, symbol currency.Pair
return resp.Data, err
}
params.Set("symbol", symbolValue)
if limit > 0 && limit <= 200 {
if limit > 0 {
params.Set("limit", strconv.FormatInt(limit, 10))
}
if !common.StringDataCompare(validFuturesIntervals, interval) {
@@ -151,7 +151,7 @@ func (by *Bybit) GetUSDTIndexPriceKline(ctx context.Context, symbol currency.Pai
return resp.Data, err
}
params.Set("symbol", symbolValue)
if limit > 0 && limit <= 200 {
if limit > 0 {
params.Set("limit", strconv.FormatInt(limit, 10))
}
if !common.StringDataCompare(validFuturesIntervals, interval) {
@@ -180,7 +180,7 @@ func (by *Bybit) GetUSDTPremiumIndexPriceKline(ctx context.Context, symbol curre
return resp.Data, err
}
params.Set("symbol", symbolValue)
if limit > 0 && limit <= 200 {
if limit > 0 {
params.Set("limit", strconv.FormatInt(limit, 10))
}
if !common.StringDataCompare(validFuturesIntervals, interval) {
@@ -327,7 +327,7 @@ func (by *Bybit) GetActiveUSDTFuturesOrders(ctx context.Context, symbol currency
if page > 0 && page <= 50 {
params.Set("page", strconv.FormatInt(page, 10))
}
if limit > 0 && limit <= 50 {
if limit > 0 {
params.Set("limit", strconv.FormatInt(limit, 10))
}
if orderID != "" {
@@ -559,7 +559,7 @@ func (by *Bybit) GetConditionalUSDTFuturesOrders(ctx context.Context, symbol cur
if direction != "" {
params.Set("order", direction)
}
if limit > 0 && limit <= 50 {
if limit > 0 {
params.Set("limit", strconv.FormatInt(limit, 10))
}
if page != 0 {
@@ -940,7 +940,7 @@ func (by *Bybit) GetUSDTTradeRecords(ctx context.Context, symbol currency.Pair,
if page != 0 {
params.Set("page", strconv.FormatInt(page, 10))
}
if limit > 0 && limit <= 200 {
if limit > 0 {
params.Set("limit", strconv.FormatInt(limit, 10))
}
return resp.Data.Trades, by.SendAuthHTTPRequest(ctx, exchange.RestUSDTMargined, http.MethodGet, ufuturesGetTrades, params, nil, &resp, uFuturesGetTradesRate)
@@ -975,7 +975,7 @@ func (by *Bybit) GetClosedUSDTTrades(ctx context.Context, symbol currency.Pair,
if page > 0 && page <= 50 {
params.Set("page", strconv.FormatInt(page, 10))
}
if limit > 0 && limit <= 50 {
if limit > 0 {
params.Set("limit", strconv.FormatInt(limit, 10))
}
return resp.Data.Trades, by.SendAuthHTTPRequest(ctx, exchange.RestUSDTMargined, http.MethodGet, ufuturesGetClosedTrades, params, nil, &resp, uFuturesGetClosedTradesRate)

View File

@@ -115,7 +115,7 @@ func (by *Bybit) GetUSDCContracts(ctx context.Context, symbol currency.Pair, dir
if direction != "" {
params.Set("direction", direction)
}
if limit > 0 && limit <= 200 {
if limit > 0 {
params.Set("limit", strconv.FormatInt(limit, 10))
}
@@ -169,7 +169,7 @@ func (by *Bybit) GetUSDCKlines(ctx context.Context, symbol currency.Pair, period
}
params.Set("startTime", strconv.FormatInt(startTime.Unix(), 10))
if limit > 0 && limit <= 200 {
if limit > 0 {
params.Set("limit", strconv.FormatInt(limit, 10))
}
return resp.Data, by.SendHTTPRequest(ctx, exchange.RestUSDCMargined, common.EncodeURLValues(usdcfuturesGetKlines, params), usdcPublicRate, &resp)
@@ -202,7 +202,7 @@ func (by *Bybit) GetUSDCMarkPriceKlines(ctx context.Context, symbol currency.Pai
}
params.Set("startTime", strconv.FormatInt(startTime.Unix(), 10))
if limit > 0 && limit <= 200 {
if limit > 0 {
params.Set("limit", strconv.FormatInt(limit, 10))
}
return resp.Data, by.SendHTTPRequest(ctx, exchange.RestUSDCMargined, common.EncodeURLValues(usdcfuturesGetMarkPriceKlines, params), usdcPublicRate, &resp)
@@ -235,7 +235,7 @@ func (by *Bybit) GetUSDCIndexPriceKlines(ctx context.Context, symbol currency.Pa
}
params.Set("startTime", strconv.FormatInt(startTime.Unix(), 10))
if limit > 0 && limit <= 200 {
if limit > 0 {
params.Set("limit", strconv.FormatInt(limit, 10))
}
return resp.Data, by.SendHTTPRequest(ctx, exchange.RestUSDCMargined, common.EncodeURLValues(usdcfuturesGetIndexPriceKlines, params), usdcPublicRate, &resp)
@@ -268,7 +268,7 @@ func (by *Bybit) GetUSDCPremiumIndexKlines(ctx context.Context, symbol currency.
}
params.Set("startTime", strconv.FormatInt(startTime.Unix(), 10))
if limit > 0 && limit <= 200 {
if limit > 0 {
params.Set("limit", strconv.FormatInt(limit, 10))
}
return resp.Data, by.SendHTTPRequest(ctx, exchange.RestUSDCMargined, common.EncodeURLValues(usdcfuturesGetPremiumIndexKlines, params), usdcPublicRate, &resp)
@@ -296,7 +296,7 @@ func (by *Bybit) GetUSDCOpenInterest(ctx context.Context, symbol currency.Pair,
}
params.Set("period", period)
if limit > 0 && limit <= 200 {
if limit > 0 {
params.Set("limit", strconv.FormatInt(limit, 10))
}
return resp.Data, by.SendHTTPRequest(ctx, exchange.RestUSDCMargined, common.EncodeURLValues(usdcfuturesGetOpenInterest, params), usdcPublicRate, &resp)
@@ -319,7 +319,7 @@ func (by *Bybit) GetUSDCLargeOrders(ctx context.Context, symbol currency.Pair, l
}
params.Set("symbol", symbolValue)
if limit > 0 && limit <= 100 {
if limit > 0 {
params.Set("limit", strconv.FormatInt(limit, 10))
}
return resp.Data, by.SendHTTPRequest(ctx, exchange.RestUSDCMargined, common.EncodeURLValues(usdcfuturesGetLargeOrders, params), usdcPublicRate, &resp)
@@ -347,7 +347,7 @@ func (by *Bybit) GetUSDCAccountRatio(ctx context.Context, symbol currency.Pair,
}
params.Set("period", period)
if limit > 0 && limit <= 500 {
if limit > 0 {
params.Set("limit", strconv.FormatInt(limit, 10))
}
return resp.Data, by.SendHTTPRequest(ctx, exchange.RestUSDCMargined, common.EncodeURLValues(usdcfuturesGetAccountRatio, params), usdcPublicRate, &resp)
@@ -378,7 +378,7 @@ func (by *Bybit) GetUSDCLatestTrades(ctx context.Context, symbol currency.Pair,
params.Set("symbol", symbolValue)
}
if limit > 0 && limit <= 500 {
if limit > 0 {
params.Set("limit", strconv.FormatInt(limit, 10))
}
return resp.Result.Data, by.SendHTTPRequest(ctx, exchange.RestUSDCMargined, common.EncodeURLValues(usdcfuturesGetLatestTrades, params), usdcPublicRate, &resp)
@@ -749,7 +749,7 @@ func (by *Bybit) GetUSDCTradeHistory(ctx context.Context, symbol currency.Pair,
req["direction"] = direction
}
if limit > 0 && limit <= 50 {
if limit > 0 {
req["limit"] = strconv.FormatInt(limit, 10)
}
@@ -792,7 +792,7 @@ func (by *Bybit) GetUSDCTransactionLog(ctx context.Context, startTime, endTime t
req["direction"] = direction
}
if limit > 0 && limit <= 50 {
if limit > 0 {
req["limit"] = strconv.FormatInt(limit, 10)
}
@@ -875,7 +875,7 @@ func (by *Bybit) GetUSDCPosition(ctx context.Context, symbol currency.Pair, cate
req["direction"] = direction
}
if limit > 0 && limit <= 50 {
if limit > 0 {
req["limit"] = strconv.FormatInt(limit, 10)
}
@@ -939,7 +939,7 @@ func (by *Bybit) GetUSDCSettlementHistory(ctx context.Context, symbol currency.P
req["direction"] = direction
}
if limit > 0 && limit <= 50 {
if limit > 0 {
req["limit"] = strconv.FormatInt(limit, 10)
}

View File

@@ -0,0 +1,71 @@
package collateral
import (
"encoding/json"
"fmt"
"strings"
)
// Valid returns whether the collateral mode is valid
func (t Mode) Valid() bool {
return t != UnsetMode && supportedCollateralModes&t == t
}
// UnmarshalJSON converts json into collateral mode
func (t *Mode) UnmarshalJSON(d []byte) error {
var mode string
err := json.Unmarshal(d, &mode)
if err != nil {
return err
}
*t, err = StringToMode(mode)
return err
}
// String returns the string representation of the collateral mode in lowercase
// the absence of a lower func should hopefully highlight that String is lower
func (t Mode) String() string {
switch t {
case UnsetMode:
return unsetCollateralStr
case SingleMode:
return singleCollateralStr
case MultiMode:
return multiCollateralStr
case PortfolioMode:
return portfolioCollateralStr
case UnknownMode:
return unknownCollateralStr
}
return ""
}
// Upper returns the upper case string representation of the collateral mode
func (t Mode) Upper() string {
return strings.ToUpper(t.String())
}
// IsValidCollateralModeString checks to see if the supplied string is a valid collateral mode
func IsValidCollateralModeString(m string) bool {
switch strings.ToLower(m) {
case singleCollateralStr, multiCollateralStr, portfolioCollateralStr, unsetCollateralStr:
return true
}
return false
}
// StringToMode converts a string to a collateral mode
// doesn't error, just returns unknown if the string is not recognised
func StringToMode(m string) (Mode, error) {
switch strings.ToLower(m) {
case singleCollateralStr:
return SingleMode, nil
case multiCollateralStr:
return MultiMode, nil
case portfolioCollateralStr:
return PortfolioMode, nil
case "":
return UnsetMode, nil
}
return UnknownMode, fmt.Errorf("%w %v", ErrInvalidCollateralMode, m)
}

View File

@@ -0,0 +1,180 @@
package collateral
import (
"encoding/json"
"errors"
"strings"
"testing"
)
func TestValidCollateralType(t *testing.T) {
t.Parallel()
if !SingleMode.Valid() {
t.Fatal("expected 'true', received 'false'")
}
if !MultiMode.Valid() {
t.Fatal("expected 'true', received 'false'")
}
if !PortfolioMode.Valid() {
t.Fatal("expected 'true', received 'false'")
}
if UnsetMode.Valid() {
t.Fatal("expected 'false', received 'true'")
}
if UnknownMode.Valid() {
t.Fatal("expected 'false', received 'true'")
}
if Mode(137).Valid() {
t.Fatal("expected 'false', received 'true'")
}
}
func TestUnmarshalJSONCollateralType(t *testing.T) {
t.Parallel()
type martian struct {
M Mode `json:"collateral"`
}
var alien martian
jason := []byte(`{"collateral":"single"}`)
err := json.Unmarshal(jason, &alien)
if err != nil {
t.Error(err)
}
if alien.M != SingleMode {
t.Errorf("received '%v' expected 'single'", alien.M)
}
jason = []byte(`{"collateral":"multi"}`)
err = json.Unmarshal(jason, &alien)
if err != nil {
t.Error(err)
}
if alien.M != MultiMode {
t.Errorf("received '%v' expected 'Multi'", alien.M)
}
jason = []byte(`{"collateral":"portfolio"}`)
err = json.Unmarshal(jason, &alien)
if err != nil {
t.Error(err)
}
if alien.M != PortfolioMode {
t.Errorf("received '%v' expected 'Portfolio'", alien.M)
}
jason = []byte(`{"collateral":"hello moto"}`)
err = json.Unmarshal(jason, &alien)
if !errors.Is(err, ErrInvalidCollateralMode) {
t.Error(err)
}
if alien.M != UnknownMode {
t.Errorf("received '%v' expected 'UnknownMode'", alien.M)
}
}
func TestStringCollateralType(t *testing.T) {
t.Parallel()
if UnknownMode.String() != unknownCollateralStr {
t.Errorf("received '%v' expected '%v'", UnknownMode.String(), unknownCollateralStr)
}
if SingleMode.String() != singleCollateralStr {
t.Errorf("received '%v' expected '%v'", SingleMode.String(), singleCollateralStr)
}
if MultiMode.String() != multiCollateralStr {
t.Errorf("received '%v' expected '%v'", MultiMode.String(), multiCollateralStr)
}
if PortfolioMode.String() != portfolioCollateralStr {
t.Errorf("received '%v' expected '%v'", PortfolioMode.String(), portfolioCollateralStr)
}
if UnsetMode.String() != unsetCollateralStr {
t.Errorf("received '%v' expected '%v'", UnsetMode.String(), unsetCollateralStr)
}
}
func TestUpperCollateralType(t *testing.T) {
t.Parallel()
if UnknownMode.Upper() != strings.ToUpper(unknownCollateralStr) {
t.Errorf("received '%v' expected '%v'", UnknownMode.Upper(), strings.ToUpper(unknownCollateralStr))
}
if SingleMode.Upper() != strings.ToUpper(singleCollateralStr) {
t.Errorf("received '%v' expected '%v'", SingleMode.Upper(), strings.ToUpper(singleCollateralStr))
}
if MultiMode.Upper() != strings.ToUpper(multiCollateralStr) {
t.Errorf("received '%v' expected '%v'", MultiMode.Upper(), strings.ToUpper(multiCollateralStr))
}
if PortfolioMode.Upper() != strings.ToUpper(portfolioCollateralStr) {
t.Errorf("received '%v' expected '%v'", PortfolioMode.Upper(), strings.ToUpper(portfolioCollateralStr))
}
if UnsetMode.Upper() != strings.ToUpper(unsetCollateralStr) {
t.Errorf("received '%v' expected '%v'", UnsetMode.Upper(), strings.ToUpper(unsetCollateralStr))
}
}
func TestIsValidCollateralTypeString(t *testing.T) {
t.Parallel()
if IsValidCollateralModeString("lol") {
t.Fatal("expected 'false', received 'true'")
}
if !IsValidCollateralModeString("single") {
t.Fatal("expected 'true', received 'false'")
}
if !IsValidCollateralModeString("multi") {
t.Fatal("expected 'true', received 'false'")
}
if !IsValidCollateralModeString("portfolio") {
t.Fatal("expected 'true', received 'false'")
}
if !IsValidCollateralModeString("unset") {
t.Fatal("expected 'true', received 'false'")
}
if IsValidCollateralModeString("") {
t.Fatal("expected 'false', received 'true'")
}
if IsValidCollateralModeString("unknown") {
t.Fatal("expected 'false', received 'true'")
}
}
func TestStringToCollateralType(t *testing.T) {
t.Parallel()
resp, err := StringToMode("lol")
if !errors.Is(err, ErrInvalidCollateralMode) {
t.Error(err)
}
if resp != UnknownMode {
t.Errorf("received '%v' expected '%v'", resp, UnknownMode)
}
resp, err = StringToMode("")
if err != nil {
t.Error(err)
}
if resp != UnsetMode {
t.Errorf("received '%v' expected '%v'", resp, UnsetMode)
}
resp, err = StringToMode("single")
if err != nil {
t.Error(err)
}
if resp != SingleMode {
t.Errorf("received '%v' expected '%v'", resp, SingleMode)
}
resp, err = StringToMode("multi")
if err != nil {
t.Error(err)
}
if resp != MultiMode {
t.Errorf("received '%v' expected '%v'", resp, MultiMode)
}
resp, err = StringToMode("portfolio")
if err != nil {
t.Error(err)
}
if resp != PortfolioMode {
t.Errorf("received '%v' expected '%v'", resp, PortfolioMode)
}
}

View File

@@ -0,0 +1,85 @@
package collateral
import (
"errors"
"github.com/shopspring/decimal"
"github.com/thrasher-corp/gocryptotrader/currency"
)
// Mode defines the different collateral types supported by exchanges
// For example, FTX had a global collateral pool
// Binance has either singular position collateral calculation
// or cross aka asset level collateral calculation
type Mode uint8
const (
// UnsetMode is the default value
UnsetMode Mode = 0
// SingleMode has allocated collateral per position
SingleMode Mode = 1 << (iota - 1)
// MultiMode has collateral allocated across the whole asset
MultiMode
// PortfolioMode has collateral allocated across account
PortfolioMode
// UnknownMode has collateral allocated in an unknown manner at present, but is not unset
UnknownMode
)
const (
unsetCollateralStr = "unset"
singleCollateralStr = "single"
multiCollateralStr = "multi"
portfolioCollateralStr = "portfolio"
unknownCollateralStr = "unknown"
)
// ErrInvalidCollateralMode is returned when converting invalid string to collateral mode
var ErrInvalidCollateralMode = errors.New("invalid collateral mode")
var supportedCollateralModes = SingleMode | MultiMode | PortfolioMode
// ByPosition shows how much collateral is used
// from positions
type ByPosition struct {
PositionCurrency currency.Pair
Size decimal.Decimal
OpenOrderSize decimal.Decimal
PositionSize decimal.Decimal
MarkPrice decimal.Decimal
RequiredMargin decimal.Decimal
CollateralUsed decimal.Decimal
}
// ByCurrency individual collateral contribution
// along with what the potentially scaled collateral
// currency it is represented as
// eg in Bybit ScaledCurrency is USDC
type ByCurrency struct {
Currency currency.Code
SkipContribution bool
TotalFunds decimal.Decimal
AvailableForUseAsCollateral decimal.Decimal
CollateralContribution decimal.Decimal
AdditionalCollateralUsed decimal.Decimal
FairMarketValue decimal.Decimal
Weighting decimal.Decimal
ScaledCurrency currency.Code
UnrealisedPNL decimal.Decimal
ScaledUsed decimal.Decimal
ScaledUsedBreakdown *UsedBreakdown
Error error
}
// UsedBreakdown provides a detailed
// breakdown of where collateral is currently being allocated
type UsedBreakdown struct {
LockedInStakes decimal.Decimal
LockedInNFTBids decimal.Decimal
LockedInFeeVoucher decimal.Decimal
LockedInSpotMarginFundingOffers decimal.Decimal
LockedInSpotOrders decimal.Decimal
LockedAsCollateral decimal.Decimal
UsedInPositions decimal.Decimal
UsedInSpotMarginBorrows decimal.Decimal
}

View File

@@ -16,6 +16,7 @@ import (
"github.com/thrasher-corp/gocryptotrader/currency"
"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/currencystate"
"github.com/thrasher-corp/gocryptotrader/exchanges/fundingrate"
"github.com/thrasher-corp/gocryptotrader/exchanges/kline"
@@ -1458,81 +1459,17 @@ func (b *Base) GetAvailableTransferChains(_ context.Context, _ currency.Code) ([
return nil, common.ErrFunctionNotSupported
}
// CalculatePNL is an overridable function to allow PNL to be calculated on an
// open position
// It will also determine whether the position is considered to be liquidated
// For live trading, an overriding function may wish to confirm the liquidation by
// requesting the status of the asset
func (b *Base) CalculatePNL(context.Context, *order.PNLCalculatorRequest) (*order.PNLResult, error) {
return nil, common.ErrNotYetImplemented
}
// ScaleCollateral is an overridable function to determine how much
// collateral is usable in futures positions
func (b *Base) ScaleCollateral(context.Context, *order.CollateralCalculator) (*order.CollateralByCurrency, error) {
return nil, common.ErrNotYetImplemented
}
// CalculateTotalCollateral takes in n collateral calculators to determine an overall
// standing in a singular currency
func (b *Base) CalculateTotalCollateral(_ context.Context, _ *order.TotalCollateralCalculator) (*order.TotalCollateralResponse, error) {
return nil, common.ErrNotYetImplemented
}
// GetCollateralCurrencyForContract returns the collateral currency for an asset and contract pair
func (b *Base) GetCollateralCurrencyForContract(_ asset.Item, _ currency.Pair) (currency.Code, asset.Item, error) {
return currency.Code{}, asset.Empty, common.ErrNotYetImplemented
}
// GetCurrencyForRealisedPNL returns where to put realised PNL
// example 1: Bybit universal margin PNL is paid out in USD to your spot wallet
// example 2: Binance coin margined futures pays returns using the same currency eg BTC
func (b *Base) GetCurrencyForRealisedPNL(_ asset.Item, _ currency.Pair) (currency.Code, asset.Item, error) {
return currency.Code{}, asset.Empty, common.ErrNotYetImplemented
}
// HasAssetTypeAccountSegregation returns if the accounts are divided into asset
// types instead of just being denoted as spot holdings.
func (b *Base) HasAssetTypeAccountSegregation() bool {
return b.Features.Supports.RESTCapabilities.HasAssetTypeAccountSegregation
}
// GetMarginRatesHistory returns the margin rate history for the supplied currency
func (b *Base) GetMarginRatesHistory(context.Context, *margin.RateHistoryRequest) (*margin.RateHistoryResponse, error) {
return nil, common.ErrNotYetImplemented
}
// GetPositionSummary returns stats for a future position
func (b *Base) GetPositionSummary(context.Context, *order.PositionSummaryRequest) (*order.PositionSummary, error) {
return nil, common.ErrNotYetImplemented
}
// GetFundingPaymentDetails returns funding payment details for a future for a specific time period
func (b *Base) GetFundingPaymentDetails(context.Context, *fundingrate.RatesRequest) (*fundingrate.Rates, error) {
return nil, common.ErrNotYetImplemented
}
// GetFuturesPositions returns futures positions for all currencies
func (b *Base) GetFuturesPositions(context.Context, *order.PositionsRequest) ([]order.PositionDetails, error) {
return nil, common.ErrNotYetImplemented
}
// GetLatestFundingRate returns the latest funding rate based on request data
func (b *Base) GetLatestFundingRate(context.Context, *fundingrate.LatestRateRequest) (*fundingrate.LatestRateResponse, error) {
return nil, common.ErrNotYetImplemented
}
// GetFundingRates returns funding rates based on request data
func (b *Base) GetFundingRates(context.Context, *fundingrate.RatesRequest) (*fundingrate.Rates, error) {
return nil, common.ErrNotYetImplemented
}
// IsPerpetualFutureCurrency ensures a given asset and currency is a perpetual future
// differs by exchange
func (b *Base) IsPerpetualFutureCurrency(asset.Item, currency.Pair) (bool, error) {
return false, common.ErrNotYetImplemented
}
// GetKlineRequest returns a helper for the fetching of candle/kline data for
// a single request within a pre-determined time window.
func (b *Base) GetKlineRequest(pair currency.Pair, a asset.Item, interval kline.Interval, start, end time.Time, fixedAPICandleLength bool) (*kline.Request, error) {
@@ -1688,3 +1625,109 @@ func (b *Base) GetStandardConfig() (*config.Exchange, error) {
return exchCfg, nil
}
// Futures section
// CalculatePNL is an overridable function to allow PNL to be calculated on an
// open position
// It will also determine whether the position is considered to be liquidated
// For live trading, an overriding function may wish to confirm the liquidation by
// requesting the status of the asset
func (b *Base) CalculatePNL(context.Context, *order.PNLCalculatorRequest) (*order.PNLResult, error) {
return nil, common.ErrNotYetImplemented
}
// ScaleCollateral is an overridable function to determine how much
// collateral is usable in futures positions
func (b *Base) ScaleCollateral(context.Context, *order.CollateralCalculator) (*collateral.ByCurrency, error) {
return nil, common.ErrNotYetImplemented
}
// CalculateTotalCollateral takes in n collateral calculators to determine an overall
// standing in a singular currency
func (b *Base) CalculateTotalCollateral(_ context.Context, _ *order.TotalCollateralCalculator) (*order.TotalCollateralResponse, error) {
return nil, common.ErrNotYetImplemented
}
// GetCollateralCurrencyForContract returns the collateral currency for an asset and contract pair
func (b *Base) GetCollateralCurrencyForContract(_ asset.Item, _ currency.Pair) (currency.Code, asset.Item, error) {
return currency.Code{}, asset.Empty, common.ErrNotYetImplemented
}
// GetCurrencyForRealisedPNL returns where to put realised PNL
// example 1: Bybit universal margin PNL is paid out in USD to your spot wallet
// example 2: Binance coin margined futures pays returns using the same currency eg BTC
func (b *Base) GetCurrencyForRealisedPNL(_ asset.Item, _ currency.Pair) (currency.Code, asset.Item, error) {
return currency.Code{}, asset.Empty, common.ErrNotYetImplemented
}
// GetMarginRatesHistory returns the margin rate history for the supplied currency
func (b *Base) GetMarginRatesHistory(context.Context, *margin.RateHistoryRequest) (*margin.RateHistoryResponse, error) {
return nil, common.ErrNotYetImplemented
}
// GetFuturesPositionSummary returns stats for a future position
func (b *Base) GetFuturesPositionSummary(context.Context, *order.PositionSummaryRequest) (*order.PositionSummary, error) {
return nil, common.ErrNotYetImplemented
}
// GetFundingPaymentDetails returns funding payment details for a future for a specific time period
func (b *Base) GetFundingPaymentDetails(context.Context, *fundingrate.RatesRequest) (*fundingrate.Rates, error) {
return nil, common.ErrNotYetImplemented
}
// GetFuturesPositions returns futures positions for all currencies
func (b *Base) GetFuturesPositions(context.Context, *order.PositionsRequest) ([]order.PositionDetails, error) {
return nil, common.ErrNotYetImplemented
}
// GetFuturesPositionOrders returns futures positions orders
func (b *Base) GetFuturesPositionOrders(context.Context, *order.PositionsRequest) ([]order.PositionResponse, error) {
return nil, common.ErrNotYetImplemented
}
// GetLatestFundingRate returns the latest funding rate based on request data
func (b *Base) GetLatestFundingRate(context.Context, *fundingrate.LatestRateRequest) (*fundingrate.LatestRateResponse, error) {
return nil, common.ErrNotYetImplemented
}
// GetFundingRates returns funding rates based on request data
func (b *Base) GetFundingRates(context.Context, *fundingrate.RatesRequest) (*fundingrate.Rates, error) {
return nil, common.ErrNotYetImplemented
}
// IsPerpetualFutureCurrency ensures a given asset and currency is a perpetual future
// differs by exchange
func (b *Base) IsPerpetualFutureCurrency(asset.Item, currency.Pair) (bool, error) {
return false, common.ErrNotYetImplemented
}
// SetCollateralMode sets the account's collateral mode for the asset type
func (b *Base) SetCollateralMode(_ context.Context, _ asset.Item, _ collateral.Mode) error {
return common.ErrNotYetImplemented
}
// GetCollateralMode returns the account's collateral mode for the asset type
func (b *Base) GetCollateralMode(_ context.Context, _ asset.Item) (collateral.Mode, error) {
return 0, common.ErrNotYetImplemented
}
// SetMarginType sets the account's margin type for the asset type
func (b *Base) SetMarginType(_ context.Context, _ asset.Item, _ currency.Pair, _ margin.Type) error {
return common.ErrNotYetImplemented
}
// ChangePositionMargin changes the margin type for a position
func (b *Base) ChangePositionMargin(_ context.Context, _ *margin.PositionChangeRequest) (*margin.PositionChangeResponse, error) {
return nil, common.ErrNotYetImplemented
}
// SetLeverage sets the account's initial leverage for the asset type and pair
func (b *Base) SetLeverage(_ context.Context, _ asset.Item, _ currency.Pair, _ margin.Type, _ float64, _ order.Side) error {
return common.ErrNotYetImplemented
}
// GetLeverage gets the account's initial leverage for the asset type and pair
func (b *Base) GetLeverage(_ context.Context, _ asset.Item, _ currency.Pair, _ margin.Type, _ order.Side) (float64, error) {
return -1, common.ErrNotYetImplemented
}

View File

@@ -12,7 +12,10 @@ import (
"github.com/thrasher-corp/gocryptotrader/config"
"github.com/thrasher-corp/gocryptotrader/currency"
"github.com/thrasher-corp/gocryptotrader/exchanges/asset"
"github.com/thrasher-corp/gocryptotrader/exchanges/collateral"
"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/protocol"
"github.com/thrasher-corp/gocryptotrader/exchanges/request"
"github.com/thrasher-corp/gocryptotrader/exchanges/stream"
@@ -2505,7 +2508,7 @@ func TestGetMarginRateHistory(t *testing.T) {
func TestGetPositionSummary(t *testing.T) {
t.Parallel()
var b Base
if _, err := b.GetPositionSummary(context.Background(), nil); !errors.Is(err, common.ErrNotYetImplemented) {
if _, err := b.GetFuturesPositionSummary(context.Background(), nil); !errors.Is(err, common.ErrNotYetImplemented) {
t.Errorf("received: %v, expected: %v", err, common.ErrNotYetImplemented)
}
}
@@ -2513,7 +2516,7 @@ func TestGetPositionSummary(t *testing.T) {
func TestGetFuturesPositions(t *testing.T) {
t.Parallel()
var b Base
if _, err := b.GetFuturesPositions(context.Background(), nil); !errors.Is(err, common.ErrNotYetImplemented) {
if _, err := b.GetFuturesPositionOrders(context.Background(), nil); !errors.Is(err, common.ErrNotYetImplemented) {
t.Errorf("received: %v, expected: %v", err, common.ErrNotYetImplemented)
}
}
@@ -2954,6 +2957,60 @@ func TestGetKlineExtendedRequest(t *testing.T) {
}
}
func TestSetCollateralMode(t *testing.T) {
t.Parallel()
b := Base{}
err := b.SetCollateralMode(context.Background(), asset.Spot, collateral.SingleMode)
if !errors.Is(err, common.ErrNotYetImplemented) {
t.Error(err)
}
}
func TestGetCollateralMode(t *testing.T) {
t.Parallel()
b := Base{}
_, err := b.GetCollateralMode(context.Background(), asset.Spot)
if !errors.Is(err, common.ErrNotYetImplemented) {
t.Error(err)
}
}
func TestSetMarginType(t *testing.T) {
t.Parallel()
b := Base{}
err := b.SetMarginType(context.Background(), asset.Spot, currency.NewBTCUSD(), margin.Multi)
if !errors.Is(err, common.ErrNotYetImplemented) {
t.Error(err)
}
}
func TestChangePositionMargin(t *testing.T) {
t.Parallel()
b := Base{}
_, err := b.ChangePositionMargin(context.Background(), nil)
if !errors.Is(err, common.ErrNotYetImplemented) {
t.Error(err)
}
}
func TestSetLeverage(t *testing.T) {
t.Parallel()
b := Base{}
err := b.SetLeverage(context.Background(), asset.Spot, currency.NewBTCUSD(), margin.Multi, 1, order.UnknownSide)
if !errors.Is(err, common.ErrNotYetImplemented) {
t.Error(err)
}
}
func TestGetLeverage(t *testing.T) {
t.Parallel()
b := Base{}
_, err := b.GetLeverage(context.Background(), asset.Spot, currency.NewBTCUSD(), margin.Multi, order.UnknownSide)
if !errors.Is(err, common.ErrNotYetImplemented) {
t.Error(err)
}
}
func TestEnsureOnePairEnabled(t *testing.T) {
t.Parallel()
b := Base{Name: "test"}

View File

@@ -178,13 +178,20 @@ type FeaturesSupported struct {
// FuturesCapabilities stores the exchange's futures capabilities
type FuturesCapabilities struct {
FundingRates bool
MaximumFundingRateHistory time.Duration
FundingRateFrequency time.Duration
Positions bool
OrderManagerPositionTracking bool
Collateral bool
CollateralMode bool
Leverage bool
MaximumFundingRateHistory time.Duration
FundingRateFrequency time.Duration
}
// MarginCapabilities stores the exchange's margin capabilities
type MarginCapabilities struct {
SetMarginType bool
ChangePositionMargin bool
GetMarginRateHistory bool
}
// Endpoints stores running url endpoints for exchanges

View File

@@ -9,6 +9,7 @@ import (
"github.com/thrasher-corp/gocryptotrader/currency"
"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/currencystate"
"github.com/thrasher-corp/gocryptotrader/exchanges/deposit"
"github.com/thrasher-corp/gocryptotrader/exchanges/fundingrate"
@@ -31,6 +32,8 @@ type IBotExchange interface {
Shutdown() error
GetName() string
SetEnabled(bool)
GetEnabledFeatures() FeaturesEnabled
GetSupportedFeatures() FeaturesSupported
FetchTicker(ctx context.Context, p currency.Pair, a asset.Item) (*ticker.Price, error)
UpdateTicker(ctx context.Context, p currency.Pair, a asset.Item) (*ticker.Price, error)
UpdateTickers(ctx context.Context, a asset.Item) error
@@ -92,6 +95,7 @@ type IBotExchange interface {
OrderManagement
CurrencyStateManagement
FuturesManagement
MarginManagement
}
// OrderManagement defines functionality for order management
@@ -141,14 +145,26 @@ type FunctionalityChecker interface {
// FuturesManagement manages futures orders, pnl and collateral calculations
type FuturesManagement interface {
GetPositionSummary(context.Context, *order.PositionSummaryRequest) (*order.PositionSummary, error)
ScaleCollateral(ctx context.Context, calculator *order.CollateralCalculator) (*order.CollateralByCurrency, error)
ScaleCollateral(ctx context.Context, calculator *order.CollateralCalculator) (*collateral.ByCurrency, error)
CalculateTotalCollateral(context.Context, *order.TotalCollateralCalculator) (*order.TotalCollateralResponse, error)
GetFuturesPositions(context.Context, *order.PositionsRequest) ([]order.PositionDetails, error)
GetFundingRates(context.Context, *fundingrate.RatesRequest) (*fundingrate.Rates, error)
GetLatestFundingRate(context.Context, *fundingrate.LatestRateRequest) (*fundingrate.LatestRateResponse, error)
IsPerpetualFutureCurrency(asset.Item, currency.Pair) (bool, error)
GetCollateralCurrencyForContract(asset.Item, currency.Pair) (currency.Code, asset.Item, error)
GetMarginRatesHistory(context.Context, *margin.RateHistoryRequest) (*margin.RateHistoryResponse, error)
order.PNLCalculation
GetFuturesPositionSummary(context.Context, *order.PositionSummaryRequest) (*order.PositionSummary, error)
GetFuturesPositionOrders(context.Context, *order.PositionsRequest) ([]order.PositionResponse, error)
SetCollateralMode(ctx context.Context, item asset.Item, mode collateral.Mode) error
GetCollateralMode(ctx context.Context, item asset.Item) (collateral.Mode, error)
SetLeverage(ctx context.Context, item asset.Item, pair currency.Pair, marginType margin.Type, amount float64, orderSide order.Side) error
GetLeverage(ctx context.Context, item asset.Item, pair currency.Pair, marginType margin.Type, orderSide order.Side) (float64, error)
}
// MarginManagement manages margin positions and rates
type MarginManagement interface {
SetMarginType(ctx context.Context, item asset.Item, pair currency.Pair, tp margin.Type) error
ChangePositionMargin(ctx context.Context, change *margin.PositionChangeRequest) (*margin.PositionChangeResponse, error)
GetMarginRatesHistory(context.Context, *margin.RateHistoryRequest) (*margin.RateHistoryResponse, error)
}

View File

@@ -0,0 +1,67 @@
package margin
import (
"encoding/json"
"fmt"
"strings"
)
// Valid returns whether the margin type is valid
func (t Type) Valid() bool {
return t != Unset && supported&t == t
}
// UnmarshalJSON converts json into margin type
func (t *Type) UnmarshalJSON(d []byte) error {
var marginType string
err := json.Unmarshal(d, &marginType)
if err != nil {
return err
}
*t, err = StringToMarginType(marginType)
return err
}
// String returns the string representation of the margin type in lowercase
// the absence of a lower func should hopefully highlight that String is lower
func (t Type) String() string {
switch t {
case Unset:
return unsetStr
case Isolated:
return isolatedStr
case Multi:
return multiStr
case Unknown:
return unknownStr
}
return ""
}
// Upper returns the upper case string representation of the margin type
func (t Type) Upper() string {
return strings.ToUpper(t.String())
}
// IsValidString checks to see if the supplied string is a valid margin type
func IsValidString(m string) bool {
switch strings.ToLower(m) {
case isolatedStr, multiStr, unsetStr, crossedStr, crossStr:
return true
}
return false
}
// StringToMarginType converts a string to a margin type
// doesn't error, just returns unknown if the string is not recognised
func StringToMarginType(m string) (Type, error) {
switch strings.ToLower(m) {
case isolatedStr:
return Isolated, nil
case multiStr, crossedStr, crossStr:
return Multi, nil
case "":
return Unset, nil
}
return Unknown, fmt.Errorf("%w %v", ErrInvalidMarginType, m)
}

View File

@@ -0,0 +1,162 @@
package margin
import (
"encoding/json"
"errors"
"strings"
"testing"
)
func TestValid(t *testing.T) {
t.Parallel()
if !Isolated.Valid() {
t.Fatal("expected 'true', received 'false'")
}
if !Multi.Valid() {
t.Fatal("expected 'true', received 'false'")
}
if Unset.Valid() {
t.Fatal("expected 'false', received 'true'")
}
if Unknown.Valid() {
t.Fatal("expected 'false', received 'true'")
}
if Type(137).Valid() {
t.Fatal("expected 'false', received 'true'")
}
}
func TestUnmarshalJSON(t *testing.T) {
t.Parallel()
type martian struct {
M Type `json:"margin"`
}
var alien martian
jason := []byte(`{"margin":"isolated"}`)
err := json.Unmarshal(jason, &alien)
if err != nil {
t.Error(err)
}
if alien.M != Isolated {
t.Errorf("received '%v' expected 'isolated'", alien.M)
}
jason = []byte(`{"margin":"cross"}`)
err = json.Unmarshal(jason, &alien)
if err != nil {
t.Error(err)
}
if alien.M != Multi {
t.Errorf("received '%v' expected 'Multi'", alien.M)
}
jason = []byte(`{"margin":"hello moto"}`)
err = json.Unmarshal(jason, &alien)
if !errors.Is(err, ErrInvalidMarginType) {
t.Error(err)
}
if alien.M != Unknown {
t.Errorf("received '%v' expected 'isolated'", alien.M)
}
}
func TestString(t *testing.T) {
t.Parallel()
if Unknown.String() != unknownStr {
t.Errorf("received '%v' expected '%v'", Unknown.String(), unknownStr)
}
if Isolated.String() != isolatedStr {
t.Errorf("received '%v' expected '%v'", Isolated.String(), isolatedStr)
}
if Multi.String() != multiStr {
t.Errorf("received '%v' expected '%v'", Multi.String(), multiStr)
}
if Unset.String() != unsetStr {
t.Errorf("received '%v' expected '%v'", Unset.String(), unsetStr)
}
}
func TestUpper(t *testing.T) {
t.Parallel()
if Unknown.Upper() != strings.ToUpper(unknownStr) {
t.Errorf("received '%v' expected '%v'", Unknown.String(), strings.ToUpper(unknownStr))
}
if Isolated.Upper() != strings.ToUpper(isolatedStr) {
t.Errorf("received '%v' expected '%v'", Isolated.String(), strings.ToUpper(isolatedStr))
}
if Multi.Upper() != strings.ToUpper(multiStr) {
t.Errorf("received '%v' expected '%v'", Multi.String(), strings.ToUpper(multiStr))
}
if Unset.Upper() != strings.ToUpper(unsetStr) {
t.Errorf("received '%v' expected '%v'", Unset.String(), strings.ToUpper(unsetStr))
}
}
func TestIsValidString(t *testing.T) {
t.Parallel()
if IsValidString("lol") {
t.Fatal("expected 'false', received 'true'")
}
if !IsValidString("isolated") {
t.Fatal("expected 'true', received 'false'")
}
if !IsValidString("cross") {
t.Fatal("expected 'true', received 'false'")
}
if !IsValidString("multi") {
t.Fatal("expected 'true', received 'false'")
}
if !IsValidString("unset") {
t.Fatal("expected 'true', received 'false'")
}
if IsValidString("") {
t.Fatal("expected 'false', received 'true'")
}
if IsValidString("unknown") {
t.Fatal("expected 'false', received 'true'")
}
}
func TestStringToMarginType(t *testing.T) {
t.Parallel()
resp, err := StringToMarginType("lol")
if !errors.Is(err, ErrInvalidMarginType) {
t.Error(err)
}
if resp != Unknown {
t.Errorf("received '%v' expected '%v'", resp, Unknown)
}
resp, err = StringToMarginType("")
if err != nil {
t.Error(err)
}
if resp != Unset {
t.Errorf("received '%v' expected '%v'", resp, Unset)
}
resp, err = StringToMarginType("cross")
if err != nil {
t.Error(err)
}
if resp != Multi {
t.Errorf("received '%v' expected '%v'", resp, Multi)
}
resp, err = StringToMarginType("multi")
if err != nil {
t.Error(err)
}
if resp != Multi {
t.Errorf("received '%v' expected '%v'", resp, Multi)
}
resp, err = StringToMarginType("isolated")
if err != nil {
t.Error(err)
}
if resp != Isolated {
t.Errorf("received '%v' expected '%v'", resp, Isolated)
}
}

View File

@@ -1,6 +1,7 @@
package margin
import (
"errors"
"time"
"github.com/shopspring/decimal"
@@ -8,6 +9,17 @@ import (
"github.com/thrasher-corp/gocryptotrader/exchanges/asset"
)
var (
// ErrInvalidMarginType returned when the margin type is invalid
ErrInvalidMarginType = errors.New("invalid margin type")
// ErrMarginTypeUnsupported returned when the margin type is unsupported
ErrMarginTypeUnsupported = errors.New("unsupported margin type")
// ErrNewAllocatedMarginRequired returned when the new allocated margin is missing and is required
ErrNewAllocatedMarginRequired = errors.New("new allocated margin required")
// ErrOriginalPositionMarginRequired is returned when original position margin is empty and is required
ErrOriginalPositionMarginRequired = errors.New("original allocated margin required")
)
// RateHistoryRequest is used to request a funding rate
type RateHistoryRequest struct {
Exchange string
@@ -32,6 +44,55 @@ type RateHistoryRequest struct {
Rates []Rate
}
// PositionChangeRequest used for wrapper functions to change margin fields for a position
type PositionChangeRequest struct {
// Required fields
Exchange string
Pair currency.Pair
Asset asset.Item
// Optional fields depending on desired outcome/exchange requirements
MarginType Type
OriginalAllocatedMargin float64
NewAllocatedMargin float64
MarginSide string
}
// PositionChangeResponse holds response data for margin change requests
type PositionChangeResponse struct {
Exchange string
Pair currency.Pair
Asset asset.Item
AllocatedMargin float64
MarginType Type
}
// Type defines the different margin types supported by exchanges
type Type uint8
// Margin types
const (
// Unset is the default value
Unset = Type(0)
// Isolated means a margin trade is isolated from other margin trades
Isolated Type = 1 << (iota - 1)
// Multi means a margin trade is not isolated from other margin trades
// it can sometimes be referred to as "cross"
Multi
// Unknown is an unknown margin type but is not unset
Unknown
)
var supported = Isolated | Multi
const (
unsetStr = "unset"
isolatedStr = "isolated"
multiStr = "multi"
crossedStr = "crossed"
crossStr = "cross"
unknownStr = "unknown"
)
// RateHistoryResponse has the funding rate details
type RateHistoryResponse struct {
Rates []Rate

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)
}
}

View File

@@ -9,7 +9,9 @@ import (
"github.com/shopspring/decimal"
"github.com/thrasher-corp/gocryptotrader/currency"
"github.com/thrasher-corp/gocryptotrader/exchanges/asset"
"github.com/thrasher-corp/gocryptotrader/exchanges/collateral"
"github.com/thrasher-corp/gocryptotrader/exchanges/fundingrate"
"github.com/thrasher-corp/gocryptotrader/exchanges/margin"
)
var (
@@ -35,6 +37,8 @@ var (
ErrNoPositionsFound = errors.New("no positions found")
// ErrGetFundingDataRequired is returned when requesting funding rate data without the prerequisite
ErrGetFundingDataRequired = errors.New("getfundingdata is a prerequisite")
// ErrOrderHistoryTooLarge is returned when you lookup order history, but with too early a start date
ErrOrderHistoryTooLarge = errors.New("order history start date too long ago")
errExchangeNameEmpty = errors.New("exchange name empty")
errExchangeNameMismatch = errors.New("exchange name mismatch")
@@ -65,57 +69,12 @@ type TotalCollateralResponse struct {
TotalValueOfPositiveSpotBalances decimal.Decimal
CollateralContributedByPositiveSpotBalances decimal.Decimal
UsedCollateral decimal.Decimal
UsedBreakdown *UsedCollateralBreakdown
UsedBreakdown *collateral.UsedBreakdown
AvailableCollateral decimal.Decimal
AvailableMaintenanceCollateral decimal.Decimal
UnrealisedPNL decimal.Decimal
BreakdownByCurrency []CollateralByCurrency
BreakdownOfPositions []CollateralByPosition
}
// CollateralByPosition shows how much collateral is used
// from positions
type CollateralByPosition struct {
PositionCurrency currency.Pair
Size decimal.Decimal
OpenOrderSize decimal.Decimal
PositionSize decimal.Decimal
MarkPrice decimal.Decimal
RequiredMargin decimal.Decimal
CollateralUsed decimal.Decimal
}
// CollateralByCurrency individual collateral contribution
// along with what the potentially scaled collateral
// currency it is represented as
// eg in Bybit ScaledCurrency is USDC
type CollateralByCurrency struct {
Currency currency.Code
SkipContribution bool
TotalFunds decimal.Decimal
AvailableForUseAsCollateral decimal.Decimal
CollateralContribution decimal.Decimal
AdditionalCollateralUsed decimal.Decimal
FairMarketValue decimal.Decimal
Weighting decimal.Decimal
ScaledCurrency currency.Code
UnrealisedPNL decimal.Decimal
ScaledUsed decimal.Decimal
ScaledUsedBreakdown *UsedCollateralBreakdown
Error error
}
// UsedCollateralBreakdown provides a detailed
// breakdown of where collateral is currently being allocated
type UsedCollateralBreakdown struct {
LockedInStakes decimal.Decimal
LockedInNFTBids decimal.Decimal
LockedInFeeVoucher decimal.Decimal
LockedInSpotMarginFundingOffers decimal.Decimal
LockedInSpotOrders decimal.Decimal
LockedAsCollateral decimal.Decimal
UsedInPositions decimal.Decimal
UsedInSpotMarginBorrows decimal.Decimal
BreakdownByCurrency []collateral.ByCurrency
BreakdownOfPositions []collateral.ByPosition
}
// PositionController manages all futures orders
@@ -310,38 +269,27 @@ type Position struct {
type PositionSummaryRequest struct {
Asset asset.Item
Pair currency.Pair
// UnderlyingPair is optional if the exchange requires it for a contract like BTCUSDT-13333337
UnderlyingPair currency.Pair
// offline calculation requirements below
CalculateOffline bool
Direction Side
FreeCollateral decimal.Decimal
TotalCollateral decimal.Decimal
OpeningPrice decimal.Decimal
CurrentPrice decimal.Decimal
OpeningSize decimal.Decimal
CurrentSize decimal.Decimal
CollateralUsed decimal.Decimal
NotionalPrice decimal.Decimal
Leverage decimal.Decimal
MaxLeverageForAccount decimal.Decimal
TotalAccountValue decimal.Decimal
TotalOpenPositionNotional decimal.Decimal
}
// PositionSummary returns basic details on an open position
type PositionSummary struct {
MaintenanceMarginRequirement decimal.Decimal
InitialMarginRequirement decimal.Decimal
EstimatedLiquidationPrice decimal.Decimal
CollateralUsed decimal.Decimal
MarkPrice decimal.Decimal
CurrentSize decimal.Decimal
BreakEvenPrice decimal.Decimal
AverageOpenPrice decimal.Decimal
RecentPNL decimal.Decimal
MarginFraction decimal.Decimal
FreeCollateral decimal.Decimal
TotalCollateral decimal.Decimal
// EstimatePosition if enabled, can be used to calculate a new position
EstimatePosition bool
// These fields are also used for offline calculation
OpeningPrice decimal.Decimal
OpeningSize decimal.Decimal
Leverage decimal.Decimal
Direction Side
TotalAccountValue decimal.Decimal
}
// PositionDetails are used to track open positions
@@ -359,4 +307,56 @@ type PositionsRequest struct {
Asset asset.Item
Pairs currency.Pairs
StartDate time.Time
EndDate time.Time
// RespectOrderHistoryLimits is designed for the order manager
// it allows for orders to be tracked if the start date in the config is
// beyond the allowable limits by the API, rather than returning an error
RespectOrderHistoryLimits bool
}
// PositionResponse are used to track open positions
// in the order manager
type PositionResponse struct {
Pair currency.Pair
Asset asset.Item
Orders []Detail
}
// PositionSummary returns basic details on an open position
type PositionSummary struct {
Pair currency.Pair
Asset asset.Item
MarginType margin.Type
CollateralMode collateral.Mode
// The currency in which the values are quoted against. Isn't always pair.Quote
// eg BTC-USDC-230929's quote in GCT is 230929, but the currency should be USDC
Currency currency.Code
AvailableEquity decimal.Decimal
CashBalance decimal.Decimal
DiscountEquity decimal.Decimal
EquityUSD decimal.Decimal
IsolatedEquity decimal.Decimal
IsolatedLiabilities decimal.Decimal
IsolatedUPL decimal.Decimal
NotionalLeverage decimal.Decimal
TotalEquity decimal.Decimal
StrategyEquity decimal.Decimal
IsolatedMargin decimal.Decimal
NotionalSize decimal.Decimal
Leverage decimal.Decimal
MaintenanceMarginRequirement decimal.Decimal
InitialMarginRequirement decimal.Decimal
EstimatedLiquidationPrice decimal.Decimal
CollateralUsed decimal.Decimal
MarkPrice decimal.Decimal
CurrentSize decimal.Decimal
AverageOpenPrice decimal.Decimal
PositionPNL decimal.Decimal
MaintenanceMarginFraction decimal.Decimal
FreeCollateral decimal.Decimal
TotalCollateral decimal.Decimal
FrozenBalance decimal.Decimal
EquityOfCurrency decimal.Decimal
}

View File

@@ -820,7 +820,7 @@ func TestStringToOrderSide(t *testing.T) {
{"any", AnySide, nil},
{"ANY", AnySide, nil},
{"aNy", AnySide, nil},
{"woahMan", UnknownSide, errUnrecognisedOrderSide},
{"woahMan", UnknownSide, ErrSideIsInvalid},
}
for i := range cases {
testData := &cases[i]
@@ -1340,8 +1340,8 @@ func TestValidationOnOrderTypes(t *testing.T) {
getOrders.AssetType = asset.Spot
err = getOrders.Validate()
if !errors.Is(err, errUnrecognisedOrderSide) {
t.Fatalf("received: '%v' but expected: '%v'", err, errUnrecognisedOrderSide)
if !errors.Is(err, ErrSideIsInvalid) {
t.Fatalf("received: '%v' but expected: '%v'", err, ErrSideIsInvalid)
}
getOrders.Side = AnySide

View File

@@ -7,6 +7,7 @@ import (
"github.com/gofrs/uuid"
"github.com/thrasher-corp/gocryptotrader/currency"
"github.com/thrasher-corp/gocryptotrader/exchanges/asset"
"github.com/thrasher-corp/gocryptotrader/exchanges/margin"
)
// var error definitions
@@ -19,10 +20,12 @@ var (
ErrPairIsEmpty = errors.New("order pair is empty")
ErrAssetNotSet = errors.New("order asset type is not set")
ErrSideIsInvalid = errors.New("order side is invalid")
ErrCollateralInvalid = errors.New("collateral type is invalid")
ErrTypeIsInvalid = errors.New("order type is invalid")
ErrAmountIsInvalid = errors.New("order amount is equal or less than zero")
ErrPriceMustBeSetIfLimitOrder = errors.New("order price must be set if limit order type is desired")
ErrOrderIDNotSet = errors.New("order id or client order id is not set")
ErrSubmitLeverageNotSupported = errors.New("leverage is not supported via order submission")
ErrClientOrderIDNotSupported = errors.New("client order id not supported")
ErrUnsupportedOrderType = errors.New("unsupported order type")
// ErrNoRates is returned when no margin rates are returned when they are expected
@@ -64,6 +67,9 @@ type Submit struct {
TriggerPrice float64
ClientID string // TODO: Shift to credentials
ClientOrderID string
// MarginType such as isolated or cross margin for when an exchange
// supports margin type definition when submitting an order eg okx
MarginType margin.Type
// RetrieveFees use if an API submit order response does not return fees
// enabling this will perform additional request(s) to retrieve them
// and set it in the SubmitResponse
@@ -103,6 +109,7 @@ type SubmitResponse struct {
Fee float64
FeeAsset currency.Code
Cost float64
MarginType margin.Type
}
// Modify contains all properties of an order
@@ -191,6 +198,7 @@ type Detail struct {
CloseTime time.Time
LastUpdated time.Time
Pair currency.Pair
MarginType margin.Type
Trades []TradeHistory
}
@@ -226,6 +234,7 @@ type Cancel struct {
Side Side
AssetType asset.Item
Pair currency.Pair
MarginType margin.Type
}
// CancelAllResponse returns the status from attempting to

View File

@@ -35,7 +35,6 @@ var (
ErrOrderNotFound = errors.New("order not found")
errTimeInForceConflict = errors.New("multiple time in force options applied")
errUnrecognisedOrderSide = errors.New("unrecognised order side")
errUnrecognisedOrderType = errors.New("unrecognised order type")
errUnrecognisedOrderStatus = errors.New("unrecognised order status")
errExchangeNameUnset = errors.New("exchange name unset")
@@ -476,6 +475,7 @@ func (s *Submit) DeriveSubmitResponse(orderID string) (*SubmitResponse, error) {
TriggerPrice: s.TriggerPrice,
ClientID: s.ClientID,
ClientOrderID: s.ClientOrderID,
MarginType: s.MarginType,
LastUpdated: time.Now(),
Date: time.Now(),
@@ -1056,7 +1056,7 @@ func StringToOrderSide(side string) (Side, error) {
case AnySide.String():
return AnySide, nil
default:
return UnknownSide, fmt.Errorf("'%s' %w", side, errUnrecognisedOrderSide)
return UnknownSide, fmt.Errorf("'%s' %w", side, ErrSideIsInvalid)
}
}
@@ -1210,7 +1210,7 @@ func (g *MultiOrderRequest) Validate(opt ...validate.Checker) error {
}
if g.Side == UnknownSide {
return errUnrecognisedOrderSide
return ErrSideIsInvalid
}
if g.Type == UnknownType {