code.vegaprotocol.io/vega@v0.79.0/datanode/entities/position.go (about)

     1  // Copyright (C) 2023 Gobalsky Labs Limited
     2  //
     3  // This program is free software: you can redistribute it and/or modify
     4  // it under the terms of the GNU Affero General Public License as
     5  // published by the Free Software Foundation, either version 3 of the
     6  // License, or (at your option) any later version.
     7  //
     8  // This program is distributed in the hope that it will be useful,
     9  // but WITHOUT ANY WARRANTY; without even the implied warranty of
    10  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    11  // GNU Affero General Public License for more details.
    12  //
    13  // You should have received a copy of the GNU Affero General Public License
    14  // along with this program.  If not, see <http://www.gnu.org/licenses/>.
    15  
    16  package entities
    17  
    18  import (
    19  	"encoding/json"
    20  	"fmt"
    21  	"time"
    22  
    23  	"code.vegaprotocol.io/vega/core/events"
    24  	"code.vegaprotocol.io/vega/core/types"
    25  	"code.vegaprotocol.io/vega/libs/num"
    26  	v2 "code.vegaprotocol.io/vega/protos/data-node/api/v2"
    27  	"code.vegaprotocol.io/vega/protos/vega"
    28  
    29  	"github.com/shopspring/decimal"
    30  )
    31  
    32  type positionSettlement interface {
    33  	Price() *num.Uint
    34  	PositionFactor() num.Decimal
    35  	Trades() []events.TradeSettlement
    36  	TxHash() string
    37  }
    38  
    39  type lossSocialization interface {
    40  	Amount() *num.Int
    41  	TxHash() string
    42  	IsFunding() bool
    43  }
    44  
    45  type settleDistressed interface {
    46  	Margin() *num.Uint
    47  	TxHash() string
    48  }
    49  
    50  type settleMarket interface {
    51  	SettledPrice() *num.Uint
    52  	PositionFactor() num.Decimal
    53  	TxHash() string
    54  }
    55  
    56  type Position struct {
    57  	MarketID                MarketID
    58  	PartyID                 PartyID
    59  	OpenVolume              int64
    60  	RealisedPnl             decimal.Decimal
    61  	UnrealisedPnl           decimal.Decimal
    62  	AverageEntryPrice       decimal.Decimal
    63  	AverageEntryMarketPrice decimal.Decimal
    64  	Loss                    decimal.Decimal // what the party lost because of loss socialization
    65  	Adjustment              decimal.Decimal // what a party was missing which triggered loss socialization
    66  	TxHash                  TxHash
    67  	VegaTime                time.Time
    68  	// keep track of trades that haven't been settled as separate fields
    69  	// these will be zeroed out once we process settlement events
    70  	PendingOpenVolume              int64
    71  	PendingRealisedPnl             decimal.Decimal
    72  	PendingUnrealisedPnl           decimal.Decimal
    73  	PendingAverageEntryPrice       decimal.Decimal
    74  	PendingAverageEntryMarketPrice decimal.Decimal
    75  	LossSocialisationAmount        decimal.Decimal
    76  	DistressedStatus               PositionStatus
    77  	TakerFeesPaid                  num.Decimal
    78  	MakerFeesReceived              num.Decimal
    79  	FeesPaid                       num.Decimal
    80  	TakerFeesPaidSince             num.Decimal
    81  	MakerFeesReceivedSince         num.Decimal
    82  	FeesPaidSince                  num.Decimal
    83  	FundingPaymentAmount           num.Decimal
    84  	FundingPaymentAmountSince      num.Decimal
    85  }
    86  
    87  func NewEmptyPosition(marketID MarketID, partyID PartyID) Position {
    88  	return Position{
    89  		MarketID:                       marketID,
    90  		PartyID:                        partyID,
    91  		OpenVolume:                     0,
    92  		RealisedPnl:                    num.DecimalZero(),
    93  		UnrealisedPnl:                  num.DecimalZero(),
    94  		AverageEntryPrice:              num.DecimalZero(),
    95  		AverageEntryMarketPrice:        num.DecimalZero(),
    96  		Loss:                           num.DecimalZero(),
    97  		Adjustment:                     num.DecimalZero(),
    98  		PendingOpenVolume:              0,
    99  		PendingRealisedPnl:             num.DecimalZero(),
   100  		PendingUnrealisedPnl:           num.DecimalZero(),
   101  		PendingAverageEntryPrice:       num.DecimalZero(),
   102  		PendingAverageEntryMarketPrice: num.DecimalZero(),
   103  		LossSocialisationAmount:        num.DecimalZero(),
   104  		DistressedStatus:               PositionStatusUnspecified,
   105  		TakerFeesPaid:                  num.DecimalZero(),
   106  		MakerFeesReceived:              num.DecimalZero(),
   107  		FeesPaid:                       num.DecimalZero(),
   108  		TakerFeesPaidSince:             num.DecimalZero(),
   109  		MakerFeesReceivedSince:         num.DecimalZero(),
   110  		FeesPaidSince:                  num.DecimalZero(),
   111  		FundingPaymentAmount:           num.DecimalZero(),
   112  		FundingPaymentAmountSince:      num.DecimalZero(),
   113  	}
   114  }
   115  
   116  func (p *Position) updateWithBadTrade(trade vega.Trade, seller bool, pf num.Decimal) {
   117  	size := int64(trade.Size)
   118  	if seller {
   119  		size *= -1
   120  	}
   121  	// update the open volume (not pending) directly, otherwise the settle position event resets the network position.
   122  	price, _ := num.UintFromString(trade.AssetPrice, 10)
   123  	mPrice, _ := num.UintFromString(trade.Price, 10)
   124  
   125  	openedVolume, closedVolume := CalculateOpenClosedVolume(p.OpenVolume, size)
   126  	realisedPnlDelta := num.DecimalFromUint(price).Sub(p.AverageEntryPrice).Mul(num.DecimalFromInt64(closedVolume)).Div(pf)
   127  	p.RealisedPnl = p.RealisedPnl.Add(realisedPnlDelta)
   128  	p.OpenVolume -= closedVolume
   129  
   130  	p.AverageEntryPrice = updateVWAP(p.AverageEntryPrice, p.OpenVolume, openedVolume, price)
   131  	p.AverageEntryMarketPrice = updateVWAP(p.AverageEntryMarketPrice, p.OpenVolume, openedVolume, mPrice)
   132  	p.OpenVolume += openedVolume
   133  	// no MTM - this isn't a settlement event, we're just adding the trade adding distressed volume to network
   134  	// for the same reason, no syncPending call.
   135  }
   136  
   137  func (p *Position) UpdateWithTrade(trade vega.Trade, seller bool, pf num.Decimal) {
   138  	// we have to ensure that we know the price/position factor
   139  	size := int64(trade.Size)
   140  	if seller {
   141  		size *= -1
   142  	}
   143  	// add fees paid/received
   144  	fees := getFeeAmountsForSide(&trade, seller)
   145  	maker, taker, other := num.DecimalFromUint(fees.maker), num.DecimalFromUint(fees.taker), num.DecimalFromUint(fees.other)
   146  	p.MakerFeesReceived = p.MakerFeesReceived.Add(maker)
   147  	p.TakerFeesPaid = p.TakerFeesPaid.Add(taker)
   148  	p.FeesPaid = p.FeesPaid.Add(other)
   149  	// check if we should reset the "since" fields for fees
   150  	since := p.PendingOpenVolume == 0
   151  	// close out trade doesn't require the MTM calculation to be performed
   152  	// the distressed trader will be handled through a settle distressed event, the network
   153  	// open volume should just be updated, the average entry price is unchanged.
   154  	assetPrice, _ := num.DecimalFromString(trade.AssetPrice)
   155  	marketPrice, _ := num.DecimalFromString(trade.Price)
   156  
   157  	// Scale the trade to the correct size
   158  	opened, closed := CalculateOpenClosedVolume(p.PendingOpenVolume, size)
   159  	realisedPnlDelta := assetPrice.Sub(p.PendingAverageEntryPrice).Mul(num.DecimalFromInt64(closed)).Div(pf)
   160  	p.PendingRealisedPnl = p.PendingRealisedPnl.Add(realisedPnlDelta)
   161  	// did we start with a positive/negative position?
   162  	pos := p.PendingOpenVolume > 0
   163  	p.PendingOpenVolume -= closed
   164  
   165  	marketPriceUint, _ := num.UintFromDecimal(marketPrice)
   166  	assetPriceUint, _ := num.UintFromDecimal(assetPrice)
   167  
   168  	p.PendingAverageEntryPrice = updateVWAP(p.PendingAverageEntryPrice, p.PendingOpenVolume, opened, assetPriceUint)
   169  	p.PendingAverageEntryMarketPrice = updateVWAP(p.PendingAverageEntryMarketPrice, p.PendingOpenVolume, opened, marketPriceUint)
   170  	p.PendingOpenVolume += opened
   171  	// either the position is no longer 0, or the position has flipped sides (and is non-zero)
   172  	if since || (pos != (p.PendingOpenVolume > 0) && p.PendingOpenVolume != 0) {
   173  		p.MakerFeesReceivedSince = num.DecimalZero()
   174  		p.TakerFeesPaidSince = num.DecimalZero()
   175  		p.FeesPaidSince = num.DecimalZero()
   176  	}
   177  	if p.PendingOpenVolume != 0 {
   178  		// running total of fees paid since get incremented
   179  		p.MakerFeesReceivedSince = p.MakerFeesReceivedSince.Add(maker)
   180  		p.TakerFeesPaidSince = p.TakerFeesPaidSince.Add(taker)
   181  		p.FeesPaidSince = p.FeesPaidSince.Add(other)
   182  	}
   183  	p.pendingMTM(assetPrice, pf)
   184  	if trade.Type == types.TradeTypeNetworkCloseOutBad {
   185  		p.updateWithBadTrade(trade, seller, pf)
   186  	} else if p.DistressedStatus == PositionStatusClosedOut {
   187  		// Not a closeout trade, but the position is currently still marked as distressed.
   188  		// This indicates the party was closed out previously, but has topped up and opened a new position.
   189  		p.DistressedStatus = PositionStatusUnspecified
   190  	}
   191  }
   192  
   193  func (p *Position) ApplyFundingPayment(amount *num.Int) {
   194  	amt := num.DecimalFromInt(amount)
   195  	p.FundingPaymentAmount = p.FundingPaymentAmount.Add(amt)
   196  	p.FundingPaymentAmountSince = p.FundingPaymentAmountSince.Add(amt)
   197  	// da := num.DecimalFromInt(amount)
   198  	// p.PendingRealisedPnl = p.PendingRealisedPnl.Add(da)
   199  	// p.RealisedPnl = p.RealisedPnl.Add(da)
   200  }
   201  
   202  func (p *Position) UpdateOrdersClosed() {
   203  	p.DistressedStatus = PositionStatusOrdersClosed
   204  }
   205  
   206  func (p *Position) ToggleDistressedStatus() {
   207  	// if currently marked as distressed -> mark as safe
   208  	if p.DistressedStatus == PositionStatusDistressed {
   209  		p.DistressedStatus = PositionStatusUnspecified
   210  		return
   211  	}
   212  	// was safe, is now distressed
   213  	p.DistressedStatus = PositionStatusDistressed
   214  }
   215  
   216  func (p *Position) UpdateWithPositionSettlement(e positionSettlement) {
   217  	pf := e.PositionFactor()
   218  	resetFP := false
   219  	for _, t := range e.Trades() {
   220  		if p.OpenVolume == 0 {
   221  			resetFP = true
   222  		}
   223  		openedVolume, closedVolume := CalculateOpenClosedVolume(p.OpenVolume, t.Size())
   224  		// Deal with any volume we have closed
   225  		realisedPnlDelta := num.DecimalFromUint(t.Price()).Sub(p.AverageEntryPrice).Mul(num.DecimalFromInt64(closedVolume)).Div(pf)
   226  		p.RealisedPnl = p.RealisedPnl.Add(realisedPnlDelta)
   227  		pos := p.OpenVolume > 0
   228  		p.OpenVolume -= closedVolume
   229  
   230  		// Then with any we have opened
   231  		p.AverageEntryPrice = updateVWAP(p.AverageEntryPrice, p.OpenVolume, openedVolume, t.Price())
   232  		p.AverageEntryMarketPrice = updateVWAP(p.AverageEntryMarketPrice, p.OpenVolume, openedVolume, t.MarketPrice())
   233  		p.OpenVolume += openedVolume
   234  		// check if position flipped
   235  		if !resetFP && (pos != (p.OpenVolume > 0) && p.OpenVolume != 0) {
   236  			resetFP = true
   237  		}
   238  	}
   239  	if resetFP {
   240  		p.FundingPaymentAmountSince = num.DecimalZero()
   241  	}
   242  	p.mtm(e.Price(), pf)
   243  	p.TxHash = TxHash(e.TxHash())
   244  	p.syncPending()
   245  }
   246  
   247  func (p *Position) syncPending() {
   248  	// update pending fields to match current ones
   249  	p.PendingOpenVolume = p.OpenVolume
   250  	p.PendingRealisedPnl = p.RealisedPnl
   251  	p.PendingUnrealisedPnl = p.UnrealisedPnl
   252  	p.PendingAverageEntryPrice = p.AverageEntryPrice
   253  	p.PendingAverageEntryMarketPrice = p.AverageEntryMarketPrice
   254  }
   255  
   256  func (p *Position) UpdateWithLossSocialization(e lossSocialization) {
   257  	amountLoss := num.DecimalFromInt(e.Amount())
   258  
   259  	if amountLoss.IsNegative() {
   260  		p.Loss = p.Loss.Add(amountLoss)
   261  		p.LossSocialisationAmount = p.LossSocialisationAmount.Sub(amountLoss)
   262  	} else {
   263  		p.Adjustment = p.Adjustment.Add(amountLoss)
   264  		p.LossSocialisationAmount = p.LossSocialisationAmount.Add(amountLoss)
   265  	}
   266  	if e.IsFunding() {
   267  		// adjust if this is a loss socialisation resulting from a funding payment settlement.
   268  		p.FundingPaymentAmount = p.FundingPaymentAmount.Add(amountLoss)
   269  		p.FundingPaymentAmountSince = p.FundingPaymentAmountSince.Add(amountLoss)
   270  	}
   271  
   272  	p.RealisedPnl = p.RealisedPnl.Add(amountLoss)
   273  	p.TxHash = TxHash(e.TxHash())
   274  	p.syncPending()
   275  }
   276  
   277  func (p *Position) UpdateWithSettleDistressed(e settleDistressed) {
   278  	margin := num.DecimalFromUint(e.Margin())
   279  	p.RealisedPnl = p.RealisedPnl.Add(p.UnrealisedPnl)
   280  	p.RealisedPnl = p.RealisedPnl.Sub(margin) // realised P&L includes whatever we had in margin account at this point
   281  	p.UnrealisedPnl = num.DecimalZero()
   282  	p.AverageEntryPrice = num.DecimalZero() // @TODO average entry price shouldn't be affected(?)
   283  	p.AverageEntryPrice = num.DecimalZero()
   284  	p.OpenVolume = 0
   285  	p.TxHash = TxHash(e.TxHash())
   286  	p.DistressedStatus = PositionStatusClosedOut
   287  	p.FundingPaymentAmountSince = num.DecimalZero()
   288  	p.FeesPaidSince = num.DecimalZero()
   289  	p.MakerFeesReceivedSince = num.DecimalZero()
   290  	p.TakerFeesPaidSince = num.DecimalZero()
   291  	p.syncPending()
   292  }
   293  
   294  func (p *Position) UpdateWithSettleMarket(e settleMarket) {
   295  	markPriceDec := num.DecimalFromUint(e.SettledPrice())
   296  	openVolumeDec := num.DecimalFromInt64(p.OpenVolume)
   297  
   298  	unrealisedPnl := openVolumeDec.Mul(markPriceDec.Sub(p.AverageEntryPrice)).Div(e.PositionFactor())
   299  	p.RealisedPnl = p.RealisedPnl.Add(unrealisedPnl)
   300  	p.UnrealisedPnl = num.DecimalZero()
   301  	p.OpenVolume = 0
   302  	p.TxHash = TxHash(e.TxHash())
   303  	p.syncPending()
   304  }
   305  
   306  func (p Position) ToProto() *vega.Position {
   307  	var timestamp int64
   308  	if !p.VegaTime.IsZero() {
   309  		timestamp = p.VegaTime.UnixNano()
   310  	}
   311  	// we use the pending values when converting to protos
   312  	// so trades are reflected as accurately as possible
   313  	return &vega.Position{
   314  		MarketId:                  p.MarketID.String(),
   315  		PartyId:                   p.PartyID.String(),
   316  		OpenVolume:                p.PendingOpenVolume,
   317  		RealisedPnl:               p.PendingRealisedPnl.Round(0).String(),
   318  		UnrealisedPnl:             p.PendingUnrealisedPnl.Round(0).String(),
   319  		AverageEntryPrice:         p.PendingAverageEntryMarketPrice.Round(0).String(),
   320  		UpdatedAt:                 timestamp,
   321  		LossSocialisationAmount:   p.LossSocialisationAmount.Round(0).String(),
   322  		PositionStatus:            vega.PositionStatus(p.DistressedStatus),
   323  		TakerFeesPaid:             p.TakerFeesPaid.String(),
   324  		MakerFeesReceived:         p.MakerFeesReceived.String(),
   325  		FeesPaid:                  p.FeesPaid.String(),
   326  		TakerFeesPaidSince:        p.TakerFeesPaidSince.String(),
   327  		MakerFeesReceivedSince:    p.MakerFeesReceivedSince.String(),
   328  		FeesPaidSince:             p.FeesPaidSince.String(),
   329  		FundingPaymentAmount:      p.FundingPaymentAmount.String(),
   330  		FundingPaymentAmountSince: p.FundingPaymentAmountSince.String(),
   331  	}
   332  }
   333  
   334  func (p Position) ToProtoEdge(_ ...any) (*v2.PositionEdge, error) {
   335  	return &v2.PositionEdge{
   336  		Node:   p.ToProto(),
   337  		Cursor: p.Cursor().Encode(),
   338  	}, nil
   339  }
   340  
   341  func (p *Position) AverageEntryPriceUint() *num.Uint {
   342  	uint, overflow := num.UintFromDecimal(p.AverageEntryPrice)
   343  	if overflow {
   344  		panic("couldn't convert average entry price from decimal to uint")
   345  	}
   346  	return uint
   347  }
   348  
   349  func (p *Position) mtm(markPrice *num.Uint, positionFactor num.Decimal) {
   350  	if p.OpenVolume == 0 {
   351  		p.UnrealisedPnl = num.DecimalZero()
   352  		return
   353  	}
   354  	markPriceDec := num.DecimalFromUint(markPrice)
   355  	openVolumeDec := num.DecimalFromInt64(p.OpenVolume)
   356  
   357  	p.UnrealisedPnl = openVolumeDec.Mul(markPriceDec.Sub(p.AverageEntryPrice)).Div(positionFactor)
   358  }
   359  
   360  func (p *Position) pendingMTM(price, sf num.Decimal) {
   361  	if p.PendingOpenVolume == 0 {
   362  		p.PendingUnrealisedPnl = num.DecimalZero()
   363  		return
   364  	}
   365  
   366  	vol := num.DecimalFromInt64(p.PendingOpenVolume)
   367  	p.PendingUnrealisedPnl = vol.Mul(price.Sub(p.PendingAverageEntryPrice)).Div(sf)
   368  }
   369  
   370  func CalculateOpenClosedVolume(currentOpenVolume, tradedVolume int64) (int64, int64) {
   371  	if currentOpenVolume != 0 && ((currentOpenVolume > 0) != (tradedVolume > 0)) {
   372  		var closedVolume int64
   373  		if absUint64(tradedVolume) > absUint64(currentOpenVolume) {
   374  			closedVolume = currentOpenVolume
   375  		} else {
   376  			closedVolume = -tradedVolume
   377  		}
   378  		return tradedVolume + closedVolume, closedVolume
   379  	}
   380  	return tradedVolume, 0
   381  }
   382  
   383  func absUint64(v int64) uint64 {
   384  	if v < 0 {
   385  		v *= -1
   386  	}
   387  	return uint64(v)
   388  }
   389  
   390  func updateVWAP(vwap num.Decimal, volume int64, addVolume int64, addPrice *num.Uint) num.Decimal {
   391  	if volume+addVolume == 0 {
   392  		return num.DecimalZero()
   393  	}
   394  
   395  	volumeDec := num.DecimalFromInt64(volume)
   396  	addVolumeDec := num.DecimalFromInt64(addVolume)
   397  	addPriceDec := num.DecimalFromUint(addPrice)
   398  
   399  	return vwap.Mul(volumeDec).Add(addPriceDec.Mul(addVolumeDec)).Div(volumeDec.Add(addVolumeDec))
   400  }
   401  
   402  type PositionKey struct {
   403  	MarketID MarketID
   404  	PartyID  PartyID
   405  	VegaTime time.Time
   406  }
   407  
   408  func (p Position) Cursor() *Cursor {
   409  	pc := PositionCursor{
   410  		MarketID: p.MarketID,
   411  		PartyID:  p.PartyID,
   412  		VegaTime: p.VegaTime,
   413  	}
   414  
   415  	return NewCursor(pc.String())
   416  }
   417  
   418  func (p Position) Key() PositionKey {
   419  	return PositionKey{p.MarketID, p.PartyID, p.VegaTime}
   420  }
   421  
   422  var PositionColumns = []string{
   423  	"market_id", "party_id", "open_volume", "realised_pnl", "unrealised_pnl",
   424  	"average_entry_price", "average_entry_market_price", "loss", "adjustment", "tx_hash", "vega_time", "pending_open_volume",
   425  	"pending_realised_pnl", "pending_unrealised_pnl", "pending_average_entry_price", "pending_average_entry_market_price",
   426  	"loss_socialisation_amount", "distressed_status", "taker_fees_paid", "maker_fees_received", "fees_paid",
   427  	"taker_fees_paid_since", "maker_fees_received_since", "fees_paid_since", "funding_payment_amount", "funding_payment_amount_since",
   428  }
   429  
   430  func (p Position) ToRow() []interface{} {
   431  	return []interface{}{
   432  		p.MarketID, p.PartyID, p.OpenVolume, p.RealisedPnl, p.UnrealisedPnl,
   433  		p.AverageEntryPrice, p.AverageEntryMarketPrice, p.Loss, p.Adjustment, p.TxHash, p.VegaTime, p.PendingOpenVolume,
   434  		p.PendingRealisedPnl, p.PendingUnrealisedPnl, p.PendingAverageEntryPrice, p.PendingAverageEntryMarketPrice,
   435  		p.LossSocialisationAmount, p.DistressedStatus, p.TakerFeesPaid, p.MakerFeesReceived, p.FeesPaid,
   436  		p.TakerFeesPaidSince, p.MakerFeesReceivedSince, p.FeesPaidSince, p.FundingPaymentAmount, p.FundingPaymentAmountSince,
   437  	}
   438  }
   439  
   440  func (p Position) Equal(q Position) bool {
   441  	return p.MarketID == q.MarketID &&
   442  		p.PartyID == q.PartyID &&
   443  		p.OpenVolume == q.OpenVolume &&
   444  		p.RealisedPnl.Equal(q.RealisedPnl) &&
   445  		p.UnrealisedPnl.Equal(q.UnrealisedPnl) &&
   446  		p.AverageEntryPrice.Equal(q.AverageEntryPrice) &&
   447  		p.AverageEntryMarketPrice.Equal(q.AverageEntryMarketPrice) &&
   448  		p.Loss.Equal(q.Loss) &&
   449  		p.Adjustment.Equal(q.Adjustment) &&
   450  		p.TxHash == q.TxHash &&
   451  		p.VegaTime.Equal(q.VegaTime) &&
   452  		p.PendingOpenVolume == q.PendingOpenVolume &&
   453  		p.PendingAverageEntryPrice.Equal(q.PendingAverageEntryPrice) &&
   454  		p.PendingAverageEntryMarketPrice.Equal(q.PendingAverageEntryMarketPrice) &&
   455  		p.PendingRealisedPnl.Equal(q.PendingRealisedPnl) &&
   456  		p.PendingUnrealisedPnl.Equal(q.PendingUnrealisedPnl)
   457  	// p.PendingUnrealisedPnl.Equal(q.PendingUnrealisedPnl) &&
   458  	// loss socialisation amount doesn't seem to work currently
   459  	// p.LossSocialisationAmount.Equal(q.LossSocialisationAmount) &&
   460  	// p.DistressedStatus == q.DistressedStatus
   461  }
   462  
   463  type PositionCursor struct {
   464  	VegaTime time.Time `json:"vega_time"`
   465  	PartyID  PartyID   `json:"party_id"`
   466  	MarketID MarketID  `json:"market_id"`
   467  }
   468  
   469  func (rc PositionCursor) String() string {
   470  	bs, err := json.Marshal(rc)
   471  	if err != nil {
   472  		// This should never happen.
   473  		panic(fmt.Errorf("could not marshal order cursor: %w", err))
   474  	}
   475  	return string(bs)
   476  }
   477  
   478  func (rc *PositionCursor) Parse(cursorString string) error {
   479  	if cursorString == "" {
   480  		return nil
   481  	}
   482  	return json.Unmarshal([]byte(cursorString), rc)
   483  }