package market import ( "fmt" "sync" "time" "git.mleku.dev/mleku/dendrite/pkg/axiom" ) // PriceFeed aggregates order books across venues and publishes a unified // view. This is the organism's first infrastructure service: making opaque // price structure visible. Revenue comes from subscribers who pay for the // consolidated feed, not from exploiting the opacity. type PriceFeed struct { mu sync.RWMutex snapshots map[string]*ConsolidatedSnapshot // asset → latest snapshot subscribers []chan *ConsolidatedSnapshot subMu sync.Mutex } // ConsolidatedSnapshot is a single-asset view across all known venues. // It exposes the best available prices and the depth of each venue, // making cross-venue structure visible to any subscriber. type ConsolidatedSnapshot struct { Asset string BestBid float64 BidVenue Venue BestAsk float64 AskVenue Venue Spread float64 Venues []VenueDepth Time time.Time } // VenueDepth is the depth summary for one venue. type VenueDepth struct { Venue Venue BidDepth float64 // total bid volume AskDepth float64 // total ask volume Spread float64 // local spread } // NewPriceFeed creates an empty consolidated price feed. func NewPriceFeed() *PriceFeed { return &PriceFeed{ snapshots: make(map[string]*ConsolidatedSnapshot), } } // Consolidate builds a unified snapshot from a VenueGraph's current state. func (pf *PriceFeed) Consolidate(vg *VenueGraph) { assets := vg.Assets() pf.mu.Lock() defer pf.mu.Unlock() for _, asset := range assets { venues := vg.VenuesForAsset(asset) if len(venues) == 0 { continue } snap := &ConsolidatedSnapshot{ Asset: asset, Time: time.Now(), } for _, v := range venues { book := vg.Book(asset, v) if book == nil { continue } bid := book.BestBid() ask := book.BestAsk() // Track best bid across venues. if bid > snap.BestBid { snap.BestBid = bid snap.BidVenue = v } // Track best ask across venues. if snap.BestAsk == 0 || (ask > 0 && ask < snap.BestAsk) { snap.BestAsk = ask snap.AskVenue = v } // Venue depth. bidDepth := topVolume(book.Bids, len(book.Bids)) askDepth := topVolume(book.Asks, len(book.Asks)) snap.Venues = append(snap.Venues, VenueDepth{ Venue: v, BidDepth: bidDepth, AskDepth: askDepth, Spread: book.Spread(), }) } if snap.BestAsk > 0 && snap.BestBid > 0 { snap.Spread = snap.BestAsk - snap.BestBid } pf.snapshots[asset] = snap // Publish to subscribers. pf.subMu.Lock() for _, ch := range pf.subscribers { select { case ch <- snap: default: // drop if subscriber is slow } } pf.subMu.Unlock() } } // Subscribe returns a channel that receives consolidated snapshots. func (pf *PriceFeed) Subscribe() chan *ConsolidatedSnapshot { ch := make(chan *ConsolidatedSnapshot, 32) pf.subMu.Lock() pf.subscribers = append(pf.subscribers, ch) pf.subMu.Unlock() return ch } // Snapshot returns the latest consolidated view for an asset. func (pf *PriceFeed) Snapshot(asset string) *ConsolidatedSnapshot { pf.mu.RLock() defer pf.mu.RUnlock() return pf.snapshots[asset] } // Assets returns the list of assets with consolidated data. func (pf *PriceFeed) Assets() []string { pf.mu.RLock() defer pf.mu.RUnlock() assets := make([]string, 0, len(pf.snapshots)) for a := range pf.snapshots { assets = append(assets, a) } return assets } // ConsolidatedSnapshotToElements decomposes a consolidated snapshot into // lattice elements. The consolidated view is structure that replaces // the opacity — information that previously existed only in the gap // between venues. func ConsolidatedSnapshotToElements(cs *ConsolidatedSnapshot) []axiom.Element { elems := []axiom.Element{ element{"asset", cs.Asset}, element{"consolidated-bid", cs.BestBid}, element{"consolidated-ask", cs.BestAsk}, element{"consolidated-spread", cs.Spread}, element{"bid-venue", string(cs.BidVenue)}, element{"ask-venue", string(cs.AskVenue)}, element{"venue-count", len(cs.Venues)}, element{"timestamp", fmt.Sprintf("%d", cs.Time.Unix())}, } for _, vd := range cs.Venues { elems = append(elems, element{"venue-depth-bid", vd.BidDepth}, element{"venue-depth-ask", vd.AskDepth}, ) } return elems }