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