github.com/fibonacci-chain/fbc@v0.0.0-20231124064014-c7636198c1e9/x/order/match/periodicauction/match.go (about) 1 package periodicauction 2 3 import ( 4 "fmt" 5 6 "github.com/fibonacci-chain/fbc/libs/tendermint/libs/log" 7 8 sdk "github.com/fibonacci-chain/fbc/libs/cosmos-sdk/types" 9 10 "github.com/fibonacci-chain/fbc/x/order/keeper" 11 "github.com/fibonacci-chain/fbc/x/order/types" 12 ) 13 14 func preMatchProcessing(book *types.DepthBook) (buyAmountSum, sellAmountSum []sdk.Dec) { 15 bookLength := len(book.Items) 16 if bookLength == 0 { 17 return 18 } 19 20 buyAmountSum = make([]sdk.Dec, bookLength) 21 sellAmountSum = make([]sdk.Dec, bookLength) 22 23 buyAmountSum[0] = book.Items[0].BuyQuantity 24 for i := 1; i < bookLength; i++ { 25 buyAmountSum[i] = buyAmountSum[i-1].Add(book.Items[i].BuyQuantity) 26 } 27 28 sellAmountSum[bookLength-1] = book.Items[bookLength-1].SellQuantity 29 for i := bookLength - 2; i >= 0; i-- { 30 sellAmountSum[i] = sellAmountSum[i+1].Add(book.Items[i].SellQuantity) 31 } 32 33 return 34 } 35 36 func execRule0(buyAmountSum, sellAmountSum []sdk.Dec) (maxExecution sdk.Dec, execution []sdk.Dec) { 37 maxExecution = sdk.ZeroDec() 38 bookLength := len(buyAmountSum) 39 execution = make([]sdk.Dec, bookLength) 40 for i := 0; i < bookLength; i++ { 41 execution[i] = sdk.MinDec(buyAmountSum[i], sellAmountSum[i]) 42 maxExecution = sdk.MaxDec(execution[i], maxExecution) 43 } 44 45 return 46 } 47 48 func execRule1(maxExecution sdk.Dec, execution []sdk.Dec) (indexesRule1 []int) { 49 bookLength := len(execution) 50 for i := 0; i < bookLength; i++ { 51 if execution[i].Equal(maxExecution) { 52 indexesRule1 = append(indexesRule1, i) 53 } 54 } 55 56 return 57 } 58 59 func execRule2(buyAmountSum, sellAmountSum []sdk.Dec, indexesRule1 []int) (indexesRule2 []int, imbalance []sdk.Dec) { 60 indexLen1 := len(indexesRule1) 61 imbalance = make([]sdk.Dec, indexLen1) 62 for i := 0; i < indexLen1; i++ { 63 imbalance[i] = buyAmountSum[indexesRule1[i]].Sub(sellAmountSum[indexesRule1[i]]) 64 } 65 minAbsImbalance := imbalance[0].Abs() 66 for i := 1; i < indexLen1; i++ { 67 minAbsImbalance = sdk.MinDec(minAbsImbalance, imbalance[i].Abs()) 68 } 69 for i := 0; i < indexLen1; i++ { 70 if imbalance[i].Abs().Equal(minAbsImbalance) { 71 indexesRule2 = append(indexesRule2, indexesRule1[i]) 72 } 73 } 74 75 return 76 } 77 78 func execRule3(book *types.DepthBook, offset int, refPrice sdk.Dec, pricePrecision int64, 79 indexesRule2 []int, imbalance []sdk.Dec) (bestPrice sdk.Dec) { 80 indexLen2 := len(indexesRule2) 81 if imbalance[indexesRule2[0]-offset].GT(sdk.ZeroDec()) { 82 // rule3a: all imbalances are positive, buy side pressure 83 newRefPrice := refPrice.Mul(sdk.MustNewDecFromStr("1.05")) 84 newRefPrice = newRefPrice.RoundDecimal(pricePrecision) 85 bestPrice = bestPriceFromRefPrice(book.Items[indexesRule2[0]].Price, 86 book.Items[indexesRule2[indexLen2-1]].Price, newRefPrice) 87 } else if imbalance[indexesRule2[indexLen2-1]-offset].LT(sdk.ZeroDec()) { 88 // rule3b: all imbalances are negative, sell side pressure 89 newRefPrice := refPrice.Mul(sdk.MustNewDecFromStr("0.95")) 90 newRefPrice = newRefPrice.RoundDecimal(pricePrecision) 91 bestPrice = bestPriceFromRefPrice(book.Items[indexesRule2[0]].Price, 92 book.Items[indexesRule2[indexLen2-1]].Price, newRefPrice) 93 } else { 94 // rule3c: some imbalance > 0, and some imbalance < 0, no buyer pressure or seller pressure 95 newRefPrice := refPrice.RoundDecimal(pricePrecision) 96 bestPrice = bestPriceFromRefPrice(book.Items[indexesRule2[0]].Price, 97 book.Items[indexesRule2[indexLen2-1]].Price, newRefPrice) 98 } 99 100 return 101 } 102 103 // Calculate periodic auction match price, return the best price and execution amount 104 // The best price is found according following rules: 105 // rule0: No match, bestPrice = 0, maxExecution=0 106 // rule1: Maximum execution volume. 107 // 108 // If there are more than one price with the same max execution, following rule2 109 // 110 // rule2: Minimum imbalance. We should select the price with minimum absolute value of imbalance. 111 // 112 // If more than one price satisfy rule2, following rule3 113 // 114 // rule3: Market Pressure. There are 3 cases: 115 // rule3a: All imbalances are positive. It indicates buy side pressure. Set reference price with 116 // 117 // last execute price plus a upper limit percentage(e.g. 5%). Then choose the price 118 // which is closest to reference price. 119 // 120 // rule3b: All imbalances are negative. It indicates sell side pressure. Set reference price with 121 // 122 // last execute price minus a lower limit percentage(e.g. 5%). Then choose the price 123 // which is closest to reference price. 124 // 125 // rule3c: Otherwise, it indicates no one side pressure. Set reference price with last execute 126 // 127 // price. Then choose the price which is closest to reference price. 128 func periodicAuctionMatchPrice(book *types.DepthBook, pricePrecision int64, 129 refPrice sdk.Dec) (bestPrice sdk.Dec, maxExecution sdk.Dec) { 130 131 buyAmountSum, sellAmountSum := preMatchProcessing(book) 132 if len(buyAmountSum) == 0 { 133 return sdk.ZeroDec(), sdk.ZeroDec() 134 } 135 136 maxExecution, execution := execRule0(buyAmountSum, sellAmountSum) 137 if maxExecution.IsZero() { 138 return refPrice, maxExecution 139 } 140 141 indexesRule1 := execRule1(maxExecution, execution) 142 if len(indexesRule1) == 1 { 143 bestPrice = book.Items[indexesRule1[0]].Price 144 return 145 } 146 147 indexesRule2, imbalance := execRule2(buyAmountSum, sellAmountSum, indexesRule1) 148 if len(indexesRule2) == 1 { 149 bestPrice = book.Items[indexesRule2[0]].Price 150 return 151 } 152 153 bestPrice = execRule3(book, indexesRule1[0], refPrice, pricePrecision, indexesRule2, imbalance) 154 155 return 156 } 157 158 // get best price from reference price 159 // if min < ref < max, choose ref; else choose the closest price to ref price 160 func bestPriceFromRefPrice(minPrice, maxPrice, refPrice sdk.Dec) sdk.Dec { 161 if minPrice.LTE(refPrice) { 162 return minPrice 163 } 164 if maxPrice.GTE(refPrice) { 165 return maxPrice 166 } 167 return refPrice 168 } 169 170 func markCurBlockToFutureExpireBlockList(ctx sdk.Context, keeper keeper.Keeper) { 171 curBlockHeight := ctx.BlockHeight() 172 feeParams := keeper.GetParams(ctx) 173 174 // Add current blockHeight to future Height 175 // which will solve expire orders in current block. 176 futureHeight := curBlockHeight + feeParams.OrderExpireBlocks 177 178 // the feeParams.OrderExpireBlocks param can be change during the blockchain running, 179 // so we use an array to record the expire blocks in the feature block height 180 futureExpireHeightList := keeper.GetExpireBlockHeight(ctx, futureHeight) 181 futureExpireHeightList = append(futureExpireHeightList, curBlockHeight) 182 keeper.SetExpireBlockHeight(ctx, futureHeight, futureExpireHeightList) 183 } 184 185 func cleanLastBlockClosedOrders(ctx sdk.Context, keeper keeper.Keeper) { 186 // drop expired data 187 lastClosedOrderIDs := keeper.GetLastClosedOrderIDs(ctx) 188 for _, orderID := range lastClosedOrderIDs { 189 keeper.DropOrder(ctx, orderID) 190 } 191 192 keeper.GetDiskCache().DecreaseStoreOrderNum(int64(len(lastClosedOrderIDs))) 193 } 194 195 // Deal the block from create to current height which is Expired 196 func cacheExpiredBlockToCurrentHeight(ctx sdk.Context, keeper keeper.Keeper) { 197 logger := ctx.Logger().With("module", "order") 198 curBlockHeight := ctx.BlockHeight() 199 200 lastExpiredBlockHeight := keeper.GetLastExpiredBlockHeight(ctx) 201 if lastExpiredBlockHeight == 0 { 202 lastExpiredBlockHeight = curBlockHeight - 1 203 } 204 205 // check orders in expired blocks, remove expired orders by order id 206 for height := lastExpiredBlockHeight + 1; height <= curBlockHeight; height++ { 207 var expiredHeight int64 208 expiredBlocks := keeper.GetExpireBlockHeight(ctx, height) 209 for _, expiredHeight = range expiredBlocks { 210 keeper.DropExpiredOrdersByBlockHeight(ctx, expiredHeight) 211 logger.Info(fmt.Sprintf("currentHeight(%d), expire orders at blockHeight(%d)", 212 curBlockHeight, expiredHeight)) 213 } 214 } 215 216 if !keeper.AnyProductLocked(ctx) { 217 height := lastExpiredBlockHeight 218 if curBlockHeight > 1 { 219 for ; height < curBlockHeight; height++ { 220 var expiredHeight int64 221 for _, expiredHeight = range keeper.GetExpireBlockHeight(ctx, height) { 222 keeper.DropBlockOrderNum(ctx, expiredHeight) 223 logger.Info(fmt.Sprintf("currentHeight(%d), drop Data at blockHeight(%d)", 224 curBlockHeight, expiredHeight)) 225 } 226 keeper.DropExpireBlockHeight(ctx, height) 227 } 228 } 229 keeper.SetLastExpiredBlockHeight(ctx, height) 230 } 231 } 232 233 func cleanupOrdersWhoseTokenPairHaveBeenDelisted(ctx sdk.Context, keeper keeper.Keeper) { 234 products := keeper.GetProductsFromDepthBookMap() 235 for _, product := range products { 236 tokenPair := keeper.GetDexKeeper().GetTokenPair(ctx, product) 237 if tokenPair == nil { 238 cleanupOrdersByProduct(ctx, keeper, product) 239 } 240 } 241 } 242 243 func cleanupOrdersByProduct(ctx sdk.Context, keeper keeper.Keeper, product string) { 244 depthBook := keeper.GetDepthBookCopy(product) 245 for _, item := range depthBook.Items { 246 buyKey := types.FormatOrderIDsKey(product, item.Price, types.BuyOrder) 247 orderIDList := keeper.GetProductPriceOrderIDs(buyKey) 248 sellKey := types.FormatOrderIDsKey(product, item.Price, types.SellOrder) 249 orderIDList = append(orderIDList, keeper.GetProductPriceOrderIDs(sellKey)...) 250 cleanOrdersByOrderIDList(ctx, keeper, orderIDList) 251 } 252 } 253 254 func cleanOrdersByOrderIDList(ctx sdk.Context, keeper keeper.Keeper, orderIDList []string) { 255 logger := ctx.Logger() 256 for _, orderID := range orderIDList { 257 order := keeper.GetOrder(ctx, orderID) 258 keeper.CancelOrder(ctx, order, logger) 259 } 260 } 261 262 func cleanupExpiredOrders(ctx sdk.Context, keeper keeper.Keeper) { 263 264 // Look forward to see what height will this block expired 265 markCurBlockToFutureExpireBlockList(ctx, keeper) 266 267 // Clean the expired orders which is collected by the last block 268 cleanLastBlockClosedOrders(ctx, keeper) 269 270 // Look backward to see who is expired and cache the expired orders 271 cacheExpiredBlockToCurrentHeight(ctx, keeper) 272 } 273 274 func matchOrders(ctx sdk.Context, keeper keeper.Keeper) { 275 blockHeight := ctx.BlockHeight() 276 orderNum := keeper.GetBlockOrderNum(ctx, blockHeight) 277 // no new orders in this block & no product lock in previous blocks, skip match 278 if orderNum == 0 && !keeper.AnyProductLocked(ctx) { 279 return 280 } 281 282 // step0: get active products 283 products := keeper.GetDiskCache().GetNewDepthbookKeys() 284 products = keeper.FilterDelistedProducts(ctx, products) 285 keeper.GetDexKeeper().SortProducts(ctx, products) // sort products 286 287 // step1: calc best price and max execution for every active product, save latest price 288 //updatedProductsBaseprice := make(map[string]types.MatchResult) 289 updatedProductsBasePrice := calcMatchPriceAndExecution(ctx, keeper, products) 290 291 // step1.1: recover locked depth book 292 lockMap := keeper.GetDexKeeper().GetLockedProductsCopy(ctx) 293 for product := range lockMap.Data { 294 products = append(products, product) 295 } 296 keeper.GetDexKeeper().SortProducts(ctx, products) // sort products 297 298 // step2: execute match results, fill orders in match results, transfer tokens and collect fees 299 executeMatch(ctx, keeper, products, updatedProductsBasePrice, lockMap) 300 301 // step3: save match results for querying 302 if len(updatedProductsBasePrice) > 0 { 303 blockMatchResult := &types.BlockMatchResult{ 304 BlockHeight: blockHeight, 305 ResultMap: updatedProductsBasePrice, 306 TimeStamp: ctx.BlockHeader().Time.Unix(), 307 } 308 keeper.SetBlockMatchResult(blockMatchResult) 309 } 310 } 311 312 func calcMatchPriceAndExecution(ctx sdk.Context, k keeper.Keeper, products []string) map[string]types.MatchResult { 313 resultMap := make(map[string]types.MatchResult) 314 315 for _, product := range products { 316 tokenPair := k.GetDexKeeper().GetTokenPair(ctx, product) 317 if tokenPair == nil { 318 continue 319 } 320 book := k.GetDepthBookCopy(product) 321 bestPrice, maxExecution := periodicAuctionMatchPrice(book, tokenPair.MaxPriceDigit, 322 k.GetLastPrice(ctx, product)) 323 if maxExecution.IsPositive() { 324 k.SetLastPrice(ctx, product, bestPrice) 325 resultMap[product] = types.MatchResult{BlockHeight: ctx.BlockHeight(), Price: bestPrice, 326 Quantity: maxExecution, Deals: []types.Deal{}} 327 } 328 } 329 330 return resultMap 331 } 332 333 func lockProduct(ctx sdk.Context, k keeper.Keeper, logger log.Logger, product string, matchResult types.MatchResult, 334 buyExecutedCnt, sellExecutedCnt sdk.Dec) { 335 blockHeight := ctx.BlockHeight() 336 337 lock := &types.ProductLock{ 338 BlockHeight: matchResult.BlockHeight, 339 Price: matchResult.Price, 340 Quantity: matchResult.Quantity, 341 BuyExecuted: buyExecutedCnt, 342 SellExecuted: sellExecutedCnt, 343 } 344 k.SetProductLock(ctx, product, lock) 345 logger.Info(fmt.Sprintf("BlockHeight<%d> lock product(%s)", blockHeight, product)) 346 } 347 348 func executeMatchedUpdatedProduct(ctx sdk.Context, k keeper.Keeper, 349 updatedProductsBasePrice map[string]types.MatchResult, feeParams *types.Params, blockRemainDeals int64, 350 product string, logger log.Logger) int64 { 351 352 matchResult := updatedProductsBasePrice[product] 353 buyExecutedCnt := sdk.ZeroDec() 354 sellExecutedCnt := sdk.ZeroDec() 355 356 if blockRemainDeals <= 0 { 357 lockProduct(ctx, k, logger, product, matchResult, buyExecutedCnt, sellExecutedCnt) 358 359 return blockRemainDeals 360 } 361 362 deals, blockRemainDeals := fillDepthBook(ctx, k, product, 363 matchResult.Price, matchResult.Quantity, &buyExecutedCnt, &sellExecutedCnt, blockRemainDeals, feeParams) 364 matchResult.Deals = deals 365 updatedProductsBasePrice[product] = matchResult 366 367 logger.Info(fmt.Sprintf("matchResult(%d-%s): price: %v, quantity: %v, buyExecuted: %v"+ 368 ", sellExecuted: %v, dealsNum: %d", matchResult.BlockHeight, product, 369 matchResult.Price, matchResult.Quantity, buyExecutedCnt, sellExecutedCnt, len(deals))) 370 371 // if filling not done, lock the product 372 if buyExecutedCnt.LT(matchResult.Quantity) || sellExecutedCnt.LT(matchResult.Quantity) { 373 lockProduct(ctx, k, logger, product, matchResult, buyExecutedCnt, sellExecutedCnt) 374 } 375 376 return blockRemainDeals 377 } 378 379 func executeLockedProduct(ctx sdk.Context, k keeper.Keeper, 380 updatedProductsBasePrice map[string]types.MatchResult, lockMap *types.ProductLockMap, 381 feeParams *types.Params, blockRemainDeals int64, product string, 382 logger log.Logger) int64 { 383 384 if blockRemainDeals <= 0 { 385 return blockRemainDeals 386 } 387 388 // fill locked product 389 blockHeight := ctx.BlockHeight() 390 lock := lockMap.Data[product] 391 392 buyExecuted := lock.BuyExecuted 393 sellExecuted := lock.SellExecuted 394 deals, blockRemainDeals := fillDepthBook(ctx, k, product, 395 lock.Price, lock.Quantity, &buyExecuted, &sellExecuted, blockRemainDeals, feeParams) 396 397 // if deals not empty, add match result 398 if len(deals) > 0 { 399 updatedProductsBasePrice[product] = types.MatchResult{ 400 BlockHeight: lock.BlockHeight, 401 Price: lock.Price, 402 Quantity: lock.Quantity, 403 Deals: deals, 404 } 405 } 406 407 logger.Info(fmt.Sprintf("BlockHeight<%d> execute locked product(%s<%d>): price: %v, "+ 408 "quantity: %v, buyExecuted: %v, sellExecuted: %v, dealsNum: %d", 409 blockHeight, product, lock.BlockHeight, lock.Price, lock.Quantity, buyExecuted, 410 sellExecuted, len(deals))) 411 412 lock.BuyExecuted = buyExecuted 413 lock.SellExecuted = sellExecuted 414 // if execution is done, unlock product 415 if buyExecuted.GTE(lock.Quantity) && sellExecuted.GTE(lock.Quantity) { 416 k.UnlockProduct(ctx, product) 417 logger.Info(fmt.Sprintf("BlockHeight<%d> unlock product(%s<%d>)", blockHeight, 418 product, lock.BlockHeight)) 419 } else { 420 // update product lock 421 k.SetProductLock(ctx, product, lock) 422 } 423 424 return blockRemainDeals 425 } 426 427 func executeMatch(ctx sdk.Context, k keeper.Keeper, products []string, 428 updatedProductsBasePrice map[string]types.MatchResult, lockMap *types.ProductLockMap) { 429 logger := ctx.Logger().With("module", "order") 430 feeParams := k.GetParams(ctx) 431 blockRemainDeals := feeParams.MaxDealsPerBlock 432 433 for _, product := range products { 434 if _, ok := updatedProductsBasePrice[product]; ok { 435 blockRemainDeals = executeMatchedUpdatedProduct(ctx, k, updatedProductsBasePrice, feeParams, 436 blockRemainDeals, product, logger) 437 } else if _, ok := lockMap.Data[product]; ok { 438 blockRemainDeals = executeLockedProduct(ctx, k, updatedProductsBasePrice, lockMap, feeParams, 439 blockRemainDeals, product, logger) 440 } 441 } 442 }