// Package fba models the exchange module's per-block frequent batch auction // (FBA) as documented for Injective's EndBlocker: // // 1. Market orders are matched first against the resting book as it stood at // the start of the block. Resting orders fill at their own prices; all // market orders on one side fill at one uniform clearing price. // 2. New limit orders are merged with the resting book and matched while a // negative spread exists. Every match clears at one uniform price inside // [last matched sell, last matched buy]: the resting mid (spot) or the // mark price (derivatives) when it falls inside that range, else the // nearer bound. // // This is a teaching model built from the public docs, not chain code. package fba import ( "sort" "github.com/edwardtay/injective-booklab/book" "github.com/edwardtay/injective-booklab/dec" ) // Order is a new order arriving in this block. For a market order Price is // the worst acceptable price (zero = no bound). type Order struct { ID string `json:"id"` Side book.Side `json:"-"` Price dec.Dec `json:"price"` Qty dec.Dec `json:"qty"` } type OrderFill struct { ID string `json:"id"` Side string `json:"side"` Filled dec.Dec `json:"filled"` Cancelled dec.Dec `json:"cancelled,omitempty"` // market orders only Rests dec.Dec `json:"rests,omitempty"` // limit remainder added to the book } type SideResult struct { Matched dec.Dec `json:"matched"` ClearingPrice dec.Dec `json:"clearing_price"` RestingFills []book.Fill `json:"resting_fills"` // resting orders fill at their own price Orders []OrderFill `json:"orders"` } // ClearMarket runs stage 1 for one side. Orders are processed best worst-price // first (highest for buys, lowest for sells); within equal prices the input // order is kept, standing in for time priority. func ClearMarket(start *book.Book, side book.Side, orders []Order) SideResult { levels := append([]book.Level(nil), start.Asks...) if side == book.Sell { levels = append([]book.Level(nil), start.Bids...) } sorted := append([]Order(nil), orders...) sort.SliceStable(sorted, func(i, j int) bool { a, b := sorted[i].Price, sorted[j].Price if a.IsZero() != b.IsZero() { // unbounded orders are the most aggressive return a.IsZero() } if side == book.Buy { return a.GT(b) } return a.LT(b) }) res := SideResult{Matched: dec.Zero(), ClearingPrice: dec.Zero()} notional := dec.Zero() li := 0 for _, o := range sorted { rem := o.Qty for rem.IsPositive() && li < len(levels) { lp := levels[li].Price if !o.Price.IsZero() && ((side == book.Buy && lp.GT(o.Price)) || (side == book.Sell && lp.LT(o.Price))) { break } take := dec.Min(rem, levels[li].Qty) res.RestingFills = appendFill(res.RestingFills, lp, take) notional = notional.Add(lp.Mul(take)) res.Matched = res.Matched.Add(take) levels[li].Qty = levels[li].Qty.Sub(take) rem = rem.Sub(take) if levels[li].Qty.IsZero() { li++ } } res.Orders = append(res.Orders, OrderFill{ID: o.ID, Side: side.String(), Filled: o.Qty.Sub(rem), Cancelled: rem}) } if res.Matched.IsPositive() { res.ClearingPrice = notional.Quo(res.Matched) } return res } func appendFill(f []book.Fill, p, q dec.Dec) []book.Fill { if n := len(f); n > 0 && f[n-1].Price.Equal(p) { f[n-1].Qty = f[n-1].Qty.Add(q) return f } return append(f, book.Fill{Price: p, Qty: q}) } type LimitResult struct { Matched dec.Dec `json:"matched"` LastBuy dec.Dec `json:"last_buy"` LastSell dec.Dec `json:"last_sell"` Reference dec.Dec `json:"reference"` // resting mid or mark price ClearingPrice dec.Dec `json:"clearing_price"` Rule string `json:"rule"` Orders []OrderFill `json:"orders"` After *book.Book `json:"after"` } type entry struct { id string px dec.Dec qty dec.Dec orig dec.Dec new bool } // ClearLimit runs stage 2. mark is the derivative mark price; pass zero for spot. func ClearLimit(rest *book.Book, orders []Order, mark dec.Dec) LimitResult { var buys, sells []*entry for _, l := range rest.Bids { buys = append(buys, &entry{px: l.Price, qty: l.Qty, orig: l.Qty}) } for _, l := range rest.Asks { sells = append(sells, &entry{px: l.Price, qty: l.Qty, orig: l.Qty}) } for _, o := range orders { e := &entry{id: o.ID, px: o.Price, qty: o.Qty, orig: o.Qty, new: true} if o.Side == book.Buy { buys = append(buys, e) } else { sells = append(sells, e) } } // Price priority; resting entries were added first, so a stable sort keeps // them ahead of new orders at the same price (time priority). sort.SliceStable(buys, func(i, j int) bool { return buys[i].px.GT(buys[j].px) }) sort.SliceStable(sells, func(i, j int) bool { return sells[i].px.LT(sells[j].px) }) r := LimitResult{Matched: dec.Zero(), LastBuy: dec.Zero(), LastSell: dec.Zero(), Reference: dec.Zero(), ClearingPrice: dec.Zero()} bi, si := 0, 0 for bi < len(buys) && si < len(sells) && buys[bi].px.GTE(sells[si].px) { take := dec.Min(buys[bi].qty, sells[si].qty) buys[bi].qty = buys[bi].qty.Sub(take) sells[si].qty = sells[si].qty.Sub(take) r.Matched = r.Matched.Add(take) r.LastBuy, r.LastSell = buys[bi].px, sells[si].px if buys[bi].qty.IsZero() { bi++ } if sells[si].qty.IsZero() { si++ } } if r.Matched.IsPositive() { ref, name := mark, "mark price" if !mark.IsPositive() { ref, name = dec.Zero(), "resting mid" if m, ok := rest.Mid(); ok { ref = m } } r.Reference = ref lo, hi := r.LastSell, r.LastBuy switch { case !ref.IsPositive(): r.ClearingPrice, r.Rule = lo.Add(hi).Quo(dec.FromInt(2)), "no reference price: midpoint of the matched range" case ref.LT(lo): r.ClearingPrice, r.Rule = lo, name+" below the matched range: last sell price" case ref.GT(hi): r.ClearingPrice, r.Rule = hi, name+" above the matched range: last buy price" default: r.ClearingPrice, r.Rule = ref, name+" inside the matched range" } } after := &book.Book{MarketID: rest.MarketID, Seq: rest.Seq + 1} collect := func(es []*entry, side book.Side) []book.Level { var lv []book.Level for _, e := range es { if e.new { r.Orders = append(r.Orders, OrderFill{ID: e.id, Side: side.String(), Filled: e.orig.Sub(e.qty), Rests: e.qty}) } if e.qty.IsPositive() { if n := len(lv); n > 0 && lv[n-1].Price.Equal(e.px) { lv[n-1].Qty = lv[n-1].Qty.Add(e.qty) } else { lv = append(lv, book.Level{Price: e.px, Qty: e.qty}) } } } return lv } after.Bids = collect(buys, book.Buy) after.Asks = collect(sells, book.Sell) r.After = after return r }