package market import ( "math" "strings" "time" "unicode" "git.mleku.dev/mleku/dendrite/pkg/nostr" ) // AssetMention is a reference to a financial asset found in a Nostr // event's content. Cashtags ($BTC, $ETH) and ticker symbols are the // structural surface where information meets price. type AssetMention struct { Asset string // normalized: "BTC", "ETH", etc. EventID string Author string // pubkey hex Content string // the event content containing the mention Time time.Time } // ExtractAssetMentions scans a Nostr event's content for cashtags // and known ticker symbols. Returns nil if no assets are mentioned. func ExtractAssetMentions(ev *nostr.Event, knownAssets map[string]bool) []AssetMention { if ev.Content == "" { return nil } var mentions []AssetMention words := strings.Fields(ev.Content) for _, word := range words { asset := "" // Cashtag: $BTC, $ETH, $DOGE if len(word) > 1 && word[0] == '$' { ticker := strings.TrimRightFunc(word[1:], func(r rune) bool { return !unicode.IsLetter(r) && !unicode.IsDigit(r) }) ticker = strings.ToUpper(ticker) if len(ticker) >= 2 && len(ticker) <= 10 { asset = ticker } } // Known ticker without cashtag prefix. if asset == "" { upper := strings.ToUpper(strings.TrimRightFunc(word, func(r rune) bool { return !unicode.IsLetter(r) && !unicode.IsDigit(r) })) if knownAssets[upper] { asset = upper } } if asset != "" { mentions = append(mentions, AssetMention{ Asset: asset, EventID: ev.ID, Author: ev.Pubkey, Content: ev.Content, Time: time.Unix(ev.CreatedAt, 0), }) } } return mentions } // AuthorWeight computes a credibility weight for an author based on // their in-degree in the event graph. More referenced authors have // higher weight. The weight is log-scaled to prevent any single // author from dominating. // // Returns a value in [0, 1]. Zero in-degree maps to a baseline of 0.1. func AuthorWeight(pubkey string, graph *nostr.EventGraph) float64 { refs := graph.PubkeysR[pubkey] if len(refs) == 0 { return 0.1 // baseline: unknown authors still contribute } // Log scale: 1 ref → 0.3, 10 refs → 0.6, 100 refs → 0.8 w := math.Log10(float64(len(refs))+1) / 3.0 if w > 1.0 { w = 1.0 } if w < 0.1 { w = 0.1 } return w } // SentimentSignal is a weighted directional signal from event chatter // about an asset. Positive = bullish language, negative = bearish. type SentimentSignal struct { Asset string Author string Weight float64 // author credibility [0, 1] Direction float64 // [-1, +1] EventID string Time time.Time } // AggregateSentiment combines multiple sentiment signals into a single // weighted sentiment value in [-1, +1]. func AggregateSentiment(signals []SentimentSignal) float64 { if len(signals) == 0 { return 0 } weightedSum := 0.0 totalWeight := 0.0 for _, s := range signals { weightedSum += s.Direction * s.Weight totalWeight += s.Weight } if totalWeight == 0 { return 0 } return weightedSum / totalWeight } // DetectDislocation compares aggregated sentiment against price // structure and returns a dislocation if they diverge. // // The dislocation magnitude measures how far apart the information // view and the price view are. A high magnitude means the market // hasn't priced the information yet — that's the negative space // where value can be harvested. func DetectDislocation(asset string, ob *OrderBook, signals []SentimentSignal) *Dislocation { if ob == nil || len(signals) == 0 { return nil } mid := ob.MidPrice() if mid == 0 { return nil } sentiment := AggregateSentiment(signals) // Imbalance as a proxy for price direction pressure. imbalance := ob.DepthImbalance(5) // Normalize imbalance to [-1, +1]: 0.5 → 0, 1.0 → +1, 0.0 → -1 priceDirection := (imbalance - 0.5) * 2 // Dislocation = divergence between sentiment and price pressure. divergence := sentiment - priceDirection magnitude := math.Abs(divergence) / 2 // normalize to [0, 1] if magnitude > 1 { magnitude = 1 } direction := 0 if divergence > 0.1 { direction = 1 // sentiment bullish, price not yet } else if divergence < -0.1 { direction = -1 // sentiment bearish, price not yet } return &Dislocation{ Asset: asset, PriceMid: mid, Sentiment: sentiment, Magnitude: magnitude, Direction: direction, Time: time.Now(), } }