spread.go raw

   1  package market
   2  
   3  import (
   4  	"fmt"
   5  	"time"
   6  
   7  	"git.mleku.dev/mleku/dendrite/pkg/axiom"
   8  )
   9  
  10  // CrossSpread is the spread between the same asset on two venues.
  11  // When this exceeds transaction costs, it is structural incoherence —
  12  // a price that exists in one place but not the other.
  13  type CrossSpread struct {
  14  	Asset    string
  15  	VenueA   Venue
  16  	VenueB   Venue
  17  	BidA     float64 // best bid on venue A
  18  	AskB     float64 // best ask on venue B
  19  	BidB     float64 // best bid on venue B
  20  	AskA     float64 // best ask on venue A
  21  	Time     time.Time
  22  }
  23  
  24  // SpreadAB returns the profit from buying on B and selling on A.
  25  // Positive means venue A's bid exceeds venue B's ask.
  26  func (cs *CrossSpread) SpreadAB() float64 {
  27  	if cs.AskB == 0 {
  28  		return 0
  29  	}
  30  	return cs.BidA - cs.AskB
  31  }
  32  
  33  // SpreadBA returns the profit from buying on A and selling on B.
  34  // Positive means venue B's bid exceeds venue A's ask.
  35  func (cs *CrossSpread) SpreadBA() float64 {
  36  	if cs.AskA == 0 {
  37  		return 0
  38  	}
  39  	return cs.BidB - cs.AskA
  40  }
  41  
  42  // BestSpread returns the best available cross-venue spread and its
  43  // direction. Positive return means profit is available.
  44  func (cs *CrossSpread) BestSpread() (spread float64, buyVenue, sellVenue Venue) {
  45  	ab := cs.SpreadAB()
  46  	ba := cs.SpreadBA()
  47  	if ab >= ba {
  48  		return ab, cs.VenueB, cs.VenueA
  49  	}
  50  	return ba, cs.VenueA, cs.VenueB
  51  }
  52  
  53  // CompareBooksForSpread compares two order books for the same asset on
  54  // different venues and returns the cross-venue spread.
  55  func CompareBooksForSpread(a, b *OrderBook) *CrossSpread {
  56  	return &CrossSpread{
  57  		Asset:  a.Asset,
  58  		VenueA: a.Venue,
  59  		VenueB: b.Venue,
  60  		BidA:   a.BestBid(),
  61  		AskA:   a.BestAsk(),
  62  		BidB:   b.BestBid(),
  63  		AskB:   b.BestAsk(),
  64  		Time:   time.Now(),
  65  	}
  66  }
  67  
  68  // Dislocation is a mismatch between information structure and price
  69  // structure. The negative space between two views of the same asset —
  70  // the shape of what the market hasn't priced yet.
  71  type Dislocation struct {
  72  	Asset     string
  73  	PriceMid  float64 // midpoint from order book
  74  	Sentiment float64 // derived from signed event chatter [-1, +1]
  75  	Magnitude float64 // absolute divergence measure [0, 1]
  76  	Direction int     // +1 = price lags sentiment up, -1 = price lags sentiment down
  77  	Time      time.Time
  78  }
  79  
  80  // DislocationToElements decomposes a dislocation into typed lattice
  81  // elements. A dislocation is the structural gap between what the
  82  // signed chatter says and what the order book shows.
  83  func DislocationToElements(d *Dislocation) []axiom.Element {
  84  	return []axiom.Element{
  85  		element{"asset", d.Asset},
  86  		element{"dislocation-magnitude", d.Magnitude},
  87  		element{"dislocation-direction", d.Direction},
  88  		element{"price-mid", d.PriceMid},
  89  		element{"sentiment", d.Sentiment},
  90  		element{"timestamp", fmt.Sprintf("%d", d.Time.Unix())},
  91  	}
  92  }
  93  
  94  // CrossSpreadToElements decomposes a cross-venue spread into lattice elements.
  95  func CrossSpreadToElements(cs *CrossSpread) []axiom.Element {
  96  	spread, buyVenue, sellVenue := cs.BestSpread()
  97  	return []axiom.Element{
  98  		element{"asset", cs.Asset},
  99  		element{"cross-spread", spread},
 100  		element{"buy-venue", string(buyVenue)},
 101  		element{"sell-venue", string(sellVenue)},
 102  		element{"venue", string(cs.VenueA)},
 103  		element{"venue", string(cs.VenueB)},
 104  		element{"timestamp", fmt.Sprintf("%d", cs.Time.Unix())},
 105  	}
 106  }
 107