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  }