diff --git a/core/integration/docs/positions.md b/core/integration/docs/positions.md new file mode 100644 index 0000000000..24da92168a --- /dev/null +++ b/core/integration/docs/positions.md @@ -0,0 +1,38 @@ +## Verifying positions and PnL + +At its heart, we are testing a trading platform. Therefore, we need to have the ability to verify the positions held by traders, and their realised or unrealised profit and loss (PnL). The data-node component implements a positions API which collates data events sent out when parties trade, positions are marked to market or settled, when perpetual markets process a funding payment, etc... +The integration test framework implements a number of steps to verify whether or not a trade took place, verifying [individual transfer data](transfers.md). In addition to this, the integration test framework implements a step that allows us to verify the position data analogous to the data-node API. + +```cucumber +Then the parties should have the following profit and loss: + | market id | party | volume | unrealised pnl | realised pnl | status | taker fees | taker fees since | maker fees | maker fees since | other fees | other fees since | funding payments | funding payments since | + | ETH/DEC19 | trader2 | 0 | 0 | 0 | POSITION_STATUS_ORDERS_CLOSED | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | + | ETH/DEC19 | trader3 | 0 | 0 | -162 | POSITION_STATUS_CLOSED_OUT | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | + | ETH/DEC19 | auxiliary1 | -10 | -900 | 0 | | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | + | ETH/DEC19 | auxiliary2 | 5 | 475 | 586 | | 0 | 0 | 0 | 0 | 1 | 0 | 0 | 0 | + ``` + + Where the fields are defined as follows: + + ``` + | name | type | required | + |------------------------|----------------|----------| + | market id | string | no | + | party | string | yes | + | volume | int64 | yes | + | unrealised pnl | Int | yes | + | realised pnl | Int | yes | + | status | PositionStatus | no | + | taker fees | Uint | no | + | maker fees | Uint | no | + | other fees | Uint | no | + | taker fees since | Uint | no | + | maker fees since | Uint | no | + | other fees since | Uint | no | + | funding payments | Int | no | + | funding payments since | Int | no | + | is amm | bool | no | + ``` + +Details for the [`PositionStatus` type](types.md#Position-status) + diff --git a/core/integration/docs/types.md b/core/integration/docs/types.md index a3eb4c035b..ca867b1c8f 100644 --- a/core/integration/docs/types.md +++ b/core/integration/docs/types.md @@ -2,6 +2,15 @@ Below is a list of types used in the docs and the possible values +## Position status + +Possible values for `PositionStatus` are: + +* POSITION_STATUS_UNSPECIFIED +* POSITION_STATUS_ORDERS_CLOSED +* POSITION_STATUS_CLOSED_OUT +* POSITION_STATUS_DISTRESSED + ## Auction trigger Possible values for `AuctionTrigger` are: diff --git a/core/integration/features/zero-position.feature b/core/integration/features/zero-position.feature index 1cc077f8f7..37b2d31bb4 100644 --- a/core/integration/features/zero-position.feature +++ b/core/integration/features/zero-position.feature @@ -142,11 +142,11 @@ Feature: Closeout scenarios | trader3 | USD | ETH/DEC19 | 0 | 0 | Then the parties should have the following profit and loss: - | party | volume | unrealised pnl | realised pnl | status | - | trader2 | 0 | 0 | 0 | POSITION_STATUS_ORDERS_CLOSED | - | trader3 | 0 | 0 | -162 | POSITION_STATUS_CLOSED_OUT | - | auxiliary1 | -10 | -900 | 0 | | - | auxiliary2 | 5 | 475 | 586 | | + | party | volume | unrealised pnl | realised pnl | status | taker fees | taker fees since | maker fees | maker fees since | other fees | other fees since | funding payments | funding payments since | + | trader2 | 0 | 0 | 0 | POSITION_STATUS_ORDERS_CLOSED | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | + | trader3 | 0 | 0 | -162 | POSITION_STATUS_CLOSED_OUT | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | + | auxiliary1 | -10 | -900 | 0 | | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | + | auxiliary2 | 5 | 475 | 586 | | 0 | 0 | 0 | 0 | 1 | 0 | 0 | 0 | And the insurance pool balance should be "0" for the market "ETH/DEC19" When the parties place the following orders: | party | market id | side | price | volume | resulting trades | type | tif | reference | diff --git a/core/integration/steps/parties_should_have_the_following_profit_and_loss.go b/core/integration/steps/parties_should_have_the_following_profit_and_loss.go index 3fe5c2d066..02db8ee82e 100644 --- a/core/integration/steps/parties_should_have_the_following_profit_and_loss.go +++ b/core/integration/steps/parties_should_have_the_following_profit_and_loss.go @@ -48,6 +48,8 @@ func positionAPIProduceTheFollowingRow(exec Execution, positionService *plugins. var pos []*types.Position // check position status if needed ps, checkPS := row.positionState() + checkFees := row.checkFees() + checkFunding := row.checkFunding() party := row.party() readableParty := party if row.isAMM() { @@ -79,12 +81,22 @@ func positionAPIProduceTheFollowingRow(exec Execution, positionService *plugins. } if areSamePosition(pos, row) { - if !checkPS { + if !checkFees && !checkFunding && !checkPS { return nil } - // check state if required - states, _ := positionService.GetPositionStatesByParty(party) - if len(states) == 1 && states[0] == ps { + match := true + if checkFees { + match = feesMatch(pos, row) + } + if checkFunding && match { + match = fundingMatches(pos, row) + } + if checkPS && match { + // check state if required + states, _ := positionService.GetPositionStatesByParty(party) + match = len(states) == 1 && states[0] == ps + } + if match { return nil } } @@ -109,15 +121,19 @@ func errProfitAndLossValuesForParty(pos []*types.Position, row pnlRow) error { } return formatDiff( fmt.Sprintf("invalid positions values for party(%v)", row.party()), + row.diffMap(), map[string]string{ - "volume": i64ToS(row.volume()), - "unrealised PNL": row.unrealisedPNL().String(), - "realised PNL": row.realisedPNL().String(), - }, - map[string]string{ - "volume": i64ToS(pos[0].OpenVolume), - "unrealised PNL": pos[0].UnrealisedPnl.String(), - "realised PNL": pos[0].RealisedPnl.String(), + "volume": i64ToS(pos[0].OpenVolume), + "unrealised PNL": pos[0].UnrealisedPnl.String(), + "realised PNL": pos[0].RealisedPnl.String(), + "taker fees": pos[0].TakerFeesPaid.String(), + "taker fees since": pos[0].TakerFeesPaidSince.String(), + "maker fees": pos[0].MakerFeesReceived.String(), + "maker fees since": pos[0].MakerFeesReceivedSince.String(), + "other fees": pos[0].FeesPaid.String(), + "other fees since": pos[0].FeesPaidSince.String(), + "funding payments": pos[0].FundingPaymentAmount.String(), + "funding payments since": pos[0].FundingPaymentAmountSince.String(), }, ) } @@ -133,6 +149,52 @@ func areSamePosition(pos []*types.Position, row pnlRow) bool { pos[0].UnrealisedPnl.Equals(row.unrealisedPNL()) } +func feesMatch(pos []*types.Position, row pnlRow) bool { + if len(pos) == 0 { + return false + } + taker, ok := row.takerFees() + if ok && !taker.EQ(pos[0].TakerFeesPaid) { + return false + } + maker, ok := row.makerFees() + if ok && !maker.EQ(pos[0].MakerFeesReceived) { + return false + } + other, ok := row.otherFees() + if ok && !other.EQ(pos[0].FeesPaid) { + return false + } + taker, ok = row.takerFeesSince() + if ok && !taker.EQ(pos[0].TakerFeesPaidSince) { + return false + } + maker, ok = row.makerFeesSince() + if ok && !maker.EQ(pos[0].MakerFeesReceivedSince) { + return false + } + other, ok = row.otherFeesSince() + if ok && !other.EQ(pos[0].FeesPaidSince) { + return false + } + return true +} + +func fundingMatches(pos []*types.Position, row pnlRow) bool { + if len(pos) == 0 { + return false + } + fp, ok := row.fundingPayment() + if ok && !fp.EQ(pos[0].FundingPaymentAmount) { + return false + } + fp, ok = row.fundingPaymentSince() + if ok && !fp.EQ(pos[0].FundingPaymentAmountSince) { + return false + } + return true +} + func errCannotGetPositionForParty(party string, err error) error { return fmt.Errorf("error getting party position, party(%v), err(%v)", party, err) } @@ -147,6 +209,14 @@ func parseProfitAndLossTable(table *godog.Table) []RowWrapper { "status", "market id", "is amm", + "taker fees", + "taker fees since", + "maker fees", + "maker fees since", + "other fees", + "other fees since", + "funding payments", + "funding payments since", }) } @@ -191,3 +261,130 @@ func (r pnlRow) isAMM() bool { } return r.row.MustBool("is amm") } + +func (r pnlRow) takerFees() (*num.Uint, bool) { + if !r.row.HasColumn("taker fees") { + return nil, false + } + return r.row.MustUint("taker fees"), true +} + +func (r pnlRow) takerFeesSince() (*num.Uint, bool) { + if !r.row.HasColumn("taker fees since") { + return nil, false + } + return r.row.MustUint("taker fees since"), true +} + +func (r pnlRow) makerFees() (*num.Uint, bool) { + if !r.row.HasColumn("maker fees") { + return nil, false + } + return r.row.MustUint("maker fees"), true +} + +func (r pnlRow) makerFeesSince() (*num.Uint, bool) { + if !r.row.HasColumn("maker fees since") { + return nil, false + } + return r.row.MustUint("maker fees since"), true +} + +func (r pnlRow) otherFees() (*num.Uint, bool) { + if !r.row.HasColumn("other fees") { + return nil, false + } + return r.row.MustUint("other fees"), true +} + +func (r pnlRow) otherFeesSince() (*num.Uint, bool) { + if !r.row.HasColumn("other fees since") { + return nil, false + } + return r.row.MustUint("other fees since"), true +} + +func (r pnlRow) fundingPayment() (*num.Int, bool) { + if !r.row.HasColumn("funding payments") { + return nil, false + } + return r.row.MustInt("funding payments"), true +} + +func (r pnlRow) fundingPaymentSince() (*num.Int, bool) { + if !r.row.HasColumn("funding payments since") { + return nil, false + } + return r.row.MustInt("funding payments since"), true +} + +func (r pnlRow) checkFees() bool { + if _, taker := r.takerFees(); taker { + return true + } + if _, maker := r.makerFees(); maker { + return true + } + if _, other := r.otherFees(); other { + return true + } + if _, ok := r.takerFeesSince(); ok { + return true + } + if _, ok := r.makerFeesSince(); ok { + return true + } + if _, ok := r.otherFeesSince(); ok { + return true + } + return false +} + +func (r pnlRow) checkFunding() bool { + if _, ok := r.fundingPayment(); ok { + return true + } + _, ok := r.fundingPaymentSince() + return ok +} + +func (r pnlRow) diffMap() map[string]string { + m := map[string]string{ + "volume": i64ToS(r.volume()), + "unrealised PNL": r.unrealisedPNL().String(), + "realised PNL": r.realisedPNL().String(), + "taker fees": "", + "taker fees since": "", + "maker fees": "", + "maker fees since": "", + "other fees": "", + "other fees since": "", + "funding payments": "", + "funding payments since": "", + } + if v, ok := r.takerFees(); ok { + m["taker fees"] = v.String() + } + if v, ok := r.makerFees(); ok { + m["maker fees"] = v.String() + } + if v, ok := r.otherFees(); ok { + m["other fees"] = v.String() + } + if v, ok := r.takerFeesSince(); ok { + m["taker fees since"] = v.String() + } + if v, ok := r.makerFeesSince(); ok { + m["maker fees since"] = v.String() + } + if v, ok := r.otherFeesSince(); ok { + m["other fees since"] = v.String() + } + if v, ok := r.fundingPayment(); ok { + m["funding payments"] = v.String() + } + if v, ok := r.fundingPaymentSince(); ok { + m["funding payments since"] = v.String() + } + return m +} diff --git a/core/plugins/positions.go b/core/plugins/positions.go index 4f82583b3f..37ef3ed08e 100644 --- a/core/plugins/positions.go +++ b/core/plugins/positions.go @@ -164,34 +164,20 @@ func (p *Positions) handleRegularTrade(e TE) { buyer, ok := partyPos[trade.Buyer] if !ok { buyer = Position{ - Position: types.Position{ - MarketID: marketID, - PartyID: types.NetworkParty, - }, + Position: types.NewPosition(marketID, trade.Buyer), AverageEntryPriceFP: num.DecimalZero(), RealisedPnlFP: num.DecimalZero(), UnrealisedPnlFP: num.DecimalZero(), - taker: newAmount(), - maker: newAmount(), - other: newAmount(), - funding: newAmount(), } } buyer.setFees(buyerFee) seller, ok := partyPos[trade.Seller] if !ok { seller = Position{ - Position: types.Position{ - MarketID: marketID, - PartyID: types.NetworkParty, - }, + Position: types.NewPosition(marketID, trade.Seller), AverageEntryPriceFP: num.DecimalZero(), RealisedPnlFP: num.DecimalZero(), UnrealisedPnlFP: num.DecimalZero(), - taker: newAmount(), - maker: newAmount(), - other: newAmount(), - funding: newAmount(), } } seller.setFees(sellerFee) @@ -223,17 +209,10 @@ func (p *Positions) handleTradeEvent(e TE) { pos, ok := partyPos[types.NetworkParty] if !ok { pos = Position{ - Position: types.Position{ - MarketID: marketID, - PartyID: types.NetworkParty, - }, + Position: types.NewPosition(marketID, types.NetworkParty), AverageEntryPriceFP: num.DecimalZero(), RealisedPnlFP: num.DecimalZero(), UnrealisedPnlFP: num.DecimalZero(), - taker: newAmount(), - maker: newAmount(), - other: newAmount(), - funding: newAmount(), } } dParty := trade.Seller @@ -246,21 +225,14 @@ func (p *Positions) handleTradeEvent(e TE) { other, ok := partyPos[dParty] if !ok { other = Position{ - Position: types.Position{ - MarketID: marketID, - PartyID: dParty, - }, + Position: types.NewPosition(marketID, dParty), AverageEntryPriceFP: num.DecimalZero(), RealisedPnlFP: num.DecimalZero(), UnrealisedPnlFP: num.DecimalZero(), - taker: newAmount(), - maker: newAmount(), - other: newAmount(), - funding: newAmount(), } } other.setFees(otherFee) - other.resetSince() + other.ResetSince() pos.setFees(networkFee) opened, closed := calculateOpenClosedVolume(pos.OpenVolume, size) realisedPnlDelta := markPriceDec.Sub(pos.AverageEntryPriceFP).Mul(num.DecimalFromInt64(closed)).Div(posFactor) @@ -297,8 +269,8 @@ func (p *Positions) handleFundingPayments(e FP) { pos.RealisedPnl = pos.RealisedPnl.Add(amt) pos.RealisedPnlFP = pos.RealisedPnlFP.Add(amt) // add funding totals - pos.funding.total.Add(iAmt) - pos.funding.since.Add(iAmt) + pos.FundingPaymentAmount.Add(iAmt) + pos.FundingPaymentAmountSince.Add(iAmt) partyPos[pay.PartyId] = pos } p.data[marketID] = partyPos @@ -354,8 +326,8 @@ func (p *Positions) applyLossSocialization(e LSE) { } if e.IsFunding() { // adjust funding amounts if needed. - pos.funding.total.Add(iAmt) - pos.funding.since.Add(iAmt) + pos.FundingPaymentAmount.Add(iAmt) + pos.FundingPaymentAmountSince.Add(iAmt) } pos.RealisedPnlFP = pos.RealisedPnlFP.Add(amountLoss) pos.RealisedPnl = pos.RealisedPnl.Add(amountLoss) @@ -587,7 +559,7 @@ func updateSettlePosition(p *Position, e SPE) { openV(p, openedVolume, pr) // was the position zero, or did the position flip sides? if reset || (before < 0 && p.OpenVolume > 0) || (before > 0 && p.OpenVolume < 0) { - p.resetSince() + p.ResetSince() } p.AverageEntryPrice, _ = num.UintFromDecimal(p.AverageEntryPriceFP.Round(0)) @@ -597,18 +569,6 @@ func updateSettlePosition(p *Position, e SPE) { p.UnrealisedPnl = p.UnrealisedPnlFP.Round(0) } -type amount struct { - total *num.Int - since *num.Int -} - -func newAmount() amount { - return amount{ - total: num.NewInt(0), - since: num.NewInt(0), - } -} - type Position struct { types.Position AverageEntryPriceFP num.Decimal @@ -620,25 +580,14 @@ type Position struct { // what a party was missing which triggered loss socialization adjustment num.Decimal state vega.PositionStatus - taker amount - maker amount - other amount - funding amount } func seToProto(e SE) Position { return Position{ - Position: types.Position{ - MarketID: e.MarketID(), - PartyID: e.PartyID(), - }, + Position: types.NewPosition(e.MarketID(), e.PartyID()), AverageEntryPriceFP: num.DecimalZero(), RealisedPnlFP: num.DecimalZero(), UnrealisedPnlFP: num.DecimalZero(), - taker: newAmount(), - maker: newAmount(), - other: newAmount(), - funding: newAmount(), } } @@ -663,17 +612,10 @@ func (p *Positions) Types() []events.Type { } func (p *Position) setFees(fee *feeAmounts) { - p.taker.total.U.AddSum(fee.taker) - p.taker.since.U.AddSum(fee.taker) - p.maker.total.U.AddSum(fee.maker) - p.maker.since.U.AddSum(fee.maker) - p.other.total.U.AddSum(fee.other) - p.other.since.U.AddSum(fee.other) -} - -func (p *Position) resetSince() { - p.taker.since = num.NewInt(0) - p.maker.since = num.NewInt(0) - p.other.since = num.NewInt(0) - p.funding.since = num.NewInt(0) + p.TakerFeesPaid.AddSum(fee.taker) + p.TakerFeesPaidSince.AddSum(fee.taker) + p.MakerFeesReceived.AddSum(fee.maker) + p.MakerFeesReceivedSince.AddSum(fee.maker) + p.FeesPaid.AddSum(fee.other) + p.FeesPaidSince.AddSum(fee.other) } diff --git a/core/types/plugins.go b/core/types/plugins.go index 55aafd762c..c89a60dd2a 100644 --- a/core/types/plugins.go +++ b/core/types/plugins.go @@ -35,18 +35,73 @@ type Position struct { // formatted price of `1.23456` assuming market configured to 5 decimal places AverageEntryPrice *num.Uint // Timestamp for the latest time the position was updated - UpdatedAt int64 + UpdatedAt int64 + TakerFeesPaid *num.Uint + TakerFeesPaidSince *num.Uint + MakerFeesReceived *num.Uint + MakerFeesReceivedSince *num.Uint + FeesPaid *num.Uint + FeesPaidSince *num.Uint + FundingPaymentAmount *num.Int + FundingPaymentAmountSince *num.Int +} + +func NewPosition(marketID, partyID string) Position { + return Position{ + MarketID: marketID, + PartyID: partyID, + AverageEntryPrice: num.UintZero(), + TakerFeesPaid: num.UintZero(), + MakerFeesReceived: num.UintZero(), + FeesPaid: num.UintZero(), + TakerFeesPaidSince: num.UintZero(), + MakerFeesReceivedSince: num.UintZero(), + FeesPaidSince: num.UintZero(), + FundingPaymentAmount: num.IntZero(), + FundingPaymentAmountSince: num.IntZero(), + } } func (p *Position) IntoProto() *proto.Position { + if p.FeesPaid == nil { + p.FeesPaid = num.UintZero() + } + if p.FeesPaidSince == nil { + p.FeesPaidSince = num.UintZero() + } + if p.TakerFeesPaid == nil { + p.TakerFeesPaid = num.UintZero() + } + if p.TakerFeesPaidSince == nil { + p.TakerFeesPaidSince = num.UintZero() + } + if p.MakerFeesReceived == nil { + p.MakerFeesReceived = num.UintZero() + } + if p.MakerFeesReceivedSince == nil { + p.MakerFeesReceivedSince = num.UintZero() + } + if p.FundingPaymentAmount == nil { + p.FundingPaymentAmount = num.IntZero() + } + if p.FundingPaymentAmountSince == nil { + p.FundingPaymentAmountSince = num.IntZero() + } return &proto.Position{ - MarketId: p.MarketID, - PartyId: p.PartyID, - OpenVolume: p.OpenVolume, - RealisedPnl: p.RealisedPnl.BigInt().String(), - UnrealisedPnl: p.UnrealisedPnl.BigInt().String(), - AverageEntryPrice: num.UintToString(p.AverageEntryPrice), - UpdatedAt: p.UpdatedAt, + MarketId: p.MarketID, + PartyId: p.PartyID, + OpenVolume: p.OpenVolume, + RealisedPnl: p.RealisedPnl.BigInt().String(), + UnrealisedPnl: p.UnrealisedPnl.BigInt().String(), + AverageEntryPrice: num.UintToString(p.AverageEntryPrice), + UpdatedAt: p.UpdatedAt, + TakerFeesPaid: p.TakerFeesPaid.String(), + MakerFeesReceived: p.MakerFeesReceived.String(), + FeesPaid: p.FeesPaid.String(), + TakerFeesPaidSince: p.TakerFeesPaidSince.String(), + MakerFeesReceivedSince: p.MakerFeesReceivedSince.String(), + FundingPaymentAmount: p.FundingPaymentAmount.String(), + FundingPaymentAmountSince: p.FundingPaymentAmountSince.String(), } } @@ -59,3 +114,10 @@ func (p Positions) IntoProto() []*proto.Position { } return out } + +func (p *Position) ResetSince() { + p.TakerFeesPaidSince = num.UintZero() + p.MakerFeesReceivedSince = num.UintZero() + p.FeesPaidSince = num.UintZero() + p.FundingPaymentAmountSince = num.IntZero() +}