code.vegaprotocol.io/vega@v0.79.0/core/execution/spot/protocol_automated_purchase.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 spot
    17  
    18  import (
    19  	"context"
    20  	"encoding/hex"
    21  	"sync"
    22  	"time"
    23  
    24  	"code.vegaprotocol.io/vega/core/datasource"
    25  	dscommon "code.vegaprotocol.io/vega/core/datasource/common"
    26  	dsdefinition "code.vegaprotocol.io/vega/core/datasource/definition"
    27  	"code.vegaprotocol.io/vega/core/events"
    28  	"code.vegaprotocol.io/vega/core/execution/common"
    29  	"code.vegaprotocol.io/vega/core/products"
    30  	"code.vegaprotocol.io/vega/core/types"
    31  	"code.vegaprotocol.io/vega/libs/crypto"
    32  	"code.vegaprotocol.io/vega/libs/num"
    33  	"code.vegaprotocol.io/vega/logging"
    34  	snapshot "code.vegaprotocol.io/vega/protos/vega/snapshot/v1"
    35  )
    36  
    37  type ProtocolAutomatedPurchase struct {
    38  	ID                   string
    39  	config               *types.NewProtocolAutomatedPurchaseChanges
    40  	nextAuctionAmount    *num.Uint
    41  	lastOraclePrice      *num.Uint
    42  	lastOracleUpdateTime time.Time
    43  	priceOracle          *products.CompositePriceOracle
    44  	scheuldingOracles    *products.AutomatedPurhcaseSchedulingOracles
    45  	side                 types.Side
    46  	activeOrder          string
    47  	lock                 sync.Mutex
    48  	readyToStop          bool
    49  }
    50  
    51  func (ap *ProtocolAutomatedPurchase) IntoProto() *snapshot.ProtocolAutomatedPurchase {
    52  	apProto := &snapshot.ProtocolAutomatedPurchase{
    53  		Id:          ap.ID,
    54  		Config:      ap.config.IntoProto(),
    55  		Side:        ap.side,
    56  		ActiveOrder: ap.activeOrder,
    57  		ReadyToStop: ap.readyToStop,
    58  	}
    59  	if ap.nextAuctionAmount != nil {
    60  		apProto.NextAuctionAmount = ap.nextAuctionAmount.String()
    61  	}
    62  	if ap.lastOraclePrice != nil {
    63  		apProto.LastOraclePrice = ap.lastOraclePrice.String()
    64  		apProto.LastOracleUpdateTime = ap.lastOracleUpdateTime.UnixNano()
    65  	}
    66  	return apProto
    67  }
    68  
    69  func (m *Market) NewProtocolAutomatedPurchase(ctx context.Context, ID string, config *types.NewProtocolAutomatedPurchaseChanges, oracleEngine common.OracleEngine) error {
    70  	if m.pap != nil {
    71  		m.log.Panic("cannot instantiate new protocol automated purchase while there is already an active one", logging.String("active-pap", m.pap.ID))
    72  	}
    73  	side := types.SideUnspecified
    74  	if config.From == m.baseAsset {
    75  		side = types.SideSell
    76  	} else if config.From == m.quoteAsset {
    77  		side = types.SideBuy
    78  	}
    79  	if side == types.SideUnspecified {
    80  		m.log.Panic("wrong market for automated purchase", logging.String("market-id", config.MarketID), logging.String("from", config.From), logging.String("market-base-asset", m.baseAsset), logging.String("market-quote-asset", m.quoteAsset))
    81  	}
    82  
    83  	pap := &ProtocolAutomatedPurchase{
    84  		ID:          ID,
    85  		config:      config,
    86  		activeOrder: "",
    87  		readyToStop: false,
    88  		side:        side,
    89  	}
    90  
    91  	auctionVolumeSnapshotSchedule := datasource.SpecFromDefinition(config.AuctionVolumeSnapshotSchedule)
    92  	auctionSchedule := datasource.SpecFromDefinition(config.AuctionSchedule)
    93  	var err error
    94  	pap.scheuldingOracles, err = products.NewProtocolAutomatedPurchaseScheduleOracle(ctx, oracleEngine, auctionSchedule, auctionVolumeSnapshotSchedule, datasource.SpecBindingForAutomatedPurchaseFromProto(config.AutomatedPurchaseSpecBinding), m.papAuctionSchedule, m.papAuctionVolumeSnapshot)
    95  	if err != nil {
    96  		return err
    97  	}
    98  	oracle, err := products.NewCompositePriceOracle(ctx, oracleEngine, config.PriceOracle, datasource.SpecBindingForCompositePriceFromProto(config.PriceOracleBinding), m.updatePAPPriceOracle)
    99  	if err != nil {
   100  		return err
   101  	}
   102  	pap.priceOracle = oracle
   103  
   104  	m.pap = pap
   105  	return nil
   106  }
   107  
   108  func (m *Market) NewProtocolAutomatedPurchaseFromSnapshot(ctx context.Context, oracleEngine common.OracleEngine, apProto *snapshot.ProtocolAutomatedPurchase) (*ProtocolAutomatedPurchase, error) {
   109  	if apProto == nil {
   110  		return nil, nil
   111  	}
   112  	ap := &ProtocolAutomatedPurchase{
   113  		ID:          apProto.Id,
   114  		config:      types.NewProtocolAutomatedPurchaseChangesFromProto(apProto.Config),
   115  		activeOrder: apProto.ActiveOrder,
   116  		side:        apProto.Side,
   117  		readyToStop: apProto.ReadyToStop,
   118  	}
   119  	if len(apProto.LastOraclePrice) > 0 {
   120  		ap.lastOraclePrice = num.MustUintFromString(apProto.LastOraclePrice, 10)
   121  		ap.lastOracleUpdateTime = time.Unix(0, apProto.LastOracleUpdateTime)
   122  	}
   123  	if len(apProto.NextAuctionAmount) > 0 {
   124  		ap.nextAuctionAmount = num.MustUintFromString(apProto.NextAuctionAmount, 10)
   125  	}
   126  
   127  	specDef, _ := dsdefinition.FromProto(apProto.Config.PriceOracle, nil)
   128  	priceOracle := datasource.SpecFromDefinition(*dsdefinition.NewWith(specDef))
   129  
   130  	oracle, err := products.NewCompositePriceOracle(ctx, oracleEngine, priceOracle, datasource.SpecBindingForCompositePriceFromProto(apProto.Config.PriceOracleSpecBinding), m.updatePAPPriceOracle)
   131  	if err != nil {
   132  		return nil, err
   133  	}
   134  	ap.priceOracle = oracle
   135  	auctionSchedule := datasource.SpecFromDefinition(ap.config.AuctionSchedule)
   136  	auctionSchedule.Data.GetInternalTimeTriggerSpecConfiguration().Triggers[0].SetNextTrigger(m.timeService.GetTimeNow().Truncate(time.Second))
   137  	auctionVolumeSnapshotSchedule := datasource.SpecFromDefinition(ap.config.AuctionVolumeSnapshotSchedule)
   138  	auctionVolumeSnapshotSchedule.Data.GetInternalTimeTriggerSpecConfiguration().Triggers[0].SetNextTrigger(m.timeService.GetTimeNow().Truncate(time.Second))
   139  	ap.scheuldingOracles, err = products.NewProtocolAutomatedPurchaseScheduleOracle(ctx, oracleEngine, auctionSchedule, auctionVolumeSnapshotSchedule, datasource.SpecBindingForAutomatedPurchaseFromProto(ap.config.AutomatedPurchaseSpecBinding), m.papAuctionSchedule, m.papAuctionVolumeSnapshot)
   140  	if err != nil {
   141  		return nil, err
   142  	}
   143  	return ap, nil
   144  }
   145  
   146  func (m *Market) scaleOraclePriceToAssetDP(price *num.Numeric, dp int64) *num.Uint {
   147  	if price == nil {
   148  		return nil
   149  	}
   150  
   151  	if !price.SupportDecimalPlaces(int64(m.quoteAssetDP)) {
   152  		return nil
   153  	}
   154  
   155  	p, err := price.ScaleTo(dp, int64(m.quoteAssetDP))
   156  	if err != nil {
   157  		m.log.Error(err.Error())
   158  		return nil
   159  	}
   160  	return p
   161  }
   162  
   163  // updatePAPPriceOracle is called by the oracle to update the price in quote asset decimals.
   164  func (m *Market) updatePAPPriceOracle(ctx context.Context, data dscommon.Data) error {
   165  	m.log.Info("updatePAPPriceOracle", logging.String("current-time", m.timeService.GetTimeNow().String()))
   166  	if m.pap == nil {
   167  		m.log.Error("unexpected pap oracle price update - no active pap")
   168  		return nil
   169  	}
   170  	m.pap.lock.Lock()
   171  	defer m.pap.lock.Unlock()
   172  
   173  	pd, err := m.pap.priceOracle.GetData(data)
   174  	if err != nil {
   175  		return err
   176  	}
   177  	p := m.scaleOraclePriceToAssetDP(pd, m.pap.priceOracle.GetDecimals())
   178  	if p == nil || p.IsZero() {
   179  		return nil
   180  	}
   181  
   182  	m.pap.lastOraclePrice = p.Clone()
   183  	m.pap.lastOracleUpdateTime = m.timeService.GetTimeNow()
   184  	return nil
   185  }
   186  
   187  // AuctionVolumeSnapshot is called from the oracle in order to take a snapshot of the source account balance in preparation for the coming auction.
   188  func (m *Market) papAuctionVolumeSnapshot(ctx context.Context, data dscommon.Data) error {
   189  	m.log.Info("papAuctionVolumeSnapshot", logging.String("current-time", m.timeService.GetTimeNow().String()))
   190  	if m.pap == nil {
   191  		m.log.Error("unexpected pap auction volume snapshot")
   192  		return nil
   193  	}
   194  
   195  	m.pap.lock.Lock()
   196  	defer m.pap.lock.Unlock()
   197  
   198  	// if the program has been stopped, the oracles unsubscribed but this was able to sneak in, ignore it.
   199  	if m.pap.readyToStop {
   200  		m.log.Info("pap is ready to stop as soon as auction completes, not taking any more snapshots")
   201  		return nil
   202  	}
   203  
   204  	// if we already have an order place in an auction that is waiting to be traded - do nothing
   205  	if len(m.pap.activeOrder) > 0 {
   206  		m.log.Info("not taking a snapshot for pap which already has an active order", logging.String("pap-id", m.pap.ID), logging.String("active-order-id", m.pap.activeOrder))
   207  		return nil
   208  	}
   209  
   210  	// if we happen to have an earmarked amount that was not submitted, unearmark it first
   211  	// this would be the case if we're seeing more than one tick from the auction volume snapshot before we see one
   212  	// tick from the auction scheduler trigger
   213  	if m.pap.nextAuctionAmount != nil && !m.pap.nextAuctionAmount.IsZero() {
   214  		if err := m.collateral.UnearmarkForAutomatedPurchase(m.pap.config.From, m.pap.config.FromAccountType, m.pap.nextAuctionAmount.Clone()); err != nil {
   215  			m.log.Panic("failed to unearmark balance for automated purchase", logging.Error(err))
   216  		}
   217  	}
   218  
   219  	// earmark the amount for the next pap round
   220  	earmarkedBalance, err := m.collateral.EarmarkForAutomatedPurchase(m.pap.config.From, m.pap.config.FromAccountType, m.pap.config.MinimumAuctionSize, m.pap.config.MaximumAuctionSize)
   221  	if err != nil {
   222  		m.log.Error("error in earmarking for automated purchase", logging.Error(err))
   223  		return err
   224  	}
   225  
   226  	m.pap.nextAuctionAmount = earmarkedBalance
   227  	// emit an event with the next auction balance
   228  	m.broker.Send(events.NewProtocolAutomatedPurchaseAnnouncedEvent(ctx, m.pap.config.From, m.pap.config.FromAccountType, m.pap.config.ToAccountType, m.pap.config.MarketID, m.pap.nextAuctionAmount))
   229  	return nil
   230  }
   231  
   232  // AuctionSchedule is called by the oracle to notify on a required auction.
   233  func (m *Market) papAuctionSchedule(ctx context.Context, data dscommon.Data) error {
   234  	m.log.Info("papAuctionSchedule", logging.String("current-time", m.timeService.GetTimeNow().String()))
   235  	if m.pap == nil {
   236  		m.log.Error("unexpected pap auction snapshot - no active pap")
   237  		return nil
   238  	}
   239  	// at the end of this function we should unearmark and reset the next auction amount no matter if we succeeded or failed to enter an auction
   240  	// at this point we can unearmark the amount - either because we were able to enter an auction and place an order -
   241  	// in which case the amount has been transferred into the holding account, or because there was an error and we failed
   242  	defer func() {
   243  		// this should be fine as the defer happen as fifo so by the time this is called the unlock of the function locking has already taken place.
   244  		m.pap.lock.Lock()
   245  		defer m.pap.lock.Unlock()
   246  		if m.pap.readyToStop {
   247  			return
   248  		}
   249  		if m.pap.nextAuctionAmount != nil {
   250  			m.collateral.UnearmarkForAutomatedPurchase(m.pap.config.From, m.pap.config.FromAccountType, m.pap.nextAuctionAmount)
   251  		}
   252  		m.pap.nextAuctionAmount = nil
   253  	}()
   254  
   255  	m.pap.lock.Lock()
   256  	defer m.pap.lock.Unlock()
   257  
   258  	if m.pap.readyToStop {
   259  		return nil
   260  	}
   261  
   262  	// if there was nothing earmarked for next auction - return
   263  	if m.pap.nextAuctionAmount == nil {
   264  		return nil
   265  	}
   266  
   267  	// no last orace price - nothing to do here
   268  	if m.pap.lastOraclePrice == nil {
   269  		m.log.Warn("auction scheduled triggered but no oracle price", logging.String("marked-id", m.pap.config.MarketID), logging.String("automated-purchase-id", m.pap.ID))
   270  		return nil
   271  	}
   272  	// stale orace price - nothing to do here
   273  	if int64(m.timeService.GetTimeNow().Nanosecond())-m.pap.lastOracleUpdateTime.UnixNano() > m.pap.config.OraclePriceStalenessTolerance.Nanoseconds() {
   274  		m.log.Warn("auction scheduled triggered but oracle price is stale", logging.String("marked-id", m.pap.config.MarketID), logging.String("automated-purchase-id", m.pap.ID), logging.String("last-oracle-update", m.pap.lastOracleUpdateTime.String()))
   275  		return nil
   276  	}
   277  
   278  	// factor the last orale price by the offset
   279  	orderPrice, overflow := num.UintFromDecimal(m.pap.lastOraclePrice.ToDecimal().Mul(m.pap.config.OracleOffsetFactor))
   280  	if overflow || orderPrice == nil {
   281  		m.log.Error("failed to get order price for automated purchase auction", logging.String("from", m.pap.config.From), logging.String("market-id", m.pap.config.MarketID))
   282  		return nil
   283  	}
   284  
   285  	// calculate the order size
   286  	// if the order is a sell, i.e. we're selling the base asset, we need to scale it by the base factor
   287  	orderSize := scaleBaseAssetDPToQuantity(m.pap.nextAuctionAmount, m.baseFactor)
   288  
   289  	// if the order is a buy, that means the auction amount is in quote asset and we need to calculate the size of base
   290  	// while factoring in the necessary fees.
   291  	if m.pap.side == types.SideBuy {
   292  		feeFactor := num.DecimalOne().Add(m.mkt.Fees.Factors.InfrastructureFee).Add(m.mkt.Fees.Factors.BuyBackFee).Add(m.mkt.Fees.Factors.TreasuryFee).Add(m.fee.GetLiquidityFee())
   293  		// this gives us a size in the quote asset decimals - we need to convert it to base asset decimals and then
   294  		// adjust it by the position factor
   295  		orderSizeI, _ := num.UintFromDecimal(m.pap.nextAuctionAmount.ToDecimal().Div(feeFactor.Mul(orderPrice.ToDecimal())).Mul(m.positionFactor))
   296  		orderSize = orderSizeI.Uint64()
   297  	}
   298  
   299  	orderPriceInMarket := m.priceToMarketPrecision(orderPrice)
   300  	orderID := hex.EncodeToString(crypto.Hash([]byte(m.pap.ID)))
   301  	orderID, err := m.enterAutomatedPurchaseAuction(ctx, orderID, m.pap.side, orderPriceInMarket, orderSize, m.pap.ID, m.pap.config.AuctionDuration)
   302  	// if there was no error save the order id as an indication that we're in an auction with active pap order
   303  	if err == nil {
   304  		m.pap.activeOrder = orderID
   305  	}
   306  
   307  	return err
   308  }
   309  
   310  func (m *Market) papOrderProcessingEnded(orderID string) {
   311  	if m.pap.activeOrder == orderID {
   312  		m.pap.activeOrder = ""
   313  	}
   314  }
   315  
   316  func (ap *ProtocolAutomatedPurchase) getACcountTypesForPAP() (types.AccountType, types.AccountType, error) {
   317  	return ap.config.FromAccountType, ap.config.ToAccountType, nil
   318  }
   319  
   320  func (m *Market) stopPAP(ctx context.Context) {
   321  	m.pap.lock.Lock()
   322  	defer m.pap.lock.Unlock()
   323  	m.pap.readyToStop = true
   324  	m.pap.priceOracle.UnsubAll(ctx)
   325  	m.pap.scheuldingOracles.UnsubAll(ctx)
   326  	m.pap.nextAuctionAmount = nil
   327  }
   328  
   329  // checkPAP checks if a pap has expired, if so and it.
   330  func (m *Market) checkPAP(ctx context.Context) {
   331  	// no pap - nothing to do
   332  	if m.pap == nil {
   333  		return
   334  	}
   335  	// pap already stopped and no active order for it - we can reset the pap
   336  	if m.pap.readyToStop && len(m.pap.activeOrder) == 0 {
   337  		m.pap = nil
   338  		return
   339  	}
   340  	// pap has expired
   341  	if !m.pap.readyToStop && m.pap.config.ExpiryTimestamp.Unix() > 0 && m.pap.config.ExpiryTimestamp.Before(m.timeService.GetTimeNow()) {
   342  		m.log.Info("protocol automated purchase has expired, going to stop", logging.String("ID", m.pap.ID))
   343  		m.stopPAP(ctx)
   344  	}
   345  }
   346  
   347  func (m *Market) MarketHasActivePAP() bool {
   348  	return m.pap != nil
   349  }
   350  
   351  func scaleBaseAssetDPToQuantity(assetQuantity *num.Uint, baseFactor num.Decimal) uint64 {
   352  	sizeU, _ := num.UintFromDecimal(assetQuantity.ToDecimal().Div(baseFactor))
   353  	return sizeU.Uint64()
   354  }