146 lines
4.1 KiB
Go
146 lines
4.1 KiB
Go
// Throughput benchmark for the Wickra Go bindings.
|
|||
|
|
//
|
||
|
|
// Measures how many indicator updates per second the cgo binding sustains,
|
||
|
|
// both per-tick (streaming Update) and bulk (Batch), over a synthetic OHLCV
|
||
|
|
// series. It is the Go counterpart of the Node throughput.js and the Rust
|
||
|
|
// criterion benches: it benchmarks Wickra's own O(1) streaming engine across
|
||
|
|
// the Go<->C-ABI boundary (there is no comparable streaming TA library to
|
||
|
|
// compare against), so the headline number is raw per-binding throughput /
|
||
|
|
// FFI overhead, not a cross-library ratio.
|
||
|
|
//
|
||
|
|
// Three indicators are timed, chosen by FFI call-signature archetype rather
|
||
|
|
// than algorithm: SMA (1-in -> 1-out), ATR (multi-in -> 1-out), and MACD
|
||
|
|
// (1-in -> multi-out). Streaming is timed for all three; batch only for the
|
||
|
|
// single-output SMA and ATR (multi-output batch is not exposed uniformly).
|
||
|
|
//
|
||
|
|
// Provision the C ABI library first (see bindings/go/README.md), then run:
|
||
|
|
//
|
||
|
|
// cd bindings/go/benchmarks
|
||
|
|
// go run . # 200k bars (default)
|
||
|
|
// go run . -bars 1000000
|
||
|
|
package main
|
||
|
|
|
||
|
|
import (
|
||
|
|
"flag"
|
||
|
|
"fmt"
|
||
|
|
"math"
|
||
|
|
"sort"
|
||
|
|
"time"
|
||
|
|
|
||
|
|
wickra "github.com/wickra-lib/wickra/bindings/go"
|
||
|
|
)
|
||
|
|
|
||
|
|
func main() {
|
||
|
|
bars := flag.Int("bars", 200_000, "number of synthetic bars to feed")
|
||
|
|
flag.Parse()
|
||
|
|
n := *bars
|
||
|
|
if n < 1000 {
|
||
|
|
fmt.Println("-bars must be >= 1000")
|
||
|
|
return
|
||
|
|
}
|
||
|
|
|
||
|
|
// Deterministic synthetic OHLCV (no RNG, so runs are comparable).
|
||
|
|
open := make([]float64, n)
|
||
|
|
high := make([]float64, n)
|
||
|
|
low := make([]float64, n)
|
||
|
|
closeP := make([]float64, n)
|
||
|
|
volume := make([]float64, n)
|
||
|
|
timestamp := make([]int64, n)
|
||
|
|
for i := 0; i < n; i++ {
|
||
|
|
mid := 100 + math.Sin(float64(i)*0.001)*20 + float64(i)*1e-4
|
||
|
|
c := mid + math.Sin(float64(i)*0.05)*2
|
||
|
|
closeP[i] = c
|
||
|
|
open[i] = mid
|
||
|
|
high[i] = math.Max(c, mid) + 1.5
|
||
|
|
low[i] = math.Min(c, mid) - 1.5
|
||
|
|
volume[i] = 1000 + float64(i%97)*13
|
||
|
|
timestamp[i] = int64(i)
|
||
|
|
}
|
||
|
|
|
||
|
|
mups := func(d time.Duration) float64 {
|
||
|
|
return float64(n) / d.Seconds() / 1e6
|
||
|
|
}
|
||
|
|
|
||
|
|
// Median elapsed over a few repetitions, after one warmup pass.
|
||
|
|
timeFn := func(fn func()) time.Duration {
|
||
|
|
fn() // warmup
|
||
|
|
const reps = 3
|
||
|
|
samples := make([]time.Duration, reps)
|
||
|
|
for r := 0; r < reps; r++ {
|
||
|
|
t0 := time.Now()
|
||
|
|
fn()
|
||
|
|
samples[r] = time.Since(t0)
|
||
|
|
}
|
||
|
|
sort.Slice(samples, func(a, b int) bool { return samples[a] < samples[b] })
|
||
|
|
return samples[reps/2]
|
||
|
|
}
|
||
|
|
|
||
|
|
type indicator struct {
|
||
|
|
name string
|
||
|
|
stream func()
|
||
|
|
batch func() // nil -> streaming only
|
||
|
|
}
|
||
|
|
|
||
|
|
indicators := []indicator{
|
||
|
|
{
|
||
|
|
name: "SMA(20)",
|
||
|
|
stream: func() {
|
||
|
|
ind, _ := wickra.NewSma(20)
|
||
|
|
for i := 0; i < n; i++ {
|
||
|
|
ind.Update(closeP[i])
|
||
|
|
}
|
||
|
|
ind.Close()
|
||
|
|
},
|
||
|
|
batch: func() {
|
||
|
|
ind, _ := wickra.NewSma(20)
|
||
|
|
ind.Batch(closeP)
|
||
|
|
ind.Close()
|
||
|
|
},
|
||
|
|
},
|
||
|
|
{
|
||
|
|
name: "ATR(14)",
|
||
|
|
stream: func() {
|
||
|
|
ind, _ := wickra.NewAtr(14)
|
||
|
|
for i := 0; i < n; i++ {
|
||
|
|
ind.Update(open[i], high[i], low[i], closeP[i], volume[i], timestamp[i])
|
||
|
|
}
|
||
|
|
ind.Close()
|
||
|
|
},
|
||
|
|
batch: func() {
|
||
|
|
ind, _ := wickra.NewAtr(14)
|
||
|
|
ind.Batch(open, high, low, closeP, volume, timestamp)
|
||
|
|
ind.Close()
|
||
|
|
},
|
||
|
|
},
|
||
|
|
{
|
||
|
|
name: "MACD(12,26,9)",
|
||
|
|
stream: func() {
|
||
|
|
ind, _ := wickra.NewMacdIndicator(12, 26, 9)
|
||
|
|
for i := 0; i < n; i++ {
|
||
|
|
ind.Update(closeP[i])
|
||
|
|
}
|
||
|
|
ind.Close()
|
||
|
|
},
|
||
|
|
batch: nil, // multi-output: streaming only
|
||
|
|
},
|
||
|
|
}
|
||
|
|
|
||
|
|
fmt.Printf("Wickra Go throughput - %d bars (median of 3 runs)\n\n", n)
|
||
|
|
fmt.Printf("%-22s%20s%18s\n", "Indicator", "streaming (Mupd/s)", "batch (Mupd/s)")
|
||
|
|
fmt.Println("------------------------------------------------------------")
|
||
|
|
|
||
|
|
for _, ind := range indicators {
|
||
|
|
streamMups := fmt.Sprintf("%.1f", mups(timeFn(ind.stream)))
|
||
|
|
batchMups := "-"
|
||
|
|
if ind.batch != nil {
|
||
|
|
batchMups = fmt.Sprintf("%.1f", mups(timeFn(ind.batch)))
|
||
|
|
}
|
||
|
|
fmt.Printf("%-22s%20s%18s\n", ind.name, streamMups, batchMups)
|
||
|
|
}
|
||
|
|
|
||
|
|
fmt.Print("\nMupd/s = million indicator updates per second. Streaming is the per-tick\n",
|
||
|
|
"Update path crossing the Go<->C-ABI boundary once per value; batch is the\n",
|
||
|
|
"bulk slice path (one boundary crossing). Higher is better. Numbers are\n",
|
||
|
|
"machine-dependent - use them for relative comparison, not as a speed claim.\n")
|
||
|
|
}
|