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).
This commit is contained in:
@@ -69,6 +69,28 @@ The offline examples run on deterministic synthetic data (and under CI on all
|
||||
three OSes); `fetch_btcusdt` and `live_binance` reach the network and are built
|
||||
but not run in CI.
|
||||
|
||||
## Go — `examples/go/`
|
||||
|
||||
Build the C ABI library first (`cargo build -p wickra-c --release`) and stage it
|
||||
under `bindings/go/lib/` (see the [Go binding README](../bindings/go)), then run
|
||||
any example from the `examples/go` module.
|
||||
|
||||
| Example | What it does | Run |
|
||||
| --- | --- | --- |
|
||||
| `streaming` | Feed a synthetic price series through SMA / EMA / RSI / MACD tick by tick. | `go run ./streaming` |
|
||||
| `backtest` | Basket of indicators over an OHLCV series (CSV arg or synthetic). | `go run ./backtest <ohlcv.csv>` |
|
||||
| `multi_timeframe` | Resample a 1-minute series to 5m / 15m and print an indicator per timeframe. | `go run ./multi_timeframe` |
|
||||
| `parallel_assets` | SMA(20) batch over a panel, serial vs goroutine fan-out, with speedup. | `go run ./parallel_assets 200 5000` |
|
||||
| `strategy_rsi_mean_reversion` | RSI(14) mean-reversion with PnL / Sharpe / max-DD summary. | `go run ./strategy_rsi_mean_reversion` |
|
||||
| `strategy_macd_adx` | Trend-follower: MACD crossover entries gated by ADX(14) > 20. | `go run ./strategy_macd_adx` |
|
||||
| `strategy_bollinger_squeeze` | Bollinger-squeeze breakout with an ATR(14) trailing stop. | `go run ./strategy_bollinger_squeeze` |
|
||||
| `fetch_btcusdt` | Download real BTCUSDT klines from the Binance REST API into a CSV. | `go run ./fetch_btcusdt` |
|
||||
| `live_binance` | Stream live Binance klines through EMA(20) over a WebSocket. | `go run ./live_binance` |
|
||||
|
||||
The offline examples run on deterministic synthetic data (and under CI on all
|
||||
three OSes); `fetch_btcusdt` and `live_binance` reach the network and are built
|
||||
but not run in CI.
|
||||
|
||||
## Python — `examples/python/`
|
||||
|
||||
| Example | What it does | Run |
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
# Data fetched at runtime by fetch_btcusdt
|
||||
**/data/
|
||||
|
||||
# Compiled example binaries
|
||||
/streaming/streaming
|
||||
/backtest/backtest
|
||||
/multi_timeframe/multi_timeframe
|
||||
/parallel_assets/parallel_assets
|
||||
/strategy_rsi_mean_reversion/strategy_rsi_mean_reversion
|
||||
/strategy_macd_adx/strategy_macd_adx
|
||||
/strategy_bollinger_squeeze/strategy_bollinger_squeeze
|
||||
/fetch_btcusdt/fetch_btcusdt
|
||||
/live_binance/live_binance
|
||||
*.exe
|
||||
@@ -0,0 +1,38 @@
|
||||
# Wickra examples — Go
|
||||
|
||||
Runnable Go examples for the [Wickra Go binding](../../bindings/go). Each example
|
||||
is a small `main` program in its own directory; they share the deterministic
|
||||
synthetic data, CSV loader, and equity summary in
|
||||
[`internal/market`](internal/market).
|
||||
|
||||
The binding links against the prebuilt Wickra C ABI library, so build and stage
|
||||
it once before running anything:
|
||||
|
||||
```bash
|
||||
cargo build -p wickra-c --release
|
||||
cp target/release/libwickra.so bindings/go/lib/ # Linux
|
||||
cp target/release/libwickra.dylib bindings/go/lib/ # macOS
|
||||
cp target/release/wickra.dll bindings/go/lib/ # Windows (also put it on PATH)
|
||||
```
|
||||
|
||||
Then run any example from the `examples/go` module:
|
||||
|
||||
```bash
|
||||
cd examples/go
|
||||
go run ./streaming
|
||||
```
|
||||
|
||||
| Example | What it does | Run |
|
||||
| --- | --- | --- |
|
||||
| `streaming` | Feed a synthetic price series through SMA / EMA / RSI / MACD tick by tick. | `go run ./streaming` |
|
||||
| `backtest` | Compute a basket of indicators over an OHLCV series and print a summary. | `go run ./backtest <ohlcv.csv>` |
|
||||
| `multi_timeframe` | Resample a 1-minute series into 5m / 15m and print an indicator per timeframe. | `go run ./multi_timeframe` |
|
||||
| `parallel_assets` | SMA(20) batch over a panel of assets, serial vs goroutine fan-out, with speedup. | `go run ./parallel_assets 200 5000` |
|
||||
| `strategy_rsi_mean_reversion` | RSI(14) mean-reversion with a PnL / Sharpe / max-DD summary. | `go run ./strategy_rsi_mean_reversion` |
|
||||
| `strategy_macd_adx` | MACD crossover entries gated by ADX(14) > 20. | `go run ./strategy_macd_adx` |
|
||||
| `strategy_bollinger_squeeze` | Bollinger-squeeze breakout with an ATR(14) trailing stop. | `go run ./strategy_bollinger_squeeze` |
|
||||
| `fetch_btcusdt` | Download real BTCUSDT klines from the Binance REST API into a CSV. | `go run ./fetch_btcusdt` |
|
||||
| `live_binance` | Stream live Binance klines through EMA(20) over a WebSocket. | `go run ./live_binance` |
|
||||
|
||||
`fetch_btcusdt` and `live_binance` require network access; the rest run offline
|
||||
on deterministic synthetic data.
|
||||
@@ -0,0 +1,56 @@
|
||||
// Compute a basket of indicators over an OHLCV series and print a summary.
|
||||
// Pass a CSV path (timestamp,open,high,low,close,volume) or run on synthetic data.
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"math"
|
||||
"os"
|
||||
|
||||
wickra "github.com/wickra-lib/wickra/bindings/go"
|
||||
"github.com/wickra-lib/wickra/examples/go/internal/market"
|
||||
)
|
||||
|
||||
func main() {
|
||||
source := "synthetic"
|
||||
var bars []market.Bar
|
||||
if len(os.Args) > 1 {
|
||||
source = os.Args[1]
|
||||
loaded, err := market.LoadOhlcvCsv(os.Args[1])
|
||||
if err != nil {
|
||||
log.Fatalf("load csv: %v", err)
|
||||
}
|
||||
bars = loaded
|
||||
} else {
|
||||
bars = market.SyntheticCandles(1000)
|
||||
}
|
||||
|
||||
fmt.Printf("Backtest over %d bars (%s):\n", len(bars), source)
|
||||
|
||||
sma, _ := wickra.NewSma(20)
|
||||
defer sma.Close()
|
||||
ema, _ := wickra.NewEma(50)
|
||||
defer ema.Close()
|
||||
rsi, _ := wickra.NewRsi(14)
|
||||
defer rsi.Close()
|
||||
atr, _ := wickra.NewAtr(14)
|
||||
defer atr.Close()
|
||||
|
||||
var lastSma, lastEma, lastRsi, lastAtr float64
|
||||
oversold := 0
|
||||
for _, b := range bars {
|
||||
lastSma = sma.Update(b.Close)
|
||||
lastEma = ema.Update(b.Close)
|
||||
lastRsi = rsi.Update(b.Close)
|
||||
lastAtr = atr.Update(b.Open, b.High, b.Low, b.Close, b.Volume, b.Timestamp)
|
||||
if !math.IsNaN(lastRsi) && lastRsi < 30.0 {
|
||||
oversold++
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Printf(" SMA(20) last = %.4f\n", lastSma)
|
||||
fmt.Printf(" EMA(50) last = %.4f\n", lastEma)
|
||||
fmt.Printf(" RSI(14) last = %.4f (%d oversold bars)\n", lastRsi, oversold)
|
||||
fmt.Printf(" ATR(14) last = %.4f\n", lastAtr)
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
// Download real BTCUSDT hourly klines from the Binance REST API into a CSV that the
|
||||
// other examples can consume. Requires network access (build-only in CI).
|
||||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
func main() {
|
||||
const url = "https://api.binance.com/api/v3/klines?symbol=BTCUSDT&interval=1h&limit=500"
|
||||
fmt.Printf("Fetching %s\n", url)
|
||||
|
||||
resp, err := http.Get(url)
|
||||
if err != nil {
|
||||
log.Fatalf("request: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
log.Fatalf("read body: %v", err)
|
||||
}
|
||||
|
||||
// Binance kline array: [openTime, open, high, low, close, volume, ...].
|
||||
var klines [][]any
|
||||
if err := json.Unmarshal(body, &klines); err != nil {
|
||||
log.Fatalf("parse json: %v", err)
|
||||
}
|
||||
|
||||
dir := "data"
|
||||
if err := os.MkdirAll(dir, 0o755); err != nil {
|
||||
log.Fatalf("mkdir: %v", err)
|
||||
}
|
||||
path := filepath.Join(dir, "btcusdt_1h.csv")
|
||||
file, err := os.Create(path)
|
||||
if err != nil {
|
||||
log.Fatalf("create: %v", err)
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
writer := bufio.NewWriter(file)
|
||||
defer writer.Flush()
|
||||
fmt.Fprintln(writer, "timestamp,open,high,low,close,volume")
|
||||
count := 0
|
||||
for _, k := range klines {
|
||||
ts := int64(k[0].(float64))
|
||||
fmt.Fprintf(writer, "%d,%s,%s,%s,%s,%s\n", ts, k[1], k[2], k[3], k[4], k[5])
|
||||
count++
|
||||
}
|
||||
|
||||
fmt.Printf("Wrote %d klines to %s\n", count, path)
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
module github.com/wickra-lib/wickra/examples/go
|
||||
|
||||
go 1.23
|
||||
|
||||
require github.com/wickra-lib/wickra/bindings/go v0.0.0
|
||||
|
||||
require github.com/coder/websocket v1.8.14
|
||||
|
||||
replace github.com/wickra-lib/wickra/bindings/go => ../../bindings/go
|
||||
@@ -0,0 +1,2 @@
|
||||
github.com/coder/websocket v1.8.14 h1:9L0p0iKiNOibykf283eHkKUHHrpG7f65OE3BhhO7v9g=
|
||||
github.com/coder/websocket v1.8.14/go.mod h1:NX3SzP+inril6yawo5CQXx8+fk145lPDC6pumgx0mVg=
|
||||
@@ -0,0 +1,156 @@
|
||||
// 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)
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
// Stream live BTCUSDT 1-minute klines from Binance and feed each close through EMA(20).
|
||||
// Requires network access (build-only in CI). Runs for up to 60 seconds.
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/coder/websocket"
|
||||
wickra "github.com/wickra-lib/wickra/bindings/go"
|
||||
)
|
||||
|
||||
func main() {
|
||||
const url = "wss://stream.binance.com:9443/ws/btcusdt@kline_1m"
|
||||
fmt.Printf("Connecting to %s (up to 60s)...\n", url)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
|
||||
defer cancel()
|
||||
|
||||
conn, _, err := websocket.Dial(ctx, url, nil)
|
||||
if err != nil {
|
||||
log.Fatalf("dial: %v", err)
|
||||
}
|
||||
defer conn.CloseNow()
|
||||
|
||||
ema, _ := wickra.NewEma(20)
|
||||
defer ema.Close()
|
||||
|
||||
for {
|
||||
_, data, err := conn.Read(ctx)
|
||||
if err != nil {
|
||||
fmt.Println("Done (time limit reached).")
|
||||
return
|
||||
}
|
||||
|
||||
var msg struct {
|
||||
K struct {
|
||||
Close string `json:"c"`
|
||||
} `json:"k"`
|
||||
}
|
||||
if err := json.Unmarshal(data, &msg); err != nil || msg.K.Close == "" {
|
||||
continue
|
||||
}
|
||||
closePx, err := strconv.ParseFloat(msg.K.Close, 64)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
fmt.Printf("close=%.2f EMA(20)=%.2f\n", closePx, ema.Update(closePx))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
// Resample a 1-minute series into higher timeframes and run an indicator per timeframe.
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
|
||||
wickra "github.com/wickra-lib/wickra/bindings/go"
|
||||
"github.com/wickra-lib/wickra/examples/go/internal/market"
|
||||
)
|
||||
|
||||
func main() {
|
||||
oneMinute := market.SyntheticCandlesStep(1200, 0, 60_000)
|
||||
|
||||
fmt.Println("EMA(20) of close across timeframes (resampled from 1-minute bars):")
|
||||
for _, factor := range []int{1, 5, 15} {
|
||||
bars := resample(oneMinute, factor)
|
||||
ema, _ := wickra.NewEma(20)
|
||||
var last float64
|
||||
for _, b := range bars {
|
||||
last = ema.Update(b.Close)
|
||||
}
|
||||
ema.Close()
|
||||
fmt.Printf(" %2dm: %5d bars EMA(20) last = %.4f\n", factor, len(bars), last)
|
||||
}
|
||||
}
|
||||
|
||||
func resample(source []market.Bar, factor int) []market.Bar {
|
||||
if factor <= 1 {
|
||||
return source
|
||||
}
|
||||
var out []market.Bar
|
||||
for i := 0; i < len(source); i += factor {
|
||||
end := i + factor
|
||||
if end > len(source) {
|
||||
end = len(source)
|
||||
}
|
||||
high, low, volume := math.Inf(-1), math.Inf(1), 0.0
|
||||
for j := i; j < end; j++ {
|
||||
high = math.Max(high, source[j].High)
|
||||
low = math.Min(low, source[j].Low)
|
||||
volume += source[j].Volume
|
||||
}
|
||||
out = append(out, market.Bar{
|
||||
Open: source[i].Open,
|
||||
High: high,
|
||||
Low: low,
|
||||
Close: source[end-1].Close,
|
||||
Volume: volume,
|
||||
Timestamp: source[i].Timestamp,
|
||||
})
|
||||
}
|
||||
return out
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
// Run SMA(20) batch over a panel of assets, serial vs goroutine fan-out, and
|
||||
// report the speedup.
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
wickra "github.com/wickra-lib/wickra/bindings/go"
|
||||
"github.com/wickra-lib/wickra/examples/go/internal/market"
|
||||
)
|
||||
|
||||
func main() {
|
||||
assets := argInt(1, 500)
|
||||
bars := argInt(2, 20_000)
|
||||
|
||||
panel := make([][]float64, assets)
|
||||
for a := 0; a < assets; a++ {
|
||||
panel[a] = market.SyntheticPricesFrom(bars, 50.0+float64(a)*0.1)
|
||||
}
|
||||
|
||||
// Warm up so the comparison is fair.
|
||||
if warm, err := wickra.NewSma(20); err == nil {
|
||||
warm.Batch(panel[0])
|
||||
warm.Close()
|
||||
}
|
||||
|
||||
sink := 0.0
|
||||
start := time.Now()
|
||||
for a := 0; a < assets; a++ {
|
||||
sma, _ := wickra.NewSma(20)
|
||||
result := sma.Batch(panel[a])
|
||||
sma.Close()
|
||||
sink += result[len(result)-1]
|
||||
}
|
||||
serial := time.Since(start)
|
||||
|
||||
lasts := make([]float64, assets)
|
||||
start = time.Now()
|
||||
var wg sync.WaitGroup
|
||||
work := make(chan int, assets)
|
||||
for w := 0; w < runtime.GOMAXPROCS(0); w++ {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
for a := range work {
|
||||
sma, _ := wickra.NewSma(20)
|
||||
result := sma.Batch(panel[a])
|
||||
sma.Close()
|
||||
lasts[a] = result[len(result)-1]
|
||||
}
|
||||
}()
|
||||
}
|
||||
for a := 0; a < assets; a++ {
|
||||
work <- a
|
||||
}
|
||||
close(work)
|
||||
wg.Wait()
|
||||
parallel := time.Since(start)
|
||||
|
||||
serialMs := float64(serial.Microseconds()) / 1000.0
|
||||
parallelMs := float64(parallel.Microseconds()) / 1000.0
|
||||
fmt.Printf("%d assets x %d bars, SMA(20) batch:\n", assets, bars)
|
||||
fmt.Printf(" serial %8.1f ms\n", serialMs)
|
||||
fmt.Printf(" parallel %8.1f ms (%.1fx speedup)\n", parallelMs, serialMs/max(parallelMs, 1e-9))
|
||||
_ = sink
|
||||
}
|
||||
|
||||
func argInt(i, def int) int {
|
||||
if len(os.Args) > i {
|
||||
if v, err := strconv.Atoi(os.Args[i]); err == nil {
|
||||
return v
|
||||
}
|
||||
}
|
||||
return def
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
// Breakout: when Bollinger bandwidth is tight (a "squeeze") and price closes above
|
||||
// the upper band, go long with an ATR(14) trailing stop.
|
||||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
"math"
|
||||
"os"
|
||||
|
||||
wickra "github.com/wickra-lib/wickra/bindings/go"
|
||||
"github.com/wickra-lib/wickra/examples/go/internal/market"
|
||||
)
|
||||
|
||||
func main() {
|
||||
bars := loadBars()
|
||||
|
||||
bollinger, _ := wickra.NewBollingerBands(20, 2.0)
|
||||
defer bollinger.Close()
|
||||
atr, _ := wickra.NewAtr(14)
|
||||
defer atr.Close()
|
||||
|
||||
var returns []float64
|
||||
trades := 0
|
||||
inPosition := false
|
||||
entry := 0.0
|
||||
stop := 0.0
|
||||
|
||||
for _, b := range bars {
|
||||
band, okBand := bollinger.Update(b.Close)
|
||||
atrValue := atr.Update(b.Open, b.High, b.Low, b.Close, b.Volume, b.Timestamp)
|
||||
if !okBand || math.IsNaN(atrValue) {
|
||||
continue
|
||||
}
|
||||
|
||||
bandwidth := math.MaxFloat64
|
||||
if band.Middle != 0.0 {
|
||||
bandwidth = (band.Upper - band.Lower) / band.Middle
|
||||
}
|
||||
|
||||
if !inPosition && bandwidth < 0.06 && b.Close > band.Upper {
|
||||
inPosition = true
|
||||
entry = b.Close
|
||||
stop = b.Close - 2.0*atrValue
|
||||
trades++
|
||||
} else if inPosition {
|
||||
stop = math.Max(stop, b.Close-2.0*atrValue) // trail the stop up
|
||||
if b.Close < stop {
|
||||
returns = append(returns, (b.Close-entry)/entry)
|
||||
inPosition = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
market.Print("Bollinger squeeze", market.Summarize(returns, trades, 252.0))
|
||||
}
|
||||
|
||||
func loadBars() []market.Bar {
|
||||
if len(os.Args) > 1 {
|
||||
bars, err := market.LoadOhlcvCsv(os.Args[1])
|
||||
if err != nil {
|
||||
log.Fatalf("load csv: %v", err)
|
||||
}
|
||||
return bars
|
||||
}
|
||||
return market.SyntheticCandles(2000)
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
// Trend follower: enter long on a MACD histogram cross up, but only when ADX(14) > 20
|
||||
// confirms a trend; exit when the histogram crosses back below zero.
|
||||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
"math"
|
||||
"os"
|
||||
|
||||
wickra "github.com/wickra-lib/wickra/bindings/go"
|
||||
"github.com/wickra-lib/wickra/examples/go/internal/market"
|
||||
)
|
||||
|
||||
func main() {
|
||||
bars := loadBars()
|
||||
|
||||
macd, _ := wickra.NewMacdIndicator(12, 26, 9)
|
||||
defer macd.Close()
|
||||
adx, _ := wickra.NewAdx(14)
|
||||
defer adx.Close()
|
||||
|
||||
var returns []float64
|
||||
trades := 0
|
||||
inPosition := false
|
||||
entry := 0.0
|
||||
prevHistogram := math.NaN()
|
||||
|
||||
for _, b := range bars {
|
||||
m, okMacd := macd.Update(b.Close)
|
||||
a, okAdx := adx.Update(b.Open, b.High, b.Low, b.Close, b.Volume, b.Timestamp)
|
||||
if !okMacd || !okAdx {
|
||||
continue
|
||||
}
|
||||
|
||||
trending := a.Adx > 20.0
|
||||
if !inPosition && trending && !math.IsNaN(prevHistogram) && prevHistogram <= 0.0 && m.Histogram > 0.0 {
|
||||
inPosition = true
|
||||
entry = b.Close
|
||||
trades++
|
||||
} else if inPosition && m.Histogram < 0.0 {
|
||||
returns = append(returns, (b.Close-entry)/entry)
|
||||
inPosition = false
|
||||
}
|
||||
prevHistogram = m.Histogram
|
||||
}
|
||||
|
||||
market.Print("MACD + ADX trend", market.Summarize(returns, trades, 252.0))
|
||||
}
|
||||
|
||||
func loadBars() []market.Bar {
|
||||
if len(os.Args) > 1 {
|
||||
bars, err := market.LoadOhlcvCsv(os.Args[1])
|
||||
if err != nil {
|
||||
log.Fatalf("load csv: %v", err)
|
||||
}
|
||||
return bars
|
||||
}
|
||||
return market.SyntheticCandles(2000)
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
// Mean reversion: go long when RSI(14) drops below 30, exit when it recovers above 50.
|
||||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
"math"
|
||||
"os"
|
||||
|
||||
wickra "github.com/wickra-lib/wickra/bindings/go"
|
||||
"github.com/wickra-lib/wickra/examples/go/internal/market"
|
||||
)
|
||||
|
||||
func main() {
|
||||
bars := loadBars()
|
||||
|
||||
rsi, _ := wickra.NewRsi(14)
|
||||
defer rsi.Close()
|
||||
|
||||
var returns []float64
|
||||
trades := 0
|
||||
inPosition := false
|
||||
entry := 0.0
|
||||
|
||||
for _, b := range bars {
|
||||
value := rsi.Update(b.Close)
|
||||
if math.IsNaN(value) {
|
||||
continue
|
||||
}
|
||||
if !inPosition && value < 30.0 {
|
||||
inPosition = true
|
||||
entry = b.Close
|
||||
trades++
|
||||
} else if inPosition && value > 50.0 {
|
||||
returns = append(returns, (b.Close-entry)/entry)
|
||||
inPosition = false
|
||||
}
|
||||
}
|
||||
|
||||
market.Print("RSI mean-reversion", market.Summarize(returns, trades, 252.0))
|
||||
}
|
||||
|
||||
func loadBars() []market.Bar {
|
||||
if len(os.Args) > 1 {
|
||||
bars, err := market.LoadOhlcvCsv(os.Args[1])
|
||||
if err != nil {
|
||||
log.Fatalf("load csv: %v", err)
|
||||
}
|
||||
return bars
|
||||
}
|
||||
return market.SyntheticCandles(2000)
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
// Feed a synthetic price series through several indicators tick by tick (O(1) each).
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
wickra "github.com/wickra-lib/wickra/bindings/go"
|
||||
"github.com/wickra-lib/wickra/examples/go/internal/market"
|
||||
)
|
||||
|
||||
func main() {
|
||||
prices := market.SyntheticPrices(500)
|
||||
|
||||
sma, _ := wickra.NewSma(20)
|
||||
defer sma.Close()
|
||||
ema, _ := wickra.NewEma(20)
|
||||
defer ema.Close()
|
||||
rsi, _ := wickra.NewRsi(14)
|
||||
defer rsi.Close()
|
||||
macd, _ := wickra.NewMacdIndicator(12, 26, 9)
|
||||
defer macd.Close()
|
||||
|
||||
var lastSma, lastEma, lastRsi float64
|
||||
var lastMacd wickra.MacdOutput
|
||||
var haveMacd bool
|
||||
for _, price := range prices {
|
||||
lastSma = sma.Update(price)
|
||||
lastEma = ema.Update(price)
|
||||
lastRsi = rsi.Update(price)
|
||||
lastMacd, haveMacd = macd.Update(price)
|
||||
}
|
||||
|
||||
fmt.Printf("Streamed %d prices through SMA(20), EMA(20), RSI(14), MACD(12,26,9):\n", len(prices))
|
||||
fmt.Printf(" SMA = %.4f\n", lastSma)
|
||||
fmt.Printf(" EMA = %.4f\n", lastEma)
|
||||
fmt.Printf(" RSI = %.4f\n", lastRsi)
|
||||
if haveMacd {
|
||||
fmt.Printf(" MACD = %.4f signal=%.4f hist=%.4f\n", lastMacd.Macd, lastMacd.Signal, lastMacd.Histogram)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user