Adds a Go binding (`bindings/go`) over the C ABI hub — the second language stecker after C#. ## What's here - **`bindings/go`** — a cgo binding exposing all 514 indicators as idiomatic Go types with `New<Indicator>` constructors and `Update`/`Batch`/`Reset`/`Close` methods. The wrappers in `indicators_gen.go` are generated from `bindings/c/include/wickra.h` (same archetype taxonomy as the C# generator: scalar/batch, multi-output, bars, profile, profile-values, array-input). Opaque handles are freed by `Close()` with a `runtime.SetFinalizer` backstop; pointer arguments are caller-owned, panics never cross the boundary. - **`examples/go`** — the full example suite mirroring C/C#: streaming, backtest, multi_timeframe, parallel_assets (goroutine fan-out), three strategies, and `fetch_btcusdt`/`live_binance`. - **CI** — a `go` job builds the C ABI library, stages it, and runs `gofmt`/`go vet`/`go test` plus the offline examples on Linux, macOS and Windows. - **Docs** — Go added to the README languages table, project layout, building/testing, CONTRIBUTING binding table + regenerate note, ARCHITECTURE, examples index, issue/PR templates, the About-description template, and the other binding READMEs. ## Linking / distribution The binding links the prebuilt C ABI library via cgo (`libwickra.so`/`.dylib`/`wickra.dll` staged under `bindings/go/lib`, gitignored). The native libraries are already shipped per target triple by the existing `c-abi-build` release job; distribution is via the subdirectory module tag `bindings/go/vX.Y.Z` (gated), so `release.yml` needs no new publish job. No Rust crate or `Cargo.toml` change — the Go module is standalone and additive. Not for merge yet (gated, per request).
157 lines
4.3 KiB
Go
157 lines
4.3 KiB
Go
// Package market provides deterministic synthetic market data, a small OHLCV
|
|
// CSV loader, and an equity-curve summary shared by the offline Go examples so
|
|
// they run without network access. It mirrors the helpers used by the Python,
|
|
// C, and C# example suites.
|
|
package market
|
|
|
|
import (
|
|
"bufio"
|
|
"fmt"
|
|
"math"
|
|
"os"
|
|
"strconv"
|
|
"strings"
|
|
)
|
|
|
|
// Bar is one OHLCV bar with a millisecond timestamp.
|
|
type Bar struct {
|
|
Open float64
|
|
High float64
|
|
Low float64
|
|
Close float64
|
|
Volume float64
|
|
Timestamp int64
|
|
}
|
|
|
|
// SyntheticPrices returns a reproducible price path (trend + two cycles), with
|
|
// no randomness, starting at 100.
|
|
func SyntheticPrices(count int) []float64 {
|
|
return SyntheticPricesFrom(count, 100.0)
|
|
}
|
|
|
|
// SyntheticPricesFrom is SyntheticPrices with an explicit starting level.
|
|
func SyntheticPricesFrom(count int, start float64) []float64 {
|
|
prices := make([]float64, count)
|
|
for i := range prices {
|
|
fi := float64(i)
|
|
prices[i] = start + 12.0*math.Sin(fi*0.05) + 5.0*math.Sin(fi*0.013) + fi*0.01
|
|
}
|
|
return prices
|
|
}
|
|
|
|
// SyntheticCandles returns a reproducible OHLCV series derived from
|
|
// SyntheticPrices, one bar per hour.
|
|
func SyntheticCandles(count int) []Bar {
|
|
return SyntheticCandlesStep(count, 0, 3_600_000)
|
|
}
|
|
|
|
// SyntheticCandlesStep is SyntheticCandles with an explicit start timestamp and
|
|
// per-bar step in milliseconds.
|
|
func SyntheticCandlesStep(count int, startTimestamp, stepMs int64) []Bar {
|
|
prices := SyntheticPrices(count + 1)
|
|
bars := make([]Bar, count)
|
|
for i := 0; i < count; i++ {
|
|
fi := float64(i)
|
|
op := prices[i]
|
|
cl := prices[i+1]
|
|
high := math.Max(op, cl) + 0.5 + math.Abs(math.Sin(fi*0.7))
|
|
low := math.Min(op, cl) - 0.5 - math.Abs(math.Cos(fi*0.7))
|
|
volume := 1000.0 + 500.0*(1.0+math.Sin(fi*0.1))
|
|
bars[i] = Bar{op, high, low, cl, volume, startTimestamp + int64(i)*stepMs}
|
|
}
|
|
return bars
|
|
}
|
|
|
|
// LoadOhlcvCsv loads an OHLCV CSV. It accepts rows of
|
|
// timestamp,open,high,low,close,volume or open,high,low,close,volume; a
|
|
// non-numeric first row is treated as a header and skipped.
|
|
func LoadOhlcvCsv(path string) ([]Bar, error) {
|
|
file, err := os.Open(path)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer file.Close()
|
|
|
|
var bars []Bar
|
|
scanner := bufio.NewScanner(file)
|
|
for scanner.Scan() {
|
|
line := strings.TrimSpace(scanner.Text())
|
|
if line == "" {
|
|
continue
|
|
}
|
|
cols := strings.Split(line, ",")
|
|
if _, err := strconv.ParseFloat(cols[0], 64); err != nil {
|
|
continue // header row
|
|
}
|
|
f := func(i int) float64 {
|
|
v, _ := strconv.ParseFloat(strings.TrimSpace(cols[i]), 64)
|
|
return v
|
|
}
|
|
if len(cols) >= 6 {
|
|
ts, _ := strconv.ParseInt(strings.TrimSpace(cols[0]), 10, 64)
|
|
bars = append(bars, Bar{f(1), f(2), f(3), f(4), f(5), ts})
|
|
} else {
|
|
bars = append(bars, Bar{f(0), f(1), f(2), f(3), f(4), int64(len(bars))})
|
|
}
|
|
}
|
|
return bars, scanner.Err()
|
|
}
|
|
|
|
// EquityResult holds summary statistics for a long-only equity curve.
|
|
type EquityResult struct {
|
|
TotalReturnPct float64
|
|
Sharpe float64
|
|
MaxDrawdownPct float64
|
|
Trades int
|
|
FinalEquity float64
|
|
}
|
|
|
|
// Summarize turns a stream of per-bar fractional returns (0.01 == +1%) into a
|
|
// PnL / Sharpe / max-drawdown summary, annualised by periodsPerYear.
|
|
func Summarize(periodReturns []float64, trades int, periodsPerYear float64) EquityResult {
|
|
equity, peak, maxDrawdown := 1.0, 1.0, 0.0
|
|
for _, r := range periodReturns {
|
|
equity *= 1.0 + r
|
|
peak = math.Max(peak, equity)
|
|
if peak > 0 {
|
|
maxDrawdown = math.Max(maxDrawdown, (peak-equity)/peak)
|
|
}
|
|
}
|
|
|
|
mean := 0.0
|
|
if len(periodReturns) > 0 {
|
|
var sum float64
|
|
for _, r := range periodReturns {
|
|
sum += r
|
|
}
|
|
mean = sum / float64(len(periodReturns))
|
|
}
|
|
variance := 0.0
|
|
if len(periodReturns) > 1 {
|
|
var ss float64
|
|
for _, r := range periodReturns {
|
|
ss += (r - mean) * (r - mean)
|
|
}
|
|
variance = ss / float64(len(periodReturns)-1)
|
|
}
|
|
stdDev := math.Sqrt(variance)
|
|
sharpe := 0.0
|
|
if stdDev > 1e-12 {
|
|
sharpe = mean / stdDev * math.Sqrt(periodsPerYear)
|
|
}
|
|
|
|
return EquityResult{
|
|
TotalReturnPct: (equity - 1.0) * 100.0,
|
|
Sharpe: sharpe,
|
|
MaxDrawdownPct: maxDrawdown * 100.0,
|
|
Trades: trades,
|
|
FinalEquity: equity,
|
|
}
|
|
}
|
|
|
|
// Print writes a one-line summary of an equity result.
|
|
func Print(name string, r EquityResult) {
|
|
fmt.Printf("%-26s return=%8.2f%% sharpe=%6.2f maxDD=%6.2f%% trades=%d\n",
|
|
name, r.TotalReturnPct, r.Sharpe, r.MaxDrawdownPct, r.Trades)
|
|
}
|