Files
gocryptotrader/exchanges/order/futures.go
Ryan O'Hara-Reid a12cd3d733 orders: Add methods to derive SubmitResponse and Detail types (#955)
* orders: deprecate SubmitResponse return and change to *order.Detail construct detail from order.Submit struct

* orders: add coverage, fix tests

* coinut: rm test for checking

* orders: revert change for return and change field ID to a more explicit name OrderID

* orders: Add method to see if the order was placed

* order: change field name in Cancel type to be more explicit

* orders: standardize field -> OrderID

* backtester: populate change

* orders: add test

* gctscript: fix field name

* linter: fix issues

* linter: more fixes

* linter: forever

* exchanges_tests: populate order.Submit field exchange name

* Update exchanges/order/order_types.go

Co-authored-by: Scott <gloriousCode@users.noreply.github.com>

* Update exchanges/order/orders.go

Co-authored-by: Scott <gloriousCode@users.noreply.github.com>

* glorious: nits

* glorious: nits

* thrasher: nits

Co-authored-by: Ryan O'Hara-Reid <ryan.oharareid@thrasher.io>
Co-authored-by: Scott <gloriousCode@users.noreply.github.com>
2022-06-06 11:52:15 +10:00

688 lines
21 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{
positionTrackerControllers: 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.positionTrackerControllers[strings.ToLower(d.Exchange)]
if !ok {
exchM = make(map[asset.Item]map[currency.Pair]*MultiPositionTracker)
c.positionTrackerControllers[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)
}
// 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.positionTrackerControllers[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.positionTrackerControllers[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
}
// 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.positionTrackerControllers[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,
})
if err != nil {
return err
}
itemM[pair] = newMPT
return 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,
}, nil
}
// GetPositions returns all positions
func (e *MultiPositionTracker) GetPositions() []PositionStats {
if e == nil {
return nil
}
e.m.Lock()
defer e.m.Unlock()
resp := make([]PositionStats, len(e.positions))
for i := range e.positions {
resp[i] = e.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 (e *MultiPositionTracker) TrackNewOrder(d *Detail) error {
if e == nil {
return fmt.Errorf("multi-position tracker %w", common.ErrNilPointer)
}
if d == nil {
return ErrSubmissionIsNil
}
e.m.Lock()
defer e.m.Unlock()
if d.AssetType != e.asset {
return errAssetMismatch
}
if tracker, ok := e.orderPositions[d.OrderID]; ok {
// this has already been associated
// update the tracker
return tracker.TrackNewOrder(d)
}
if len(e.positions) > 0 {
for i := range e.positions {
if e.positions[i].status == Open && i != len(e.positions)-1 {
return fmt.Errorf("%w %v at position %v/%v", errPositionDiscrepancy, e.positions[i], i, len(e.positions)-1)
}
}
if e.positions[len(e.positions)-1].status == Open {
err := e.positions[len(e.positions)-1].TrackNewOrder(d)
if err != nil && !errors.Is(err, ErrPositionClosed) {
return err
}
e.orderPositions[d.OrderID] = e.positions[len(e.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: e.useExchangePNLCalculations,
}
tracker, err := e.SetupPositionTracker(setup)
if err != nil {
return err
}
e.positions = append(e.positions, tracker)
err = tracker.TrackNewOrder(d)
if err != nil {
return err
}
e.orderPositions[d.OrderID] = tracker
return nil
}
// SetupPositionTracker creates a new position tracker to track n futures orders
// until the position(s) are closed
func (e *MultiPositionTracker) SetupPositionTracker(setup *PositionTrackerSetup) (*PositionTracker, error) {
if e == nil {
return nil, fmt.Errorf("multi-position tracker %w", common.ErrNilPointer)
}
if e.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(e.exchange),
asset: setup.Asset,
contractPair: setup.Pair,
underlyingAsset: setup.Underlying,
status: Open,
entryPrice: setup.EntryPrice,
currentDirection: setup.Side,
openingDirection: setup.Side,
useExchangePNLCalculation: setup.UseExchangePNLCalculation,
offlinePNLCalculation: e.offlinePNLCalculation,
}
if !setup.UseExchangePNLCalculation {
// use position tracker's pnl calculation by default
resp.PNLCalculation = &PNLCalculator{}
} else {
if e.exchangePNLCalculation == nil {
return nil, ErrNilPNLCalculator
}
resp.PNLCalculation = e.exchangePNLCalculation
}
return resp, nil
}
// 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,
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,
}
}
// 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,
}
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 {
result.RealisedPNLBeforeFees = p.pnlHistory[len(p.pnlHistory)-1].RealisedPNLBeforeFees
result.Exposure = p.pnlHistory[len(p.pnlHistory)-1].Exposure
}
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)
}
// 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) error {
if p == nil {
return fmt.Errorf("position tracker %w", common.ErrNilPointer)
}
p.m.Lock()
defer p.m.Unlock()
if p.status == Closed {
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 p.currentDirection == UnknownSide {
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
}
if (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
}
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)
} else {
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
}
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 = UnknownSide
}
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
} else if p.exposure.IsNegative() {
if p.currentDirection.IsLong() {
p.currentDirection = Short
} else {
p.currentDirection = Long
}
p.exposure = p.exposure.Abs()
}
return 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 {
previousPNL = &calc.PNLHistory[len(calc.PNLHistory)-1]
}
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{
Time: calc.Time,
UnrealisedPNL: unrealisedPNL,
RealisedPNLBeforeFees: realisedPNL,
Price: calc.CurrentPrice,
Exposure: currentExposure,
Fee: calc.Fee,
}
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 {
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) {
pnlHistory[i] = *entry
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
}