go.charczuk.com@v0.0.0-20240327042549-bc490516bd1a/experiments/cb-feed/processor/main.go (about) 1 package main 2 3 import ( 4 "context" 5 "encoding/json" 6 "flag" 7 "fmt" 8 "os" 9 "os/signal" 10 "strings" 11 "time" 12 13 "github.com/wcharczuk/go-diskq" 14 "github.com/wcharczuk/go-incr" 15 "github.com/wcharczuk/go-incr/incrutil" 16 "github.com/wcharczuk/go-incr/incrutil/slicei" 17 18 "go.charczuk.com/sdk/ansi" 19 20 "go.charczuk.com/experiments/cb-feed/pkg/coinbase" 21 "go.charczuk.com/experiments/cb-feed/pkg/price" 22 ) 23 24 var ( 25 flagPath = flag.String("path", "_data", "The data stream path") 26 flagReplayDuration = flag.Duration("replay", -5*time.Minute, "The duration of time to replay from the data file") 27 ) 28 29 func main() { 30 flag.Parse() 31 32 partitions, err := diskq.GetPartitions(*flagPath) 33 if err != nil { 34 fmt.Fprintf(os.Stderr, "%v\n", err) 35 os.Exit(1) 36 } 37 38 startOffsets := make(map[uint32]uint64) 39 for _, p := range partitions { 40 startOffset, _, err := diskq.GetOffsetAfter(*flagPath, p, time.Now().UTC().Add(*flagReplayDuration)) 41 if err != nil { 42 fmt.Fprintf(os.Stderr, "%v\n", err) 43 os.Exit(1) 44 } 45 fmt.Printf("starting consumer for partition %d at %d\n", p, startOffset) 46 startOffsets[p] = startOffset 47 } 48 dataReader, err := diskq.OpenConsumerGroup(*flagPath, diskq.ConsumerGroupOptions{ 49 OnCreateConsumer: func(partitionIndex uint32) (diskq.ConsumerOptions, error) { 50 return diskq.ConsumerOptions{ 51 StartBehavior: diskq.ConsumerStartBehaviorAtOffset, 52 StartOffset: startOffsets[partitionIndex], 53 EndBehavior: diskq.ConsumerEndBehaviorWait, 54 }, nil 55 }, 56 }) 57 if err != nil { 58 fmt.Fprintf(os.Stderr, "%v\n", err) 59 os.Exit(1) 60 } 61 defer dataReader.Close() 62 63 g := incr.New() 64 65 products := make(map[string]*productGraph) 66 for _, productID := range [...]string{ 67 "BTC-USD", 68 "ETH-USD", 69 } { 70 products[productID] = createProductGraph(g, productID) 71 } 72 73 ctx, cancel := context.WithCancel(context.Background()) 74 75 done := make(chan struct{}) 76 started := make(chan struct{}) 77 go func() { 78 defer close(done) 79 close(started) 80 var b coinbase.BasicMessage 81 for { 82 select { 83 case <-ctx.Done(): 84 return 85 case msg, ok := <-dataReader.Messages(): 86 if !ok { 87 return 88 } 89 _ = json.Unmarshal(msg.Data, &b) 90 switch b.Type { 91 case "ticker": 92 var t coinbase.Ticker 93 _ = json.Unmarshal(msg.Data, &t) 94 products[t.ProductID].latest.Set(t) 95 g.Stabilize(ctx) 96 case "match": 97 var m coinbase.Match 98 _ = json.Unmarshal(msg.Data, &m) 99 // g.Stabilize(ctx) 100 } 101 } 102 } 103 }() 104 105 interrupt := make(chan os.Signal, 1) 106 signal.Notify(interrupt, os.Interrupt) 107 <-interrupt 108 cancel() 109 <-done 110 } 111 112 type productGraph struct { 113 productID string 114 latest incr.VarIncr[coinbase.Ticker] 115 extractPrice incr.Incr[price.Price] 116 prices incr.Incr[[]price.Price] 117 sortedPrices incr.Incr[[]price.Price] 118 latestPrice incr.Incr[price.Price] 119 minPrice incr.Incr[price.Price] 120 maxPrice incr.Incr[price.Price] 121 122 latestPriceChangeDir incr.Incr[bool] 123 minPriceChangeDir incr.Incr[bool] 124 maxPriceChangeDir incr.Incr[bool] 125 126 observeLatestPriceChangeDir incr.ObserveIncr[bool] 127 observeMinPriceChangeDir incr.ObserveIncr[bool] 128 observeMaxPriceChangeDir incr.ObserveIncr[bool] 129 130 observeLatestPrice incr.ObserveIncr[price.Price] 131 observeMinPrice incr.ObserveIncr[price.Price] 132 observeMaxPrice incr.ObserveIncr[price.Price] 133 } 134 135 func (pg *productGraph) announce(which string) { 136 var tokens []string 137 if which == "min" { 138 if pg.observeMinPriceChangeDir.Value() { 139 tokens = append(tokens, ansi.Green(fmt.Sprintf("min=%0.2f", pg.observeMinPrice.Value().Float64()))) 140 } else { 141 tokens = append(tokens, ansi.Red(fmt.Sprintf("min=%0.2f", pg.observeMinPrice.Value().Float64()))) 142 } 143 } else { 144 tokens = append(tokens, fmt.Sprintf("min=%0.2f", pg.observeMinPrice.Value().Float64())) 145 } 146 if which == "max" { 147 if pg.observeMaxPriceChangeDir.Value() { 148 tokens = append(tokens, ansi.Green(fmt.Sprintf("max=%0.2f", pg.observeMaxPrice.Value().Float64()))) 149 } else { 150 tokens = append(tokens, ansi.Red(fmt.Sprintf("max=%0.2f", pg.observeMaxPrice.Value().Float64()))) 151 } 152 } else { 153 tokens = append(tokens, fmt.Sprintf("max=%0.2f", pg.observeMaxPrice.Value().Float64())) 154 } 155 if which == "latest" { 156 if pg.observeLatestPriceChangeDir.Value() { 157 tokens = append(tokens, ansi.Green(fmt.Sprintf("latest=%0.2f", pg.observeLatestPrice.Value().Float64()))) 158 } else { 159 tokens = append(tokens, ansi.Red(fmt.Sprintf("latest=%0.2f", pg.observeLatestPrice.Value().Float64()))) 160 } 161 } else { 162 tokens = append(tokens, fmt.Sprintf("latest=%0.2f", pg.observeLatestPrice.Value().Float64())) 163 } 164 fmt.Printf("%s | %s\n", 165 pg.productID, 166 strings.Join(tokens, " "), 167 ) 168 } 169 170 func createProductGraph(g *incr.Graph, productID string) *productGraph { 171 output := productGraph{ 172 productID: productID, 173 } 174 output.latest = incr.Var(g, coinbase.Ticker{}) 175 output.extractPrice = incr.Map(g, output.latest, func(m coinbase.Ticker) price.Price { 176 return price.Parse(m.Price) 177 }) 178 output.prices = slicei.Accumulate(g, output.extractPrice, func(values []price.Price, newValue price.Price) []price.Price { 179 return append(values, newValue) 180 }) 181 output.sortedPrices = slicei.AccumulateSorted(g, output.extractPrice, Asc) 182 output.latestPrice = slicei.Last(g, output.prices) 183 output.minPrice = slicei.First(g, output.sortedPrices) 184 output.maxPrice = slicei.Last(g, output.sortedPrices) 185 186 output.latestPriceChangeDir = changeDirection(g, output.latestPrice) 187 output.minPriceChangeDir = changeDirection(g, output.minPrice) 188 output.maxPriceChangeDir = changeDirection(g, output.maxPrice) 189 190 output.observeLatestPriceChangeDir = incr.MustObserve(g, output.latestPriceChangeDir) 191 output.observeLatestPrice = incr.MustObserve(g, incrutil.CutoffUnchanged(g, output.latestPrice)) 192 output.observeLatestPrice.OnUpdate(func(_ context.Context, p price.Price) { 193 output.announce("latest") 194 }) 195 output.observeMinPriceChangeDir = incr.MustObserve(g, output.minPriceChangeDir) 196 output.observeMinPrice = incr.MustObserve(g, incrutil.CutoffUnchanged(g, output.minPrice)) 197 output.observeMinPrice.OnUpdate(func(_ context.Context, p price.Price) { 198 output.announce("min") 199 }) 200 output.observeMaxPriceChangeDir = incr.MustObserve(g, output.maxPriceChangeDir) 201 output.observeMaxPrice = incr.MustObserve(g, incrutil.CutoffUnchanged(g, output.maxPrice)) 202 output.observeMaxPrice.OnUpdate(func(_ context.Context, p price.Price) { 203 output.announce("max") 204 }) 205 return &output 206 } 207 208 func changeDirection(g *incr.Graph, from incr.Incr[price.Price]) incr.Incr[bool] { 209 return incrutil.MapLast(g, from, func(v0, v1 price.Price) bool { 210 return v0 < v1 211 }) 212 } 213 214 type numbers interface { 215 ~int64 | ~uint64 | ~float64 | ~string 216 } 217 218 // Asc returns a sorted comparer for sortable values in ascending order. 219 func Asc[A numbers](testValue, newValue A) int { 220 if testValue == newValue { 221 return 0 222 } 223 if testValue < newValue { 224 return -1 225 } 226 return 1 227 }