mirror of
https://github.com/d0zingcat/gocryptotrader.git
synced 2026-05-22 23:16:48 +00:00
backtester: Futures handling & FTX Cash and Carry example strategy (#930)
* 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
This commit is contained in:
@@ -2838,7 +2838,7 @@ func TestFetchSpotExchangeLimits(t *testing.T) {
|
||||
t.Parallel()
|
||||
limits, err := b.FetchSpotExchangeLimits(context.Background())
|
||||
if !errors.Is(err, nil) {
|
||||
t.Errorf("received '%v', epected '%v'", err, nil)
|
||||
t.Errorf("received '%v', expected '%v'", err, nil)
|
||||
}
|
||||
if len(limits) == 0 {
|
||||
t.Error("expected a response")
|
||||
|
||||
@@ -1278,7 +1278,7 @@ func (b *Base) GetAvailableTransferChains(_ context.Context, _ currency.Code) ([
|
||||
// CalculatePNL is an overridable function to allow PNL to be calculated on an
|
||||
// open position
|
||||
// It will also determine whether the position is considered to be liquidated
|
||||
// For live trading, an overrided function may wish to confirm the liquidation by
|
||||
// For live trading, an overriding function may wish to confirm the liquidation by
|
||||
// requesting the status of the asset
|
||||
func (b *Base) CalculatePNL(context.Context, *order.PNLCalculatorRequest) (*order.PNLResult, error) {
|
||||
return nil, common.ErrNotYetImplemented
|
||||
@@ -1301,6 +1301,18 @@ func (b *Base) GetFuturesPositions(context.Context, asset.Item, currency.Pair, t
|
||||
return nil, common.ErrNotYetImplemented
|
||||
}
|
||||
|
||||
// GetCollateralCurrencyForContract returns the collateral currency for an asset and contract pair
|
||||
func (b *Base) GetCollateralCurrencyForContract(asset.Item, currency.Pair) (currency.Code, asset.Item, error) {
|
||||
return currency.Code{}, asset.Empty, common.ErrNotYetImplemented
|
||||
}
|
||||
|
||||
// GetCurrencyForRealisedPNL returns where to put realised PNL
|
||||
// example 1: FTX PNL is paid out in USD to your spot wallet
|
||||
// example 2: Binance coin margined futures pays returns using the same currency eg BTC
|
||||
func (b *Base) GetCurrencyForRealisedPNL(_ asset.Item, _ currency.Pair) (currency.Code, asset.Item, error) {
|
||||
return currency.Code{}, asset.Empty, common.ErrNotYetImplemented
|
||||
}
|
||||
|
||||
// HasAssetTypeAccountSegregation returns if the accounts are divided into asset
|
||||
// types instead of just being denoted as spot holdings.
|
||||
func (b *Base) HasAssetTypeAccountSegregation() bool {
|
||||
|
||||
@@ -1847,12 +1847,11 @@ func TestScaleCollateral(t *testing.T) {
|
||||
Asset: asset.Spot,
|
||||
Side: order.Buy,
|
||||
FreeCollateral: decimal.NewFromFloat(v[v2].Total),
|
||||
USDPrice: decimal.Zero,
|
||||
IsLiquidating: true,
|
||||
CalculateOffline: true,
|
||||
})
|
||||
if !errors.Is(err, order.ErrUSDValueRequired) {
|
||||
t.Errorf("received '%v' exepected '%v'", err, order.ErrUSDValueRequired)
|
||||
t.Errorf("received '%v' expected '%v'", err, order.ErrUSDValueRequired)
|
||||
}
|
||||
|
||||
_, err = f.ScaleCollateral(
|
||||
|
||||
@@ -1290,7 +1290,8 @@ func (f *FTX) CalculatePNL(ctx context.Context, pnl *order.PNLCalculatorRequest)
|
||||
return nil, fmt.Errorf("%v %w", f.Name, order.ErrNilPNLCalculator)
|
||||
}
|
||||
result := &order.PNLResult{
|
||||
Time: pnl.Time,
|
||||
Time: pnl.Time,
|
||||
IsOrder: true,
|
||||
}
|
||||
creds, err := f.GetCredentials(ctx)
|
||||
if err != nil {
|
||||
@@ -1310,7 +1311,7 @@ func (f *FTX) CalculatePNL(ctx context.Context, pnl *order.PNLCalculatorRequest)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if info.Liquidating || info.Collateral == 0 {
|
||||
if info.Liquidating || info.Collateral <= 0 {
|
||||
result.IsLiquidated = true
|
||||
return result, fmt.Errorf("%s %s %w", f.Name, creds.SubAccount, order.ErrPositionLiquidated)
|
||||
}
|
||||
@@ -1682,3 +1683,13 @@ func (f *FTX) GetFuturesPositions(ctx context.Context, a asset.Item, cp currency
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// GetCollateralCurrencyForContract returns the collateral currency for an asset and contract pair
|
||||
func (f *FTX) GetCollateralCurrencyForContract(_ asset.Item, _ currency.Pair) (currency.Code, asset.Item, error) {
|
||||
return currency.USD, asset.Futures, nil
|
||||
}
|
||||
|
||||
// GetCurrencyForRealisedPNL returns where to put realised PNL
|
||||
func (f *FTX) GetCurrencyForRealisedPNL(_ asset.Item, _ currency.Pair) (currency.Code, asset.Item, error) {
|
||||
return currency.USD, asset.Spot, nil
|
||||
}
|
||||
|
||||
@@ -82,6 +82,7 @@ var (
|
||||
type Item struct {
|
||||
Exchange string
|
||||
Pair currency.Pair
|
||||
UnderlyingPair currency.Pair
|
||||
Asset asset.Item
|
||||
Interval Interval
|
||||
Candles []Candle
|
||||
|
||||
@@ -179,7 +179,7 @@ func (l *LocalBitcoins) Getads(ctx context.Context, args ...string) (AdData, err
|
||||
//
|
||||
// params - see localbitcoins_types.go AdEdit for reference
|
||||
// adID - string for the ad you already created
|
||||
// TODO
|
||||
// TODO use parameter
|
||||
func (l *LocalBitcoins) EditAd(ctx context.Context, _ *AdEdit, adID string) error {
|
||||
resp := struct {
|
||||
Data AdData `json:"data"`
|
||||
@@ -209,7 +209,7 @@ func (l *LocalBitcoins) EditAd(ctx context.Context, _ *AdEdit, adID string) erro
|
||||
// CreateAd creates a new advertisement
|
||||
//
|
||||
// params - see localbitcoins_types.go AdCreate for reference
|
||||
// TODO
|
||||
// TODO use parameter
|
||||
func (l *LocalBitcoins) CreateAd(ctx context.Context, _ *AdCreate) error {
|
||||
return l.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, http.MethodPost, localbitcoinsAPIAdCreate, nil, nil)
|
||||
}
|
||||
@@ -220,7 +220,7 @@ func (l *LocalBitcoins) CreateAd(ctx context.Context, _ *AdCreate) error {
|
||||
//
|
||||
// equation - string of equation
|
||||
// adID - string of specific ad identification
|
||||
// TODO
|
||||
// TODO use parameter
|
||||
func (l *LocalBitcoins) UpdatePriceEquation(ctx context.Context, adID string) error {
|
||||
return l.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, http.MethodPost, localbitcoinsAPIUpdateEquation+adID, nil, nil)
|
||||
}
|
||||
@@ -228,7 +228,6 @@ func (l *LocalBitcoins) UpdatePriceEquation(ctx context.Context, adID string) er
|
||||
// DeleteAd deletes the advertisement by adID.
|
||||
//
|
||||
// adID - string of specific ad identification
|
||||
// TODO
|
||||
func (l *LocalBitcoins) DeleteAd(ctx context.Context, adID string) error {
|
||||
resp := struct {
|
||||
Error struct {
|
||||
@@ -263,7 +262,6 @@ func (l *LocalBitcoins) ReleaseFunds(ctx context.Context, contactID string) erro
|
||||
// ReleaseFundsByPin releases Bitcoin trades specified by ID {contact_id}. if
|
||||
// the current pincode is provided. If the release was successful a message is
|
||||
// returned on the data key.
|
||||
// TODO
|
||||
func (l *LocalBitcoins) ReleaseFundsByPin(ctx context.Context, contactID string) error {
|
||||
return l.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, http.MethodPost, localbitcoinsAPIReleaseByPin+contactID, nil, nil)
|
||||
}
|
||||
@@ -286,7 +284,6 @@ func (l *LocalBitcoins) GetMessages(ctx context.Context, contactID string) (Mess
|
||||
|
||||
// SendMessage posts a message and/or uploads an image to the trade. Encode
|
||||
// images with multipart/form-data encoding.
|
||||
// TODO
|
||||
func (l *LocalBitcoins) SendMessage(ctx context.Context, contactID string) error {
|
||||
return l.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, http.MethodPost, localbitcoinsAPISendMessage+contactID, nil, nil)
|
||||
}
|
||||
@@ -295,7 +292,7 @@ func (l *LocalBitcoins) SendMessage(ctx context.Context, contactID string) error
|
||||
// starting the dispute has been fulfilled.
|
||||
//
|
||||
// topic - [optional] String Short description of issue to LocalBitcoins customer support.
|
||||
// TODO
|
||||
// TODO use parameter
|
||||
func (l *LocalBitcoins) Dispute(ctx context.Context, _, contactID string) error {
|
||||
return l.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, http.MethodPost, localbitcoinsAPIDispute+contactID, nil, nil)
|
||||
}
|
||||
@@ -325,7 +322,6 @@ func (l *LocalBitcoins) VerifyIdentity(ctx context.Context, contactID string) er
|
||||
|
||||
// InitiateTrade sttempts to start a Bitcoin trade from the specified
|
||||
// advertisement ID.
|
||||
// TODO
|
||||
func (l *LocalBitcoins) InitiateTrade(ctx context.Context, adID string) error {
|
||||
return l.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, http.MethodPost, localbitcoinsAPIInitiateTrade+adID, nil, nil)
|
||||
}
|
||||
@@ -422,7 +418,7 @@ func (l *LocalBitcoins) GetDashboardClosedTrades(ctx context.Context) ([]DashBoa
|
||||
// msg - [optional] Feedback message displayed alongside feedback on receivers
|
||||
// profile page.
|
||||
// username - username of trade contact
|
||||
// TODO
|
||||
// TODO add support
|
||||
func (l *LocalBitcoins) SetFeedback(ctx context.Context) error {
|
||||
return l.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, http.MethodPost, localbitcoinsAPIFeedback, nil, nil)
|
||||
}
|
||||
@@ -435,14 +431,14 @@ func (l *LocalBitcoins) Logout(ctx context.Context) error {
|
||||
}
|
||||
|
||||
// CreateNewInvoice creates a new invoice.
|
||||
// TODO
|
||||
// TODO add support
|
||||
func (l *LocalBitcoins) CreateNewInvoice(ctx context.Context) error {
|
||||
return l.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, http.MethodPost, localbitcoinsAPICreateInvoice, nil, nil)
|
||||
}
|
||||
|
||||
// GetInvoice returns information about a specific invoice created by the token
|
||||
// owner.
|
||||
// TODO
|
||||
// TODO add support
|
||||
func (l *LocalBitcoins) GetInvoice(ctx context.Context) (Invoice, error) {
|
||||
resp := Invoice{}
|
||||
return resp, l.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, http.MethodPost, localbitcoinsAPICreateInvoice, nil, &resp)
|
||||
@@ -452,7 +448,7 @@ func (l *LocalBitcoins) GetInvoice(ctx context.Context) (Invoice, error) {
|
||||
// it is sure that receiver cannot accidentally pay the invoice at the same time
|
||||
// as the merchant is deleting it. You can use the API request
|
||||
// /api/merchant/invoice/{invoice_id}/ to check if deleting is possible.
|
||||
// TODO
|
||||
// TODO add support
|
||||
func (l *LocalBitcoins) DeleteInvoice(ctx context.Context) (Invoice, error) {
|
||||
resp := Invoice{}
|
||||
return resp, l.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, http.MethodPost, localbitcoinsAPICreateInvoice, nil, &resp)
|
||||
@@ -465,7 +461,7 @@ func (l *LocalBitcoins) GetNotifications(ctx context.Context) ([]NotificationInf
|
||||
}
|
||||
|
||||
// MarkNotifications marks a specific notification as read.
|
||||
// TODO
|
||||
// TODO add support
|
||||
func (l *LocalBitcoins) MarkNotifications(ctx context.Context) error {
|
||||
return l.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, http.MethodPost, localbitcoinsAPIMarkNotification, nil, nil)
|
||||
}
|
||||
@@ -513,7 +509,7 @@ func (l *LocalBitcoins) CheckPincode(ctx context.Context, pin int) (bool, error)
|
||||
|
||||
// GetPlaces Looks up places near lat, lon and provides full URLs to buy and
|
||||
// sell listings for each.
|
||||
// TODO
|
||||
// TODO add support
|
||||
func (l *LocalBitcoins) GetPlaces(ctx context.Context) error {
|
||||
return l.SendHTTPRequest(ctx, exchange.RestSpot, localbitcoinsAPIPlaces, nil, request.Unset)
|
||||
}
|
||||
@@ -527,7 +523,7 @@ func (l *LocalBitcoins) VerifyUsername(ctx context.Context) error {
|
||||
// GetRecentMessages returns maximum of 25 newest trade messages. Does not
|
||||
// return messages older than one month. Messages are ordered by sending time,
|
||||
// and the newest one is first.
|
||||
// TODO
|
||||
// TODO add support
|
||||
func (l *LocalBitcoins) GetRecentMessages(ctx context.Context) error {
|
||||
return l.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, http.MethodPost, localbitcoinsAPIVerifyUsername, nil, nil)
|
||||
}
|
||||
@@ -641,13 +637,13 @@ func (l *LocalBitcoins) GetWalletAddress(ctx context.Context) (string, error) {
|
||||
}
|
||||
|
||||
// GetBitcoinsWithCashAd returns buy or sell as cash local advertisements.
|
||||
// TODO
|
||||
// TODO add support
|
||||
func (l *LocalBitcoins) GetBitcoinsWithCashAd(ctx context.Context) error {
|
||||
return l.SendHTTPRequest(ctx, exchange.RestSpot, localbitcoinsAPICashBuy, nil, request.Unset)
|
||||
}
|
||||
|
||||
// GetBitcoinsOnlineAd this API returns buy or sell Bitcoin online ads.
|
||||
// TODO
|
||||
// TODO add support
|
||||
func (l *LocalBitcoins) GetBitcoinsOnlineAd(ctx context.Context) error {
|
||||
return l.SendHTTPRequest(ctx, exchange.RestSpot, localbitcoinsAPIOnlineBuy, nil, request.Unset)
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ import (
|
||||
// to track futures orders
|
||||
func SetupPositionController() *PositionController {
|
||||
return &PositionController{
|
||||
positionTrackerControllers: make(map[string]map[asset.Item]map[currency.Pair]*MultiPositionTracker),
|
||||
multiPositionTrackers: make(map[string]map[asset.Item]map[currency.Pair]*MultiPositionTracker),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,10 +38,10 @@ func (c *PositionController) TrackNewOrder(d *Detail) error {
|
||||
}
|
||||
c.m.Lock()
|
||||
defer c.m.Unlock()
|
||||
exchM, ok := c.positionTrackerControllers[strings.ToLower(d.Exchange)]
|
||||
exchM, ok := c.multiPositionTrackers[strings.ToLower(d.Exchange)]
|
||||
if !ok {
|
||||
exchM = make(map[asset.Item]map[currency.Pair]*MultiPositionTracker)
|
||||
c.positionTrackerControllers[strings.ToLower(d.Exchange)] = exchM
|
||||
c.multiPositionTrackers[strings.ToLower(d.Exchange)] = exchM
|
||||
}
|
||||
itemM, ok := exchM[d.AssetType]
|
||||
if !ok {
|
||||
@@ -65,6 +65,44 @@ func (c *PositionController) TrackNewOrder(d *Detail) error {
|
||||
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) {
|
||||
@@ -76,7 +114,7 @@ func (c *PositionController) GetPositionsForExchange(exch string, item asset.Ite
|
||||
if !item.IsFutures() {
|
||||
return nil, fmt.Errorf("%v %v %v %w", exch, item, pair, ErrNotFuturesAsset)
|
||||
}
|
||||
exchM, ok := c.positionTrackerControllers[strings.ToLower(exch)]
|
||||
exchM, ok := c.multiPositionTrackers[strings.ToLower(exch)]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("%v %v %v %w", exch, item, pair, ErrPositionsNotLoadedForExchange)
|
||||
}
|
||||
@@ -105,7 +143,7 @@ func (c *PositionController) UpdateOpenPositionUnrealisedPNL(exch string, item a
|
||||
|
||||
c.m.Lock()
|
||||
defer c.m.Unlock()
|
||||
exchM, ok := c.positionTrackerControllers[strings.ToLower(exch)]
|
||||
exchM, ok := c.multiPositionTrackers[strings.ToLower(exch)]
|
||||
if !ok {
|
||||
return decimal.Zero, fmt.Errorf("%v %v %v %w", exch, item, pair, ErrPositionsNotLoadedForExchange)
|
||||
}
|
||||
@@ -137,45 +175,6 @@ func (c *PositionController) UpdateOpenPositionUnrealisedPNL(exch string, item a
|
||||
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 {
|
||||
@@ -205,85 +204,17 @@ func SetupMultiPositionTracker(setup *MultiPositionTrackerSetup) (*MultiPosition
|
||||
orderPositions: make(map[string]*PositionTracker),
|
||||
useExchangePNLCalculations: setup.UseExchangePNLCalculation,
|
||||
exchangePNLCalculation: setup.ExchangePNLCalculation,
|
||||
collateralCurrency: setup.CollateralCurrency,
|
||||
}, 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 {
|
||||
func (m *MultiPositionTracker) SetupPositionTracker(setup *PositionTrackerSetup) (*PositionTracker, error) {
|
||||
if m == nil {
|
||||
return nil, fmt.Errorf("multi-position tracker %w", common.ErrNilPointer)
|
||||
}
|
||||
if e.exchange == "" {
|
||||
if m.exchange == "" {
|
||||
return nil, errExchangeNameEmpty
|
||||
}
|
||||
if setup == nil {
|
||||
@@ -297,7 +228,7 @@ func (e *MultiPositionTracker) SetupPositionTracker(setup *PositionTrackerSetup)
|
||||
}
|
||||
|
||||
resp := &PositionTracker{
|
||||
exchange: strings.ToLower(e.exchange),
|
||||
exchange: strings.ToLower(m.exchange),
|
||||
asset: setup.Asset,
|
||||
contractPair: setup.Pair,
|
||||
underlyingAsset: setup.Underlying,
|
||||
@@ -306,20 +237,167 @@ func (e *MultiPositionTracker) SetupPositionTracker(setup *PositionTrackerSetup)
|
||||
currentDirection: setup.Side,
|
||||
openingDirection: setup.Side,
|
||||
useExchangePNLCalculation: setup.UseExchangePNLCalculation,
|
||||
offlinePNLCalculation: e.offlinePNLCalculation,
|
||||
collateralCurrency: setup.CollateralCurrency,
|
||||
offlinePNLCalculation: m.offlinePNLCalculation,
|
||||
}
|
||||
if !setup.UseExchangePNLCalculation {
|
||||
// use position tracker's pnl calculation by default
|
||||
resp.PNLCalculation = &PNLCalculator{}
|
||||
} else {
|
||||
if e.exchangePNLCalculation == nil {
|
||||
if m.exchangePNLCalculation == nil {
|
||||
return nil, ErrNilPNLCalculator
|
||||
}
|
||||
resp.PNLCalculation = e.exchangePNLCalculation
|
||||
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 {
|
||||
@@ -330,20 +408,23 @@ func (p *PositionTracker) GetStats() PositionStats {
|
||||
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,
|
||||
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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -360,8 +441,9 @@ func (p *PositionTracker) TrackPNLByTime(t time.Time, currentPrice float64) erro
|
||||
}()
|
||||
price := decimal.NewFromFloat(currentPrice)
|
||||
result := &PNLResult{
|
||||
Time: t,
|
||||
Price: price,
|
||||
Time: t,
|
||||
Price: price,
|
||||
Status: p.status,
|
||||
}
|
||||
if p.currentDirection.IsLong() {
|
||||
diff := price.Sub(p.entryPrice)
|
||||
@@ -371,8 +453,12 @@ func (p *PositionTracker) TrackPNLByTime(t time.Time, currentPrice float64) erro
|
||||
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
|
||||
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)
|
||||
@@ -391,6 +477,37 @@ func (p *PositionTracker) GetRealisedPNL() decimal.Decimal {
|
||||
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) {
|
||||
@@ -402,13 +519,16 @@ func (p *PositionTracker) GetLatestPNLSnapshot() (PNLResult, error) {
|
||||
|
||||
// TrackNewOrder knows how things are going for a given
|
||||
// futures contract
|
||||
func (p *PositionTracker) TrackNewOrder(d *Detail) error {
|
||||
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 p.status == Closed {
|
||||
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 {
|
||||
@@ -426,6 +546,7 @@ func (p *PositionTracker) TrackNewOrder(d *Detail) error {
|
||||
return fmt.Errorf("%w asset '%v' received: '%v'",
|
||||
errOrderNotEqualToTracker, d.AssetType, p.asset)
|
||||
}
|
||||
|
||||
if d.Side == UnknownSide {
|
||||
return ErrSideIsInvalid
|
||||
}
|
||||
@@ -484,7 +605,8 @@ func (p *PositionTracker) TrackNewOrder(d *Detail) error {
|
||||
longSide = longSide.Add(decimal.NewFromFloat(p.longPositions[i].Amount))
|
||||
}
|
||||
|
||||
if p.currentDirection == UnknownSide {
|
||||
if isInitialOrder {
|
||||
p.openingDirection = d.Side
|
||||
p.currentDirection = d.Side
|
||||
}
|
||||
|
||||
@@ -514,8 +636,18 @@ func (p *PositionTracker) TrackNewOrder(d *Detail) error {
|
||||
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) {
|
||||
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
|
||||
@@ -527,6 +659,7 @@ func (p *PositionTracker) TrackNewOrder(d *Detail) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
result.Status = p.status
|
||||
p.pnlHistory, err = upsertPNLEntry(cal.PNLHistory, result)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -548,7 +681,7 @@ func (p *PositionTracker) TrackNewOrder(d *Detail) error {
|
||||
cal.Time = cal.Time.Add(1)
|
||||
cal.PNLHistory = p.pnlHistory
|
||||
result, err = p.PNLCalculation.CalculatePNL(context.TODO(), cal)
|
||||
} else {
|
||||
default:
|
||||
result, err = p.PNLCalculation.CalculatePNL(context.TODO(), cal)
|
||||
}
|
||||
if err != nil {
|
||||
@@ -559,6 +692,7 @@ func (p *PositionTracker) TrackNewOrder(d *Detail) error {
|
||||
result.RealisedPNLBeforeFees = decimal.Zero
|
||||
p.status = Closed
|
||||
}
|
||||
result.Status = p.status
|
||||
p.pnlHistory, err = upsertPNLEntry(p.pnlHistory, result)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -571,7 +705,7 @@ func (p *PositionTracker) TrackNewOrder(d *Detail) error {
|
||||
case shortSide.GreaterThan(longSide):
|
||||
p.currentDirection = Short
|
||||
default:
|
||||
p.currentDirection = UnknownSide
|
||||
p.currentDirection = ClosePosition
|
||||
}
|
||||
|
||||
if p.currentDirection.IsLong() {
|
||||
@@ -585,6 +719,9 @@ func (p *PositionTracker) TrackNewOrder(d *Detail) error {
|
||||
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
|
||||
@@ -596,6 +733,12 @@ func (p *PositionTracker) TrackNewOrder(d *Detail) error {
|
||||
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) {
|
||||
@@ -604,7 +747,13 @@ func (p *PNLCalculator) CalculatePNL(_ context.Context, calc *PNLCalculatorReque
|
||||
}
|
||||
var previousPNL *PNLResult
|
||||
if len(calc.PNLHistory) > 0 {
|
||||
previousPNL = &calc.PNLHistory[len(calc.PNLHistory)-1]
|
||||
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 {
|
||||
@@ -646,13 +795,16 @@ func (p *PNLCalculator) CalculatePNL(_ context.Context, calc *PNLCalculatorReque
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
@@ -661,6 +813,9 @@ func (p *PNLCalculator) CalculatePNL(_ context.Context, calc *PNLCalculatorReque
|
||||
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)
|
||||
}
|
||||
@@ -674,10 +829,24 @@ func upsertPNLEntry(pnlHistory []PNLResult, entry *PNLResult) ([]PNLResult, erro
|
||||
return nil, errTimeUnset
|
||||
}
|
||||
for i := range pnlHistory {
|
||||
if entry.Time.Equal(pnlHistory[i].Time) {
|
||||
pnlHistory[i] = *entry
|
||||
return pnlHistory, nil
|
||||
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 {
|
||||
|
||||
@@ -28,10 +28,20 @@ func (f *FakePNL) CalculatePNL(context.Context, *PNLCalculatorRequest) (*PNLResu
|
||||
return f.result, nil
|
||||
}
|
||||
|
||||
// GetCurrencyForRealisedPNL overrides default pnl calculations
|
||||
func (f *FakePNL) GetCurrencyForRealisedPNL(realisedAsset asset.Item, realisedPair currency.Pair) (currency.Code, asset.Item, error) {
|
||||
if f.err != nil {
|
||||
return realisedPair.Base, asset.Empty, f.err
|
||||
}
|
||||
return realisedPair.Base, realisedAsset, nil
|
||||
}
|
||||
|
||||
func TestUpsertPNLEntry(t *testing.T) {
|
||||
t.Parallel()
|
||||
var results []PNLResult
|
||||
result := &PNLResult{}
|
||||
result := &PNLResult{
|
||||
IsOrder: true,
|
||||
}
|
||||
_, err := upsertPNLEntry(results, result)
|
||||
if !errors.Is(err, errTimeUnset) {
|
||||
t.Error(err)
|
||||
@@ -79,11 +89,11 @@ func TestTrackNewOrder(t *testing.T) {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
err = f.TrackNewOrder(nil)
|
||||
err = f.TrackNewOrder(nil, false)
|
||||
if !errors.Is(err, ErrSubmissionIsNil) {
|
||||
t.Error(err)
|
||||
}
|
||||
err = f.TrackNewOrder(&Detail{})
|
||||
err = f.TrackNewOrder(&Detail{}, false)
|
||||
if !errors.Is(err, errOrderNotEqualToTracker) {
|
||||
t.Error(err)
|
||||
}
|
||||
@@ -95,7 +105,7 @@ func TestTrackNewOrder(t *testing.T) {
|
||||
OrderID: "1",
|
||||
Price: 1337,
|
||||
}
|
||||
err = f.TrackNewOrder(od)
|
||||
err = f.TrackNewOrder(od, false)
|
||||
if !errors.Is(err, ErrSideIsInvalid) {
|
||||
t.Error(err)
|
||||
}
|
||||
@@ -103,13 +113,13 @@ func TestTrackNewOrder(t *testing.T) {
|
||||
od.Side = Long
|
||||
od.Amount = 1
|
||||
od.OrderID = "2"
|
||||
err = f.TrackNewOrder(od)
|
||||
err = f.TrackNewOrder(od, false)
|
||||
if !errors.Is(err, errTimeUnset) {
|
||||
t.Error(err)
|
||||
}
|
||||
f.openingDirection = Long
|
||||
od.Date = time.Now()
|
||||
err = f.TrackNewOrder(od)
|
||||
err = f.TrackNewOrder(od, false)
|
||||
if !errors.Is(err, nil) {
|
||||
t.Error(err)
|
||||
}
|
||||
@@ -130,7 +140,7 @@ func TestTrackNewOrder(t *testing.T) {
|
||||
od.Amount = 0.4
|
||||
od.Side = Short
|
||||
od.OrderID = "3"
|
||||
err = f.TrackNewOrder(od)
|
||||
err = f.TrackNewOrder(od, false)
|
||||
if !errors.Is(err, nil) {
|
||||
t.Error(err)
|
||||
}
|
||||
@@ -149,7 +159,7 @@ func TestTrackNewOrder(t *testing.T) {
|
||||
od.Side = Short
|
||||
od.OrderID = "4"
|
||||
od.Fee = 0.1
|
||||
err = f.TrackNewOrder(od)
|
||||
err = f.TrackNewOrder(od, false)
|
||||
if !errors.Is(err, nil) {
|
||||
t.Error(err)
|
||||
}
|
||||
@@ -164,27 +174,41 @@ func TestTrackNewOrder(t *testing.T) {
|
||||
od.OrderID = "5"
|
||||
od.Side = Long
|
||||
od.Amount = 0.2
|
||||
err = f.TrackNewOrder(od)
|
||||
err = f.TrackNewOrder(od, false)
|
||||
if !errors.Is(err, nil) {
|
||||
t.Error(err)
|
||||
}
|
||||
if f.currentDirection != UnknownSide {
|
||||
if f.currentDirection != ClosePosition {
|
||||
t.Errorf("expected recognition that its unknown, received '%v'", f.currentDirection)
|
||||
}
|
||||
if f.status != Closed {
|
||||
t.Errorf("expected recognition that its closed, received '%v'", f.status)
|
||||
}
|
||||
|
||||
err = f.TrackNewOrder(od)
|
||||
err = f.TrackNewOrder(od, false)
|
||||
if !errors.Is(err, ErrPositionClosed) {
|
||||
t.Error(err)
|
||||
}
|
||||
if f.currentDirection != UnknownSide {
|
||||
if f.currentDirection != ClosePosition {
|
||||
t.Errorf("expected recognition that its unknown, received '%v'", f.currentDirection)
|
||||
}
|
||||
if f.status != Closed {
|
||||
t.Errorf("expected recognition that its closed, received '%v'", f.status)
|
||||
}
|
||||
|
||||
err = f.TrackNewOrder(od, true)
|
||||
if !errors.Is(err, errCannotTrackInvalidParams) {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
f, err = e.SetupPositionTracker(setup)
|
||||
if !errors.Is(err, nil) {
|
||||
t.Error(err)
|
||||
}
|
||||
err = f.TrackNewOrder(od, true)
|
||||
if !errors.Is(err, nil) {
|
||||
t.Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetupMultiPositionTracker(t *testing.T) {
|
||||
@@ -359,7 +383,7 @@ func TestExchangeTrackNewOrder(t *testing.T) {
|
||||
func TestSetupPositionControllerReal(t *testing.T) {
|
||||
t.Parallel()
|
||||
pc := SetupPositionController()
|
||||
if pc.positionTrackerControllers == nil {
|
||||
if pc.multiPositionTrackers == nil {
|
||||
t.Error("unexpected nil")
|
||||
}
|
||||
}
|
||||
@@ -489,14 +513,14 @@ func TestGetPositionsForExchange(t *testing.T) {
|
||||
if len(pos) != 0 {
|
||||
t.Error("expected zero")
|
||||
}
|
||||
c.positionTrackerControllers = make(map[string]map[asset.Item]map[currency.Pair]*MultiPositionTracker)
|
||||
c.positionTrackerControllers[testExchange] = nil
|
||||
c.multiPositionTrackers = make(map[string]map[asset.Item]map[currency.Pair]*MultiPositionTracker)
|
||||
c.multiPositionTrackers[testExchange] = nil
|
||||
_, err = c.GetPositionsForExchange(testExchange, asset.Futures, p)
|
||||
if !errors.Is(err, ErrPositionsNotLoadedForAsset) {
|
||||
t.Errorf("received '%v' expected '%v", err, ErrPositionsNotLoadedForExchange)
|
||||
}
|
||||
c.positionTrackerControllers[testExchange] = make(map[asset.Item]map[currency.Pair]*MultiPositionTracker)
|
||||
c.positionTrackerControllers[testExchange][asset.Futures] = nil
|
||||
c.multiPositionTrackers[testExchange] = make(map[asset.Item]map[currency.Pair]*MultiPositionTracker)
|
||||
c.multiPositionTrackers[testExchange][asset.Futures] = nil
|
||||
_, err = c.GetPositionsForExchange(testExchange, asset.Futures, p)
|
||||
if !errors.Is(err, ErrPositionsNotLoadedForPair) {
|
||||
t.Errorf("received '%v' expected '%v", err, ErrPositionsNotLoadedForPair)
|
||||
@@ -506,8 +530,8 @@ func TestGetPositionsForExchange(t *testing.T) {
|
||||
t.Errorf("received '%v' expected '%v", err, ErrNotFuturesAsset)
|
||||
}
|
||||
|
||||
c.positionTrackerControllers[testExchange][asset.Futures] = make(map[currency.Pair]*MultiPositionTracker)
|
||||
c.positionTrackerControllers[testExchange][asset.Futures][p] = &MultiPositionTracker{
|
||||
c.multiPositionTrackers[testExchange][asset.Futures] = make(map[currency.Pair]*MultiPositionTracker)
|
||||
c.multiPositionTrackers[testExchange][asset.Futures][p] = &MultiPositionTracker{
|
||||
exchange: testExchange,
|
||||
}
|
||||
|
||||
@@ -518,7 +542,7 @@ func TestGetPositionsForExchange(t *testing.T) {
|
||||
if len(pos) != 0 {
|
||||
t.Fatal("expected zero")
|
||||
}
|
||||
c.positionTrackerControllers[testExchange][asset.Futures][p] = &MultiPositionTracker{
|
||||
c.multiPositionTrackers[testExchange][asset.Futures][p] = &MultiPositionTracker{
|
||||
exchange: testExchange,
|
||||
positions: []*PositionTracker{
|
||||
{
|
||||
@@ -551,14 +575,14 @@ func TestClearPositionsForExchange(t *testing.T) {
|
||||
if !errors.Is(err, ErrPositionsNotLoadedForExchange) {
|
||||
t.Errorf("received '%v' expected '%v", err, ErrPositionsNotLoadedForExchange)
|
||||
}
|
||||
c.positionTrackerControllers = make(map[string]map[asset.Item]map[currency.Pair]*MultiPositionTracker)
|
||||
c.positionTrackerControllers[testExchange] = nil
|
||||
c.multiPositionTrackers = make(map[string]map[asset.Item]map[currency.Pair]*MultiPositionTracker)
|
||||
c.multiPositionTrackers[testExchange] = nil
|
||||
err = c.ClearPositionsForExchange(testExchange, asset.Futures, p)
|
||||
if !errors.Is(err, ErrPositionsNotLoadedForAsset) {
|
||||
t.Errorf("received '%v' expected '%v", err, ErrPositionsNotLoadedForExchange)
|
||||
}
|
||||
c.positionTrackerControllers[testExchange] = make(map[asset.Item]map[currency.Pair]*MultiPositionTracker)
|
||||
c.positionTrackerControllers[testExchange][asset.Futures] = nil
|
||||
c.multiPositionTrackers[testExchange] = make(map[asset.Item]map[currency.Pair]*MultiPositionTracker)
|
||||
c.multiPositionTrackers[testExchange][asset.Futures] = nil
|
||||
err = c.ClearPositionsForExchange(testExchange, asset.Futures, p)
|
||||
if !errors.Is(err, ErrPositionsNotLoadedForPair) {
|
||||
t.Errorf("received '%v' expected '%v", err, ErrPositionsNotLoadedForPair)
|
||||
@@ -568,11 +592,11 @@ func TestClearPositionsForExchange(t *testing.T) {
|
||||
t.Errorf("received '%v' expected '%v", err, ErrNotFuturesAsset)
|
||||
}
|
||||
|
||||
c.positionTrackerControllers[testExchange][asset.Futures] = make(map[currency.Pair]*MultiPositionTracker)
|
||||
c.positionTrackerControllers[testExchange][asset.Futures][p] = &MultiPositionTracker{
|
||||
c.multiPositionTrackers[testExchange][asset.Futures] = make(map[currency.Pair]*MultiPositionTracker)
|
||||
c.multiPositionTrackers[testExchange][asset.Futures][p] = &MultiPositionTracker{
|
||||
exchange: testExchange,
|
||||
}
|
||||
c.positionTrackerControllers[testExchange][asset.Futures][p] = &MultiPositionTracker{
|
||||
c.multiPositionTrackers[testExchange][asset.Futures][p] = &MultiPositionTracker{
|
||||
exchange: testExchange,
|
||||
underlying: currency.DOGE,
|
||||
positions: []*PositionTracker{
|
||||
@@ -585,7 +609,7 @@ func TestClearPositionsForExchange(t *testing.T) {
|
||||
if !errors.Is(err, nil) {
|
||||
t.Errorf("received '%v' expected '%v", err, nil)
|
||||
}
|
||||
if len(c.positionTrackerControllers[testExchange][asset.Futures][p].positions) != 0 {
|
||||
if len(c.multiPositionTrackers[testExchange][asset.Futures][p].positions) != 0 {
|
||||
t.Fatal("expected 0")
|
||||
}
|
||||
c = nil
|
||||
@@ -599,29 +623,32 @@ func TestCalculateRealisedPNL(t *testing.T) {
|
||||
t.Parallel()
|
||||
result := calculateRealisedPNL(nil)
|
||||
if !result.IsZero() {
|
||||
t.Error("expected zero")
|
||||
t.Errorf("received '%v' expected '0'", result)
|
||||
}
|
||||
result = calculateRealisedPNL([]PNLResult{
|
||||
{
|
||||
IsOrder: true,
|
||||
RealisedPNLBeforeFees: decimal.NewFromInt(1337),
|
||||
},
|
||||
})
|
||||
if !result.Equal(decimal.NewFromInt(1337)) {
|
||||
t.Error("expected 1337")
|
||||
t.Errorf("received '%v' expected '1337'", result)
|
||||
}
|
||||
|
||||
result = calculateRealisedPNL([]PNLResult{
|
||||
{
|
||||
IsOrder: true,
|
||||
RealisedPNLBeforeFees: decimal.NewFromInt(1339),
|
||||
Fee: decimal.NewFromInt(2),
|
||||
},
|
||||
{
|
||||
IsOrder: true,
|
||||
RealisedPNLBeforeFees: decimal.NewFromInt(2),
|
||||
Fee: decimal.NewFromInt(2),
|
||||
},
|
||||
})
|
||||
if !result.Equal(decimal.NewFromInt(1337)) {
|
||||
t.Error("expected 1337")
|
||||
t.Errorf("received '%v' expected '1337'", result)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -802,3 +829,245 @@ func TestUpdateOpenPositionUnrealisedPNL(t *testing.T) {
|
||||
t.Errorf("received '%v' expected '%v", err, common.ErrNilPointer)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetCollateralCurrency(t *testing.T) {
|
||||
t.Parallel()
|
||||
var expectedError = ErrNotFuturesAsset
|
||||
pc := SetupPositionController()
|
||||
err := pc.SetCollateralCurrency("hi", asset.Spot, currency.Pair{}, currency.Code{})
|
||||
if !errors.Is(err, expectedError) {
|
||||
t.Errorf("received '%v' expected '%v", err, expectedError)
|
||||
}
|
||||
cp := currency.NewPair(currency.BTC, currency.USDT)
|
||||
pc.multiPositionTrackers = make(map[string]map[asset.Item]map[currency.Pair]*MultiPositionTracker)
|
||||
err = pc.SetCollateralCurrency("hi", asset.Futures, cp, currency.DOGE)
|
||||
expectedError = ErrPositionsNotLoadedForExchange
|
||||
if !errors.Is(err, expectedError) {
|
||||
t.Fatalf("received '%v' expected '%v", err, expectedError)
|
||||
}
|
||||
pc.multiPositionTrackers["hi"] = make(map[asset.Item]map[currency.Pair]*MultiPositionTracker)
|
||||
err = pc.SetCollateralCurrency("hi", asset.Futures, cp, currency.DOGE)
|
||||
expectedError = ErrPositionsNotLoadedForAsset
|
||||
if !errors.Is(err, expectedError) {
|
||||
t.Fatalf("received '%v' expected '%v", err, expectedError)
|
||||
}
|
||||
|
||||
pc.multiPositionTrackers["hi"][asset.Futures] = make(map[currency.Pair]*MultiPositionTracker)
|
||||
err = pc.SetCollateralCurrency("hi", asset.Futures, cp, currency.DOGE)
|
||||
expectedError = ErrPositionsNotLoadedForPair
|
||||
if !errors.Is(err, expectedError) {
|
||||
t.Fatalf("received '%v' expected '%v", err, expectedError)
|
||||
}
|
||||
|
||||
pc.multiPositionTrackers["hi"][asset.Futures][cp] = nil
|
||||
err = pc.SetCollateralCurrency("hi", asset.Futures, cp, currency.DOGE)
|
||||
expectedError = common.ErrNilPointer
|
||||
if !errors.Is(err, expectedError) {
|
||||
t.Fatalf("received '%v' expected '%v", err, expectedError)
|
||||
}
|
||||
|
||||
pc.multiPositionTrackers["hi"][asset.Futures][cp] = &MultiPositionTracker{
|
||||
exchange: "hi",
|
||||
asset: asset.Futures,
|
||||
pair: cp,
|
||||
orderPositions: make(map[string]*PositionTracker),
|
||||
}
|
||||
err = pc.TrackNewOrder(&Detail{
|
||||
Date: time.Now(),
|
||||
Exchange: "hi",
|
||||
Pair: cp,
|
||||
AssetType: asset.Futures,
|
||||
Side: Long,
|
||||
OrderID: "lol",
|
||||
Price: 1,
|
||||
Amount: 1,
|
||||
})
|
||||
if !errors.Is(err, nil) {
|
||||
t.Fatalf("received '%v' expected '%v", err, nil)
|
||||
}
|
||||
|
||||
err = pc.SetCollateralCurrency("hi", asset.Futures, cp, currency.DOGE)
|
||||
expectedError = nil
|
||||
if !errors.Is(err, expectedError) {
|
||||
t.Fatalf("received '%v' expected '%v", err, expectedError)
|
||||
}
|
||||
|
||||
if !pc.multiPositionTrackers["hi"][asset.Futures][cp].collateralCurrency.Equal(currency.DOGE) {
|
||||
t.Errorf("received '%v' expected '%v'", pc.multiPositionTrackers["hi"][asset.Futures][cp].collateralCurrency, currency.DOGE)
|
||||
}
|
||||
|
||||
if !pc.multiPositionTrackers["hi"][asset.Futures][cp].positions[0].collateralCurrency.Equal(currency.DOGE) {
|
||||
t.Errorf("received '%v' expected '%v'", pc.multiPositionTrackers["hi"][asset.Futures][cp].positions[0].collateralCurrency, currency.DOGE)
|
||||
}
|
||||
|
||||
pc = nil
|
||||
err = pc.SetCollateralCurrency("hi", asset.Spot, currency.Pair{}, currency.Code{})
|
||||
expectedError = common.ErrNilPointer
|
||||
if !errors.Is(err, expectedError) {
|
||||
t.Errorf("received '%v' expected '%v", err, expectedError)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMPTUpdateOpenPositionUnrealisedPNL(t *testing.T) {
|
||||
t.Parallel()
|
||||
var err, expectedError error
|
||||
expectedError = nil
|
||||
cp := currency.NewPair(currency.BTC, currency.USDT)
|
||||
pc := SetupPositionController()
|
||||
err = pc.TrackNewOrder(&Detail{
|
||||
Date: time.Now(),
|
||||
Exchange: "hi",
|
||||
Pair: cp,
|
||||
AssetType: asset.Futures,
|
||||
Side: Long,
|
||||
OrderID: "lol",
|
||||
Price: 1,
|
||||
Amount: 1,
|
||||
})
|
||||
if !errors.Is(err, expectedError) {
|
||||
t.Fatalf("received '%v' expected '%v", err, expectedError)
|
||||
}
|
||||
result, err := pc.multiPositionTrackers["hi"][asset.Futures][cp].UpdateOpenPositionUnrealisedPNL(1337, time.Now())
|
||||
if !errors.Is(err, expectedError) {
|
||||
t.Fatalf("received '%v' expected '%v", err, expectedError)
|
||||
}
|
||||
if result.Equal(decimal.NewFromInt(1337)) {
|
||||
t.Error("")
|
||||
}
|
||||
|
||||
expectedError = ErrPositionClosed
|
||||
pc.multiPositionTrackers["hi"][asset.Futures][cp].positions[0].status = Closed
|
||||
_, err = pc.multiPositionTrackers["hi"][asset.Futures][cp].UpdateOpenPositionUnrealisedPNL(1337, time.Now())
|
||||
if !errors.Is(err, expectedError) {
|
||||
t.Fatalf("received '%v' expected '%v", err, expectedError)
|
||||
}
|
||||
|
||||
expectedError = ErrPositionsNotLoadedForPair
|
||||
pc.multiPositionTrackers["hi"][asset.Futures][cp].positions = nil
|
||||
_, err = pc.multiPositionTrackers["hi"][asset.Futures][cp].UpdateOpenPositionUnrealisedPNL(1337, time.Now())
|
||||
if !errors.Is(err, expectedError) {
|
||||
t.Fatalf("received '%v' expected '%v", err, expectedError)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMPTLiquidate(t *testing.T) {
|
||||
t.Parallel()
|
||||
item := asset.Futures
|
||||
pair, err := currency.NewPairFromStrings("BTC", "1231")
|
||||
if !errors.Is(err, nil) {
|
||||
t.Error(err)
|
||||
}
|
||||
e := &MultiPositionTracker{
|
||||
exchange: testExchange,
|
||||
exchangePNLCalculation: &FakePNL{},
|
||||
asset: item,
|
||||
orderPositions: make(map[string]*PositionTracker),
|
||||
}
|
||||
|
||||
err = e.Liquidate(decimal.Zero, time.Time{})
|
||||
if !errors.Is(err, ErrPositionsNotLoadedForPair) {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
setup := &PositionTrackerSetup{
|
||||
Pair: pair,
|
||||
Asset: item,
|
||||
}
|
||||
_, err = e.SetupPositionTracker(setup)
|
||||
if !errors.Is(err, nil) {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
tt := time.Now()
|
||||
err = e.TrackNewOrder(&Detail{
|
||||
Date: tt,
|
||||
Exchange: testExchange,
|
||||
Pair: pair,
|
||||
AssetType: item,
|
||||
Side: Long,
|
||||
OrderID: "lol",
|
||||
Price: 1,
|
||||
Amount: 1,
|
||||
})
|
||||
if !errors.Is(err, nil) {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
err = e.Liquidate(decimal.Zero, time.Time{})
|
||||
if !errors.Is(err, errCannotLiquidate) {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
err = e.Liquidate(decimal.Zero, tt)
|
||||
if !errors.Is(err, nil) {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
if e.positions[0].status != Liquidated {
|
||||
t.Errorf("received '%v' expected '%v'", e.positions[0].status, Liquidated)
|
||||
}
|
||||
if !e.positions[0].exposure.IsZero() {
|
||||
t.Errorf("received '%v' expected '%v'", e.positions[0].exposure, 0)
|
||||
}
|
||||
|
||||
e = nil
|
||||
err = e.Liquidate(decimal.Zero, tt)
|
||||
if !errors.Is(err, common.ErrNilPointer) {
|
||||
t.Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPositionLiquidate(t *testing.T) {
|
||||
t.Parallel()
|
||||
item := asset.Futures
|
||||
pair, err := currency.NewPairFromStrings("BTC", "1231")
|
||||
if !errors.Is(err, nil) {
|
||||
t.Error(err)
|
||||
}
|
||||
p := &PositionTracker{
|
||||
contractPair: pair,
|
||||
asset: item,
|
||||
exchange: testExchange,
|
||||
PNLCalculation: &PNLCalculator{},
|
||||
status: Open,
|
||||
openingDirection: Long,
|
||||
}
|
||||
|
||||
tt := time.Now()
|
||||
err = p.TrackNewOrder(&Detail{
|
||||
Date: tt,
|
||||
Exchange: testExchange,
|
||||
Pair: pair,
|
||||
AssetType: item,
|
||||
Side: Long,
|
||||
OrderID: "lol",
|
||||
Price: 1,
|
||||
Amount: 1,
|
||||
}, false)
|
||||
if !errors.Is(err, nil) {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
err = p.Liquidate(decimal.Zero, time.Time{})
|
||||
if !errors.Is(err, errCannotLiquidate) {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
err = p.Liquidate(decimal.Zero, tt)
|
||||
if !errors.Is(err, nil) {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
if p.status != Liquidated {
|
||||
t.Errorf("received '%v' expected '%v'", p.status, Liquidated)
|
||||
}
|
||||
if !p.exposure.IsZero() {
|
||||
t.Errorf("received '%v' expected '%v'", p.exposure, 0)
|
||||
}
|
||||
|
||||
p = nil
|
||||
err = p.Liquidate(decimal.Zero, tt)
|
||||
if !errors.Is(err, common.ErrNilPointer) {
|
||||
t.Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,18 +44,21 @@ var (
|
||||
errNilOrder = errors.New("nil order received")
|
||||
errNoPNLHistory = errors.New("no pnl history")
|
||||
errCannotCalculateUnrealisedPNL = errors.New("cannot calculate unrealised PNL")
|
||||
errCannotTrackInvalidParams = errors.New("parameters set incorrectly, cannot track")
|
||||
)
|
||||
|
||||
// PNLCalculation is an interface to allow multiple
|
||||
// ways of calculating PNL to be used for futures positions
|
||||
type PNLCalculation interface {
|
||||
CalculatePNL(context.Context, *PNLCalculatorRequest) (*PNLResult, error)
|
||||
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)
|
||||
}
|
||||
@@ -125,8 +128,8 @@ type UsedCollateralBreakdown struct {
|
||||
// and so all you need to do is send all orders to
|
||||
// the position controller and its all tracked happily
|
||||
type PositionController struct {
|
||||
m sync.Mutex
|
||||
positionTrackerControllers map[string]map[asset.Item]map[currency.Pair]*MultiPositionTracker
|
||||
m sync.Mutex
|
||||
multiPositionTrackers map[string]map[asset.Item]map[currency.Pair]*MultiPositionTracker
|
||||
}
|
||||
|
||||
// MultiPositionTracker will track the performance of
|
||||
@@ -134,12 +137,13 @@ type PositionController struct {
|
||||
// is closed, then the position controller will create a new one
|
||||
// to track the current positions
|
||||
type MultiPositionTracker struct {
|
||||
m sync.Mutex
|
||||
exchange string
|
||||
asset asset.Item
|
||||
pair currency.Pair
|
||||
underlying currency.Code
|
||||
positions []*PositionTracker
|
||||
m sync.Mutex
|
||||
exchange string
|
||||
asset asset.Item
|
||||
pair currency.Pair
|
||||
underlying currency.Code
|
||||
collateralCurrency currency.Code
|
||||
positions []*PositionTracker
|
||||
// order positions allows for an easier time knowing which order is
|
||||
// part of which position tracker
|
||||
orderPositions map[string]*PositionTracker
|
||||
@@ -155,6 +159,7 @@ type MultiPositionTrackerSetup struct {
|
||||
Asset asset.Item
|
||||
Pair currency.Pair
|
||||
Underlying currency.Code
|
||||
CollateralCurrency currency.Code
|
||||
OfflineCalculation bool
|
||||
UseExchangePNLCalculation bool
|
||||
ExchangePNLCalculation PNLCalculation
|
||||
@@ -173,6 +178,7 @@ type PositionTracker struct {
|
||||
asset asset.Item
|
||||
contractPair currency.Pair
|
||||
underlyingAsset currency.Code
|
||||
collateralCurrency currency.Code
|
||||
exposure decimal.Decimal
|
||||
currentDirection Side
|
||||
openingDirection Side
|
||||
@@ -196,6 +202,7 @@ type PositionTrackerSetup struct {
|
||||
Pair currency.Pair
|
||||
EntryPrice decimal.Decimal
|
||||
Underlying currency.Code
|
||||
CollateralCurrency currency.Code
|
||||
Asset asset.Item
|
||||
Side Side
|
||||
UseExchangePNLCalculation bool
|
||||
@@ -256,29 +263,36 @@ type PNLCalculatorRequest struct {
|
||||
|
||||
// PNLResult stores a PNL result from a point in time
|
||||
type PNLResult struct {
|
||||
Status Status
|
||||
Time time.Time
|
||||
UnrealisedPNL decimal.Decimal
|
||||
RealisedPNLBeforeFees decimal.Decimal
|
||||
RealisedPNL decimal.Decimal
|
||||
Price decimal.Decimal
|
||||
Exposure decimal.Decimal
|
||||
Direction Side
|
||||
Fee decimal.Decimal
|
||||
IsLiquidated bool
|
||||
// Is event is supposed to show that something has happened and it isnt just tracking in time
|
||||
IsOrder bool
|
||||
}
|
||||
|
||||
// PositionStats is a basic holder
|
||||
// for position information
|
||||
type PositionStats struct {
|
||||
Exchange string
|
||||
Asset asset.Item
|
||||
Pair currency.Pair
|
||||
Underlying currency.Code
|
||||
Orders []Detail
|
||||
RealisedPNL decimal.Decimal
|
||||
UnrealisedPNL decimal.Decimal
|
||||
LatestDirection Side
|
||||
Status Status
|
||||
OpeningDirection Side
|
||||
OpeningPrice decimal.Decimal
|
||||
LatestPrice decimal.Decimal
|
||||
PNLHistory []PNLResult
|
||||
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
|
||||
OpeningPrice decimal.Decimal
|
||||
LatestPrice decimal.Decimal
|
||||
PNLHistory []PNLResult
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ var (
|
||||
ErrAmountIsInvalid = errors.New("order amount is equal or less than zero")
|
||||
ErrPriceMustBeSetIfLimitOrder = errors.New("order price must be set if limit order type is desired")
|
||||
ErrOrderIDNotSet = errors.New("order id or client order id is not set")
|
||||
errCannotLiquidate = errors.New("cannot liquidate position")
|
||||
)
|
||||
|
||||
// Submit contains all properties of an order that may be required
|
||||
@@ -278,6 +279,7 @@ const (
|
||||
Closed
|
||||
Pending
|
||||
Cancelling
|
||||
Liquidated
|
||||
)
|
||||
|
||||
// Type enforces a standard for order types across the code base
|
||||
@@ -304,7 +306,7 @@ const (
|
||||
)
|
||||
|
||||
// Side enforces a standard for order sides across the code base
|
||||
type Side uint16
|
||||
type Side uint32
|
||||
|
||||
// Order side types
|
||||
const (
|
||||
@@ -316,11 +318,16 @@ const (
|
||||
AnySide
|
||||
Long
|
||||
Short
|
||||
ClosePosition
|
||||
// Backtester signal types
|
||||
DoNothing
|
||||
TransferredFunds
|
||||
CouldNotBuy
|
||||
CouldNotSell
|
||||
CouldNotShort
|
||||
CouldNotLong
|
||||
CouldNotCloseShort
|
||||
CouldNotCloseLong
|
||||
MissingData
|
||||
)
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ const (
|
||||
orderSubmissionValidSides = Buy | Sell | Bid | Ask | Long | Short
|
||||
shortSide = Short | Sell | Ask
|
||||
longSide = Long | Buy | Bid
|
||||
inactiveStatuses = Filled | Cancelled | InsufficientBalance | MarketUnavailable | Rejected | PartiallyCancelled | Expired | Closed | AnyStatus | Cancelling
|
||||
inactiveStatuses = Filled | Cancelled | InsufficientBalance | MarketUnavailable | Rejected | PartiallyCancelled | Expired | Closed | AnyStatus | Cancelling | Liquidated
|
||||
activeStatuses = Active | Open | PartiallyFilled | New | PendingCancel | Hidden | AutoDeleverage | Pending
|
||||
notPlaced = InsufficientBalance | MarketUnavailable | Rejected
|
||||
)
|
||||
@@ -375,7 +375,13 @@ func (d *Detail) IsActive() bool {
|
||||
func (d *Detail) IsInactive() bool {
|
||||
return d.Amount <= 0 ||
|
||||
d.Amount <= d.ExecutedAmount ||
|
||||
inactiveStatuses&d.Status == d.Status
|
||||
d.Status.IsInactive()
|
||||
}
|
||||
|
||||
// IsInactive returns true if the status indicates it is
|
||||
// currently not available on the exchange
|
||||
func (s Status) IsInactive() bool {
|
||||
return inactiveStatuses&s == s
|
||||
}
|
||||
|
||||
// WasOrderPlaced returns true if an order has a status that indicates that it
|
||||
@@ -636,6 +642,8 @@ func (s Side) String() string {
|
||||
return "SHORT"
|
||||
case AnySide:
|
||||
return "ANY"
|
||||
case ClosePosition:
|
||||
return "CLOSE POSITION"
|
||||
// Backtester signal types below.
|
||||
case DoNothing:
|
||||
return "DO NOTHING"
|
||||
@@ -645,6 +653,14 @@ func (s Side) String() string {
|
||||
return "COULD NOT BUY"
|
||||
case CouldNotSell:
|
||||
return "COULD NOT SELL"
|
||||
case CouldNotShort:
|
||||
return "COULD NOT SHORT"
|
||||
case CouldNotLong:
|
||||
return "COULD NOT LONG"
|
||||
case CouldNotCloseShort:
|
||||
return "COULD NOT CLOSE SHORT"
|
||||
case CouldNotCloseLong:
|
||||
return "COULD NOT CLOSE LONG"
|
||||
case MissingData:
|
||||
return "MISSING DATA"
|
||||
default:
|
||||
|
||||
Reference in New Issue
Block a user