package market import ( "fmt" "time" "git.mleku.dev/mleku/dendrite/pkg/axiom" ) // element is the concrete axiom.Element type for market data. // Same pattern as nostr/enzyme.go — no special machinery. type element struct { tag string val any } func (e element) Type() string { return e.tag } func (e element) Value() any { return e.val } // assetElement extends element with symbol annotation. // Used by BarToElements so per-asset lattice regions can filter by symbol. type assetElement struct { element symbol string } func (e assetElement) Symbol() string { return e.symbol } // OrderBookToElements decomposes an order book snapshot into typed // lattice elements. Each book produces elements for its structural // parts: asset, venue, price levels, spread, depth imbalance. // // The output is structurally indistinguishable from EventToElements. // Price is structure. Events are structure. The lattice sees structure. func OrderBookToElements(ob *OrderBook) []axiom.Element { var elems []axiom.Element // Asset identifier. elems = append(elems, element{"asset", ob.Asset}) // Venue. elems = append(elems, element{"venue", string(ob.Venue)}) // Timestamp. elems = append(elems, element{"timestamp", fmt.Sprintf("%d", ob.Time.Unix())}) // Price levels — each is a typed element. for i, bid := range ob.Bids { elems = append(elems, element{"bid", bid.Price}) elems = append(elems, element{"bid-volume", bid.Volume}) if i >= 9 { break // top 10 levels } } for i, ask := range ob.Asks { elems = append(elems, element{"ask", ask.Price}) elems = append(elems, element{"ask-volume", ask.Volume}) if i >= 9 { break } } // Spread as a derived structural element. spread := ob.Spread() if spread != 0 { elems = append(elems, element{"spread", spread}) mid := ob.MidPrice() if mid != 0 { elems = append(elems, element{"spread-bps", (spread / mid) * 10000}) } } // Depth imbalance (top 5 levels). elems = append(elems, element{"depth-imbalance", ob.DepthImbalance(5)}) return elems } // Bar is an OHLCV candlestick — the primary market data structure from Alpaca. // Structurally identical to a Nostr event: a timestamped data point that // decomposes into typed elements through enzymatic decomposition. type Bar struct { Symbol string Open float64 High float64 Low float64 Close float64 Volume float64 VWAP float64 TradeCount uint64 Timestamp time.Time } // BarToElements decomposes an OHLCV bar into typed lattice elements. // The output is structurally indistinguishable from EventToElements // or OrderBookToElements. The lattice sees structure. func BarToElements(b *Bar) []axiom.Element { s := b.Symbol // all elements carry their asset's symbol ae := func(tag string, val any) axiom.Element { return assetElement{element{tag, val}, s} } elems := []axiom.Element{ ae("asset", b.Symbol), ae("bar-open", b.Open), ae("bar-high", b.High), ae("bar-low", b.Low), ae("bar-close", b.Close), ae("bar-volume", b.Volume), ae("bar-vwap", b.VWAP), ae("bar-trades", b.TradeCount), ae("timestamp", fmt.Sprintf("%d", b.Timestamp.Unix())), } // Derived structural elements — the shape of the candle. body := b.Close - b.Open elems = append(elems, ae("bar-body", body)) rang := b.High - b.Low if rang > 0 { elems = append(elems, ae("bar-range", rang)) // Upper and lower shadows as fractions of range. if body >= 0 { elems = append(elems, ae("bar-upper-shadow", (b.High-b.Close)/rang)) elems = append(elems, ae("bar-lower-shadow", (b.Open-b.Low)/rang)) } else { elems = append(elems, ae("bar-upper-shadow", (b.High-b.Open)/rang)) elems = append(elems, ae("bar-lower-shadow", (b.Close-b.Low)/rang)) } } // Volume-weighted price deviation: how far VWAP is from the midpoint. if b.VWAP > 0 { mid := (b.High + b.Low) / 2 elems = append(elems, ae("bar-vwap-deviation", b.VWAP-mid)) } return elems } // OrderEntryToElements decomposes a single order entry into elements. func OrderEntryToElements(entry *OrderEntry) []axiom.Element { return []axiom.Element{ element{"price", entry.Price}, element{"volume", entry.Volume}, element{"side", entry.Side.String()}, element{"venue", string(entry.Venue)}, element{"timestamp", fmt.Sprintf("%d", entry.Time.Unix())}, } }