FTX: Funding rates, payments & stats + order manager tracking (#976)

* Adds basic PoC for calculating/retrieving position data

* A very unfortunate day of miscalculations

* Adds position summary and funding rate details to RPC

* Offline funding rate calculations

* More helpers, more stats, refining data, automated retrieval

* Adds new rpc server commands and attempts some organisation

* lower string, lower stress

* Adds ordermanager config. Fleshes outcli. Tracks positions automatically

* Adds new separation for funding payments/rates

* Combines funding rates and payments

* Fun test coverage

* ALL THE TESTS... I hope

* Fixes

* polishes ftx tests. improves perp check. Loops rates

* Final touches before nit attax

* buff 💪

* Stops NotYetImplemented spam with one simple trick!

* Some lovely little niteroos

* linteroo

* Clarifies a couple of errors to help narrow likely end user problems

* Fixes asset type bug, fixes closed position order return, fixes unset status bug

* Fixes order manager handling when no rates are available yet

* Continues on no funding rates instead. Removes err

* Don't show predicted rate if the time is zero

* Addresses scenario with no funding rate payments

* Bug fixes and commentary before updating maps to use *currency.Item

* Adds a pair key type

* Polishes pKey, fixes map order bug

* key is not a property in the event someone changes the base/quote

* Adds improvements to order processing...Breaks it all

* Shakes up the design of things by removing a function

* Fixes issues with order manager positions. Limits update range

* Fixes build issues. Identification of bad tests.

* Merges and fixes features from master and this branch

* buff linter 💪

* re-gen

* proto regen

* Addresses some nits. But not all of them.

* Fixes issue where funding rates weren't returned 🎉

* completes transition futures tracking to map[*currency.Item]map[*currency.Item]

* who did that? not me

* removes redundant check on account of being redundant and unnecessary

* so buf

* addresses nits: duplications, startTime, loops, go tidy, typos

* fixes minor mistakes

* fixes 🍣 🐻 changes to int64
This commit is contained in:
Scott
2022-08-23 12:16:50 +10:00
committed by GitHub
parent e93ee83563
commit 46cadd6f15
50 changed files with 9249 additions and 3730 deletions

View File

@@ -694,11 +694,11 @@ func TestGetCrossMarginInterestHistory(t *testing.T) {
func TestGetFundingRates(t *testing.T) {
t.Parallel()
_, err := b.GetFundingRates(context.Background(), currency.NewPair(currency.BTC, currency.USDT), "", time.Time{}, time.Time{})
_, err := b.FundingRates(context.Background(), currency.NewPair(currency.BTC, currency.USDT), "", time.Time{}, time.Time{})
if err != nil {
t.Error(err)
}
_, err = b.GetFundingRates(context.Background(), currency.NewPair(currency.BTC, currency.USDT), "2", time.Unix(1577836800, 0), time.Unix(1580515200, 0))
_, err = b.FundingRates(context.Background(), currency.NewPair(currency.BTC, currency.USDT), "2", time.Unix(1577836800, 0), time.Unix(1580515200, 0))
if err != nil {
t.Error(err)
}

View File

@@ -1127,8 +1127,8 @@ func (b *Binance) GetPerpMarkets(ctx context.Context) (PerpsExchangeInfo, error)
return resp, b.SendHTTPRequest(ctx, exchange.RestUSDTMargined, perpExchangeInfo, uFuturesDefaultRate, &resp)
}
// GetFundingRates gets funding rate history for perpetual contracts
func (b *Binance) GetFundingRates(ctx context.Context, symbol currency.Pair, limit string, startTime, endTime time.Time) ([]FundingRateData, error) {
// FundingRates gets funding rate history for perpetual contracts
func (b *Binance) FundingRates(ctx context.Context, symbol currency.Pair, limit string, startTime, endTime time.Time) ([]FundingRateData, error) {
var resp []FundingRateData
params := url.Values{}
symbolValue, err := b.FormatSymbol(symbol, asset.USDTMarginedFutures)

View File

@@ -237,8 +237,8 @@ func (c *COINUT) GetPositionHistory(ctx context.Context, secType string, start,
return result, c.SendHTTPRequest(ctx, exchange.RestSpot, coinutPositionHistory, params, true, &result)
}
// GetOpenPositions returns all your current opened positions
func (c *COINUT) GetOpenPositions(ctx context.Context, instrumentID int) ([]OpenPosition, error) {
// GetOpenPositionsForInstrument returns all your current opened positions
func (c *COINUT) GetOpenPositionsForInstrument(ctx context.Context, instrumentID int) ([]OpenPosition, error) {
type Response struct {
Positions []OpenPosition `json:"positions"`
}

View File

@@ -1298,11 +1298,6 @@ func (b *Base) CalculateTotalCollateral(ctx context.Context, calculator *order.T
return nil, common.ErrNotYetImplemented
}
// GetFuturesPositions returns futures positions according to the provided parameters
func (b *Base) GetFuturesPositions(context.Context, asset.Item, currency.Pair, time.Time, time.Time) ([]order.Detail, 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
@@ -1322,7 +1317,7 @@ func (b *Base) HasAssetTypeAccountSegregation() bool {
}
// GetServerTime returns the current exchange server time.
func (b *Base) GetServerTime(_ context.Context, _ asset.Item) (time.Time, error) {
func (b *Base) GetServerTime(context.Context, asset.Item) (time.Time, error) {
return time.Time{}, common.ErrNotYetImplemented
}
@@ -1330,3 +1325,29 @@ func (b *Base) GetServerTime(_ context.Context, _ asset.Item) (time.Time, error)
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, *order.FundingRatesRequest) (*order.FundingRates, 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
}
// GetFundingRates returns funding rates based on request data
func (b *Base) GetFundingRates(ctx context.Context, request *order.FundingRatesRequest) ([]order.FundingRates, 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
}

View File

@@ -2265,14 +2265,6 @@ func TestCalculateTotalCollateral(t *testing.T) {
}
}
func TestGetFuturesPositions(t *testing.T) {
t.Parallel()
var b Base
if _, err := b.GetFuturesPositions(context.Background(), asset.Spot, currency.Pair{}, time.Time{}, time.Time{}); !errors.Is(err, common.ErrNotYetImplemented) {
t.Errorf("received: %v, expected: %v", err, common.ErrNotYetImplemented)
}
}
func TestUpdateCurrencyStates(t *testing.T) {
t.Parallel()
var b Base
@@ -2340,3 +2332,43 @@ func TestGetFundingRateHistory(t *testing.T) {
t.Errorf("received: %v, expected: %v", err, common.ErrNotYetImplemented)
}
}
func TestGetPositionSummary(t *testing.T) {
t.Parallel()
var b Base
if _, err := b.GetPositionSummary(context.Background(), nil); !errors.Is(err, common.ErrNotYetImplemented) {
t.Errorf("received: %v, expected: %v", err, common.ErrNotYetImplemented)
}
}
func TestGetFuturesPositions(t *testing.T) {
t.Parallel()
var b Base
if _, err := b.GetFuturesPositions(context.Background(), nil); !errors.Is(err, common.ErrNotYetImplemented) {
t.Errorf("received: %v, expected: %v", err, common.ErrNotYetImplemented)
}
}
func TestGetFundingPaymentDetails(t *testing.T) {
t.Parallel()
var b Base
if _, err := b.GetFundingPaymentDetails(context.Background(), nil); !errors.Is(err, common.ErrNotYetImplemented) {
t.Errorf("received: %v, expected: %v", err, common.ErrNotYetImplemented)
}
}
func TestGetFundingRates(t *testing.T) {
t.Parallel()
var b Base
if _, err := b.GetFundingRates(context.Background(), nil); !errors.Is(err, common.ErrNotYetImplemented) {
t.Errorf("received: %v, expected: %v", err, common.ErrNotYetImplemented)
}
}
func TestIsPerpetualFutureCurrency(t *testing.T) {
t.Parallel()
var b Base
if _, err := b.IsPerpetualFutureCurrency(asset.Spot, currency.NewPair(currency.BTC, currency.USD)); !errors.Is(err, common.ErrNotYetImplemented) {
t.Errorf("received: %v, expected: %v", err, common.ErrNotYetImplemented)
}
}

View File

@@ -145,6 +145,7 @@ var (
errInvalidOrderAmounts = errors.New("filled amount should not exceed order amount")
errCollateralCurrencyNotFound = errors.New("no collateral scaling information found")
errCollateralInitialMarginFractionMissing = errors.New("cannot scale collateral, missing initial margin fraction information")
errDepositAddressDoesNotExist = errors.New("deposit address does not exist")
validResolutionData = []int64{15, 60, 300, 900, 3600, 14400, 86400}
)
@@ -307,11 +308,15 @@ func (f *FTX) GetFuture(ctx context.Context, futureName string) (FuturesData, er
}
// GetFutureStats gets data on a given future's stats
func (f *FTX) GetFutureStats(ctx context.Context, futureName string) (FutureStatsData, error) {
func (f *FTX) GetFutureStats(ctx context.Context, pair currency.Pair) (FutureStatsData, error) {
resp := struct {
Data FutureStatsData `json:"result"`
}{}
return resp.Data, f.SendHTTPRequest(ctx, exchange.RestSpot, fmt.Sprintf(getFutureStats, futureName), &resp)
p, err := f.FormatSymbol(pair, asset.Futures)
if err != nil {
return FutureStatsData{}, err
}
return resp.Data, f.SendHTTPRequest(ctx, exchange.RestSpot, fmt.Sprintf(getFutureStats, p), &resp)
}
// GetExpiredFuture returns information on an expired futures contract
@@ -340,21 +345,28 @@ func (f *FTX) GetExpiredFutures(ctx context.Context) ([]FuturesData, error) {
return resp.Data, f.SendHTTPRequest(ctx, exchange.RestSpot, getExpiredFutures, &resp)
}
// GetFundingRates gets data on funding rates
func (f *FTX) GetFundingRates(ctx context.Context, startTime, endTime time.Time, future string) ([]FundingRatesData, error) {
// FundingRates gets data on funding rates
func (f *FTX) FundingRates(ctx context.Context, startTime, endTime time.Time, pair currency.Pair, limit int64) ([]FundingRatesData, error) {
resp := struct {
Data []FundingRatesData `json:"result"`
}{}
params := url.Values{}
if !startTime.IsZero() && !endTime.IsZero() {
if startTime.After(endTime) {
return resp.Data, errStartTimeCannotBeAfterEndTime
return nil, errStartTimeCannotBeAfterEndTime
}
params.Set("start_time", strconv.FormatInt(startTime.Unix(), 10))
params.Set("end_time", strconv.FormatInt(endTime.Unix(), 10))
}
if future != "" {
params.Set("future", future)
if !pair.IsEmpty() {
p, err := f.FormatSymbol(pair, asset.Futures)
if err != nil {
return nil, err
}
params.Set("future", p)
}
if limit > 0 {
params.Set("limit", strconv.FormatInt(limit, 10))
}
endpoint := common.EncodeURLValues(getFundingRates, params)
return resp.Data, f.SendHTTPRequest(ctx, exchange.RestSpot, endpoint, &resp)
@@ -521,11 +533,17 @@ func (f *FTX) GetAccountInfo(ctx context.Context) (AccountInfoData, error) {
}
// GetPositions gets the users positions
func (f *FTX) GetPositions(ctx context.Context) ([]PositionData, error) {
func (f *FTX) GetPositions(ctx context.Context, includeAverages bool) ([]PositionData, error) {
resp := struct {
Data []PositionData `json:"result"`
}{}
return resp.Data, f.SendAuthHTTPRequest(ctx, exchange.RestSpot, http.MethodGet, getPositions, nil, &resp)
requestURL := getPositions
if includeAverages {
vals := url.Values{}
vals.Set("showAvgPrice", "true")
requestURL = common.EncodeURLValues(getPositions, vals)
}
return resp.Data, f.SendAuthHTTPRequest(ctx, exchange.RestSpot, http.MethodGet, requestURL, nil, &resp)
}
// ChangeAccountLeverage changes default leverage used by account
@@ -569,15 +587,31 @@ func (f *FTX) GetAllWalletBalances(ctx context.Context) (AllWalletBalances, erro
// FetchDepositAddress gets deposit address for a given coin
func (f *FTX) FetchDepositAddress(ctx context.Context, coin currency.Code, chain string) (*DepositData, error) {
resp := struct {
Data DepositData `json:"result"`
var jsonResp json.RawMessage
resp := &struct {
Data *DepositData `json:"result"`
}{}
addressDoesNotExistResponse := &struct {
Data bool `json:"result"`
}{}
vals := url.Values{}
if chain != "" {
vals.Set("method", strings.ToLower(chain))
}
path := common.EncodeURLValues(getDepositAddress+coin.Upper().String(), vals)
return &resp.Data, f.SendAuthHTTPRequest(ctx, exchange.RestSpot, http.MethodGet, path, nil, &resp)
err := f.SendAuthHTTPRequest(ctx, exchange.RestSpot, http.MethodGet, path, nil, &jsonResp)
if err != nil {
return nil, err
}
err = json.Unmarshal(jsonResp, resp)
if err != nil {
errSecondPass := json.Unmarshal(jsonResp, addressDoesNotExistResponse)
if errSecondPass != nil {
return nil, errSecondPass
}
return nil, fmt.Errorf("%w %v %v", errDepositAddressDoesNotExist, coin, chain)
}
return resp.Data, nil
}
// FetchDepositHistory gets deposit history
@@ -935,8 +969,8 @@ func (f *FTX) GetFills(ctx context.Context, market currency.Pair, item asset.Ite
return resp, nil
}
// GetFundingPayments gets funding payments
func (f *FTX) GetFundingPayments(ctx context.Context, startTime, endTime time.Time, future string) ([]FundingPaymentsData, error) {
// FundingPayments gets funding payments
func (f *FTX) FundingPayments(ctx context.Context, startTime, endTime time.Time, future currency.Pair, limit int64) ([]FundingPaymentsData, error) {
resp := struct {
Data []FundingPaymentsData `json:"result"`
}{}
@@ -948,8 +982,11 @@ func (f *FTX) GetFundingPayments(ctx context.Context, startTime, endTime time.Ti
params.Set("start_time", strconv.FormatInt(startTime.Unix(), 10))
params.Set("end_time", strconv.FormatInt(endTime.Unix(), 10))
}
if future != "" {
params.Set("future", future)
if !future.IsEmpty() {
params.Set("future", future.String())
}
if limit > 0 {
params.Set("limit", strconv.FormatInt(limit, 10))
}
endpoint := common.EncodeURLValues(getFundingPayments, params)
return resp.Data, f.SendAuthHTTPRequest(ctx, exchange.RestSpot, http.MethodGet, endpoint, nil, &resp)

View File

@@ -72,8 +72,17 @@ func TestMain(m *testing.M) {
if err != nil {
log.Fatal(err)
}
err = f.CurrencyPairs.EnablePair(asset.Futures, currency.NewPair(currency.BTC, currency.PERP))
if err != nil {
log.Fatal(err)
}
err = f.CurrencyPairs.EnablePair(asset.Futures, currency.NewPair(currency.OKB, currency.PERP))
if err != nil {
log.Fatal(err)
}
f.Websocket.DataHandler = sharedtestvalues.GetWebsocketInterfaceChannelOverride()
f.Websocket.TrafficAlert = sharedtestvalues.GetWebsocketStructChannelOverride()
os.Exit(m.Run())
}
@@ -232,11 +241,11 @@ func TestGetFuture(t *testing.T) {
func TestGetFutureStats(t *testing.T) {
t.Parallel()
_, err := f.GetFutureStats(context.Background(), "BTC-PERP")
_, err := f.GetFutureStats(context.Background(), currency.NewPair(currency.BTC, currency.PERP))
if err != nil {
t.Error(err)
}
future, err := f.GetFutureStats(context.Background(), "BTC-MOVE-2021Q4")
future, err := f.GetFutureStats(context.Background(), currency.NewPair(currency.BTC, currency.NewCode("MOVE-2021Q4")))
if err != nil {
t.Error(err)
}
@@ -245,15 +254,15 @@ func TestGetFutureStats(t *testing.T) {
}
}
func TestGetFundingRates(t *testing.T) {
func TestFundingRates(t *testing.T) {
t.Parallel()
// optional params
_, err := f.GetFundingRates(context.Background(), time.Time{}, time.Time{}, "")
_, err := f.FundingRates(context.Background(), time.Time{}, time.Time{}, currency.EMPTYPAIR, -1)
if err != nil {
t.Error(err)
}
_, err = f.GetFundingRates(context.Background(),
time.Now().Add(-time.Hour), time.Now(), "BTC-PERP")
_, err = f.FundingRates(context.Background(),
time.Now().Add(-time.Hour), time.Now(), currency.NewPair(currency.BTC, currency.PERP), 1)
if err != nil {
t.Error(err)
}
@@ -275,7 +284,12 @@ func TestGetPositions(t *testing.T) {
if !areTestAPIKeysSet() {
t.Skip()
}
_, err := f.GetPositions(context.Background())
_, err := f.GetPositions(context.Background(), false)
if err != nil {
t.Error(err)
}
_, err = f.GetPositions(context.Background(), true)
if err != nil {
t.Error(err)
}
@@ -479,6 +493,11 @@ func TestFetchDepositAddress(t *testing.T) {
if r.Method != "trx" {
t.Error("expected trx method")
}
_, err = f.FetchDepositAddress(context.Background(), currency.NewCode("SUSHIBEAR"), "")
if !errors.Is(err, errDepositAddressDoesNotExist) {
t.Error(err)
}
}
func TestFetchDepositHistory(t *testing.T) {
@@ -771,24 +790,27 @@ func TestGetFills(t *testing.T) {
}
}
func TestGetFundingPayments(t *testing.T) {
func TestFundingPayments(t *testing.T) {
t.Parallel()
if !areTestAPIKeysSet() {
t.Skip()
}
// optional params
_, err := f.GetFundingPayments(context.Background(),
time.Time{}, time.Time{}, "")
_, err := f.FundingPayments(context.Background(), time.Time{}, time.Time{}, currency.EMPTYPAIR, -1)
if err != nil {
t.Error(err)
}
_, err = f.GetFundingPayments(context.Background(),
time.Unix(authStartTime, 0), time.Unix(authEndTime, 0), futuresPair)
cp, err := currency.NewPairFromString("BTC-PERP")
if err != nil {
t.Fatal(err)
}
_, err = f.FundingPayments(context.Background(), time.Unix(authStartTime, 0), time.Unix(authEndTime, 0), cp, 1)
if err != nil {
t.Error(err)
}
_, err = f.GetFundingPayments(context.Background(),
time.Unix(authEndTime, 0), time.Unix(authStartTime, 0), futuresPair)
_, err = f.FundingPayments(context.Background(), time.Unix(authEndTime, 0), time.Unix(authStartTime, 0), cp, -1)
if err != errStartTimeCannotBeAfterEndTime {
t.Errorf("should have thrown errStartTimeCannotBeAfterEndTime, got %v", err)
}
@@ -1237,7 +1259,7 @@ func TestGetDepositAddress(t *testing.T) {
if !areTestAPIKeysSet() {
t.Skip("API keys required but not set, skipping test")
}
_, err := f.GetDepositAddress(context.Background(), currency.NewCode("FTT"), "", "")
_, err := f.GetDepositAddress(context.Background(), currency.NewCode("BTC"), "", "")
if err != nil {
t.Error(err)
}
@@ -1785,51 +1807,57 @@ func TestScaleCollateral(t *testing.T) {
if err != nil {
t.Error(err)
}
walletInfo, err := f.GetAllWalletBalances(context.Background())
walletInfo, err := f.GetBalances(context.Background(), true, true)
if err != nil {
t.Error(err)
}
localScaling := 0.0
providedUSDValue := 0.0
for _, v := range walletInfo {
for v2 := range v {
coin := v[v2].Coin
if coin.Equal(currency.USD) {
localScaling += v[v2].Total
providedUSDValue += v[v2].USDValue
var coverageTested bool
for i := range walletInfo {
coin := walletInfo[i].Coin
if coin.Equal(currency.USD) {
localScaling += walletInfo[i].Total
continue
}
var tick MarketData
usdPrice := walletInfo[i].USDValue / walletInfo[i].Total
tick, err = f.GetMarket(context.Background(), currency.NewPairWithDelimiter(coin.String(), "usd", "/").String())
if err != nil {
if walletInfo[i].USDValue == 0 {
// sometimes spot market for currency/USD doesn't exist and has no value, skip
continue
}
var tick MarketData
tick, err = f.GetMarket(context.Background(), currency.NewPairWithDelimiter(coin.String(), "usd", "/").String())
if err != nil {
// not all markets exist like this, skip
t.Logf("using wallet USD price for %v - %v", coin, usdPrice)
} else {
usdPrice = tick.Price
}
var offlineScaledCollateral *order.CollateralByCurrency
offlineScaledCollateral, err = f.ScaleCollateral(
context.Background(),
&order.CollateralCalculator{
CollateralCurrency: coin,
Asset: asset.Spot,
Side: order.Buy,
FreeCollateral: decimal.NewFromFloat(walletInfo[i].Total),
USDPrice: decimal.NewFromFloat(usdPrice),
CalculateOffline: true,
})
if err != nil {
if errors.Is(err, errCollateralCurrencyNotFound) ||
errors.Is(err, order.ErrUSDValueRequired) {
continue
}
_, err = f.ScaleCollateral(
context.Background(),
&order.CollateralCalculator{
CollateralCurrency: coin,
Asset: asset.Spot,
Side: order.Buy,
FreeCollateral: decimal.NewFromFloat(v[v2].Total),
USDPrice: decimal.NewFromFloat(tick.Price),
CalculateOffline: true,
})
if err != nil {
if errors.Is(err, errCollateralCurrencyNotFound) ||
errors.Is(err, order.ErrUSDValueRequired) {
continue
}
t.Error(err)
}
providedUSDValue += v[v2].USDValue
t.Error(err)
}
localScaling += offlineScaledCollateral.CollateralContribution.InexactFloat64()
if !coverageTested {
_, err = f.ScaleCollateral(context.Background(),
&order.CollateralCalculator{
CollateralCurrency: coin,
Asset: asset.Spot,
Side: order.Buy,
FreeCollateral: decimal.NewFromFloat(v[v2].Total),
USDPrice: decimal.NewFromFloat(tick.Price),
FreeCollateral: decimal.NewFromFloat(walletInfo[i].Total),
USDPrice: decimal.NewFromFloat(usdPrice),
IsForNewPosition: true,
CalculateOffline: true,
})
@@ -1841,7 +1869,7 @@ func TestScaleCollateral(t *testing.T) {
CollateralCurrency: coin,
Asset: asset.Spot,
Side: order.Buy,
FreeCollateral: decimal.NewFromFloat(v[v2].Total),
FreeCollateral: decimal.NewFromFloat(walletInfo[i].Total),
IsLiquidating: true,
CalculateOffline: true,
})
@@ -1859,13 +1887,14 @@ func TestScaleCollateral(t *testing.T) {
if err != nil {
t.Error(err)
}
coverageTested = true
}
}
if accountInfo.Collateral == 0 {
return
}
if (math.Abs((localScaling-accountInfo.Collateral)/accountInfo.Collateral) * 100) > 5 {
t.Errorf("collateral scaling less than 95%% accurate, received '%v' expected roughly '%v'", localScaling, accountInfo.Collateral)
t.Errorf("collateral scaling less than 95%% accurate, received '%v'/%v%% expected roughly '%v'", localScaling, math.Abs((localScaling-accountInfo.Collateral)/accountInfo.Collateral)*100, accountInfo.Collateral)
}
}
@@ -1874,44 +1903,40 @@ func TestCalculateTotalCollateral(t *testing.T) {
if !areTestAPIKeysSet() {
t.Skip("skipping test, api keys not set")
}
walletInfo, err := f.GetAllWalletBalances(context.Background())
walletInfo, err := f.GetBalances(context.Background(), true, true)
if err != nil {
t.Error(err)
}
var scales []order.CollateralCalculator
for _, v := range walletInfo {
for v2 := range v {
coin := v[v2].Coin
if coin.Equal(currency.USD) {
total := decimal.NewFromFloat(v[v2].Total)
scales = append(scales, order.CollateralCalculator{
CollateralCurrency: coin,
Asset: asset.Spot,
Side: order.Buy,
FreeCollateral: total,
USDPrice: total,
CalculateOffline: true,
})
continue
}
var tick MarketData
tick, err = f.GetMarket(context.Background(), currency.NewPairWithDelimiter(coin.String(), "usd", "/").String())
if err != nil {
// some assumed markets don't exist, just don't process them
t.Log(err)
continue
}
if tick.Price == 0 {
continue
}
scales = append(scales, order.CollateralCalculator{
scales := make([]order.CollateralCalculator, len(walletInfo))
for i := range walletInfo {
coin := walletInfo[i].Coin
if coin.Equal(currency.USD) {
total := decimal.NewFromFloat(walletInfo[i].Total)
scales[i] = order.CollateralCalculator{
CollateralCurrency: coin,
Asset: asset.Spot,
Side: order.Buy,
FreeCollateral: decimal.NewFromFloat(v[v2].Total),
USDPrice: decimal.NewFromFloat(tick.Price),
FreeCollateral: total,
CalculateOffline: true,
})
}
continue
}
var tick MarketData
tick, err = f.GetMarket(context.Background(), currency.NewPairWithDelimiter(coin.String(), "usd", "/").String())
if err != nil {
// some assumed markets don't exist, just don't process them
continue
}
if tick.Price == 0 {
continue
}
scales[i] = order.CollateralCalculator{
CollateralCurrency: coin,
Asset: asset.Spot,
Side: order.Buy,
FreeCollateral: decimal.NewFromFloat(walletInfo[i].Total),
USDPrice: decimal.NewFromFloat(tick.Price),
CalculateOffline: true,
}
}
calc := &order.TotalCollateralCalculator{
@@ -2026,22 +2051,29 @@ func TestCalculatePNL(t *testing.T) {
t.Skip("skipping test, api keys not set")
}
pair := currency.NewPair(currency.BTC, currency.NewCode("20211231"))
positions, err := f.GetFuturesPositions(context.Background(), asset.Futures, pair, time.Date(2021, 1, 6, 4, 28, 0, 0, time.UTC), time.Date(2021, 12, 31, 4, 32, 0, 0, time.UTC))
positions, err := f.GetFuturesPositions(context.Background(), &order.PositionsRequest{
Asset: asset.Futures,
Pairs: currency.Pairs{pair},
StartDate: time.Date(2021, 1, 6, 4, 28, 0, 0, time.UTC),
})
if err != nil {
t.Error(err)
}
if len(positions) != 1 {
t.Fatal("expected 1 position")
}
orders := make([]order.Detail, len(positions))
for i := range positions {
for i := range positions[0].Orders {
orders[i] = order.Detail{
Side: positions[i].Side,
Side: positions[0].Orders[i].Side,
Pair: pair,
OrderID: positions[i].OrderID,
Price: positions[i].Price,
Amount: positions[i].Amount,
OrderID: positions[0].Orders[i].OrderID,
Price: positions[0].Orders[i].Price,
Amount: positions[0].Orders[i].Amount,
AssetType: asset.Futures,
Exchange: f.Name,
Fee: positions[i].Fee,
Date: positions[i].Date,
Fee: positions[0].Orders[i].Fee,
Date: positions[0].Orders[i].Date,
}
}
@@ -2076,14 +2108,25 @@ func TestGetFuturesPositions(t *testing.T) {
if !areTestAPIKeysSet() {
t.Skip("skipping test, api keys not set")
}
cp := currency.NewPair(currency.BTC, currency.NewCode("20211231"))
cp := currency.Pairs{currency.NewPair(currency.BTC, currency.PERP)}
start := time.Now().Add(-time.Hour * 24 * 365)
end := time.Now()
a := asset.Futures
_, err := f.GetFuturesPositions(context.Background(), a, cp, start, end)
_, err := f.GetFuturesPositions(context.Background(), &order.PositionsRequest{
Asset: asset.Futures,
Pairs: cp,
StartDate: start,
})
if err != nil {
t.Error(err)
}
_, err = f.GetFuturesPositions(context.Background(), &order.PositionsRequest{
Asset: asset.Spot,
Pairs: cp,
StartDate: start,
})
if !errors.Is(err, order.ErrNotFuturesAsset) {
t.Errorf("received '%v' expected '%v'", err, order.ErrNotFuturesAsset)
}
}
func TestLoadCollateralWeightings(t *testing.T) {
@@ -2464,3 +2507,234 @@ func TestGetMarginRatesHistory(t *testing.T) {
t.Errorf("expected '%v' received '%v'", online.Rates[0].BorrowCost.Cost, offline.Rates[0].BorrowCost.Cost)
}
}
func TestGetPositionSummary(t *testing.T) {
t.Parallel()
if !areTestAPIKeysSet() {
t.Skip()
}
positions, err := f.GetFuturesPositions(
context.Background(),
&order.PositionsRequest{
Asset: asset.Futures,
Pairs: currency.Pairs{currency.NewPair(currency.BTC, currency.NewCode("PERP"))},
StartDate: time.Now().Add(-time.Hour * 24 * 365),
})
if err != nil {
t.Error(err)
}
if len(positions) == 0 {
t.Skip("no positions to get summary")
}
if len(positions) != 1 {
t.Fatal("expected 1 position")
}
if len(positions[0].Orders) == 0 {
t.Skip("no positions to get summary")
}
onlineCalculation, err := f.GetPositionSummary(context.Background(), &order.PositionSummaryRequest{Asset: asset.Futures, Pair: positions[0].Pair})
if err != nil {
t.Error(err)
}
if onlineCalculation.CurrentSize.IsZero() {
// you have no positions to calculate offline summary for
return
}
acc, err := f.GetAccountInfo(context.Background())
if err != nil {
t.Error(err)
}
size := decimal.NewFromFloat(positions[0].Orders[0].Amount)
underlyingStr, err := f.FormatSymbol(currency.NewPair(currency.BTC, currency.USD), asset.Spot)
if err != nil {
t.Error(err)
}
underlying, err := f.GetMarket(context.Background(), underlyingStr)
if err != nil {
t.Error(err)
}
offlineCalculation, err := f.GetPositionSummary(context.Background(), &order.PositionSummaryRequest{
Asset: asset.Futures,
Pair: positions[0].Pair,
CalculateOffline: true,
Direction: positions[0].Orders[0].Side,
FreeCollateral: decimal.NewFromFloat(acc.FreeCollateral),
TotalCollateral: decimal.NewFromFloat(acc.Collateral),
OpeningPrice: decimal.NewFromFloat(positions[0].Orders[0].Price),
CurrentPrice: onlineCalculation.MarkPrice,
OpeningSize: size,
CurrentSize: size,
CollateralUsed: onlineCalculation.CollateralUsed,
NotionalPrice: decimal.NewFromFloat(underlying.Last),
Leverage: decimal.NewFromFloat(acc.Leverage),
MaxLeverageForAccount: decimal.NewFromFloat(acc.Leverage),
TotalAccountValue: decimal.NewFromFloat(acc.TotalAccountValue),
TotalOpenPositionNotional: decimal.NewFromFloat(acc.TotalPositionSize),
})
if err != nil {
t.Error(err)
}
if !onlineCalculation.MaintenanceMarginRequirement.Equal(offlineCalculation.MaintenanceMarginRequirement) {
t.Errorf("expected '%v' received '%v'", onlineCalculation.MaintenanceMarginRequirement, offlineCalculation.MaintenanceMarginRequirement)
}
if !onlineCalculation.MarkPrice.Equal(offlineCalculation.MarkPrice) {
t.Errorf("expected '%v' received '%v'", onlineCalculation.MarkPrice, offlineCalculation.MarkPrice)
}
if !onlineCalculation.CurrentSize.Equal(offlineCalculation.CurrentSize) {
t.Errorf("expected '%v' received '%v'", onlineCalculation.CurrentSize, offlineCalculation.CurrentSize)
}
if !onlineCalculation.TotalCollateral.Equal(offlineCalculation.TotalCollateral) {
t.Errorf("expected '%v' received '%v'", onlineCalculation.TotalCollateral, offlineCalculation.TotalCollateral)
}
if !onlineCalculation.FreeCollateral.Equal(offlineCalculation.FreeCollateral) {
t.Errorf("expected '%v' received '%v'", onlineCalculation.FreeCollateral, offlineCalculation.FreeCollateral)
}
if !onlineCalculation.MarginFraction.Equal(offlineCalculation.MarginFraction) {
t.Errorf("expected '%v' received '%v'", onlineCalculation.MarginFraction, offlineCalculation.MarginFraction)
}
if !onlineCalculation.RecentPNL.Equal(offlineCalculation.RecentPNL) {
t.Errorf("expected '%v' received '%v'", onlineCalculation.RecentPNL, offlineCalculation.RecentPNL)
}
if !onlineCalculation.AverageOpenPrice.Equal(offlineCalculation.AverageOpenPrice) {
t.Errorf("expected '%v' received '%v'", onlineCalculation.AverageOpenPrice, offlineCalculation.AverageOpenPrice)
}
if !onlineCalculation.BreakEvenPrice.Equal(offlineCalculation.BreakEvenPrice) {
t.Errorf("expected '%v' received '%v'", onlineCalculation.BreakEvenPrice, offlineCalculation.BreakEvenPrice)
}
if !onlineCalculation.CollateralUsed.Equal(offlineCalculation.CollateralUsed) {
t.Errorf("expected '%v' received '%v'", onlineCalculation.CollateralUsed, offlineCalculation.CollateralUsed)
}
if !onlineCalculation.InitialMarginRequirement.Equal(offlineCalculation.InitialMarginRequirement) {
t.Errorf("expected '%v' received '%v'", onlineCalculation.InitialMarginRequirement, offlineCalculation.InitialMarginRequirement)
}
}
func TestGetFundingRates(t *testing.T) {
t.Parallel()
_, err := f.GetFundingRates(context.Background(), nil)
if !errors.Is(err, common.ErrNilPointer) {
t.Errorf("received '%v' expected '%v'", err, common.ErrNilPointer)
}
request := &order.FundingRatesRequest{}
_, err = f.GetFundingRates(context.Background(), request)
if !errors.Is(err, currency.ErrCurrencyPairsEmpty) {
t.Errorf("received '%v' expected '%v'", err, currency.ErrCurrencyPairsEmpty)
}
request.Pairs = currency.Pairs{
currency.NewPair(currency.DOGE, currency.USD),
}
_, err = f.GetFundingRates(context.Background(), request)
if !errors.Is(err, common.ErrDateUnset) {
t.Errorf("received '%v' expected '%v'", err, common.ErrDateUnset)
}
request.StartDate = time.Now().Add(-time.Hour * 24 * 31)
request.EndDate = time.Now()
request.Asset = asset.Spot
_, err = f.GetFundingRates(context.Background(), request)
if !errors.Is(err, currency.ErrPairNotFound) {
t.Errorf("received '%v' expected '%v'", err, currency.ErrPairNotFound)
}
request.Pairs = currency.Pairs{
currency.NewPair(currency.BTC, currency.PERP),
}
request.Asset = asset.Futures
_, err = f.GetFundingRates(context.Background(), request)
if !errors.Is(err, nil) {
t.Errorf("received '%v' expected '%v'", err, nil)
}
if !areTestAPIKeysSet() {
return
}
request.IncludePayments = true
request.IncludePredictedRate = true
resp, err := f.GetFundingRates(context.Background(), request)
if !errors.Is(err, nil) {
t.Errorf("received '%v' expected '%v'", err, nil)
}
if len(resp) != 1 {
t.Error("expected one result")
}
if resp[0].PredictedUpcomingRate.Time.IsZero() {
t.Error("expected predicted rates")
}
if resp[0].PaymentSum.IsZero() {
t.Log("expected payments, but you may not have had a position open, so not a failure")
}
}
func TestIsPerpetualFutureCurrency(t *testing.T) {
t.Parallel()
cp1 := currency.NewPair(currency.BTC, currency.USD)
cp2 := currency.NewPair(currency.BTC, currency.PERP)
result, err := f.IsPerpetualFutureCurrency(asset.Spot, cp1)
if !errors.Is(err, nil) {
t.Errorf("received '%v' expected '%v'", err, nil)
}
if result {
t.Error("expected false")
}
result, err = f.IsPerpetualFutureCurrency(asset.Spot, cp1)
if !errors.Is(err, nil) {
t.Errorf("received '%v' expected '%v'", err, nil)
}
if result {
t.Error("expected false")
}
_, err = f.IsPerpetualFutureCurrency(asset.Spot, cp2)
if !errors.Is(err, currency.ErrPairNotFound) {
t.Errorf("received '%v' expected '%v'", err, currency.ErrPairNotFound)
}
_, err = f.IsPerpetualFutureCurrency(asset.Spot, cp2)
if !errors.Is(err, currency.ErrPairNotFound) {
t.Errorf("received '%v' expected '%v'", err, currency.ErrPairNotFound)
}
_, err = f.IsPerpetualFutureCurrency(asset.Futures, cp1)
if !errors.Is(err, currency.ErrPairNotFound) {
t.Errorf("received '%v' expected '%v'", err, currency.ErrPairNotFound)
}
result, err = f.IsPerpetualFutureCurrency(asset.Futures, cp2)
if !errors.Is(err, nil) {
t.Errorf("received '%v' expected '%v'", err, nil)
}
if !result {
t.Error("expected true")
}
}
func TestGetFundingPayments(t *testing.T) {
t.Parallel()
if !areTestAPIKeysSet() {
t.Skip()
}
_, err := f.getFundingPayments(context.Background(), time.Time{}, time.Time{}, currency.EMPTYPAIR, -1)
if err != nil {
t.Error(err)
}
cp, err := currency.NewPairFromString("BTC-PERP")
if err != nil {
t.Fatal(err)
}
startDate := time.Now().Add(-time.Hour * 24 * 31)
endDate := time.Now()
_, err = f.getFundingPayments(context.Background(), startDate, endDate, cp, 100)
if err != nil {
t.Error(err)
}
_, err = f.getFundingPayments(context.Background(), time.Unix(authEndTime, 0), time.Unix(authStartTime, 0), cp, -1)
if err != errStartTimeCannotBeAfterEndTime {
t.Errorf("should have thrown errStartTimeCannotBeAfterEndTime, got %v", err)
}
}

View File

@@ -187,26 +187,26 @@ type IndexWeights struct {
// PositionData stores data of an open position
type PositionData struct {
CollateralUsed float64 `json:"collateralUsed"`
Future currency.Pair `json:"future"`
Size float64 `json:"size"`
Side string `json:"side"`
NetSize float64 `json:"netSize"`
LongOrderSize float64 `json:"longOrderSize"`
ShortOrderSize float64 `json:"shortOrderSize"`
Cost float64 `json:"cost"`
EntryPrice float64 `json:"entryPrice"`
UnrealizedPNL float64 `json:"unrealizedPnl"`
RealizedPNL float64 `json:"realizedPnl"`
InitialMarginRequirement float64 `json:"initialMarginRequirement"`
MaintenanceMarginRequirement float64 `json:"maintenanceMarginRequirement"`
OpenSize float64 `json:"openSize"`
CollateralUsed float64 `json:"collateralUsed"`
EstimatedLiquidationPrice float64 `json:"estimatedLiquidationPrice"`
RecentAverageOpenPrice float64 `json:"recentAverageOpenPrice"`
RecentPNL float64 `json:"recentPnl"`
RecentBreakEvenPrice float64 `json:"recentBreakEvenPrice"`
CumulativeBuySize float64 `json:"cumulativeBuySize"`
CumulativeSellSize float64 `json:"cumulativeSellSize"`
EntryPrice float64 `json:"entryPrice"`
EstimatedLiquidationPrice float64 `json:"estimatedLiquidationPrice"`
Future currency.Pair `json:"future"`
InitialMarginRequirement float64 `json:"initialMarginRequirement"`
LongOrderSize float64 `json:"longOrderSize"`
MaintenanceMarginRequirement float64 `json:"maintenanceMarginRequirement"`
NetSize float64 `json:"netSize"`
OpenSize float64 `json:"openSize"`
RealizedPNL float64 `json:"realizedPnl"`
RecentAverageOpenPrice float64 `json:"recentAverageOpenPrice"`
RecentBreakEvenPrice float64 `json:"recentBreakEvenPrice"`
RecentPnl float64 `json:"recentPnl"`
ShortOrderSize float64 `json:"shortOrderSize"`
Side string `json:"side"`
Size float64 `json:"size"`
UnrealizedPNL float64 `json:"unrealizedPnl"`
}
// AccountInfoData stores account data
@@ -331,22 +331,22 @@ type WithdrawItem struct {
// OrderData stores open order data
type OrderData struct {
CreatedAt time.Time `json:"createdAt"`
FilledSize float64 `json:"filledSize"`
Future string `json:"future"`
ID int64 `json:"id"`
Market string `json:"market"`
Price float64 `json:"price"`
AvgFillPrice float64 `json:"avgFillPrice"`
RemainingSize float64 `json:"remainingSize"`
Side string `json:"side"`
Size float64 `json:"size"`
Status string `json:"status"`
OrderType string `json:"type"`
ReduceOnly bool `json:"reduceOnly"`
IOC bool `json:"ioc"`
PostOnly bool `json:"postOnly"`
ClientID string `json:"clientId"`
AvgFillPrice float64 `json:"avgFillPrice"`
ClientID string `json:"clientId"`
CreatedAt time.Time `json:"createdAt"`
FilledSize float64 `json:"filledSize"`
Future currency.Pair `json:"future"`
ID int64 `json:"id"`
IOC bool `json:"ioc"`
Market currency.Pair `json:"market"`
PostOnly bool `json:"postOnly"`
Price float64 `json:"price"`
ReduceOnly bool `json:"reduceOnly"`
RemainingSize float64 `json:"remainingSize"`
Side string `json:"side"`
Size float64 `json:"size"`
Status string `json:"status"`
Type string `json:"type"`
}
// TriggerOrderData stores trigger order data
@@ -384,22 +384,22 @@ type TriggerData struct {
// FillsData stores fills' data
type FillsData struct {
Fee float64 `json:"fee"`
FeeCurrency string `json:"feeCurrency"`
FeeRate float64 `json:"feeRate"`
Future string `json:"future"`
ID int64 `json:"id"`
Liquidity string `json:"liquidity"`
Market string `json:"market"`
BaseCurrency string `json:"baseCurrency"`
QuoteCurrency string `json:"quoteCurrency"`
OrderID int64 `json:"orderId"`
TradeID int64 `json:"tradeId"`
Price float64 `json:"price"`
Side string `json:"side"`
Size float64 `json:"size"`
Time time.Time `json:"time"`
OrderType string `json:"type"`
Fee float64 `json:"fee"`
FeeCurrency currency.Code `json:"feeCurrency"`
FeeRate float64 `json:"feeRate"`
Future string `json:"future"`
ID int64 `json:"id"`
Liquidity string `json:"liquidity"`
Market string `json:"market"`
BaseCurrency string `json:"baseCurrency"`
QuoteCurrency string `json:"quoteCurrency"`
OrderID int64 `json:"orderId"`
TradeID int64 `json:"tradeId"`
Price float64 `json:"price"`
Side string `json:"side"`
Size float64 `json:"size"`
Time time.Time `json:"time"`
OrderType string `json:"type"`
}
// FundingPaymentsData stores funding payments' data

View File

@@ -820,7 +820,7 @@ func (s *OrderData) GetCompatible(ctx context.Context, f *FTX) (OrderVars, error
feeBuilder.PurchasePrice = s.AvgFillPrice
feeBuilder.Amount = s.Size
resp.OrderType = order.Market
if strings.EqualFold(s.OrderType, order.Limit.String()) {
if strings.EqualFold(s.Type, order.Limit.String()) {
resp.OrderType = order.Limit
feeBuilder.IsMaker = true
}
@@ -839,11 +839,7 @@ func (f *FTX) GetOrderInfo(ctx context.Context, orderID string, _ currency.Pair,
if err != nil {
return resp, err
}
p, err := currency.NewPairFromString(orderData.Market)
if err != nil {
return resp, err
}
orderAssetType, err := f.GetPairAssetType(p)
orderAssetType, err := f.GetPairAssetType(orderData.Market)
if err != nil {
return resp, err
}
@@ -853,7 +849,7 @@ func (f *FTX) GetOrderInfo(ctx context.Context, orderID string, _ currency.Pair,
resp.Date = orderData.CreatedAt
resp.Exchange = f.Name
resp.ExecutedAmount = orderData.Size - orderData.RemainingSize
resp.Pair = p
resp.Pair = orderData.Market
resp.AssetType = orderAssetType
resp.Price = orderData.Price
resp.RemainingAmount = orderData.RemainingSize
@@ -945,12 +941,6 @@ func (f *FTX) GetActiveOrders(ctx context.Context, getOrdersRequest *order.GetOr
return resp, err
}
for y := range orderData {
var p currency.Pair
p, err = currency.NewPairFromString(orderData[y].Market)
if err != nil {
return nil, err
}
tempResp.OrderID = strconv.FormatInt(orderData[y].ID, 10)
tempResp.Amount = orderData[y].Size
tempResp.AssetType = assetType
@@ -958,14 +948,14 @@ func (f *FTX) GetActiveOrders(ctx context.Context, getOrdersRequest *order.GetOr
tempResp.Date = orderData[y].CreatedAt
tempResp.Exchange = f.Name
tempResp.ExecutedAmount = orderData[y].Size - orderData[y].RemainingSize
tempResp.Pair = p
tempResp.Pair = orderData[y].Market
tempResp.Price = orderData[y].Price
tempResp.RemainingAmount = orderData[y].RemainingSize
var orderVars OrderVars
orderVars, err = f.compatibleOrderVars(ctx,
orderData[y].Side,
orderData[y].Status,
orderData[y].OrderType,
orderData[y].Type,
orderData[y].Size,
orderData[y].FilledSize,
orderData[y].AvgFillPrice)
@@ -1023,75 +1013,71 @@ func (f *FTX) GetActiveOrders(ctx context.Context, getOrdersRequest *order.GetOr
// GetOrderHistory retrieves account order information
// Can Limit response to specific order status
func (f *FTX) GetOrderHistory(ctx context.Context, getOrdersRequest *order.GetOrdersRequest) ([]order.Detail, error) {
if err := getOrdersRequest.Validate(); err != nil {
func (f *FTX) GetOrderHistory(ctx context.Context, request *order.GetOrdersRequest) ([]order.Detail, error) {
if err := request.Validate(); err != nil {
return nil, err
}
var resp []order.Detail
for x := range getOrdersRequest.Pairs {
var tempResp order.Detail
assetType, err := f.GetPairAssetType(getOrdersRequest.Pairs[x])
if err != nil {
return resp, err
}
formattedPair, err := f.FormatExchangeCurrency(getOrdersRequest.Pairs[x],
assetType)
for x := range request.Pairs {
var d order.Detail
fp, err := f.FormatExchangeCurrency(request.Pairs[x], request.AssetType)
if err != nil {
return nil, err
}
orderData, err := f.FetchOrderHistory(ctx,
formattedPair.String(),
getOrdersRequest.StartTime,
getOrdersRequest.EndTime,
history, err := f.FetchOrderHistory(ctx,
fp.String(),
request.StartTime,
request.EndTime,
"")
if err != nil {
return resp, err
return nil, err
}
for y := range orderData {
var p currency.Pair
p, err = currency.NewPairFromString(orderData[y].Market)
if err != nil {
return nil, err
}
tempResp.OrderID = strconv.FormatInt(orderData[y].ID, 10)
tempResp.Amount = orderData[y].Size
tempResp.AssetType = assetType
tempResp.AverageExecutedPrice = orderData[y].AvgFillPrice
tempResp.ClientOrderID = orderData[y].ClientID
tempResp.Date = orderData[y].CreatedAt
tempResp.Exchange = f.Name
tempResp.ExecutedAmount = orderData[y].Size - orderData[y].RemainingSize
tempResp.Pair = p
tempResp.Price = orderData[y].Price
tempResp.RemainingAmount = orderData[y].RemainingSize
for y := range history {
d.OrderID = strconv.FormatInt(history[y].ID, 10)
d.Amount = history[y].Size
d.AssetType = request.AssetType
d.AverageExecutedPrice = history[y].AvgFillPrice
d.ClientOrderID = history[y].ClientID
d.Date = history[y].CreatedAt
d.Exchange = f.Name
d.ExecutedAmount = history[y].Size - history[y].RemainingSize
d.Pair = history[y].Market
d.Price = history[y].Price
d.RemainingAmount = history[y].RemainingSize
var orderVars OrderVars
orderVars, err = f.compatibleOrderVars(ctx,
orderData[y].Side,
orderData[y].Status,
orderData[y].OrderType,
orderData[y].Size,
orderData[y].FilledSize,
orderData[y].AvgFillPrice)
history[y].Side,
history[y].Status,
history[y].Type,
history[y].Size,
history[y].FilledSize,
history[y].AvgFillPrice)
if err != nil {
return resp, err
}
tempResp.Status = orderVars.Status
tempResp.Side = orderVars.Side
tempResp.Type = orderVars.OrderType
tempResp.Fee = orderVars.Fee
resp = append(resp, tempResp)
d.Status = orderVars.Status
d.Side = orderVars.Side
d.Type = orderVars.OrderType
d.Fee = orderVars.Fee
resp = append(resp, d)
}
var side, t string
if request.Side != order.UnknownSide {
side = request.Side.Lower()
}
if request.Type != order.UnknownType {
t = request.Type.Lower()
}
triggerOrderData, err := f.GetTriggerOrderHistory(ctx,
formattedPair.String(),
getOrdersRequest.StartTime,
getOrdersRequest.EndTime,
strings.ToLower(getOrdersRequest.Side.String()),
strings.ToLower(getOrdersRequest.Type.String()),
fp.String(),
request.StartTime,
request.EndTime,
side,
t,
"")
if err != nil {
return resp, err
return nil, err
}
for z := range triggerOrderData {
var p currency.Pair
@@ -1099,17 +1085,18 @@ func (f *FTX) GetOrderHistory(ctx context.Context, getOrdersRequest *order.GetOr
if err != nil {
return nil, err
}
tempResp.OrderID = strconv.FormatInt(triggerOrderData[z].ID, 10)
tempResp.Amount = triggerOrderData[z].Size
tempResp.AssetType = assetType
tempResp.Date = triggerOrderData[z].CreatedAt
tempResp.Exchange = f.Name
tempResp.ExecutedAmount = triggerOrderData[z].FilledSize
tempResp.Pair = p
tempResp.Price = triggerOrderData[z].AvgFillPrice
tempResp.RemainingAmount = triggerOrderData[z].Size - triggerOrderData[z].FilledSize
tempResp.TriggerPrice = triggerOrderData[z].TriggerPrice
orderVars, err := f.compatibleOrderVars(ctx,
d.OrderID = strconv.FormatInt(triggerOrderData[z].ID, 10)
d.Amount = triggerOrderData[z].Size
d.AssetType = request.AssetType
d.Date = triggerOrderData[z].CreatedAt
d.Exchange = f.Name
d.ExecutedAmount = triggerOrderData[z].FilledSize
d.Pair = p
d.Price = triggerOrderData[z].AvgFillPrice
d.RemainingAmount = triggerOrderData[z].Size - triggerOrderData[z].FilledSize
d.TriggerPrice = triggerOrderData[z].TriggerPrice
var orderVars OrderVars
orderVars, err = f.compatibleOrderVars(ctx,
triggerOrderData[z].Side,
triggerOrderData[z].Status,
triggerOrderData[z].OrderType,
@@ -1117,14 +1104,14 @@ func (f *FTX) GetOrderHistory(ctx context.Context, getOrdersRequest *order.GetOr
triggerOrderData[z].FilledSize,
triggerOrderData[z].AvgFillPrice)
if err != nil {
return resp, err
return nil, err
}
tempResp.Status = orderVars.Status
tempResp.Side = orderVars.Side
tempResp.Type = orderVars.OrderType
tempResp.Fee = orderVars.Fee
tempResp.InferCostsAndTimes()
resp = append(resp, tempResp)
d.Status = orderVars.Status
d.Side = orderVars.Side
d.Type = orderVars.OrderType
d.Fee = orderVars.Fee
d.InferCostsAndTimes()
resp = append(resp, d)
}
}
return resp, nil
@@ -1427,7 +1414,7 @@ func (f *FTX) CalculateTotalCollateral(ctx context.Context, calc *order.TotalCol
var pos []PositionData
var err error
if calc.FetchPositions {
pos, err = f.GetPositions(ctx)
pos, err = f.GetPositions(ctx, true)
if err != nil {
return nil, fmt.Errorf("%v CalculateTotalCollateral GetPositions %w", f.Name, err)
}
@@ -1651,41 +1638,96 @@ func (f *FTX) calculateTotalCollateralOnline(ctx context.Context, calc *order.To
return &result, nil
}
// GetFuturesPositions returns all futures positions within provided params
func (f *FTX) GetFuturesPositions(ctx context.Context, a asset.Item, cp currency.Pair, start, end time.Time) ([]order.Detail, error) {
if !a.IsFutures() {
return nil, fmt.Errorf("%w futures asset type only", common.ErrFunctionNotSupported)
// GetFuturesPositions returns futures positions based on supplied request
func (f *FTX) GetFuturesPositions(ctx context.Context, request *order.PositionsRequest) ([]order.PositionDetails, error) {
if request == nil {
return nil, fmt.Errorf("%w position request", common.ErrNilPointer)
}
fills, err := f.GetFills(ctx, cp, a, start, end)
if !request.Asset.IsFutures() {
return nil, fmt.Errorf("%w '%s'", order.ErrNotFuturesAsset, request.Asset)
}
if err := f.CurrencyPairs.IsAssetEnabled(request.Asset); err != nil {
return nil, err
}
enabledPairs, err := f.CurrencyPairs.GetPairs(request.Asset, true)
if err != nil {
return nil, err
}
resp := make([]order.Detail, len(fills))
for i := range fills {
price := fills[i].Price
side, err := order.StringToOrderSide(fills[i].Side)
if err != nil {
return nil, err
}
resp[i] = order.Detail{
Side: side,
Pair: cp,
OrderID: strconv.FormatInt(fills[i].ID, 10),
Price: price,
Amount: fills[i].Size,
AssetType: a,
Exchange: f.Name,
Fee: fills[i].Fee,
Date: fills[i].Time,
for i := range request.Pairs {
if !enabledPairs.Contains(request.Pairs[i], false) {
return nil, fmt.Errorf("%w %v", currency.ErrPairNotFound, request.Pairs[i])
}
}
sort.Slice(resp, func(i, j int) bool {
return resp[i].Date.Before(resp[j].Date)
})
return resp, nil
positionsDetails := make([]order.PositionDetails, len(request.Pairs))
for x := range request.Pairs {
fillsOrders := make(map[string]*order.Detail)
endTime := time.Now()
allPositions:
for {
var fills []FillsData
fills, err = f.GetFills(ctx, request.Pairs[x], request.Asset, request.StartDate, endTime)
if err != nil {
return nil, err
}
if len(fills) == 0 {
break allPositions
}
sort.Slice(fills, func(i, j int) bool {
return fills[i].ID < (fills[j].ID)
})
for y := range fills {
if request.StartDate.Equal(fills[y].Time) || fills[y].Time.Before(request.StartDate) {
// reached end of trades to crawl
break allPositions
}
if fills[y].Time.After(endTime) {
continue
}
var side order.Side
side, err = order.StringToOrderSide(fills[y].Side)
if err != nil {
return nil, err
}
oID := strconv.FormatInt(fills[y].ID, 10)
_, ok := fillsOrders[oID]
if !ok {
fillsOrders[oID] = &order.Detail{
Fee: fills[y].Fee,
FeeAsset: fills[y].FeeCurrency,
Pair: request.Pairs[x],
Price: fills[y].Price,
Amount: fills[y].Size,
Exchange: f.Name,
OrderID: oID,
Side: side,
Status: order.Filled,
AssetType: request.Asset,
Date: fills[y].Time,
}
}
}
if endTime.Equal(fills[len(fills)-1].Time) {
break allPositions
}
endTime = fills[len(fills)-1].Time
}
var ods []order.Detail
for _, v := range fillsOrders {
ods = append(ods, *v)
}
sort.Slice(ods, func(i, j int) bool {
return ods[i].OrderID < (ods[j].OrderID)
})
positionsDetails[x] = order.PositionDetails{
Exchange: f.Name,
Asset: request.Asset,
Pair: enabledPairs[x],
Orders: ods,
}
}
return positionsDetails, nil
}
// GetCollateralCurrencyForContract returns the collateral currency for an asset and contract pair
@@ -1918,3 +1960,254 @@ func (f *FTX) GetMarginRatesHistory(ctx context.Context, request *margin.RateHis
return response, nil
}
// GetPositionSummary returns an overview of a future position
func (f *FTX) GetPositionSummary(ctx context.Context, request *order.PositionSummaryRequest) (*order.PositionSummary, error) {
if request == nil {
return nil, fmt.Errorf("%w PositionSummaryRequest", common.ErrNilPointer)
}
if !request.Asset.IsFutures() {
return nil, fmt.Errorf("%w '%s' is not a futures asset", asset.ErrNotSupported, request.Asset)
}
if err := f.CurrencyPairs.IsAssetEnabled(request.Asset); err != nil {
return nil, err
}
if request.CalculateOffline {
one := decimal.NewFromInt(1)
positionSize := request.CurrentSize.Mul(request.CurrentPrice)
var marginFraction decimal.Decimal
if positionSize.IsPositive() && request.TotalCollateral.IsPositive() {
marginFraction = request.TotalCollateral.Div(positionSize).Mul(decimal.NewFromFloat(100))
}
breakEvenPrice := request.OpeningPrice
if !request.OpeningSize.Equal(request.CurrentSize) {
breakEvenPrice = request.OpeningPrice.Mul(request.OpeningSize).Sub(request.CurrentSize.Mul(request.CurrentPrice)).Div(request.OpeningSize.Sub(request.CurrentSize))
}
var maintenanceMarginRequirement, positionMaintenanceMarginFraction decimal.Decimal
currSize := request.CurrentSize
openSize := request.OpeningSize
if request.Leverage.LessThanOrEqual(decimal.NewFromFloat(20)) {
positionMaintenanceMarginFraction = decimal.NewFromFloat(0.03)
} else {
// leverage can never be above 20, but will remain in event of change in policy
positionMaintenanceMarginFraction = decimal.NewFromFloat(0.006)
}
// baseIMF is always 1/20 as 20 is the max leverage
baseIMF := one.Div(decimal.NewFromInt(20))
maintenanceMarginRequirement = decimal.Max(positionMaintenanceMarginFraction, decimal.NewFromFloat(0.6).Mul(baseIMF))
if request.Direction.IsShort() {
currSize = currSize.Neg()
openSize = openSize.Neg()
}
imf := one.Div(request.Leverage)
// estimated liquidation price is not included in offline summary
// the formula does not match the API output - despite the example matching
// see https://help.ftx.com/hc/en-us/articles/360027668712-Liquidations vs
// https://docs.ftx.com/#get-account-information
return &order.PositionSummary{
MaintenanceMarginRequirement: maintenanceMarginRequirement,
InitialMarginRequirement: imf,
CollateralUsed: request.CollateralUsed,
MarkPrice: request.CurrentPrice,
CurrentSize: request.CurrentSize.Abs(),
BreakEvenPrice: breakEvenPrice,
AverageOpenPrice: request.OpeningPrice,
RecentPNL: request.CurrentPrice.Mul(currSize).Sub(request.OpeningPrice.Mul(openSize)),
MarginFraction: marginFraction,
FreeCollateral: request.FreeCollateral,
TotalCollateral: request.TotalCollateral,
}, nil
}
positions, err := f.GetPositions(ctx, true)
if err != nil {
return nil, err
}
acc, err := f.GetAccountInfo(ctx)
if err != nil {
return nil, err
}
for i := range positions {
if !positions[i].Future.Equal(request.Pair) {
continue
}
return &order.PositionSummary{
MaintenanceMarginRequirement: decimal.NewFromFloat(positions[i].MaintenanceMarginRequirement),
InitialMarginRequirement: decimal.NewFromFloat(positions[i].InitialMarginRequirement),
EstimatedLiquidationPrice: decimal.NewFromFloat(positions[i].EstimatedLiquidationPrice),
CollateralUsed: decimal.NewFromFloat(positions[i].CollateralUsed),
MarkPrice: decimal.NewFromFloat(positions[i].EntryPrice),
CurrentSize: decimal.NewFromFloat(positions[i].Size),
BreakEvenPrice: decimal.NewFromFloat(positions[i].RecentBreakEvenPrice),
AverageOpenPrice: decimal.NewFromFloat(positions[i].RecentAverageOpenPrice),
RecentPNL: decimal.NewFromFloat(positions[i].RecentPNL),
MarginFraction: decimal.NewFromFloat(acc.MarginFraction * 100),
FreeCollateral: decimal.NewFromFloat(acc.FreeCollateral),
TotalCollateral: decimal.NewFromFloat(acc.Collateral),
}, nil
}
return nil, fmt.Errorf("unable to calculate position summary %w for %v %v", order.ErrPositionNotFound, request.Asset, request.Pair)
}
// GetFundingRates returns stats about funding rates for pairs
func (f *FTX) GetFundingRates(ctx context.Context, request *order.FundingRatesRequest) ([]order.FundingRates, error) {
if request == nil {
return nil, fmt.Errorf("%w FundingRatesRequest", common.ErrNilPointer)
}
if len(request.Pairs) == 0 {
return nil, currency.ErrCurrencyPairsEmpty
}
var limit int64 = 1000
err := common.StartEndTimeCheck(request.StartDate, request.EndDate)
if err != nil {
return nil, err
}
pairFmt, err := f.GetPairFormat(request.Asset, true)
if err != nil {
return nil, err
}
request.Pairs = request.Pairs.Format(pairFmt.Delimiter, pairFmt.Index, pairFmt.Uppercase)
response := make([]order.FundingRates, 0, len(request.Pairs))
for x := range request.Pairs {
var isPerp bool
isPerp, err = f.IsPerpetualFutureCurrency(request.Asset, request.Pairs[x])
if err != nil {
return nil, err
}
if !isPerp {
return nil, fmt.Errorf("%w '%v' '%v'", order.ErrNotPerpetualFuture, request.Asset, request.Pairs[x])
}
var (
rates []FundingRatesData
fundingDetails []FundingPaymentsData
stats FutureStatsData
)
pairResponse := order.FundingRates{
Exchange: f.Name,
Asset: request.Asset,
Pair: request.Pairs[x],
StartDate: request.StartDate,
EndDate: request.EndDate,
}
endTime := request.EndDate
allRates:
for {
rates, err = f.FundingRates(ctx, request.StartDate, endTime, request.Pairs[x], limit)
if err != nil {
return nil, err
}
if len(rates) == 0 {
break allRates
}
responseRates:
for y := range rates {
if rates[y].Time.Before(request.StartDate) {
break allRates
}
if rates[y].Time.After(endTime) {
continue
}
for z := range pairResponse.FundingRates {
if rates[y].Time.Equal(pairResponse.FundingRates[z].Time) {
continue responseRates
}
}
pairResponse.FundingRates = append(pairResponse.FundingRates, order.FundingRate{
Rate: decimal.NewFromFloat(rates[y].Rate),
Time: rates[y].Time,
})
}
if endTime.Equal(rates[len(rates)-1].Time) || int64(len(rates)) < limit {
break allRates
}
endTime = rates[len(rates)-1].Time
}
if len(pairResponse.FundingRates) == 0 {
continue
}
if request.IncludePayments {
fundingDetails, err = f.getFundingPayments(ctx, request.StartDate, request.EndDate, request.Pairs[x], limit)
if err != nil {
return nil, err
}
for y := range fundingDetails {
for z := range pairResponse.FundingRates {
if !fundingDetails[y].Time.Equal(pairResponse.FundingRates[z].Time) {
continue
}
pairResponse.FundingRates[z].Payment = decimal.NewFromFloat(fundingDetails[y].Payment)
pairResponse.PaymentSum = pairResponse.PaymentSum.Add(decimal.NewFromFloat(fundingDetails[y].Payment))
break
}
}
}
if request.IncludePredictedRate {
stats, err = f.GetFutureStats(ctx, request.Pairs[x])
if err != nil {
return nil, err
}
upcoming := order.FundingRate{
Rate: decimal.NewFromFloat(stats.NextFundingRate),
Time: stats.NextFundingTime,
}
pairResponse.PredictedUpcomingRate = upcoming
}
sort.Slice(pairResponse.FundingRates, func(i, j int) bool {
return pairResponse.FundingRates[i].Time.Before(pairResponse.FundingRates[j].Time)
})
pairResponse.LatestRate = pairResponse.FundingRates[len(pairResponse.FundingRates)-1]
response = append(response, pairResponse)
}
return response, nil
}
func (f *FTX) getFundingPayments(ctx context.Context, startDate, endDate time.Time, future currency.Pair, limit int64) ([]FundingPaymentsData, error) {
requestEndTime := endDate
var payments []FundingPaymentsData
allRates:
for {
fundingDetails, err := f.FundingPayments(ctx, startDate, requestEndTime, future, limit)
if err != nil {
return nil, err
}
if len(fundingDetails) == 0 {
break allRates
}
responseRates:
for x := range fundingDetails {
if fundingDetails[x].Time.Before(startDate) {
break allRates
}
if fundingDetails[x].Time.After(requestEndTime) {
continue
}
for y := range payments {
if fundingDetails[x].Time.Equal(payments[y].Time) {
continue responseRates
}
}
payments = append(payments, fundingDetails[x])
}
if requestEndTime.Equal(fundingDetails[len(fundingDetails)-1].Time) || int64(len(fundingDetails)) < limit {
break allRates
}
requestEndTime = fundingDetails[len(fundingDetails)-1].Time
}
return payments, nil
}
// IsPerpetualFutureCurrency returns whether a currency is a perpetual future
func (f *FTX) IsPerpetualFutureCurrency(a asset.Item, cp currency.Pair) (bool, error) {
if err := f.CurrencyPairs.IsAssetEnabled(a); err != nil {
return false, err
}
pairs, err := f.GetEnabledPairs(a)
if err != nil {
return false, err
}
if !pairs.Contains(cp, false) {
return false, fmt.Errorf("%w '%v'", currency.ErrPairNotFound, cp)
}
return cp.Quote.Equal(currency.PERP) && a.IsFutures(), nil
}

View File

@@ -89,7 +89,7 @@ func (h *HUOBI) GetMarginRates(ctx context.Context, symbol currency.Pair) (Margi
// GetSpotKline returns kline data
// KlinesRequestParams contains symbol currency.Pair, period and size
func (h *HUOBI) GetSpotKline(ctx context.Context, arg KlinesRequestParams) ([]KlineItem, error) {
func (h *HUOBI) GetSpotKline(ctx context.Context, arg *KlinesRequestParams) ([]KlineItem, error) {
vals := url.Values{}
symbolValue, err := h.FormatSymbol(arg.Symbol, asset.Spot)
if err != nil {

View File

@@ -1587,7 +1587,7 @@ func TestGetSpotKline(t *testing.T) {
t.Error(err)
}
_, err = h.GetSpotKline(context.Background(),
KlinesRequestParams{
&KlinesRequestParams{
Symbol: cp,
Period: "1min",
Size: 0,

View File

@@ -1772,7 +1772,7 @@ func (h *HUOBI) GetHistoricCandles(ctx context.Context, pair currency.Pair, a as
if err := h.ValidateKline(pair, a, interval); err != nil {
return kline.Item{}, err
}
klineParams := KlinesRequestParams{
klineParams := &KlinesRequestParams{
Period: h.FormatExchangeKlineInterval(interval),
Symbol: pair,
}

View File

@@ -47,9 +47,6 @@ type IBotExchange interface {
GetWithdrawPermissions() uint32
FormatWithdrawPermissions() string
GetFundingHistory(ctx context.Context) ([]FundHistory, error)
OrderManagement
GetDepositAddress(ctx context.Context, cryptocurrency currency.Code, accountID, chain string) (*deposit.Address, error)
GetAvailableTransferChains(ctx context.Context, cryptocurrency currency.Code) ([]string, error)
GetWithdrawalsHistory(ctx context.Context, code currency.Code, a asset.Item) ([]WithdrawalHistory, error)
@@ -66,29 +63,23 @@ type IBotExchange interface {
DisableRateLimiter() error
EnableRateLimiter() error
GetServerTime(ctx context.Context, ai asset.Item) (time.Time, error)
CurrencyStateManagement
GetMarginRatesHistory(context.Context, *margin.RateHistoryRequest) (*margin.RateHistoryResponse, error)
order.PNLCalculation
order.CollateralManagement
GetFuturesPositions(context.Context, asset.Item, currency.Pair, time.Time, time.Time) ([]order.Detail, error)
GetWebsocket() (*stream.Websocket, error)
SubscribeToWebsocketChannels(channels []stream.ChannelSubscription) error
UnsubscribeToWebsocketChannels(channels []stream.ChannelSubscription) error
GetSubscriptions() ([]stream.ChannelSubscription, error)
FlushWebsocketChannels() error
AuthenticateWebsocket(ctx context.Context) error
GetOrderExecutionLimits(a asset.Item, cp currency.Pair) (order.MinMaxLevel, error)
CheckOrderExecutionLimits(a asset.Item, cp currency.Pair, price, amount float64, orderType order.Type) error
UpdateOrderExecutionLimits(ctx context.Context, a asset.Item) error
AccountManagement
GetCredentials(ctx context.Context) (*account.Credentials, error)
ValidateCredentials(ctx context.Context, a asset.Item) error
FunctionalityChecker
AccountManagement
OrderManagement
CurrencyStateManagement
FuturesManagement
}
// OrderManagement defines functionality for order management
@@ -135,3 +126,16 @@ type FunctionalityChecker interface {
IsWebsocketAuthenticationSupported() bool
IsRESTAuthenticationSupported() bool
}
// 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)
CalculateTotalCollateral(context.Context, *order.TotalCollateralCalculator) (*order.TotalCollateralResponse, error)
GetFuturesPositions(context.Context, *order.PositionsRequest) ([]order.PositionDetails, error)
GetFundingRates(context.Context, *order.FundingRatesRequest) ([]order.FundingRates, 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
}

View File

@@ -16,9 +16,9 @@ import (
// SetupPositionController creates a position controller
// to track futures orders
func SetupPositionController() *PositionController {
return &PositionController{
multiPositionTrackers: make(map[string]map[asset.Item]map[currency.Pair]*MultiPositionTracker),
func SetupPositionController() PositionController {
return PositionController{
multiPositionTrackers: make(map[string]map[asset.Item]map[*currency.Item]map[*currency.Item]*MultiPositionTracker),
}
}
@@ -26,33 +26,38 @@ func SetupPositionController() *PositionController {
// multi position tracker which funnels down into the
// position tracker, to then track an order's pnl
func (c *PositionController) TrackNewOrder(d *Detail) error {
if d == nil {
return errNilOrder
}
if !d.AssetType.IsFutures() {
return fmt.Errorf("order %v %v %v %v %w",
d.Exchange, d.AssetType, d.Pair, d.OrderID, ErrNotFuturesAsset)
}
if c == nil {
return fmt.Errorf("position controller %w", common.ErrNilPointer)
}
c.m.Lock()
defer c.m.Unlock()
exchM, ok := c.multiPositionTrackers[strings.ToLower(d.Exchange)]
if !ok {
exchM = make(map[asset.Item]map[currency.Pair]*MultiPositionTracker)
c.multiPositionTrackers[strings.ToLower(d.Exchange)] = exchM
}
itemM, ok := exchM[d.AssetType]
if !ok {
itemM = make(map[currency.Pair]*MultiPositionTracker)
exchM[d.AssetType] = itemM
if d == nil {
return errNilOrder
}
var err error
multiPositionTracker, ok := itemM[d.Pair]
d.Exchange, err = checkTrackerPrerequisitesLowerExchange(d.Exchange, d.AssetType, d.Pair)
if err != nil {
return err
}
c.m.Lock()
defer c.m.Unlock()
exchMap, ok := c.multiPositionTrackers[d.Exchange]
if !ok {
multiPositionTracker, err = SetupMultiPositionTracker(&MultiPositionTrackerSetup{
Exchange: strings.ToLower(d.Exchange),
exchMap = make(map[asset.Item]map[*currency.Item]map[*currency.Item]*MultiPositionTracker)
c.multiPositionTrackers[d.Exchange] = exchMap
}
itemMap, ok := exchMap[d.AssetType]
if !ok {
itemMap = make(map[*currency.Item]map[*currency.Item]*MultiPositionTracker)
exchMap[d.AssetType] = itemMap
}
baseMap, ok := itemMap[d.Pair.Base.Item]
if !ok {
baseMap = make(map[*currency.Item]*MultiPositionTracker)
itemMap[d.Pair.Base.Item] = baseMap
}
quoteMap, ok := baseMap[d.Pair.Quote.Item]
if !ok {
quoteMap, err = SetupMultiPositionTracker(&MultiPositionTrackerSetup{
Exchange: d.Exchange,
Asset: d.AssetType,
Pair: d.Pair,
Underlying: d.Pair.Base,
@@ -60,9 +65,14 @@ func (c *PositionController) TrackNewOrder(d *Detail) error {
if err != nil {
return err
}
itemM[d.Pair] = multiPositionTracker
baseMap[d.Pair.Quote.Item] = quoteMap
}
return multiPositionTracker.TrackNewOrder(d)
err = quoteMap.TrackNewOrder(d)
if err != nil {
return err
}
c.updated = time.Now()
return nil
}
// SetCollateralCurrency allows the setting of a collateral currency to all child trackers
@@ -71,63 +81,142 @@ func (c *PositionController) SetCollateralCurrency(exch string, item asset.Item,
if c == nil {
return fmt.Errorf("position controller %w", common.ErrNilPointer)
}
var err error
exch, err = checkTrackerPrerequisitesLowerExchange(exch, item, pair)
if err != nil {
return err
}
c.m.Lock()
defer c.m.Unlock()
if !item.IsFutures() {
return fmt.Errorf("%v %v %v %w", exch, item, pair, ErrNotFuturesAsset)
}
exchM, ok := c.multiPositionTrackers[strings.ToLower(exch)]
if !ok {
return fmt.Errorf("cannot set collateral %v for %v %v %v %w", collateralCurrency, exch, item, pair, ErrPositionsNotLoadedForExchange)
tracker := c.multiPositionTrackers[exch][item][pair.Base.Item][pair.Quote.Item]
if tracker == nil {
return fmt.Errorf("%w no open position for %v %v %v", ErrPositionNotFound, exch, item, pair)
}
itemM, ok := exchM[item]
if !ok {
return fmt.Errorf("cannot set collateral %v for %v %v %v %w", collateralCurrency, exch, item, pair, ErrPositionsNotLoadedForAsset)
tracker.m.Lock()
defer tracker.m.Unlock()
tracker.collateralCurrency = collateralCurrency
for i := range tracker.positions {
tracker.positions[i].m.Lock()
tracker.positions[i].collateralCurrency = collateralCurrency
tracker.positions[i].m.Unlock()
}
multiPositionTracker, ok := itemM[pair]
if !ok {
return fmt.Errorf("cannot set collateral %v for %v %v %v %w", collateralCurrency, exch, item, pair, ErrPositionsNotLoadedForPair)
}
if multiPositionTracker == nil {
return fmt.Errorf("cannot set collateral %v for %v %v %v %w", collateralCurrency, exch, item, pair, common.ErrNilPointer)
}
multiPositionTracker.m.Lock()
multiPositionTracker.collateralCurrency = collateralCurrency
for i := range multiPositionTracker.positions {
multiPositionTracker.positions[i].m.Lock()
multiPositionTracker.positions[i].collateralCurrency = collateralCurrency
multiPositionTracker.positions[i].m.Unlock()
}
multiPositionTracker.m.Unlock()
return nil
}
// GetPositionsForExchange returns all positions for an
// exchange, asset pair that is stored in the position controller
func (c *PositionController) GetPositionsForExchange(exch string, item asset.Item, pair currency.Pair) ([]PositionStats, error) {
func (c *PositionController) GetPositionsForExchange(exch string, item asset.Item, pair currency.Pair) ([]Position, error) {
if c == nil {
return nil, fmt.Errorf("position controller %w", common.ErrNilPointer)
}
var err error
exch, err = checkTrackerPrerequisitesLowerExchange(exch, item, pair)
if err != nil {
return nil, err
}
c.m.Lock()
defer c.m.Unlock()
tracker := c.multiPositionTrackers[exch][item][pair.Base.Item][pair.Quote.Item]
if tracker == nil {
return nil, fmt.Errorf("%w no open position for %v %v %v", ErrPositionNotFound, exch, item, pair)
}
return tracker.GetPositions(), nil
}
// TrackFundingDetails applies funding rate details to a tracked position
func (c *PositionController) TrackFundingDetails(d *FundingRates) error {
if c == nil {
return fmt.Errorf("position controller %w", common.ErrNilPointer)
}
if d == nil {
return fmt.Errorf("%w funding rate details", common.ErrNilPointer)
}
var err error
d.Exchange, err = checkTrackerPrerequisitesLowerExchange(d.Exchange, d.Asset, d.Pair)
if err != nil {
return err
}
c.m.Lock()
defer c.m.Unlock()
tracker := c.multiPositionTrackers[d.Exchange][d.Asset][d.Pair.Base.Item][d.Pair.Quote.Item]
if tracker == nil {
return fmt.Errorf("%w no open position for %v %v %v", ErrPositionNotFound, d.Exchange, d.Asset, d.Pair)
}
err = tracker.TrackFundingDetails(d)
if err != nil {
return err
}
c.updated = time.Now()
return nil
}
// LastUpdated is used for the order manager as a way of knowing
// what span of time to check for orders
func (c *PositionController) LastUpdated() (time.Time, error) {
if c == nil {
return time.Time{}, fmt.Errorf("position controller %w", common.ErrNilPointer)
}
c.m.Lock()
defer c.m.Unlock()
return c.updated, nil
}
// GetOpenPosition returns an open positions that matches the exchange, asset, pair
func (c *PositionController) GetOpenPosition(exch string, item asset.Item, pair currency.Pair) (*Position, error) {
if c == nil {
return nil, fmt.Errorf("position controller %w", common.ErrNilPointer)
}
var err error
exch, err = checkTrackerPrerequisitesLowerExchange(exch, item, pair)
if err != nil {
return nil, err
}
c.m.Lock()
defer c.m.Unlock()
tracker := c.multiPositionTrackers[exch][item][pair.Base.Item][pair.Quote.Item]
if tracker == nil {
return nil, fmt.Errorf("%w no open position for %v %v %v", ErrPositionNotFound, exch, item, pair)
}
positions := tracker.GetPositions()
for i := range positions {
if positions[i].Status.IsInactive() {
continue
}
return &positions[i], nil
}
return nil, fmt.Errorf("%w no open position for %v %v %v", ErrPositionNotFound, exch, item, pair)
}
// GetAllOpenPositions returns all open positions with optional filters
func (c *PositionController) GetAllOpenPositions() ([]Position, error) {
if c == nil {
return nil, fmt.Errorf("position controller %w", common.ErrNilPointer)
}
c.m.Lock()
defer c.m.Unlock()
if !item.IsFutures() {
return nil, fmt.Errorf("%v %v %v %w", exch, item, pair, ErrNotFuturesAsset)
var openPositions []Position
for _, exchMap := range c.multiPositionTrackers {
for _, itemMap := range exchMap {
for _, baseMap := range itemMap {
for _, multiPositionTracker := range baseMap {
positions := multiPositionTracker.GetPositions()
for i := range positions {
if positions[i].Status.IsInactive() {
continue
}
openPositions = append(openPositions, positions[i])
}
}
}
}
}
exchM, ok := c.multiPositionTrackers[strings.ToLower(exch)]
if !ok {
return nil, fmt.Errorf("%v %v %v %w", exch, item, pair, ErrPositionsNotLoadedForExchange)
if len(openPositions) == 0 {
return nil, ErrNoPositionsFound
}
itemM, ok := exchM[item]
if !ok {
return nil, fmt.Errorf("%v %v %v %w", exch, item, pair, ErrPositionsNotLoadedForAsset)
}
multiPositionTracker, ok := itemM[pair]
if !ok {
return nil, fmt.Errorf("%v %v %v %w", exch, item, pair, ErrPositionsNotLoadedForPair)
}
return multiPositionTracker.GetPositions(), nil
return openPositions, nil
}
// UpdateOpenPositionUnrealisedPNL finds an open position from
@@ -137,36 +226,29 @@ func (c *PositionController) UpdateOpenPositionUnrealisedPNL(exch string, item a
if c == nil {
return decimal.Zero, fmt.Errorf("position controller %w", common.ErrNilPointer)
}
if !item.IsFutures() {
return decimal.Zero, fmt.Errorf("%v %v %v %w", exch, item, pair, ErrNotFuturesAsset)
var err error
exch, err = checkTrackerPrerequisitesLowerExchange(exch, item, pair)
if err != nil {
return decimal.Zero, err
}
c.m.Lock()
defer c.m.Unlock()
exchM, ok := c.multiPositionTrackers[strings.ToLower(exch)]
if !ok {
return decimal.Zero, fmt.Errorf("%v %v %v %w", exch, item, pair, ErrPositionsNotLoadedForExchange)
}
itemM, ok := exchM[item]
if !ok {
return decimal.Zero, fmt.Errorf("%v %v %v %w", exch, item, pair, ErrPositionsNotLoadedForAsset)
}
multiPositionTracker, ok := itemM[pair]
if !ok {
return decimal.Zero, fmt.Errorf("%v %v %v %w", exch, item, pair, ErrPositionsNotLoadedForPair)
tracker := c.multiPositionTrackers[exch][item][pair.Base.Item][pair.Quote.Item]
if tracker == nil {
return decimal.Zero, fmt.Errorf("%v %v %v %w", exch, item, pair, ErrPositionNotFound)
}
multiPositionTracker.m.Lock()
defer multiPositionTracker.m.Unlock()
pos := multiPositionTracker.positions
tracker.m.Lock()
defer tracker.m.Unlock()
pos := tracker.positions
if len(pos) == 0 {
return decimal.Zero, fmt.Errorf("%v %v %v %w", exch, item, pair, ErrPositionsNotLoadedForPair)
return decimal.Zero, fmt.Errorf("%v %v %v %w", exch, item, pair, ErrPositionNotFound)
}
latestPos := pos[len(pos)-1]
if latestPos.status != Open {
return decimal.Zero, fmt.Errorf("%v %v %v %w", exch, item, pair, ErrPositionClosed)
}
err := latestPos.TrackPNLByTime(updated, last)
err = latestPos.TrackPNLByTime(updated, last)
if err != nil {
return decimal.Zero, fmt.Errorf("%w for position %v %v %v", err, exch, item, pair)
}
@@ -183,11 +265,10 @@ func SetupMultiPositionTracker(setup *MultiPositionTrackerSetup) (*MultiPosition
if setup.Exchange == "" {
return nil, errExchangeNameEmpty
}
if !setup.Asset.IsValid() || !setup.Asset.IsFutures() {
return nil, ErrNotFuturesAsset
}
if setup.Pair.IsEmpty() {
return nil, ErrPairIsEmpty
var err error
setup.Exchange, err = checkTrackerPrerequisitesLowerExchange(setup.Exchange, setup.Asset, setup.Pair)
if err != nil {
return nil, err
}
if setup.Underlying.IsEmpty() {
return nil, errEmptyUnderlying
@@ -196,7 +277,7 @@ func SetupMultiPositionTracker(setup *MultiPositionTrackerSetup) (*MultiPosition
return nil, errMissingPNLCalculationFunctions
}
return &MultiPositionTracker{
exchange: strings.ToLower(setup.Exchange),
exchange: setup.Exchange,
asset: setup.Asset,
pair: setup.Pair,
underlying: setup.Underlying,
@@ -208,50 +289,6 @@ func SetupMultiPositionTracker(setup *MultiPositionTrackerSetup) (*MultiPosition
}, nil
}
// SetupPositionTracker creates a new position tracker to track n futures orders
// until the position(s) are closed
func (m *MultiPositionTracker) SetupPositionTracker(setup *PositionTrackerSetup) (*PositionTracker, error) {
if m == nil {
return nil, fmt.Errorf("multi-position tracker %w", common.ErrNilPointer)
}
if m.exchange == "" {
return nil, errExchangeNameEmpty
}
if setup == nil {
return nil, errNilSetup
}
if !setup.Asset.IsValid() || !setup.Asset.IsFutures() {
return nil, ErrNotFuturesAsset
}
if setup.Pair.IsEmpty() {
return nil, ErrPairIsEmpty
}
resp := &PositionTracker{
exchange: strings.ToLower(m.exchange),
asset: setup.Asset,
contractPair: setup.Pair,
underlyingAsset: setup.Underlying,
status: Open,
entryPrice: setup.EntryPrice,
currentDirection: setup.Side,
openingDirection: setup.Side,
useExchangePNLCalculation: setup.UseExchangePNLCalculation,
collateralCurrency: setup.CollateralCurrency,
offlinePNLCalculation: m.offlinePNLCalculation,
}
if !setup.UseExchangePNLCalculation {
// use position tracker's pnl calculation by default
resp.PNLCalculation = &PNLCalculator{}
} else {
if m.exchangePNLCalculation == nil {
return nil, ErrNilPNLCalculator
}
resp.PNLCalculation = m.exchangePNLCalculation
}
return resp, nil
}
// UpdateOpenPositionUnrealisedPNL updates the pnl for the latest open position
// based on the last price and the time
func (m *MultiPositionTracker) UpdateOpenPositionUnrealisedPNL(last float64, updated time.Time) (decimal.Decimal, error) {
@@ -259,7 +296,7 @@ func (m *MultiPositionTracker) UpdateOpenPositionUnrealisedPNL(last float64, upd
defer m.m.Unlock()
pos := m.positions
if len(pos) == 0 {
return decimal.Zero, fmt.Errorf("%v %v %v %w", m.exchange, m.asset, m.pair, ErrPositionsNotLoadedForPair)
return decimal.Zero, fmt.Errorf("%v %v %v %w", m.exchange, m.asset, m.pair, ErrPositionNotFound)
}
latestPos := pos[len(pos)-1]
if latestPos.status.IsInactive() {
@@ -280,51 +317,50 @@ func (c *PositionController) ClearPositionsForExchange(exch string, item asset.I
if c == nil {
return fmt.Errorf("position controller %w", common.ErrNilPointer)
}
var err error
exch, err = checkTrackerPrerequisitesLowerExchange(exch, item, pair)
if err != nil {
return err
}
c.m.Lock()
defer c.m.Unlock()
if !item.IsFutures() {
return fmt.Errorf("%v %v %v %w", exch, item, pair, ErrNotFuturesAsset)
}
exchM, ok := c.multiPositionTrackers[strings.ToLower(exch)]
if !ok {
return fmt.Errorf("%v %v %v %w", exch, item, pair, ErrPositionsNotLoadedForExchange)
}
itemM, ok := exchM[item]
if !ok {
return fmt.Errorf("%v %v %v %w", exch, item, pair, ErrPositionsNotLoadedForAsset)
}
multiPositionTracker, ok := itemM[pair]
if !ok {
return fmt.Errorf("%v %v %v %w", exch, item, pair, ErrPositionsNotLoadedForPair)
tracker := c.multiPositionTrackers[exch][item][pair.Base.Item][pair.Quote.Item]
if tracker == nil {
return fmt.Errorf("%v %v %v %w", exch, item, pair, ErrPositionNotFound)
}
newMPT, err := SetupMultiPositionTracker(&MultiPositionTrackerSetup{
Exchange: exch,
Asset: item,
Pair: pair,
Underlying: multiPositionTracker.underlying,
OfflineCalculation: multiPositionTracker.offlinePNLCalculation,
UseExchangePNLCalculation: multiPositionTracker.useExchangePNLCalculations,
ExchangePNLCalculation: multiPositionTracker.exchangePNLCalculation,
CollateralCurrency: multiPositionTracker.collateralCurrency,
Underlying: tracker.underlying,
OfflineCalculation: tracker.offlinePNLCalculation,
UseExchangePNLCalculation: tracker.useExchangePNLCalculations,
ExchangePNLCalculation: tracker.exchangePNLCalculation,
CollateralCurrency: tracker.collateralCurrency,
})
if err != nil {
return err
}
itemM[pair] = newMPT
c.multiPositionTrackers[exch][item][pair.Base.Item][pair.Quote.Item] = newMPT
return nil
}
// GetPositions returns all positions
func (m *MultiPositionTracker) GetPositions() []PositionStats {
func (m *MultiPositionTracker) GetPositions() []Position {
if m == nil {
return nil
}
m.m.Lock()
defer m.m.Unlock()
resp := make([]PositionStats, len(m.positions))
resp := make([]Position, len(m.positions))
for i := range m.positions {
resp[i] = m.positions[i].GetStats()
resp[i] = *m.positions[i].GetStats()
}
sort.Slice(resp, func(i, j int) bool {
return resp[i].OpeningDate.Before(resp[j].OpeningDate)
})
return resp
}
@@ -335,10 +371,18 @@ func (m *MultiPositionTracker) TrackNewOrder(d *Detail) error {
return fmt.Errorf("multi-position tracker %w", common.ErrNilPointer)
}
if d == nil {
return ErrSubmissionIsNil
return fmt.Errorf("order detail %w", common.ErrNilPointer)
}
var err error
d.Exchange, err = checkTrackerPrerequisitesLowerExchange(d.Exchange, d.AssetType, d.Pair)
if err != nil {
return err
}
m.m.Lock()
defer m.m.Unlock()
if m.exchange != d.Exchange {
return fmt.Errorf("%w received %v expected %v", errExchangeNameMismatch, d.Exchange, m.exchange)
}
if d.AssetType != m.asset {
return errAssetMismatch
}
@@ -354,8 +398,8 @@ func (m *MultiPositionTracker) TrackNewOrder(d *Detail) error {
}
}
if m.positions[len(m.positions)-1].status == Open {
err := m.positions[len(m.positions)-1].TrackNewOrder(d, false)
if err != nil && !errors.Is(err, ErrPositionClosed) {
err = m.positions[len(m.positions)-1].TrackNewOrder(d, false)
if err != nil {
return err
}
m.orderPositions[d.OrderID] = m.positions[len(m.positions)-1]
@@ -366,12 +410,15 @@ func (m *MultiPositionTracker) TrackNewOrder(d *Detail) error {
Pair: d.Pair,
EntryPrice: decimal.NewFromFloat(d.Price),
Underlying: d.Pair.Base,
CollateralCurrency: m.collateralCurrency,
Asset: d.AssetType,
Side: d.Side,
UseExchangePNLCalculation: m.useExchangePNLCalculations,
CollateralCurrency: m.collateralCurrency,
OfflineCalculation: m.offlinePNLCalculation,
PNLCalculator: m.exchangePNLCalculation,
Exchange: m.exchange,
}
tracker, err := m.SetupPositionTracker(setup)
tracker, err := SetupPositionTracker(setup)
if err != nil {
return err
}
@@ -384,6 +431,75 @@ func (m *MultiPositionTracker) TrackNewOrder(d *Detail) error {
return nil
}
// TrackFundingDetails applies funding rate details to a tracked position
func (m *MultiPositionTracker) TrackFundingDetails(d *FundingRates) error {
if m == nil {
return fmt.Errorf("multi-position tracker %w", common.ErrNilPointer)
}
if d == nil {
return fmt.Errorf("%w FundingRates", common.ErrNilPointer)
}
var err error
d.Exchange, err = checkTrackerPrerequisitesLowerExchange(d.Exchange, d.Asset, d.Pair)
if err != nil {
return err
}
m.m.Lock()
defer m.m.Unlock()
if m.exchange != d.Exchange {
return fmt.Errorf("%w received '%v' expected '%v'", errExchangeNameMismatch, d.Exchange, m.exchange)
}
if d.Asset != m.asset {
return fmt.Errorf("%w tracker: %v supplied: %v", errAssetMismatch, m.asset, d.Asset)
}
if len(m.positions) == 0 {
return fmt.Errorf("%w %v %v %v", ErrPositionNotFound, d.Exchange, d.Asset, d.Pair)
}
for i := range m.positions {
err = m.positions[i].TrackFundingDetails(d)
if err != nil {
return err
}
}
return nil
}
// SetupPositionTracker creates a new position tracker to track n futures orders
// until the position(s) are closed
func SetupPositionTracker(setup *PositionTrackerSetup) (*PositionTracker, error) {
if setup == nil {
return nil, errNilSetup
}
var err error
setup.Exchange, err = checkTrackerPrerequisitesLowerExchange(setup.Exchange, setup.Asset, setup.Pair)
if err != nil {
return nil, err
}
resp := &PositionTracker{
exchange: setup.Exchange,
asset: setup.Asset,
contractPair: setup.Pair,
underlying: setup.Underlying,
status: Open,
openingPrice: setup.EntryPrice,
latestDirection: setup.Side,
openingDirection: setup.Side,
useExchangePNLCalculation: setup.UseExchangePNLCalculation,
offlinePNLCalculation: setup.OfflineCalculation,
lastUpdated: time.Now(),
}
if !setup.UseExchangePNLCalculation {
// use position tracker's pnl calculation by default
resp.PNLCalculation = &PNLCalculator{}
} else {
if setup.PNLCalculator == nil {
return nil, ErrNilPNLCalculator
}
resp.PNLCalculation = setup.PNLCalculator
}
return resp, nil
}
// Liquidate will update the latest open position's
// to reflect its liquidated status
func (m *MultiPositionTracker) Liquidate(price decimal.Decimal, t time.Time) error {
@@ -393,39 +509,63 @@ func (m *MultiPositionTracker) Liquidate(price decimal.Decimal, t time.Time) err
m.m.Lock()
defer m.m.Unlock()
if len(m.positions) == 0 {
return fmt.Errorf("%v %v %v %w", m.exchange, m.asset, m.pair, ErrPositionsNotLoadedForPair)
return fmt.Errorf("%v %v %v %w", m.exchange, m.asset, m.pair, ErrPositionNotFound)
}
return m.positions[len(m.positions)-1].Liquidate(price, t)
}
// GetStats returns a summary of a future position
func (p *PositionTracker) GetStats() PositionStats {
func (p *PositionTracker) GetStats() *Position {
if p == nil {
return PositionStats{}
return nil
}
p.m.Lock()
defer p.m.Unlock()
var orders []Detail
orders = append(orders, p.longPositions...)
orders = append(orders, p.shortPositions...)
sort.Slice(orders, func(i, j int) bool {
return orders[i].Date.Before(orders[j].Date)
})
return PositionStats{
Exchange: p.exchange,
Asset: p.asset,
Pair: p.contractPair,
Underlying: p.underlyingAsset,
CollateralCurrency: p.collateralCurrency,
Status: p.status,
Orders: orders,
RealisedPNL: p.realisedPNL,
UnrealisedPNL: p.unrealisedPNL,
LatestDirection: p.currentDirection,
OpeningDirection: p.openingDirection,
OpeningPrice: p.entryPrice,
LatestPrice: p.latestPrice,
PNLHistory: p.pnlHistory,
Exposure: p.exposure,
pos := &Position{
Exchange: p.exchange,
Asset: p.asset,
Pair: p.contractPair,
Underlying: p.underlying,
RealisedPNL: p.realisedPNL,
UnrealisedPNL: p.unrealisedPNL,
Status: p.status,
OpeningDate: p.openingDate,
OpeningPrice: p.openingPrice,
OpeningSize: p.openingSize,
OpeningDirection: p.openingDirection,
LatestPrice: p.latestPrice,
LatestSize: p.exposure,
LatestDirection: p.latestDirection,
CloseDate: p.closingDate,
Orders: orders,
PNLHistory: p.pnlHistory,
LastUpdated: p.lastUpdated,
}
if p.fundingRateDetails != nil {
frs := make([]FundingRate, len(p.fundingRateDetails.FundingRates))
copy(frs, p.fundingRateDetails.FundingRates)
pos.FundingRates = FundingRates{
Exchange: p.fundingRateDetails.Exchange,
Asset: p.fundingRateDetails.Asset,
Pair: p.fundingRateDetails.Pair,
StartDate: p.fundingRateDetails.StartDate,
EndDate: p.fundingRateDetails.EndDate,
LatestRate: p.fundingRateDetails.LatestRate,
PredictedUpcomingRate: p.fundingRateDetails.PredictedUpcomingRate,
FundingRates: frs,
PaymentSum: p.fundingRateDetails.PaymentSum,
}
}
return pos
}
// TrackPNLByTime calculates the PNL based on a position tracker's exposure
@@ -445,11 +585,11 @@ func (p *PositionTracker) TrackPNLByTime(t time.Time, currentPrice float64) erro
Price: price,
Status: p.status,
}
if p.currentDirection.IsLong() {
diff := price.Sub(p.entryPrice)
if p.latestDirection.IsLong() {
diff := price.Sub(p.openingPrice)
result.UnrealisedPNL = p.exposure.Mul(diff)
} else if p.currentDirection.IsShort() {
diff := p.entryPrice.Sub(price)
} else if p.latestDirection.IsShort() {
diff := p.openingPrice.Sub(price)
result.UnrealisedPNL = p.exposure.Mul(diff)
}
if len(p.pnlHistory) > 0 {
@@ -463,6 +603,8 @@ func (p *PositionTracker) TrackPNLByTime(t time.Time, currentPrice float64) erro
var err error
p.pnlHistory, err = upsertPNLEntry(p.pnlHistory, result)
p.unrealisedPNL = result.UnrealisedPNL
p.lastUpdated = time.Now()
return err
}
@@ -492,7 +634,7 @@ func (p *PositionTracker) Liquidate(price decimal.Decimal, t time.Time) error {
return fmt.Errorf("%w cannot liquidate from a different time. PNL snapshot %v. Liquidation request on %v Status: %v", errCannotLiquidate, latest.Time, t, p.status)
}
p.status = Liquidated
p.currentDirection = ClosePosition
p.latestDirection = ClosePosition
p.exposure = decimal.Zero
p.realisedPNL = decimal.Zero
p.unrealisedPNL = decimal.Zero
@@ -517,19 +659,100 @@ func (p *PositionTracker) GetLatestPNLSnapshot() (PNLResult, error) {
return p.pnlHistory[len(p.pnlHistory)-1], nil
}
// TrackFundingDetails sets funding rates to a position
func (p *PositionTracker) TrackFundingDetails(d *FundingRates) error {
if p == nil {
return fmt.Errorf("position tracker %w", common.ErrNilPointer)
}
if d == nil {
return fmt.Errorf("funding rate details %w", common.ErrNilPointer)
}
var err error
d.Exchange, err = checkTrackerPrerequisitesLowerExchange(d.Exchange, d.Asset, d.Pair)
if err != nil {
return err
}
p.m.Lock()
defer p.m.Unlock()
if p.exchange != d.Exchange ||
p.asset != d.Asset ||
!p.contractPair.Equal(d.Pair) {
return fmt.Errorf("provided details %v %v %v %w %v %v %v tracker",
d.Exchange, d.Asset, d.Pair, errDoesntMatch, p.exchange, p.asset, p.contractPair)
}
if err := common.StartEndTimeCheck(d.StartDate, d.EndDate); err != nil && !errors.Is(err, common.ErrStartEqualsEnd) {
// start end being equal is valid if only one funding rate is retrieved
return err
}
if len(p.pnlHistory) == 0 {
return fmt.Errorf("%w for timeframe %v %v %v %v-%v", ErrNoPositionsFound, p.exchange, p.asset, p.contractPair, d.StartDate, d.EndDate)
}
if p.fundingRateDetails == nil {
p.fundingRateDetails = &FundingRates{
Exchange: d.Exchange,
Asset: d.Asset,
Pair: d.Pair,
StartDate: d.StartDate,
EndDate: d.EndDate,
LatestRate: d.LatestRate,
PredictedUpcomingRate: d.PredictedUpcomingRate,
PaymentSum: d.PaymentSum,
}
}
rates := make([]FundingRate, 0, len(d.FundingRates))
fundingRates:
for i := range d.FundingRates {
if d.FundingRates[i].Time.Before(p.openingDate) ||
(!p.closingDate.IsZero() && d.FundingRates[i].Time.After(p.closingDate)) {
continue
}
for j := range p.fundingRateDetails.FundingRates {
if !p.fundingRateDetails.FundingRates[j].Time.Equal(d.FundingRates[i].Time) {
continue
}
p.fundingRateDetails.FundingRates[j] = d.FundingRates[i]
continue fundingRates
}
rates = append(rates, d.FundingRates[i])
}
p.fundingRateDetails.FundingRates = append(p.fundingRateDetails.FundingRates, rates...)
p.lastUpdated = time.Now()
return nil
}
// TrackNewOrder knows how things are going for a given
// futures contract
func (p *PositionTracker) TrackNewOrder(d *Detail, isInitialOrder bool) error {
if p == nil {
return fmt.Errorf("position tracker %w", common.ErrNilPointer)
}
if d == nil {
return fmt.Errorf("order %w", common.ErrNilPointer)
}
var err error
d.Exchange, err = checkTrackerPrerequisitesLowerExchange(d.Exchange, d.AssetType, d.Pair)
if err != nil {
return err
}
p.m.Lock()
defer p.m.Unlock()
if isInitialOrder && len(p.pnlHistory) > 0 {
return fmt.Errorf("%w received isInitialOrder = true with existing position", errCannotTrackInvalidParams)
}
if p.status.IsInactive() {
return ErrPositionClosed
for i := range p.longPositions {
if p.longPositions[i].OrderID == d.OrderID {
return nil
}
}
for i := range p.shortPositions {
if p.shortPositions[i].OrderID == d.OrderID {
return nil
}
}
// adding a new position to something that is already closed
return fmt.Errorf("%w cannot process new order %v", ErrPositionClosed, d.OrderID)
}
if d == nil {
return ErrSubmissionIsNil
@@ -538,7 +761,7 @@ func (p *PositionTracker) TrackNewOrder(d *Detail, isInitialOrder bool) error {
return fmt.Errorf("%w pair '%v' received: '%v'",
errOrderNotEqualToTracker, d.Pair, p.contractPair)
}
if !strings.EqualFold(p.exchange, d.Exchange) {
if p.exchange != d.Exchange {
return fmt.Errorf("%w exchange '%v' received: '%v'",
errOrderNotEqualToTracker, d.Exchange, p.exchange)
}
@@ -558,7 +781,9 @@ func (p *PositionTracker) TrackNewOrder(d *Detail, isInitialOrder bool) error {
errTimeUnset, d.Exchange, d.AssetType, d.Pair, d.OrderID)
}
if len(p.shortPositions) == 0 && len(p.longPositions) == 0 {
p.entryPrice = decimal.NewFromFloat(d.Price)
p.openingPrice = decimal.NewFromFloat(d.Price)
p.openingSize = decimal.NewFromFloat(d.Amount)
p.openingDate = d.Date
}
var updated bool
@@ -567,12 +792,13 @@ func (p *PositionTracker) TrackNewOrder(d *Detail, isInitialOrder bool) error {
continue
}
ord := p.shortPositions[i].Copy()
err := ord.UpdateOrderFromDetail(d)
err = ord.UpdateOrderFromDetail(d)
if err != nil {
return err
}
p.shortPositions[i] = ord
updated = true
p.lastUpdated = time.Now()
break
}
for i := range p.longPositions {
@@ -580,12 +806,13 @@ func (p *PositionTracker) TrackNewOrder(d *Detail, isInitialOrder bool) error {
continue
}
ord := p.longPositions[i].Copy()
err := ord.UpdateOrderFromDetail(d)
err = ord.UpdateOrderFromDetail(d)
if err != nil {
return err
}
p.longPositions[i] = ord
updated = true
p.lastUpdated = time.Now()
break
}
@@ -597,7 +824,6 @@ func (p *PositionTracker) TrackNewOrder(d *Detail, isInitialOrder bool) error {
}
}
var shortSide, longSide decimal.Decimal
for i := range p.shortPositions {
shortSide = shortSide.Add(decimal.NewFromFloat(p.shortPositions[i].Amount))
}
@@ -607,27 +833,26 @@ func (p *PositionTracker) TrackNewOrder(d *Detail, isInitialOrder bool) error {
if isInitialOrder {
p.openingDirection = d.Side
p.currentDirection = d.Side
p.latestDirection = d.Side
}
var result *PNLResult
var err error
var price, amount, leverage decimal.Decimal
price = decimal.NewFromFloat(d.Price)
amount = decimal.NewFromFloat(d.Amount)
leverage = decimal.NewFromFloat(d.Leverage)
cal := &PNLCalculatorRequest{
Underlying: p.underlyingAsset,
Underlying: p.underlying,
Asset: p.asset,
OrderDirection: d.Side,
Leverage: leverage,
EntryPrice: p.entryPrice,
EntryPrice: p.openingPrice,
Amount: amount,
CurrentPrice: price,
Pair: p.contractPair,
Time: d.Date,
OpeningDirection: p.openingDirection,
CurrentDirection: p.currentDirection,
CurrentDirection: p.latestDirection,
PNLHistory: p.pnlHistory,
Exposure: p.exposure,
Fee: decimal.NewFromFloat(d.Fee),
@@ -690,6 +915,8 @@ func (p *PositionTracker) TrackNewOrder(d *Detail, isInitialOrder bool) error {
}
result.UnrealisedPNL = decimal.Zero
result.RealisedPNLBeforeFees = decimal.Zero
p.closingPrice = result.Price
p.closingDate = result.Time
p.status = Closed
}
result.Status = p.status
@@ -701,14 +928,14 @@ func (p *PositionTracker) TrackNewOrder(d *Detail, isInitialOrder bool) error {
switch {
case longSide.GreaterThan(shortSide):
p.currentDirection = Long
p.latestDirection = Long
case shortSide.GreaterThan(longSide):
p.currentDirection = Short
p.latestDirection = Short
default:
p.currentDirection = ClosePosition
p.latestDirection = ClosePosition
}
if p.currentDirection.IsLong() {
if p.latestDirection.IsLong() {
p.exposure = longSide.Sub(shortSide)
} else {
p.exposure = shortSide.Sub(longSide)
@@ -721,12 +948,13 @@ func (p *PositionTracker) TrackNewOrder(d *Detail, isInitialOrder bool) error {
p.unrealisedPNL = decimal.Zero
p.pnlHistory[len(p.pnlHistory)-1].RealisedPNL = p.realisedPNL
p.pnlHistory[len(p.pnlHistory)-1].UnrealisedPNL = p.unrealisedPNL
p.pnlHistory[len(p.pnlHistory)-1].Direction = p.currentDirection
p.pnlHistory[len(p.pnlHistory)-1].Direction = p.latestDirection
p.closingDate = d.Date
} else if p.exposure.IsNegative() {
if p.currentDirection.IsLong() {
p.currentDirection = Short
if p.latestDirection.IsLong() {
p.latestDirection = Short
} else {
p.currentDirection = Long
p.latestDirection = Long
}
p.exposure = p.exposure.Abs()
}
@@ -854,3 +1082,29 @@ func upsertPNLEntry(pnlHistory []PNLResult, entry *PNLResult) ([]PNLResult, erro
})
return pnlHistory, nil
}
// CheckFundingRatePrerequisites is a simple check to see if the requested data meets the prerequisite
func CheckFundingRatePrerequisites(getFundingData, includePredicted, includePayments bool) error {
if !getFundingData && includePredicted {
return fmt.Errorf("%w please include in request to get predicted funding rates", ErrGetFundingDataRequired)
}
if !getFundingData && includePayments {
return fmt.Errorf("%w please include in request to get predicted funding rates", ErrGetFundingDataRequired)
}
return nil
}
// checkTrackerPrerequisitesLowerExchange is a common set of checks for futures position tracking
func checkTrackerPrerequisitesLowerExchange(exch string, item asset.Item, cp currency.Pair) (string, error) {
if exch == "" {
return "", errExchangeNameEmpty
}
exch = strings.ToLower(exch)
if !item.IsFutures() {
return exch, fmt.Errorf("%w %v %v %v", ErrNotFuturesAsset, exch, item, cp)
}
if cp.IsEmpty() {
return exch, fmt.Errorf("%w %v %v", ErrPairIsEmpty, exch, item)
}
return exch, nil
}

File diff suppressed because it is too large Load Diff

View File

@@ -14,12 +14,6 @@ import (
var (
// ErrPositionClosed returned when attempting to amend a closed position
ErrPositionClosed = errors.New("the position is closed")
// ErrPositionsNotLoadedForExchange returned when no position data exists for an exchange
ErrPositionsNotLoadedForExchange = errors.New("no positions loaded for exchange")
// ErrPositionsNotLoadedForAsset returned when no position data exists for an asset
ErrPositionsNotLoadedForAsset = errors.New("no positions loaded for asset")
// ErrPositionsNotLoadedForPair returned when no position data exists for a pair
ErrPositionsNotLoadedForPair = errors.New("no positions loaded for pair")
// ErrNilPNLCalculator is raised when pnl calculation is requested for
// an exchange, but the fields are not set properly
ErrNilPNLCalculator = errors.New("nil pnl calculator received")
@@ -32,8 +26,17 @@ var (
ErrUSDValueRequired = errors.New("USD value required")
// ErrOfflineCalculationSet is raised when collateral calculation is set to be offline, yet is attempted online
ErrOfflineCalculationSet = errors.New("offline calculation set")
// ErrPositionNotFound is raised when a position is not found
ErrPositionNotFound = errors.New("position not found")
// ErrNotPerpetualFuture is returned when a currency is not a perpetual future
ErrNotPerpetualFuture = errors.New("not a perpetual future")
// ErrNoPositionsFound returned when there is no positions returned
ErrNoPositionsFound = errors.New("no positions found")
// ErrGetFundingDataRequired is returned when requesting funding rate data without the prerequisite
ErrGetFundingDataRequired = errors.New("getfundingdata is a prerequisite")
errExchangeNameEmpty = errors.New("exchange name empty")
errExchangeNameMismatch = errors.New("exchange name mismatch")
errTimeUnset = errors.New("time unset")
errMissingPNLCalculationFunctions = errors.New("futures tracker requires exchange PNL calculation functions")
errOrderNotEqualToTracker = errors.New("order does not match tracker data")
@@ -44,6 +47,7 @@ var (
errNilOrder = errors.New("nil order received")
errNoPNLHistory = errors.New("no pnl history")
errCannotCalculateUnrealisedPNL = errors.New("cannot calculate unrealised PNL")
errDoesntMatch = errors.New("doesn't match")
errCannotTrackInvalidParams = errors.New("parameters set incorrectly, cannot track")
)
@@ -54,15 +58,6 @@ type PNLCalculation interface {
GetCurrencyForRealisedPNL(realisedAsset asset.Item, realisedPair currency.Pair) (currency.Code, asset.Item, error)
}
// CollateralManagement is an interface that allows
// multiple ways of calculating the size of collateral
// on an exchange
type CollateralManagement interface {
GetCollateralCurrencyForContract(asset.Item, currency.Pair) (currency.Code, asset.Item, error)
ScaleCollateral(ctx context.Context, calculator *CollateralCalculator) (*CollateralByCurrency, error)
CalculateTotalCollateral(context.Context, *TotalCollateralCalculator) (*TotalCollateralResponse, error)
}
// TotalCollateralResponse holds all collateral
type TotalCollateralResponse struct {
CollateralCurrency currency.Code
@@ -129,7 +124,8 @@ type UsedCollateralBreakdown struct {
// the position controller and its all tracked happily
type PositionController struct {
m sync.Mutex
multiPositionTrackers map[string]map[asset.Item]map[currency.Pair]*MultiPositionTracker
multiPositionTrackers map[string]map[asset.Item]map[*currency.Item]map[*currency.Item]*MultiPositionTracker
updated time.Time
}
// MultiPositionTracker will track the performance of
@@ -173,39 +169,47 @@ type MultiPositionTrackerSetup struct {
// completely within this position tracker, however, can still provide a good
// timeline of performance until the position is closed
type PositionTracker struct {
m sync.Mutex
exchange string
asset asset.Item
contractPair currency.Pair
underlyingAsset currency.Code
collateralCurrency currency.Code
exposure decimal.Decimal
currentDirection Side
openingDirection Side
status Status
unrealisedPNL decimal.Decimal
realisedPNL decimal.Decimal
shortPositions []Detail
longPositions []Detail
pnlHistory []PNLResult
entryPrice decimal.Decimal
closingPrice decimal.Decimal
offlinePNLCalculation bool
PNLCalculation
latestPrice decimal.Decimal
m sync.Mutex
useExchangePNLCalculation bool
collateralCurrency currency.Code
offlinePNLCalculation bool
PNLCalculation
exchange string
asset asset.Item
contractPair currency.Pair
underlying currency.Code
exposure decimal.Decimal
openingDirection Side
openingPrice decimal.Decimal
openingSize decimal.Decimal
openingDate time.Time
latestDirection Side
latestPrice decimal.Decimal
lastUpdated time.Time
unrealisedPNL decimal.Decimal
realisedPNL decimal.Decimal
status Status
closingPrice decimal.Decimal
closingDate time.Time
shortPositions []Detail
longPositions []Detail
pnlHistory []PNLResult
fundingRateDetails *FundingRates
}
// PositionTrackerSetup contains all required fields to
// setup a position tracker
type PositionTrackerSetup struct {
Exchange string
Asset asset.Item
Pair currency.Pair
EntryPrice decimal.Decimal
Underlying currency.Code
CollateralCurrency currency.Code
Asset asset.Item
Side Side
UseExchangePNLCalculation bool
OfflineCalculation bool
PNLCalculator PNLCalculation
}
// TotalCollateralCalculator holds many collateral calculators
@@ -277,22 +281,111 @@ type PNLResult struct {
IsOrder bool
}
// PositionStats is a basic holder
// for position information
type PositionStats struct {
// Position is a basic holder for position information
type Position struct {
Exchange string
Asset asset.Item
Pair currency.Pair
Underlying currency.Code
CollateralCurrency currency.Code
Orders []Detail
RealisedPNL decimal.Decimal
UnrealisedPNL decimal.Decimal
Exposure decimal.Decimal
LatestDirection Side
Status Status
OpeningDirection Side
OpeningDate time.Time
OpeningPrice decimal.Decimal
OpeningSize decimal.Decimal
OpeningDirection Side
LatestPrice decimal.Decimal
LatestSize decimal.Decimal
LatestDirection Side
LastUpdated time.Time
CloseDate time.Time
Orders []Detail
PNLHistory []PNLResult
FundingRates FundingRates
}
// PositionSummaryRequest is used to request a summary of an open position
type PositionSummaryRequest struct {
Asset asset.Item
Pair 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
}
// FundingRatesRequest is used to request funding rate details for a position
type FundingRatesRequest struct {
Asset asset.Item
Pairs currency.Pairs
StartDate time.Time
EndDate time.Time
IncludePayments bool
IncludePredictedRate bool
}
// FundingRates is used to return funding rate details for a position
type FundingRates struct {
Exchange string
Asset asset.Item
Pair currency.Pair
StartDate time.Time
EndDate time.Time
LatestRate FundingRate
PredictedUpcomingRate FundingRate
FundingRates []FundingRate
PaymentSum decimal.Decimal
}
// FundingRate holds details for an individual funding rate
type FundingRate struct {
Time time.Time
Rate decimal.Decimal
Payment decimal.Decimal
}
// PositionDetails are used to track open positions
// in the order manager
type PositionDetails struct {
Exchange string
Asset asset.Item
Pair currency.Pair
Orders []Detail
}
// PositionsRequest defines the request to
// retrieve futures position data
type PositionsRequest struct {
Asset asset.Item
Pairs currency.Pairs
StartDate time.Time
}