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 }