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  }