A Synthetic Stock Price Generator in Go: Part 1#
Intro#
In this post, we are going to create a grpc service that will let you users get live and synthetic price information for a list of symbols. A request with a list of symbol will create a new market that publishes live price data every second for every symbol in the list. We will generate our prices using a gaussian walk, meaning we will choose the percentage change for our next step from a normal distribution.
All the code in this article can be found on my GitHub
Now that we have a general idea let’s init our go module
go mod init <your module name here>
The StockPublisher Proto#
We will begin our gRPC service with one service: “StockPublisher”. This will accept a list of tickers and return a live stream. Let’s create our proto file
syntax = "proto3";
import "google/protobuf/timestamp.proto";
package stockpb;
option go_package = "protos/stockpb";
service StockPublisher {
rpc StartMarket (StartMarketRequest) returns (stream Stock);
}
message StartMarketRequest {
repeated string stocks = 1;
}
message Stock {
string id = 1;
// Use protobuf Timestamp for Go's time.Time
google.protobuf.Timestamp time_stamp = 2;
double last = 3;
int32 volume = 4;
int32 total_volume = 5;
// Note: lowercase fields in Go are unexported and not serialized.
// If you want to include volatility, make it public.
double volatility = 6;
}
Let’s look at how we modeled our stock.
- id is a unique string this will be the stock ticker
- time_stamp is when the message is generated by our market
- last is our price. We are not publishing bid/ask values yet. For the first iteration the last price is enough
- volume is how many shares were traded as part of this trade
- total_volume is the sum of shares traded for the day
- volatility represents how wildly our stock swings. This will play a crucial role in our pricing model
Generating the go code for the protobuf#
Now that we have our protobuf, we need to generate the go code from it that will let us use the go interfaces and structs. To be able to run the code generation we’ll need to setup protoc which is the proto compiler along with its go plugins. Refer to the https://grpc.io/docs/languages/go/quickstart/ for this setup
Once you are setup with all that let’s run our code gen
protoc --go_out=. --go-grpc_out=. ./protos/stock_service.proto
This will create two files protos/stockpb/stock_service.pb.go and protos/stockpb/stock_service_grpc.pb.go
The Stock Data Model and Generating New Prices with Brownian Motion#
Let’s see how the generated Stock struct looks like
type Stock struct {
state protoimpl.MessageState `protogen:"open.v1"`
Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"`
// Use protobuf Timestamp for Go's time.Time
TimeStamp *timestamppb.Timestamp `protobuf:"bytes,2,opt,name=time_stamp,json=timeStamp,proto3" json:"time_stamp,omitempty"`
Last float64 `protobuf:"fixed64,3,opt,name=last,proto3" json:"last,omitempty"`
Volume int32 `protobuf:"varint,4,opt,name=volume,proto3" json:"volume,omitempty"`
TotalVolume int32 `protobuf:"varint,5,opt,name=total_volume,json=totalVolume,proto3" json:"total_volume,omitempty"`
// Note: lowercase fields in Go are unexported and not serialized.
// If you want to include volatility, make it public.
Volatility float64 `protobuf:"fixed64,6,opt,name=volatility,proto3" json:"volatility,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
We could use this struct directly in our code, but to be able to attach more methods, and/or to add more properties, we will wrap it with our own Stock struct
type Stock struct {
Data *stockpb.Stock
gen Generator
}
Data holds the Stock reference which has the price, volume, symbol etc information. But what is that “gen”? Remember we need to generate our prices somehow? In our design each stock is responsible for their own next price generation. So, they have their own price generator.
Brownian Motion#
We will generate our prices quite naively for now. We will assume the price change in every step follows a normal distribution (aka Brownian motion). So at any step, we will randomly get a positive or negative change. And the change, will mostly be in our std dev range. I recommend looking at the famous normal distribution bell curve to understand how most of the values fall within std dev.
What should we use as our std dev? For now, I want to keep it the same for every symbol in the market. So, let’s look at the annual volatility of SP500 and call it 20%. From here, we can determine the volatility we need to use for our 1 second changes. For our brownian motion, we can calculate the second volatility as follows:
$$ \sigma_{1\text{s}} \;=\; \frac{\sigma_{\text{ann}}}{\sqrt{S}} $$With \(\sigma_{ann}=0.2 \text{ and } S=5,896,800 \text{ seconds per year, } \sigma{1s}=0.000082\)
Now that we know our volatility, what should our mean be for the price changes? Since we can either get + or - changes it needs to be at 0.
Another value we need to generate is volume. For now, I’ll give it a 50 shares/second volatility and a mean of 100 shares. We are not doing anything with the volume, but we’ll have more realistic looking data with it.
Let’s look at how we can define a Generator struct that implements random price change and volume generation.
package generation
import (
"math"
"math/rand/v2"
)
const (
priceVolatility = 0.000082
volumeVolatility = 50
volumeMean = 100
// min and max starting prices. we randomly generate a starting price also
minPrice = 100
maxPrice = 1000
)
type Generator struct {
random *rand.Rand
}
// MakeGenerator creates a Generator with a random number generator
func MakeGenerator() Generator {
return Generator{
random: rand.New(rand.NewPCG(1, 2)),
}
}
// GetInitialPrice gets an initial price => minPrice <= price <= maxPrice
func (g Generator) GetInitialPrice() float64 {
return minPrice + rand.Float64()*(maxPrice-minPrice)
}
// GetRandomPriceChange creates a random normal number and scales it with the preset volatility
func (g Generator) GetRandomPriceChange() float64 {
return g.random.NormFloat64() * priceVolatility
}
// GetRandomVolume generates a volume similar to a price.
// We can't have 0 volume so if the generated number is less than 0 we give it the lowest volume possible = 1
func (g Generator) GetRandomVolume() int {
vol := int(math.Floor(g.random.NormFloat64()*float64(volumeVolatility) + volumeMean))
// low chance that vol < 0, put a small volume if this is the case vol can't be negative
if vol <= 0 {
vol = 1
}
return vol
}
Market: An Infinite Loop Go Routine#
In a regular financial market/exchange, there are tradable securities and traders that buy and sell these. The buy and sell transactions decide the price of the asset. Of course, this ignores the market makers that provide liquidity and order matching engines, but who knows maybe we will implement those later and have a full fledged trading simulator. Well our market doesn’t even have any traders it just makes up prices. Thanks to the generator we just created this should look realistic.
Our Market is given a list of symbols. When we “Run” it, it creates a go routine that calculates the next price for each of the symbols given to it initially. It then publishes these updated stocks to a channel. This channel is returned to the caller at the end of the Run function to let the caller listen to it.
package main
import (
"context"
g "levelzero/generation"
"time"
)
type Market struct {
Stocks []*g.Stock
}
func MakeMarket(stockIds []string) *Market {
stocks := make([]*g.Stock, 0)
for _, id := range stockIds {
stock := g.MakeStock(id)
stocks = append(stocks, stock)
}
return &Market{stocks}
}
func (m *Market) Run(ctx context.Context) <-chan g.Stock {
ch := make(chan g.Stock)
go func() {
defer close(ch)
for {
select {
// look at the context to see if we should stop processing
case <-ctx.Done():
return
default:
timeStamp := time.Now()
for _, stock := range m.Stocks {
stock.Advance(timeStamp)
// pass the value of stock
// pass struct by value to the listener since they only care about the values
ch <- *stock
}
time.Sleep(1 * time.Second)
}
}
}()
return ch
}
Implementing StockPublisherServer#
It’s time to use the Market as part of our gRPC server. In our main method, we need to do the following
- Setup a tcp listener on a port
- Create a new grpc server
- Implement StockPublisherServer
- We defined this in our proto and protoc created a go interface for us
- Create an instance of StockPublisherServer and register it with the grpc server
- And finally bind the grpc server to the tcp listener
package main
import (
"fmt"
"levelzero/protos/stockpb"
"log"
"net"
"google.golang.org/grpc"
"google.golang.org/grpc/reflection"
)
type stockServer struct {
stockpb.UnimplementedStockPublisherServer
}
// StartMarket proto method
// Makes a market each request, runs it, writes each update to the grpc stream
func (s *stockServer) StartMarket(req *stockpb.StartMarketRequest, stream grpc.ServerStreamingServer[stockpb.Stock]) error {
market := MakeMarket(req.Stocks)
stockChannel := market.Run(stream.Context())
for update := range stockChannel {
if err := stream.Send(update.Data); err != nil {
return err
}
}
return nil
}
func main() {
port := ":3131"
lis, err := net.Listen("tcp", port)
if err != nil {
log.Fatalf("failed to listen: %v", err)
}
grpcServer := grpc.NewServer()
stockpb.RegisterStockPublisherServer(grpcServer, &stockServer{})
// this will let the clients inspect what services are available
reflection.Register(grpcServer)
fmt.Printf("Grpc service is running on %s\n", port)
if err := grpcServer.Serve(lis); err != nil {
log.Fatalf("failed to serve: %v", err)
}
}
Testing the StartMarket RPC with grpcurl#
Let’s see how we can call the StartMarket RPC with some symbols and get random market data every second. We won’t be implementing a go grpc client in this article. But that can be an exercise for you. Instead, we will use grpcurl. It’s curl, but for calling grpc endpoints.
grpcurl -plaintext -d '{"stocks":["AAPL", "MSFT", "NVDA", "META"]}' localhost:3131 stockpb.StockPublisher.StartMarket
Here we pass our request, specify the endpoint our server is running on, and call the fully qualified name of our RPC. Remember stockpb is the package name we defined in the stock_service.proto.
Next Steps#
I want to keep iterating on this project and document the journey along the way. Next up I’m planning to hook up to a price API to get the initial prices of the stocks as opposed to manually assigning them prices.
Finally the link to the source can be found here GitHub