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

* Adds basic PoC for calculating/retrieving position data

* A very unfortunate day of miscalculations

* Adds position summary and funding rate details to RPC

* Offline funding rate calculations

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

* Adds new rpc server commands and attempts some organisation

* lower string, lower stress

* Adds ordermanager config. Fleshes outcli. Tracks positions automatically

* Adds new separation for funding payments/rates

* Combines funding rates and payments

* Fun test coverage

* ALL THE TESTS... I hope

* Fixes

* polishes ftx tests. improves perp check. Loops rates

* Final touches before nit attax

* buff 💪

* Stops NotYetImplemented spam with one simple trick!

* Some lovely little niteroos

* linteroo

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

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

* Fixes order manager handling when no rates are available yet

* Continues on no funding rates instead. Removes err

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

* Addresses scenario with no funding rate payments

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

* Adds a pair key type

* Polishes pKey, fixes map order bug

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

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

* Shakes up the design of things by removing a function

* Fixes issues with order manager positions. Limits update range

* Fixes build issues. Identification of bad tests.

* Merges and fixes features from master and this branch

* buff linter 💪

* re-gen

* proto regen

* Addresses some nits. But not all of them.

* Fixes issue where funding rates weren't returned 🎉

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

* who did that? not me

* removes redundant check on account of being redundant and unnecessary

* so buf

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

* fixes minor mistakes

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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