package orderbook import ( "errors" "fmt" "github.com/thrasher-corp/gocryptotrader/common/math" ) // FullLiquidityExhaustedPercentage defines when a book has been completely // wiped out of potential liquidity. const FullLiquidityExhaustedPercentage = -100 var ( errIDCannotBeMatched = errors.New("cannot match ID on linked list") errCollisionDetected = errors.New("cannot insert update, collision detected") errAmountCannotBeLessOrEqualToZero = errors.New("amount cannot be less than or equal to zero") errInvalidNominalSlippage = errors.New("invalid slippage amount, its value must be greater than or equal to zero") errInvalidImpactSlippage = errors.New("invalid slippage amount, its value must be greater than zero") errInvalidSlippageCannotExceed100 = errors.New("invalid slippage amount, its value cannot exceed 100%") errBaseAmountInvalid = errors.New("invalid base amount") errInvalidReferencePrice = errors.New("invalid reference price") errQuoteAmountInvalid = errors.New("quote amount invalid") errInvalidCost = errors.New("invalid cost amount") errInvalidAmount = errors.New("invalid amount") errInvalidHeadPrice = errors.New("invalid head price") ) // linkedList defines a linked list for a depth level, reutilisation of nodes // to and from a stack. type linkedList struct { length int head *Node } // comparison defines expected functionality to compare between two reference // price levels type comparison func(float64, float64) bool // load iterates across new items and refreshes linked list. It creates a linked // list exactly the same as the item slice that is supplied, if items is of nil // value it will flush entire list. func (ll *linkedList) load(items Items, stack *stack) { // Tip sets up a pointer to a struct field variable pointer. This is used // so when a node is popped from the stack we can reference that current // nodes' struct 'next' field and set on next iteration without utilising // assignment for example `prev.Next = *node`. var tip = &ll.head // Prev denotes a place holder to node and all of its next references need // to be pushed back onto stack. var prev *Node for i := range items { if *tip == nil { // Extend node chain *tip = stack.Pop() // Set current node prev to last node (*tip).Prev = prev ll.length++ } // Set item value (*tip).Value = items[i] // Set previous to current node prev = *tip // Set tip to next node tip = &(*tip).Next } // Push has references to dangling nodes that need to be removed and pushed // back onto stack for re-use var push *Node // Cleave unused reference chain from main chain if prev == nil { // The entire chain will need to be pushed back on to stack push = *tip ll.head = nil } else { push = prev.Next prev.Next = nil } // Push unused pointers back on stack for push != nil { pending := push.Next stack.Push(push, getNow()) ll.length-- push = pending } } // updateByID amends price by corresponding ID and returns an error if not found func (ll *linkedList) updateByID(updts []Item) error { updates: for x := range updts { for tip := ll.head; tip != nil; tip = tip.Next { if updts[x].ID != tip.Value.ID { // Filter IDs that don't match continue } if updts[x].Price > 0 { // Only apply changes when zero values are not present, Bitmex // for example sends 0 price values. tip.Value.Price = updts[x].Price } tip.Value.Amount = updts[x].Amount continue updates } return fmt.Errorf("update error: %w ID: %d not found", errIDCannotBeMatched, updts[x].ID) } return nil } // deleteByID deletes reference by ID func (ll *linkedList) deleteByID(updts Items, stack *stack, bypassErr bool) error { updates: for x := range updts { for tip := &ll.head; *tip != nil; tip = &(*tip).Next { if updts[x].ID != (*tip).Value.ID { continue } stack.Push(deleteAtTip(ll, tip), getNow()) continue updates } if !bypassErr { return fmt.Errorf("delete error: %w %d not found", errIDCannotBeMatched, updts[x].ID) } } return nil } // cleanup reduces the max size of the depth length if exceeded. Is used after // updates have been applied instead of adhoc, reason being its easier to prune // at the end. (can't inline) func (ll *linkedList) cleanup(maxChainLength int, stack *stack) { // Reduces the max length of total linked list chain, occurs after updates // have been implemented as updates can push length out of bounds, if // cleaved after that update, new update might not applied correctly. n := ll.head for i := 0; i < maxChainLength; i++ { if n.Next == nil { return } n = n.Next } // cleave reference to current node if n.Prev != nil { n.Prev.Next = nil } else { ll.head = nil } var pruned int for n != nil { pruned++ pending := n.Next stack.Push(n, getNow()) n = pending } ll.length -= pruned } // amount returns total depth liquidity and value func (ll *linkedList) amount() (liquidity, value float64) { for tip := ll.head; tip != nil; tip = tip.Next { liquidity += tip.Value.Amount value += tip.Value.Amount * tip.Value.Price } return } // retrieve returns a full slice of contents from the linked list func (ll *linkedList) retrieve() Items { depth := make(Items, 0, ll.length) for tip := ll.head; tip != nil; tip = tip.Next { depth = append(depth, tip.Value) } return depth } // updateInsertByPrice amends, inserts, moves and cleaves length of depth by // updates func (ll *linkedList) updateInsertByPrice(updts Items, stack *stack, maxChainLength int, compare func(float64, float64) bool, tn now) { for x := range updts { for tip := &ll.head; ; tip = &(*tip).Next { if *tip == nil { insertHeadSpecific(ll, updts[x], stack) break } if (*tip).Value.Price == updts[x].Price { // Match check if updts[x].Amount <= 0 { // Capture delete update stack.Push(deleteAtTip(ll, tip), tn) } else { // Amend current amount value (*tip).Value.Amount = updts[x].Amount } break // Continue updates } if compare((*tip).Value.Price, updts[x].Price) { // Insert // This check below filters zero values and provides an // optimisation for when select exchanges send a delete update // to a non-existent price level (OTC/Hidden order) so we can // break instantly and reduce the traversal of the entire chain. if updts[x].Amount > 0 { insertAtTip(ll, tip, updts[x], stack) } break // Continue updates } if (*tip).Next == nil { // Tip is at tail // This check below is just a catch all in the event the above // zero value check fails if updts[x].Amount > 0 { insertAtTail(ll, tip, updts[x], stack) } break } } } // Reduces length of total linked list chain to a maxChainLength value if maxChainLength != 0 && ll.length > maxChainLength { ll.cleanup(maxChainLength, stack) } } // updateInsertByID updates or inserts if not found for a bid or ask depth // 1) node ID found amount amended (best case) // 2) node ID found amount and price amended and node moved to correct position // (medium case) // 3) Update price exceeds traversal node price before ID found, save node // address for either; node ID matches then re-address node or end of depth pop // a node from the stack (worst case) func (ll *linkedList) updateInsertByID(updts Items, stack *stack, compare comparison) error { updates: for x := range updts { if updts[x].Amount <= 0 { return errAmountCannotBeLessOrEqualToZero } // bookmark allows for saving of a position of a node in the event that // an update price exceeds the current node price. We can then match an // ID and re-assign that ID's node to that positioning without popping // from the stack and then pushing to the stack later for cleanup. // If the ID is not found we can pop from stack then insert into that // price level var bookmark *Node for tip := ll.head; tip != nil; tip = tip.Next { if tip.Value.ID == updts[x].ID { if tip.Value.Price != updts[x].Price { // Price level change if tip.Next == nil { // no movement needed just a re-adjustment tip.Value.Price = updts[x].Price tip.Value.Amount = updts[x].Amount continue updates } // bookmark tip to move this node to correct price level bookmark = tip continue // continue through node depth } // no price change, amend amount and continue update tip.Value.Amount = updts[x].Amount continue updates // continue to next update } if compare(tip.Value.Price, updts[x].Price) { if bookmark != nil { // shift bookmarked node to current tip bookmark.Value = updts[x] move(&ll.head, bookmark, tip) continue updates } // search for ID for n := tip.Next; n != nil; n = n.Next { if n.Value.ID == updts[x].ID { n.Value = updts[x] // inserting before the tip move(&ll.head, n, tip) continue updates } } // ID not matched in depth so add correct level for insert if tip.Next == nil { n := stack.Pop() n.Value = updts[x] ll.length++ if tip.Prev == nil { tip.Prev = n n.Next = tip ll.head = n continue updates } tip.Prev.Next = n n.Prev = tip.Prev tip.Prev = n n.Next = tip continue updates } bookmark = tip break } if tip.Next == nil { if shiftBookmark(tip, &bookmark, &ll.head, updts[x]) { continue updates } } } n := stack.Pop() n.Value = updts[x] insertNodeAtBookmark(ll, bookmark, n) // Won't inline with stack } return nil } // insertUpdates inserts new updates for bids or asks based on price level func (ll *linkedList) insertUpdates(updts Items, stack *stack, comp comparison) error { for x := range updts { var prev *Node for tip := &ll.head; ; tip = &(*tip).Next { if *tip == nil { // Head n := stack.Pop() n.Value = updts[x] n.Prev = prev ll.length++ *tip = n break // Continue updates } if (*tip).Value.Price == updts[x].Price { // Price already found return fmt.Errorf("%w for price %f", errCollisionDetected, updts[x].Price) } if comp((*tip).Value.Price, updts[x].Price) { // Alignment n := stack.Pop() n.Value = updts[x] n.Prev = prev ll.length++ // Reference current with new node (*tip).Prev = n // Push tip to the right n.Next = *tip // This is the same as prev.Next = n *tip = n break // Continue updates } if (*tip).Next == nil { // Tail insertAtTail(ll, tip, updts[x], stack) break // Continue updates } prev = *tip } } return nil } // getHeadPriceNoLock gets best/head price func (ll *linkedList) getHeadPriceNoLock() (float64, error) { if ll.head == nil { return 0, errNoLiquidity } return ll.head.Value.Price, nil } // getHeadVolumeNoLock gets best/head volume func (ll *linkedList) getHeadVolumeNoLock() (float64, error) { if ll.head == nil { return 0, errNoLiquidity } return ll.head.Value.Amount, nil } // getMovementByQuotation traverses through orderbook liquidity using quotation // currency as a limiter and returns orderbook movement details. Swap boolean // allows the swap of sold and purchased to reduce code so it doesn't need to be // specific to bid or ask. func (ll *linkedList) getMovementByQuotation(quote, refPrice float64, swap bool) (*Movement, error) { if quote <= 0 { return nil, errQuoteAmountInvalid } if refPrice <= 0 { return nil, errInvalidReferencePrice } head, err := ll.getHeadPriceNoLock() if err != nil { return nil, err } m := Movement{StartPrice: refPrice} for tip := ll.head; tip != nil; tip = tip.Next { trancheValue := tip.Value.Amount * tip.Value.Price leftover := quote - trancheValue if leftover < 0 { m.Purchased += quote m.Sold += quote / trancheValue * tip.Value.Amount // This tranche is not consumed so the book shifts to this price. m.EndPrice = tip.Value.Price quote = 0 break } // Full tranche consumed m.Purchased += tip.Value.Price * tip.Value.Amount m.Sold += tip.Value.Amount quote = leftover if leftover == 0 { // Price no longer exists on the book so use next full price tranche // to calculate book impact. If available. if tip.Next != nil { m.EndPrice = tip.Next.Value.Price } else { m.FullBookSideConsumed = true } } } return m.finalizeFields(m.Purchased, m.Sold, head, quote, swap) } // getMovementByBase traverses through orderbook liquidity using base currency // as a limiter and returns orderbook movement details. Swap boolean allows the // swap of sold and purchased to reduce code so it doesn't need to be specific // to bid or ask. func (ll *linkedList) getMovementByBase(base, refPrice float64, swap bool) (*Movement, error) { if base <= 0 { return nil, errBaseAmountInvalid } if refPrice <= 0 { return nil, errInvalidReferencePrice } head, err := ll.getHeadPriceNoLock() if err != nil { return nil, err } m := Movement{StartPrice: refPrice} for tip := ll.head; tip != nil; tip = tip.Next { leftover := base - tip.Value.Amount if leftover < 0 { m.Purchased += tip.Value.Price * base m.Sold += base // This tranche is not consumed so the book shifts to this price. m.EndPrice = tip.Value.Price base = 0 break } // Full tranche consumed m.Purchased += tip.Value.Price * tip.Value.Amount m.Sold += tip.Value.Amount base = leftover if leftover == 0 { // Price no longer exists on the book so use next full price tranche // to calculate book impact. if tip.Next != nil { m.EndPrice = tip.Next.Value.Price } else { m.FullBookSideConsumed = true } } } return m.finalizeFields(m.Purchased, m.Sold, head, base, swap) } // bids embed a linked list to attach methods for bid depth specific // functionality type bids struct { linkedList } // bidCompare ensures price is in correct descending alignment (can inline) func bidCompare(left, right float64) bool { return left < right } // updateInsertByPrice amends, inserts, moves and cleaves length of depth by // updates func (ll *bids) updateInsertByPrice(updts Items, stack *stack, maxChainLength int, tn now) { ll.linkedList.updateInsertByPrice(updts, stack, maxChainLength, bidCompare, tn) } // updateInsertByID updates or inserts if not found func (ll *bids) updateInsertByID(updts Items, stack *stack) error { return ll.linkedList.updateInsertByID(updts, stack, bidCompare) } // insertUpdates inserts new updates for bids based on price level func (ll *bids) insertUpdates(updts Items, stack *stack) error { return ll.linkedList.insertUpdates(updts, stack, bidCompare) } // hitBidsByNominalSlippage hits the bids by the required nominal slippage // percentage, calculated from the reference price and returns orderbook // movement details. func (ll *bids) hitBidsByNominalSlippage(slippage, refPrice float64) (*Movement, error) { if slippage < 0 { return nil, errInvalidNominalSlippage } if slippage > 100 { return nil, errInvalidSlippageCannotExceed100 } if refPrice <= 0 { return nil, errInvalidReferencePrice } if ll.head == nil { return nil, errNoLiquidity } nominal := &Movement{StartPrice: refPrice, EndPrice: refPrice} var cumulativeValue, cumulativeAmounts float64 for tip := ll.head; tip != nil; tip = tip.Next { totalTrancheValue := tip.Value.Price * tip.Value.Amount currentFullValue := totalTrancheValue + cumulativeValue currentTotalAmounts := cumulativeAmounts + tip.Value.Amount nominal.AverageOrderCost = currentFullValue / currentTotalAmounts percent := math.CalculatePercentageGainOrLoss(nominal.AverageOrderCost, refPrice) if percent != 0 { percent *= -1 } if slippage < percent { targetCost := (1 - slippage/100) * refPrice if targetCost == refPrice { nominal.AverageOrderCost = cumulativeValue / cumulativeAmounts // Rounding issue on requested nominal percentage return nominal, nil } comparative := targetCost * cumulativeAmounts comparativeDiff := comparative - cumulativeValue trancheTargetPriceDiff := tip.Value.Price - targetCost trancheAmountExpectation := comparativeDiff / trancheTargetPriceDiff nominal.NominalPercentage = slippage nominal.Sold = cumulativeAmounts + trancheAmountExpectation nominal.Purchased += trancheAmountExpectation * tip.Value.Price nominal.AverageOrderCost = nominal.Purchased / nominal.Sold nominal.EndPrice = tip.Value.Price return nominal, nil } nominal.EndPrice = tip.Value.Price cumulativeValue = currentFullValue nominal.NominalPercentage = percent nominal.Sold += tip.Value.Amount nominal.Purchased += totalTrancheValue cumulativeAmounts = currentTotalAmounts if slippage == percent { nominal.FullBookSideConsumed = tip.Next == nil return nominal, nil } } nominal.FullBookSideConsumed = true return nominal, nil } // hitBidsByImpactSlippage hits the bids by the required impact slippage // percentage, calculated from the reference price and returns orderbook // movement details. func (ll *bids) hitBidsByImpactSlippage(slippage, refPrice float64) (*Movement, error) { if slippage <= 0 { return nil, errInvalidImpactSlippage } if slippage > 100 { return nil, errInvalidSlippageCannotExceed100 } if refPrice <= 0 { return nil, errInvalidReferencePrice } if ll.head == nil { return nil, errNoLiquidity } impact := &Movement{StartPrice: refPrice, EndPrice: refPrice} for tip := ll.head; tip != nil; tip = tip.Next { percent := math.CalculatePercentageGainOrLoss(tip.Value.Price, refPrice) if percent != 0 { percent *= -1 } impact.EndPrice = tip.Value.Price impact.ImpactPercentage = percent if slippage <= percent { // Don't include this tranche amount as this consumes the tranche // book price, thus obtaining a higher percentage impact. return impact, nil } impact.Sold += tip.Value.Amount impact.Purchased += tip.Value.Amount * tip.Value.Price impact.AverageOrderCost = impact.Purchased / impact.Sold } impact.FullBookSideConsumed = true impact.ImpactPercentage = FullLiquidityExhaustedPercentage return impact, nil } // asks embed a linked list to attach methods for ask depth specific // functionality type asks struct { linkedList } // askCompare ensures price is in correct ascending alignment (can inline) func askCompare(left, right float64) bool { return left > right } // updateInsertByPrice amends, inserts, moves and cleaves length of depth by // updates func (ll *asks) updateInsertByPrice(updts Items, stack *stack, maxChainLength int, tn now) { ll.linkedList.updateInsertByPrice(updts, stack, maxChainLength, askCompare, tn) } // updateInsertByID updates or inserts if not found func (ll *asks) updateInsertByID(updts Items, stack *stack) error { return ll.linkedList.updateInsertByID(updts, stack, askCompare) } // insertUpdates inserts new updates for asks based on price level func (ll *asks) insertUpdates(updts Items, stack *stack) error { return ll.linkedList.insertUpdates(updts, stack, askCompare) } // liftAsksByNominalSlippage lifts the asks by the required nominal slippage // percentage, calculated from the reference price and returns orderbook // movement details. func (ll *asks) liftAsksByNominalSlippage(slippage, refPrice float64) (*Movement, error) { if slippage < 0 { return nil, errInvalidNominalSlippage } if refPrice <= 0 { return nil, errInvalidReferencePrice } if ll.head == nil { return nil, errNoLiquidity } nominal := &Movement{StartPrice: refPrice, EndPrice: refPrice} var cumulativeAmounts float64 for tip := ll.head; tip != nil; tip = tip.Next { totalTrancheValue := tip.Value.Price * tip.Value.Amount currentValue := totalTrancheValue + nominal.Sold currentAmounts := cumulativeAmounts + tip.Value.Amount nominal.AverageOrderCost = currentValue / currentAmounts percent := math.CalculatePercentageGainOrLoss(nominal.AverageOrderCost, refPrice) if slippage < percent { targetCost := (1 + slippage/100) * refPrice if targetCost == refPrice { nominal.AverageOrderCost = nominal.Sold / nominal.Purchased // Rounding issue on requested nominal percentage return nominal, nil } comparative := targetCost * cumulativeAmounts comparativeDiff := comparative - nominal.Sold trancheTargetPriceDiff := tip.Value.Price - targetCost trancheAmountExpectation := comparativeDiff / trancheTargetPriceDiff nominal.NominalPercentage = slippage nominal.Sold += trancheAmountExpectation * tip.Value.Price nominal.Purchased += trancheAmountExpectation nominal.AverageOrderCost = nominal.Sold / nominal.Purchased nominal.EndPrice = tip.Value.Price return nominal, nil } nominal.EndPrice = tip.Value.Price nominal.Sold = currentValue nominal.Purchased += tip.Value.Amount nominal.NominalPercentage = percent if slippage == percent { return nominal, nil } cumulativeAmounts = currentAmounts } nominal.FullBookSideConsumed = true return nominal, nil } // liftAsksByImpactSlippage lifts the asks by the required impact slippage // percentage, calculated from the reference price and returns orderbook // movement details. func (ll *asks) liftAsksByImpactSlippage(slippage, refPrice float64) (*Movement, error) { if slippage <= 0 { return nil, errInvalidImpactSlippage } if refPrice <= 0 { return nil, errInvalidReferencePrice } if ll.head == nil { return nil, errNoLiquidity } impact := &Movement{StartPrice: refPrice, EndPrice: refPrice} for tip := ll.head; tip != nil; tip = tip.Next { percent := math.CalculatePercentageGainOrLoss(tip.Value.Price, refPrice) impact.ImpactPercentage = percent impact.EndPrice = tip.Value.Price if slippage <= percent { // Don't include this tranche amount as this consumes the tranche // book price, thus obtaining a higher percentage impact. return impact, nil } impact.Sold += tip.Value.Amount * tip.Value.Price impact.Purchased += tip.Value.Amount impact.AverageOrderCost = impact.Sold / impact.Purchased } impact.FullBookSideConsumed = true impact.ImpactPercentage = FullLiquidityExhaustedPercentage return impact, nil } // move moves a node from a point in a node chain to another node position, // this left justified towards head as element zero is the top of the depth // side. (can inline) func move(head **Node, from, to *Node) { if from.Next != nil { // From is at tail from.Next.Prev = from.Prev } if from.Prev == nil { // From is at head (*head).Next.Prev = nil *head = (*head).Next } else { from.Prev.Next = from.Next } // insert from node next to 'to' node if to.Prev == nil { // Destination is at head position *head = from } else { to.Prev.Next = from } from.Prev = to.Prev to.Prev = from from.Next = to } // deleteAtTip removes a node from tip target returns old node (can inline) func deleteAtTip(ll *linkedList, tip **Node) *Node { // Old is a placeholder for current tips node value to push // back on to the stack. old := *tip switch { case old.Prev == nil: // At head position // shift current tip head to the right *tip = old.Next // Remove reference to node from chain if old.Next != nil { // This is when liquidity hits zero old.Next.Prev = nil } case old.Next == nil: // At tail position // Remove reference to node from chain old.Prev.Next = nil default: // Reference prior node in chain to next node in chain // bypassing current node old.Prev.Next = old.Next old.Next.Prev = old.Prev } ll.length-- return old } // insertAtTip inserts at a tip target (can inline) func insertAtTip(ll *linkedList, tip **Node, updt Item, stack *stack) { n := stack.Pop() n.Value = updt n.Next = *tip n.Prev = (*tip).Prev if (*tip).Prev == nil { // Tip is at head // Replace head which will push everything to the right // when this node will reference new node below *tip = n } else { // Reference new node to previous node (*tip).Prev.Next = n } // Reference next node to new node n.Next.Prev = n ll.length++ } // insertAtTail inserts at tail end of node chain (can inline) func insertAtTail(ll *linkedList, tip **Node, updt Item, stack *stack) { n := stack.Pop() n.Value = updt // Reference tip to new node (*tip).Next = n // Reference new node with current tip n.Prev = *tip ll.length++ } // insertHeadSpecific inserts at head specifically there might be an instance // where the liquidity on an exchange does fall to zero through a streaming // endpoint then it comes back online. (can inline) func insertHeadSpecific(ll *linkedList, updt Item, stack *stack) { n := stack.Pop() n.Value = updt ll.head = n ll.length++ } // insertNodeAtBookmark inserts a new node at a bookmarked node position // returns if a node needs to replace head (can inline) func insertNodeAtBookmark(ll *linkedList, bookmark, n *Node) { switch { case bookmark == nil: // Zero liquidity and we are rebuilding from scratch ll.head = n case bookmark.Prev == nil: n.Prev = bookmark.Prev bookmark.Prev = n n.Next = bookmark ll.head = n case bookmark.Next == nil: n.Prev = bookmark bookmark.Next = n default: bookmark.Prev.Next = n n.Prev = bookmark.Prev bookmark.Prev = n n.Next = bookmark } ll.length++ } // shiftBookmark moves a bookmarked node to the tip's next position or if nil, // sets tip as bookmark (can inline) func shiftBookmark(tip *Node, bookmark, head **Node, updt Item) bool { if *bookmark == nil { // End of the chain and no bookmark set *bookmark = tip // Set tip to bookmark so we can set a new node there return false } (*bookmark).Value = updt (*bookmark).Next.Prev = (*bookmark).Prev if (*bookmark).Prev == nil { // Bookmark is at head *head = (*bookmark).Next } else { (*bookmark).Prev.Next = (*bookmark).Next } tip.Next = *bookmark (*bookmark).Prev = tip (*bookmark).Next = nil return true } // finalizeFields sets average order costing, percentages, slippage cost and // preserves existing fields. func (m *Movement) finalizeFields(cost, amount, headPrice, leftover float64, swap bool) (*Movement, error) { if cost <= 0 { return nil, errInvalidCost } if amount <= 0 { return nil, errInvalidAmount } if headPrice <= 0 { return nil, errInvalidHeadPrice } if m.StartPrice != m.EndPrice { // Average order cost defines the actual cost price as capital is // deployed through the orderbook liquidity. m.AverageOrderCost = cost / amount } else { // Edge case rounding issue for float64 with small numbers. m.AverageOrderCost = m.StartPrice } // Nominal percentage is the difference from the reference price to average // order cost. m.NominalPercentage = math.CalculatePercentageGainOrLoss(m.AverageOrderCost, m.StartPrice) if m.NominalPercentage < 0 { m.NominalPercentage *= -1 } if !m.FullBookSideConsumed && leftover == 0 { // Impact percentage is how much the orderbook slips from the reference // price to the remaining tranche price. m.ImpactPercentage = math.CalculatePercentageGainOrLoss(m.EndPrice, m.StartPrice) if m.ImpactPercentage < 0 { m.ImpactPercentage *= -1 } } else { // Full liquidity exhausted by request amount m.ImpactPercentage = FullLiquidityExhaustedPercentage m.FullBookSideConsumed = true } // Slippage cost is the difference in quotation terms between the actual // cost and the amounts at head price e.g. // Let P(n)=Price A(n)=Amount and iterate through a descending bid order example; // Cost: $270 (P1:100 x A1:1 + P2:90 x A2:1 + P3:80 x A3:1) // No slippage cost: $300 (P1:100 x A1:1 + P1:100 x A2:1 + P1:100 x A3:1) // $300 - $270 = $30 of slippage. m.SlippageCost = cost - (headPrice * amount) if m.SlippageCost < 0 { m.SlippageCost *= -1 } // Swap saves on code duplication for difference in ask or bid amounts. if swap { m.Sold, m.Purchased = m.Purchased, m.Sold } return m, nil }