mirror of
https://github.com/d0zingcat/gocryptotrader.git
synced 2026-06-02 07:26:53 +00:00
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:
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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"`
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user