package book import "github.com/edwardtay/injective-booklab/dec" type Fill struct { Price dec.Dec `json:"price"` Qty dec.Dec `json:"qty"` } // SimResult is what a taker order of a given size would do against a // snapshot: the fills level by level and the cost of crossing the book. type SimResult struct { Side string `json:"side"` Requested dec.Dec `json:"requested"` Filled dec.Dec `json:"filled"` Unfilled dec.Dec `json:"unfilled"` Notional dec.Dec `json:"notional"` VWAP dec.Dec `json:"vwap"` WorstPrice dec.Dec `json:"worst_price"` Mid dec.Dec `json:"mid"` SlippageBps dec.Dec `json:"slippage_bps"` // VWAP vs mid, always >= 0 for a sane book Fee dec.Dec `json:"fee"` LevelsUsed int `json:"levels_used"` Fills []Fill `json:"fills"` } // SimulateTaker walks the opposite side of the book for qty base units with // an optional worst-price limit (zero means unlimited). It does not mutate // the book. Fee is charged on notional at feeRate. func SimulateTaker(b *Book, side Side, qty, limit, feeRate dec.Dec) SimResult { levels := b.Asks worse := dec.Dec.GT // a buy stops when the ask price exceeds the limit if side == Sell { levels = b.Bids worse = dec.Dec.LT } r := SimResult{Side: side.String(), Requested: qty, Filled: dec.Zero(), Notional: dec.Zero(), VWAP: dec.Zero(), WorstPrice: dec.Zero(), SlippageBps: dec.Zero(), Fee: dec.Zero()} remaining := qty for _, l := range levels { if !remaining.IsPositive() { break } if limit.IsPositive() && worse(l.Price, limit) { break } take := dec.Min(remaining, l.Qty) r.Fills = append(r.Fills, Fill{Price: l.Price, Qty: take}) r.Filled = r.Filled.Add(take) r.Notional = r.Notional.Add(take.Mul(l.Price)) r.WorstPrice = l.Price remaining = remaining.Sub(take) r.LevelsUsed++ } r.Unfilled = remaining r.Fee = r.Notional.Mul(feeRate) if mid, ok := b.Mid(); ok { r.Mid = mid if r.Filled.IsPositive() { r.VWAP = r.Notional.Quo(r.Filled) diff := r.VWAP.Sub(mid) if side == Sell { diff = diff.Neg() } r.SlippageBps = diff.Quo(mid).Mul(dec.FromInt(10000)) } } else if r.Filled.IsPositive() { r.VWAP = r.Notional.Quo(r.Filled) } return r } // Change is one price level that differs between two snapshots. type Change struct { Side string `json:"side"` Price dec.Dec `json:"price"` Old dec.Dec `json:"old"` New dec.Dec `json:"new"` // zero means the level was removed } // Diff lists level changes from prev to next. Both books must be valid. // It runs in O(n) by merging the two sorted sides. func Diff(prev, next *Book) []Change { var out []Change out = diffSide(out, "bid", prev.Bids, next.Bids, dec.Dec.GT) out = diffSide(out, "ask", prev.Asks, next.Asks, dec.Dec.LT) return out } func diffSide(out []Change, side string, a, b []Level, before func(x, y dec.Dec) bool) []Change { i, j := 0, 0 for i < len(a) || j < len(b) { switch { case j >= len(b) || (i < len(a) && before(a[i].Price, b[j].Price)): out = append(out, Change{side, a[i].Price, a[i].Qty, dec.Zero()}) i++ case i >= len(a) || before(b[j].Price, a[i].Price): out = append(out, Change{side, b[j].Price, dec.Zero(), b[j].Qty}) j++ default: if !a[i].Qty.Equal(b[j].Qty) { out = append(out, Change{side, a[i].Price, a[i].Qty, b[j].Qty}) } i++ j++ } } return out }