mirror of
https://github.com/d0zingcat/gocryptotrader.git
synced 2026-05-17 15:09:59 +00:00
* implements futures functions and GRPC functions on new branch * lint and test fixes * Fix uneven split pnl. Adds collateral weight test. docs. New clear func * Test protection if someone has zero collateral * Uses string instead of double for accuracy * Fixes old code panic * context, match, docs * Addresses Shazniterinos, var names, expanded tests * Returns subaccount name, provides USD values when offlinecalc * Fixes oopsie * Fixes cool bug which allowed made up subaccount results * Subaccount override on FTX, subaccount results for collateral * Strenghten collateral account info checks. Improve FTX test * English is my first language * Fixes oopsies * Adds some conceptual futures order details to track PNL * Initial design of future order processing in the backtester * Introduces futures concept for collateral and spot/futures config diffs * Fixes most tests * Simple designs for collateral funding pair concept * Expands interface use so much it hurts * Implements more collateral interfaces * Adds liquidation, adds strategy, struggles with Binance * Attempts at getting FTX to work * Adds calculatePNL as a wrapper function and adds an `IsFutures` asset check * Successfully loads backtester with collateral currency * Fails to really get much going for supporting futures * Merges master changes * Fleshes out how FTX processes collateral * Further FTX collateral workings * hooks up more ftx collateral and pnl calculations * more funcs to flesh out handling * Adds more links, just can't fit the pieces together :( * Greatly expands futures order processing * Fleshes out position tracker to also handle asset and exchange +testing * RM linkedOrderID. rn positioncontroller, unexport * Successfully tracks futures order positions * Fails to calculate PNL * Calculates pnl from orders accurately with exception to flipping orders * Calculates PNL from orders * Adds another controller layer to make it ez from orderstore * Backtester now compiles. Adds test coverage * labels things add scaling collateral test * Calculates pnl in line with fees * Mostly accurate PNL, with exception to appending with diff prices * Adds locks, adds rpc function * grpc implementations * Gracefully handles rpc function * beautiful tests! * rejiggles tests to polish * Finishes FTX testing, adds comments * Exposes collateral calculations to rpc * Adds commands and testing for rpcserver.go functions * Increase testing and fix up backtester code * Returns cool changes to original branch * end of day fixes * Fixing some tests * Fixing tests 🎉 * Fixes all the tests * Splits the backtester setup and running into different files * Merge, minor fixes * Messing with some strategy updates * Failed understanding at collateral usage * Begins the creation of cash and carry strategy * Adds underlying pair, adds filldependentevent for futures * Completes fill prerequsite event implementation. Can't short though * Some bug fixes * investigating funds * CAN NOW CREATE A SHORT ORDER * Minor change in short size * Fixes for unrealised PNL & collateral rendering * Fixes lint and tests * Adds some verbosity * Updates to pnl calc * Tracks pnl for short orders, minor update to strategy * Close and open event based on conditions * Adds pnl data for currency statistics * Working through PNL calculation automatically. Now panics * Adds tracking, is blocked from design * Work to flesh out closing a position * vain attempts at tracking zeroing out bugs * woww, super fun new subloggers 🎉 * Begins attempt at automatically handling contracts and collateral based on direction * Merge master + fixes * Investigating issues with pnl and holdings * Minor pnl fixes * Fixes future position sizing, needs contract sizing * Can render pnl results, focussing on funding statistics * tracking candles for futures, but why not btc * Improves funding statistics * Colours and stats * Fixes collateral and snapshot bugs * Completes test * Fixes totals bug * Fix double buy, expand stats, fixes usd totals, introduce interface * Begins report formatting and calculations * Appends pnl to receiving curr. Fixes map[time]. accurate USD * Improves report output rendering * PNL stats in report. New tests for futures * Fixes existing tests before adding new coverage * Test coverage * Completes portfolio coverage * Increase coverage exchange, portfolio. fix size bug. NEW CHART * WHAT IS GOING ON WITH PNL * Fixes PNL calculation. Adds ability to skip om futures tracking * minor commit before merge * Adds basic liquidation to backtester * Changes liquidation to order based * Liquidationnnnnn * Further fleshes out liquidations * Completes liquidations in a honorable manner. Adds AppendReasonf * Beginnings of spot futures gap chart. Needs to link currencies to render difference * Removes fake liquidation. Adds cool new chart * Fixes somet tests,allows for zero fee value v nil distinction,New tests * Some annoying test fixes that took too long * portfolio coverage * holding coverage, privatisation funding * Testwork * boring tests * engine coverage * More backtesting coverage * Funding, strategy, report test coverage * Completes coverage of report package * Documentation, fixes some assumptions on asset errors * Changes before master merge * Lint and Tests * defaults to non-coloured rendering * Chart rendering * Fixes surprise non-local-lints * Niterinos to the extremeos * Fixes merge problems * The linter splintered across the glinting plinths * Many nits addressed. Now sells spot position on final candle * Adds forgotten coverage * Adds ability to size futures contracts to match spot positions. * fixes order sell sizing * Adds tests to sizing. Fixes charting issue * clint splintered the linters with flint * Improves stats, stat rendering * minifix * Fixes tests and fee bug * Merge fixeroos * Microfixes * Updates orderPNL on first Correctly utilises fees. Adds committed funds * New base funcs. New order summary * Fun test updates * Fix logo colouring * Fixes niteroonies * Fix report * BAD COMMIT * Fixes funding issues.Updates default fee rates.Combines cashcarry case * doc regen * Now returns err * Fixes sizing bug issue introduced in PR * Fixes fun fee/total US value bug * Fix chart bug. Show log charts with disclaimer * sellside fee * fixes fee and slippage view * Fixed slippage price issue * Fixes calculation and removes rendering * Fixes stats and some rendering * Merge fix * Fixes merge issues * go mod tidy, lint updates * New linter attempt * Version bump in appveyor and makefile * Regex filename, config fixes, template h2 fixes * Removes bad stats. * neatens config builder. Moves filename generator * Fixes issue where linter wants to fix my spelling * Fixes pointers and starts
857 lines
27 KiB
Go
857 lines
27 KiB
Go
package order
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"sort"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/shopspring/decimal"
|
|
"github.com/thrasher-corp/gocryptotrader/common"
|
|
"github.com/thrasher-corp/gocryptotrader/currency"
|
|
"github.com/thrasher-corp/gocryptotrader/exchanges/asset"
|
|
)
|
|
|
|
// 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),
|
|
}
|
|
}
|
|
|
|
// TrackNewOrder sets up the maps to then create a
|
|
// 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
|
|
}
|
|
var err error
|
|
multiPositionTracker, ok := itemM[d.Pair]
|
|
if !ok {
|
|
multiPositionTracker, err = SetupMultiPositionTracker(&MultiPositionTrackerSetup{
|
|
Exchange: strings.ToLower(d.Exchange),
|
|
Asset: d.AssetType,
|
|
Pair: d.Pair,
|
|
Underlying: d.Pair.Base,
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
itemM[d.Pair] = multiPositionTracker
|
|
}
|
|
return multiPositionTracker.TrackNewOrder(d)
|
|
}
|
|
|
|
// SetCollateralCurrency allows the setting of a collateral currency to all child trackers
|
|
// when using position controller for futures orders tracking
|
|
func (c *PositionController) SetCollateralCurrency(exch string, item asset.Item, pair currency.Pair, collateralCurrency currency.Code) error {
|
|
if c == nil {
|
|
return fmt.Errorf("position controller %w", common.ErrNilPointer)
|
|
}
|
|
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)
|
|
}
|
|
itemM, ok := exchM[item]
|
|
if !ok {
|
|
return fmt.Errorf("cannot set collateral %v for %v %v %v %w", collateralCurrency, exch, item, pair, ErrPositionsNotLoadedForAsset)
|
|
}
|
|
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) {
|
|
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)
|
|
}
|
|
exchM, ok := c.multiPositionTrackers[strings.ToLower(exch)]
|
|
if !ok {
|
|
return nil, fmt.Errorf("%v %v %v %w", exch, item, pair, ErrPositionsNotLoadedForExchange)
|
|
}
|
|
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
|
|
}
|
|
|
|
// UpdateOpenPositionUnrealisedPNL finds an open position from
|
|
// an exchange asset pair, then calculates the unrealisedPNL
|
|
// using the latest ticker data
|
|
func (c *PositionController) UpdateOpenPositionUnrealisedPNL(exch string, item asset.Item, pair currency.Pair, last float64, updated time.Time) (decimal.Decimal, error) {
|
|
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)
|
|
}
|
|
|
|
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)
|
|
}
|
|
|
|
multiPositionTracker.m.Lock()
|
|
defer multiPositionTracker.m.Unlock()
|
|
pos := multiPositionTracker.positions
|
|
if len(pos) == 0 {
|
|
return decimal.Zero, fmt.Errorf("%v %v %v %w", exch, item, pair, ErrPositionsNotLoadedForPair)
|
|
}
|
|
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)
|
|
if err != nil {
|
|
return decimal.Zero, fmt.Errorf("%w for position %v %v %v", err, exch, item, pair)
|
|
}
|
|
latestPos.m.Lock()
|
|
defer latestPos.m.Unlock()
|
|
return latestPos.unrealisedPNL, nil
|
|
}
|
|
|
|
// SetupMultiPositionTracker creates a futures order tracker for a specific exchange
|
|
func SetupMultiPositionTracker(setup *MultiPositionTrackerSetup) (*MultiPositionTracker, error) {
|
|
if setup == nil {
|
|
return nil, errNilSetup
|
|
}
|
|
if setup.Exchange == "" {
|
|
return nil, errExchangeNameEmpty
|
|
}
|
|
if !setup.Asset.IsValid() || !setup.Asset.IsFutures() {
|
|
return nil, ErrNotFuturesAsset
|
|
}
|
|
if setup.Pair.IsEmpty() {
|
|
return nil, ErrPairIsEmpty
|
|
}
|
|
if setup.Underlying.IsEmpty() {
|
|
return nil, errEmptyUnderlying
|
|
}
|
|
if setup.ExchangePNLCalculation == nil && setup.UseExchangePNLCalculation {
|
|
return nil, errMissingPNLCalculationFunctions
|
|
}
|
|
return &MultiPositionTracker{
|
|
exchange: strings.ToLower(setup.Exchange),
|
|
asset: setup.Asset,
|
|
pair: setup.Pair,
|
|
underlying: setup.Underlying,
|
|
offlinePNLCalculation: setup.OfflineCalculation,
|
|
orderPositions: make(map[string]*PositionTracker),
|
|
useExchangePNLCalculations: setup.UseExchangePNLCalculation,
|
|
exchangePNLCalculation: setup.ExchangePNLCalculation,
|
|
collateralCurrency: setup.CollateralCurrency,
|
|
}, 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) {
|
|
m.m.Lock()
|
|
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)
|
|
}
|
|
latestPos := pos[len(pos)-1]
|
|
if latestPos.status.IsInactive() {
|
|
return decimal.Zero, fmt.Errorf("%v %v %v %w", m.exchange, m.asset, m.pair, ErrPositionClosed)
|
|
}
|
|
err := latestPos.TrackPNLByTime(updated, last)
|
|
if err != nil {
|
|
return decimal.Zero, fmt.Errorf("%w for position %v %v %v", err, m.exchange, m.asset, m.pair)
|
|
}
|
|
latestPos.m.Lock()
|
|
defer latestPos.m.Unlock()
|
|
return latestPos.unrealisedPNL, nil
|
|
}
|
|
|
|
// ClearPositionsForExchange resets positions for an
|
|
// exchange, asset, pair that has been stored
|
|
func (c *PositionController) ClearPositionsForExchange(exch string, item asset.Item, pair currency.Pair) error {
|
|
if c == nil {
|
|
return fmt.Errorf("position controller %w", common.ErrNilPointer)
|
|
}
|
|
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)
|
|
}
|
|
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,
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
itemM[pair] = newMPT
|
|
return nil
|
|
}
|
|
|
|
// GetPositions returns all positions
|
|
func (m *MultiPositionTracker) GetPositions() []PositionStats {
|
|
if m == nil {
|
|
return nil
|
|
}
|
|
m.m.Lock()
|
|
defer m.m.Unlock()
|
|
resp := make([]PositionStats, len(m.positions))
|
|
for i := range m.positions {
|
|
resp[i] = m.positions[i].GetStats()
|
|
}
|
|
return resp
|
|
}
|
|
|
|
// TrackNewOrder upserts an order to the tracker and updates position
|
|
// status and exposure. PNL is calculated separately as it requires mark prices
|
|
func (m *MultiPositionTracker) TrackNewOrder(d *Detail) error {
|
|
if m == nil {
|
|
return fmt.Errorf("multi-position tracker %w", common.ErrNilPointer)
|
|
}
|
|
if d == nil {
|
|
return ErrSubmissionIsNil
|
|
}
|
|
m.m.Lock()
|
|
defer m.m.Unlock()
|
|
if d.AssetType != m.asset {
|
|
return errAssetMismatch
|
|
}
|
|
if tracker, ok := m.orderPositions[d.OrderID]; ok {
|
|
// this has already been associated
|
|
// update the tracker
|
|
return tracker.TrackNewOrder(d, false)
|
|
}
|
|
if len(m.positions) > 0 {
|
|
for i := range m.positions {
|
|
if m.positions[i].status == Open && i != len(m.positions)-1 {
|
|
return fmt.Errorf("%w %v at position %v/%v", errPositionDiscrepancy, m.positions[i], i, len(m.positions)-1)
|
|
}
|
|
}
|
|
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) {
|
|
return err
|
|
}
|
|
m.orderPositions[d.OrderID] = m.positions[len(m.positions)-1]
|
|
return nil
|
|
}
|
|
}
|
|
setup := &PositionTrackerSetup{
|
|
Pair: d.Pair,
|
|
EntryPrice: decimal.NewFromFloat(d.Price),
|
|
Underlying: d.Pair.Base,
|
|
Asset: d.AssetType,
|
|
Side: d.Side,
|
|
UseExchangePNLCalculation: m.useExchangePNLCalculations,
|
|
CollateralCurrency: m.collateralCurrency,
|
|
}
|
|
tracker, err := m.SetupPositionTracker(setup)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
m.positions = append(m.positions, tracker)
|
|
err = tracker.TrackNewOrder(d, true)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
m.orderPositions[d.OrderID] = tracker
|
|
return 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 {
|
|
if m == nil {
|
|
return fmt.Errorf("multi-position tracker %w", common.ErrNilPointer)
|
|
}
|
|
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 m.positions[len(m.positions)-1].Liquidate(price, t)
|
|
}
|
|
|
|
// GetStats returns a summary of a future position
|
|
func (p *PositionTracker) GetStats() PositionStats {
|
|
if p == nil {
|
|
return PositionStats{}
|
|
}
|
|
p.m.Lock()
|
|
defer p.m.Unlock()
|
|
var orders []Detail
|
|
orders = append(orders, p.longPositions...)
|
|
orders = append(orders, p.shortPositions...)
|
|
|
|
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,
|
|
}
|
|
}
|
|
|
|
// TrackPNLByTime calculates the PNL based on a position tracker's exposure
|
|
// and current pricing. Adds the entry to PNL history to track over time
|
|
func (p *PositionTracker) TrackPNLByTime(t time.Time, currentPrice float64) error {
|
|
if p == nil {
|
|
return fmt.Errorf("position tracker %w", common.ErrNilPointer)
|
|
}
|
|
p.m.Lock()
|
|
defer func() {
|
|
p.latestPrice = decimal.NewFromFloat(currentPrice)
|
|
p.m.Unlock()
|
|
}()
|
|
price := decimal.NewFromFloat(currentPrice)
|
|
result := &PNLResult{
|
|
Time: t,
|
|
Price: price,
|
|
Status: p.status,
|
|
}
|
|
if p.currentDirection.IsLong() {
|
|
diff := price.Sub(p.entryPrice)
|
|
result.UnrealisedPNL = p.exposure.Mul(diff)
|
|
} else if p.currentDirection.IsShort() {
|
|
diff := p.entryPrice.Sub(price)
|
|
result.UnrealisedPNL = p.exposure.Mul(diff)
|
|
}
|
|
if len(p.pnlHistory) > 0 {
|
|
latest := p.pnlHistory[len(p.pnlHistory)-1]
|
|
result.RealisedPNLBeforeFees = latest.RealisedPNLBeforeFees
|
|
result.Exposure = latest.Exposure
|
|
result.Direction = latest.Direction
|
|
result.RealisedPNL = latest.RealisedPNL
|
|
result.IsLiquidated = latest.IsLiquidated
|
|
}
|
|
var err error
|
|
p.pnlHistory, err = upsertPNLEntry(p.pnlHistory, result)
|
|
p.unrealisedPNL = result.UnrealisedPNL
|
|
return err
|
|
}
|
|
|
|
// GetRealisedPNL returns the realised pnl if the order
|
|
// is closed
|
|
func (p *PositionTracker) GetRealisedPNL() decimal.Decimal {
|
|
if p == nil {
|
|
return decimal.Zero
|
|
}
|
|
p.m.Lock()
|
|
defer p.m.Unlock()
|
|
return calculateRealisedPNL(p.pnlHistory)
|
|
}
|
|
|
|
// Liquidate will update the positions stats to reflect its liquidation
|
|
func (p *PositionTracker) Liquidate(price decimal.Decimal, t time.Time) error {
|
|
if p == nil {
|
|
return fmt.Errorf("position tracker %w", common.ErrNilPointer)
|
|
}
|
|
p.m.Lock()
|
|
defer p.m.Unlock()
|
|
latest, err := p.GetLatestPNLSnapshot()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if !latest.Time.Equal(t) {
|
|
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.exposure = decimal.Zero
|
|
p.realisedPNL = decimal.Zero
|
|
p.unrealisedPNL = decimal.Zero
|
|
_, err = upsertPNLEntry(p.pnlHistory, &PNLResult{
|
|
Time: t,
|
|
Price: price,
|
|
Direction: ClosePosition,
|
|
IsLiquidated: true,
|
|
IsOrder: true,
|
|
Status: p.status,
|
|
})
|
|
|
|
return err
|
|
}
|
|
|
|
// GetLatestPNLSnapshot takes the latest pnl history value
|
|
// and returns it
|
|
func (p *PositionTracker) GetLatestPNLSnapshot() (PNLResult, error) {
|
|
if len(p.pnlHistory) == 0 {
|
|
return PNLResult{}, fmt.Errorf("%v %v %v %w", p.exchange, p.asset, p.contractPair, errNoPNLHistory)
|
|
}
|
|
return p.pnlHistory[len(p.pnlHistory)-1], 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)
|
|
}
|
|
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
|
|
}
|
|
if d == nil {
|
|
return ErrSubmissionIsNil
|
|
}
|
|
if !p.contractPair.Equal(d.Pair) {
|
|
return fmt.Errorf("%w pair '%v' received: '%v'",
|
|
errOrderNotEqualToTracker, d.Pair, p.contractPair)
|
|
}
|
|
if !strings.EqualFold(p.exchange, d.Exchange) {
|
|
return fmt.Errorf("%w exchange '%v' received: '%v'",
|
|
errOrderNotEqualToTracker, d.Exchange, p.exchange)
|
|
}
|
|
if p.asset != d.AssetType {
|
|
return fmt.Errorf("%w asset '%v' received: '%v'",
|
|
errOrderNotEqualToTracker, d.AssetType, p.asset)
|
|
}
|
|
|
|
if d.Side == UnknownSide {
|
|
return ErrSideIsInvalid
|
|
}
|
|
if d.OrderID == "" {
|
|
return ErrOrderIDNotSet
|
|
}
|
|
if d.Date.IsZero() {
|
|
return fmt.Errorf("%w for %v %v %v order ID: %v unset",
|
|
errTimeUnset, d.Exchange, d.AssetType, d.Pair, d.OrderID)
|
|
}
|
|
if len(p.shortPositions) == 0 && len(p.longPositions) == 0 {
|
|
p.entryPrice = decimal.NewFromFloat(d.Price)
|
|
}
|
|
|
|
var updated bool
|
|
for i := range p.shortPositions {
|
|
if p.shortPositions[i].OrderID != d.OrderID {
|
|
continue
|
|
}
|
|
ord := p.shortPositions[i].Copy()
|
|
err := ord.UpdateOrderFromDetail(d)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
p.shortPositions[i] = ord
|
|
updated = true
|
|
break
|
|
}
|
|
for i := range p.longPositions {
|
|
if p.longPositions[i].OrderID != d.OrderID {
|
|
continue
|
|
}
|
|
ord := p.longPositions[i].Copy()
|
|
err := ord.UpdateOrderFromDetail(d)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
p.longPositions[i] = ord
|
|
updated = true
|
|
break
|
|
}
|
|
|
|
if !updated {
|
|
if d.Side.IsShort() {
|
|
p.shortPositions = append(p.shortPositions, d.Copy())
|
|
} else {
|
|
p.longPositions = append(p.longPositions, d.Copy())
|
|
}
|
|
}
|
|
var shortSide, longSide decimal.Decimal
|
|
|
|
for i := range p.shortPositions {
|
|
shortSide = shortSide.Add(decimal.NewFromFloat(p.shortPositions[i].Amount))
|
|
}
|
|
for i := range p.longPositions {
|
|
longSide = longSide.Add(decimal.NewFromFloat(p.longPositions[i].Amount))
|
|
}
|
|
|
|
if isInitialOrder {
|
|
p.openingDirection = d.Side
|
|
p.currentDirection = 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,
|
|
Asset: p.asset,
|
|
OrderDirection: d.Side,
|
|
Leverage: leverage,
|
|
EntryPrice: p.entryPrice,
|
|
Amount: amount,
|
|
CurrentPrice: price,
|
|
Pair: p.contractPair,
|
|
Time: d.Date,
|
|
OpeningDirection: p.openingDirection,
|
|
CurrentDirection: p.currentDirection,
|
|
PNLHistory: p.pnlHistory,
|
|
Exposure: p.exposure,
|
|
Fee: decimal.NewFromFloat(d.Fee),
|
|
CalculateOffline: p.offlinePNLCalculation,
|
|
}
|
|
if len(p.pnlHistory) != 0 {
|
|
cal.PreviousPrice = p.pnlHistory[len(p.pnlHistory)-1].Price
|
|
}
|
|
switch {
|
|
case isInitialOrder:
|
|
result = &PNLResult{
|
|
IsOrder: true,
|
|
Time: cal.Time,
|
|
Price: cal.CurrentPrice,
|
|
Exposure: cal.Amount,
|
|
Fee: cal.Fee,
|
|
Direction: cal.OpeningDirection,
|
|
UnrealisedPNL: cal.Fee.Neg(),
|
|
}
|
|
case (cal.OrderDirection.IsShort() && cal.CurrentDirection.IsLong() || cal.OrderDirection.IsLong() && cal.CurrentDirection.IsShort()) && cal.Exposure.LessThan(amount):
|
|
// latest order swaps directions!
|
|
// split the order to calculate PNL from each direction
|
|
first := cal.Exposure
|
|
second := amount.Sub(cal.Exposure)
|
|
baseFee := cal.Fee.Div(amount)
|
|
cal.Fee = baseFee.Mul(first)
|
|
cal.Amount = first
|
|
result, err = p.PNLCalculation.CalculatePNL(context.TODO(), cal)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
result.Status = p.status
|
|
p.pnlHistory, err = upsertPNLEntry(cal.PNLHistory, result)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if cal.OrderDirection.IsLong() {
|
|
cal.OrderDirection = Short
|
|
} else if cal.OrderDirection.IsShort() {
|
|
cal.OrderDirection = Long
|
|
}
|
|
if p.openingDirection.IsLong() {
|
|
p.openingDirection = Short
|
|
} else if p.openingDirection.IsShort() {
|
|
p.openingDirection = Long
|
|
}
|
|
|
|
cal.Fee = baseFee.Mul(second)
|
|
cal.Amount = second
|
|
cal.EntryPrice = price
|
|
cal.Time = cal.Time.Add(1)
|
|
cal.PNLHistory = p.pnlHistory
|
|
result, err = p.PNLCalculation.CalculatePNL(context.TODO(), cal)
|
|
default:
|
|
result, err = p.PNLCalculation.CalculatePNL(context.TODO(), cal)
|
|
}
|
|
if err != nil {
|
|
if !errors.Is(err, ErrPositionLiquidated) {
|
|
return err
|
|
}
|
|
result.UnrealisedPNL = decimal.Zero
|
|
result.RealisedPNLBeforeFees = decimal.Zero
|
|
p.status = Closed
|
|
}
|
|
result.Status = p.status
|
|
p.pnlHistory, err = upsertPNLEntry(p.pnlHistory, result)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
p.unrealisedPNL = result.UnrealisedPNL
|
|
|
|
switch {
|
|
case longSide.GreaterThan(shortSide):
|
|
p.currentDirection = Long
|
|
case shortSide.GreaterThan(longSide):
|
|
p.currentDirection = Short
|
|
default:
|
|
p.currentDirection = ClosePosition
|
|
}
|
|
|
|
if p.currentDirection.IsLong() {
|
|
p.exposure = longSide.Sub(shortSide)
|
|
} else {
|
|
p.exposure = shortSide.Sub(longSide)
|
|
}
|
|
|
|
if p.exposure.Equal(decimal.Zero) {
|
|
p.status = Closed
|
|
p.closingPrice = decimal.NewFromFloat(d.Price)
|
|
p.realisedPNL = calculateRealisedPNL(p.pnlHistory)
|
|
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
|
|
} else if p.exposure.IsNegative() {
|
|
if p.currentDirection.IsLong() {
|
|
p.currentDirection = Short
|
|
} else {
|
|
p.currentDirection = Long
|
|
}
|
|
p.exposure = p.exposure.Abs()
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// GetCurrencyForRealisedPNL is a generic handling of determining the asset
|
|
// to assign realised PNL into, which is just itself
|
|
func (p *PNLCalculator) GetCurrencyForRealisedPNL(realisedAsset asset.Item, realisedPair currency.Pair) (currency.Code, asset.Item, error) {
|
|
return realisedPair.Base, realisedAsset, nil
|
|
}
|
|
|
|
// CalculatePNL this is a localised generic way of calculating open
|
|
// positions' worth, it is an implementation of the PNLCalculation interface
|
|
func (p *PNLCalculator) CalculatePNL(_ context.Context, calc *PNLCalculatorRequest) (*PNLResult, error) {
|
|
if calc == nil {
|
|
return nil, ErrNilPNLCalculator
|
|
}
|
|
var previousPNL *PNLResult
|
|
if len(calc.PNLHistory) > 0 {
|
|
for i := len(calc.PNLHistory) - 1; i >= 0; i-- {
|
|
if calc.PNLHistory[i].Time.Equal(calc.Time) || !calc.PNLHistory[i].IsOrder {
|
|
continue
|
|
}
|
|
previousPNL = &calc.PNLHistory[i]
|
|
break
|
|
}
|
|
}
|
|
var prevExposure decimal.Decimal
|
|
if previousPNL != nil {
|
|
prevExposure = previousPNL.Exposure
|
|
}
|
|
var currentExposure, realisedPNL, unrealisedPNL, first, second decimal.Decimal
|
|
if calc.OpeningDirection.IsLong() {
|
|
first = calc.CurrentPrice
|
|
if previousPNL != nil {
|
|
second = previousPNL.Price
|
|
}
|
|
} else if calc.OpeningDirection.IsShort() {
|
|
if previousPNL != nil {
|
|
first = previousPNL.Price
|
|
}
|
|
second = calc.CurrentPrice
|
|
}
|
|
switch {
|
|
case calc.OpeningDirection.IsShort() && calc.OrderDirection.IsShort(),
|
|
calc.OpeningDirection.IsLong() && calc.OrderDirection.IsLong():
|
|
// appending to your position
|
|
currentExposure = prevExposure.Add(calc.Amount)
|
|
unrealisedPNL = currentExposure.Mul(first.Sub(second))
|
|
case calc.OpeningDirection.IsShort() && calc.OrderDirection.IsLong(),
|
|
calc.OpeningDirection.IsLong() && calc.OrderDirection.IsShort():
|
|
// selling/closing your position by "amount"
|
|
currentExposure = prevExposure.Sub(calc.Amount)
|
|
unrealisedPNL = currentExposure.Mul(first.Sub(second))
|
|
realisedPNL = calc.Amount.Mul(first.Sub(second))
|
|
default:
|
|
return nil, fmt.Errorf("%w openinig direction: '%v' order direction: '%v' exposure: '%v'", errCannotCalculateUnrealisedPNL, calc.OpeningDirection, calc.OrderDirection, currentExposure)
|
|
}
|
|
totalFees := calc.Fee
|
|
for i := range calc.PNLHistory {
|
|
totalFees = totalFees.Add(calc.PNLHistory[i].Fee)
|
|
}
|
|
if !unrealisedPNL.IsZero() {
|
|
unrealisedPNL = unrealisedPNL.Sub(totalFees)
|
|
}
|
|
|
|
response := &PNLResult{
|
|
IsOrder: true,
|
|
Time: calc.Time,
|
|
UnrealisedPNL: unrealisedPNL,
|
|
RealisedPNLBeforeFees: realisedPNL,
|
|
Price: calc.CurrentPrice,
|
|
Exposure: currentExposure,
|
|
Fee: calc.Fee,
|
|
Direction: calc.CurrentDirection,
|
|
}
|
|
|
|
return response, nil
|
|
}
|
|
|
|
// calculateRealisedPNL calculates the total realised PNL
|
|
// based on PNL history, minus fees
|
|
func calculateRealisedPNL(pnlHistory []PNLResult) decimal.Decimal {
|
|
var realisedPNL, totalFees decimal.Decimal
|
|
for i := range pnlHistory {
|
|
if !pnlHistory[i].IsOrder {
|
|
continue
|
|
}
|
|
realisedPNL = realisedPNL.Add(pnlHistory[i].RealisedPNLBeforeFees)
|
|
totalFees = totalFees.Add(pnlHistory[i].Fee)
|
|
}
|
|
return realisedPNL.Sub(totalFees)
|
|
}
|
|
|
|
// upsertPNLEntry upserts an entry to PNLHistory field
|
|
// with some basic checks
|
|
func upsertPNLEntry(pnlHistory []PNLResult, entry *PNLResult) ([]PNLResult, error) {
|
|
if entry.Time.IsZero() {
|
|
return nil, errTimeUnset
|
|
}
|
|
for i := range pnlHistory {
|
|
if !entry.Time.Equal(pnlHistory[i].Time) {
|
|
continue
|
|
}
|
|
pnlHistory[i].UnrealisedPNL = entry.UnrealisedPNL
|
|
pnlHistory[i].RealisedPNL = entry.RealisedPNL
|
|
pnlHistory[i].RealisedPNLBeforeFees = entry.RealisedPNLBeforeFees
|
|
pnlHistory[i].Exposure = entry.Exposure
|
|
pnlHistory[i].Direction = entry.Direction
|
|
pnlHistory[i].Price = entry.Price
|
|
pnlHistory[i].Status = entry.Status
|
|
pnlHistory[i].Fee = entry.Fee
|
|
if entry.IsOrder {
|
|
pnlHistory[i].IsOrder = true
|
|
}
|
|
if entry.IsLiquidated {
|
|
pnlHistory[i].IsLiquidated = true
|
|
}
|
|
return pnlHistory, nil
|
|
}
|
|
pnlHistory = append(pnlHistory, *entry)
|
|
sort.Slice(pnlHistory, func(i, j int) bool {
|
|
return pnlHistory[i].Time.Before(pnlHistory[j].Time)
|
|
})
|
|
return pnlHistory, nil
|
|
}
|