Files
wickra/examples/go/internal/market/market.go
T
kingchencandGitHub 23d636fd97 Add the Go binding over the C ABI hub (#228)
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).
2026-06-09 17:33:37 +02:00

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)
}