Compare commits

...

8 Commits

Author SHA1 Message Date
kingchenc d0061c73b8 release: bump 0.7.6 -> 0.7.7 (#229)
Bumps the workspace to 0.7.7 to ship the Go binding.
2026-06-09 17:35:25 +02:00
kingchenc 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
kingchenc fce26cf881 release: bump 0.7.5 -> 0.7.6 (#227)
Bumps the workspace to 0.7.6 to ship the C# (.NET) binding to NuGet.
2026-06-09 14:34:18 +02:00
kingchenc 91f6f67257 Add C# (.NET) binding over the C ABI hub (#226)
The first language stecker on the C ABI hub: a .NET binding exposing all 514
indicators as idiomatic `IDisposable` classes, generated from `wickra.h`.

## What's here

- **`bindings/csharp/`** — the `Wickra` .NET 8 package. `[LibraryImport]`
  source-generated P/Invoke (`NativeMethods.g.cs`) plus idiomatic wrappers
  (`Indicators.g.cs`), both generated from the committed `bindings/c/include/wickra.h`.
  The binding owns no indicator maths — it only marshals types across the C ABI.
- **Marshalling, verified end-to-end against the native library.** Opaque handles
  cross as `nint` kept alive per call via a `SafeHandle`; `bool` as
  `[MarshalAs(U1)]` (Rust `bool` is one byte); a self-correcting
  `DllImportResolver` validates the loaded library actually exports the Wickra
  ABI. Tests cover one representative per FFI archetype (scalar, candle, pairwise,
  multi-output, bars, profile, values-profile, order-book / array-input) plus
  exact Sma reference values.
- **NuGet packaging** — `dotnet pack` produces `Wickra.<version>.nupkg`; the
  release pipeline stages prebuilt native libraries under `runtimes/<rid>/native/`
  for six target triples (win/linux/osx × x64/arm64).
- **`examples/csharp/`** — nine examples mirroring `examples/c/`: streaming,
  backtest, multi_timeframe, parallel_assets, three strategies, and
  fetch_btcusdt + live_binance.
- **CI** — a `csharp` job on the three OSes builds the C ABI, tests the binding,
  and runs the offline examples. **Release** — a gated `csharp-publish` job packs
  and pushes to NuGet (gated on `NUGET_API_KEY`, independent of the GitHub-release
  job so a C# hiccup never blocks the C/C++ asset release).
- **Docs consistency wave** — README, CONTRIBUTING, CHANGELOG, examples/README,
  the issue / PR templates, `sync-about.yml`, and `.gitattributes`.

The native Python / Node / WASM bindings and the C ABI are untouched; this is
additive. Publishing to NuGet stays gated behind the release tag and the secret.
2026-06-09 14:32:05 +02:00
kingchenc 4caaa1db97 release: bump 0.7.4 -> 0.7.5 (#225)
Version bump for the C ABI hub release (0.7.4 -> 0.7.5). See #222 + #224.
2026-06-09 02:26:27 +02:00
kingchenc 12681e4b1b C ABI: full example suite + docs & About coverage (#224)
Stacked on #222 (base `feat/c-abi-hub`), so the diff is just the additions on top of the hub foundation — no merge of #222 required.

## What this adds

**Examples — full parity with rust/python/node (`examples/c/`)**
- `streaming.c` upgraded to the multi-indicator (SMA/EMA/RSI/MACD + signals) demo
- `backtest.c`, `multi_timeframe.c` (manual time-bucket resampling), `parallel_assets.c` (serial vs OpenMP fan-out, one handle per asset)
- three educational strategies: `strategy_rsi_mean_reversion.c`, `strategy_macd_adx.c`, `strategy_bollinger_squeeze.c`
- two network examples shelling out to `curl`: `fetch_btcusdt.c`, `live_binance.c` (REST poll)
- two header-only helpers (`wickra_csv.h`, `wickra_strategy.h`) since the C ABI ships no IO layer
- CMake builds all 11; the 9 offline ones run under `ctest` on 3 OS; the network two are built-only

**Docs & metadata — surface the C ABI everywhere it was missing**
- ARCHITECTURE diagram + crate table, SECURITY + THREAT_MODEL (the C ABI as the sole `unsafe` FFI surface), the three binding package READMEs, issue/PR templates, CHANGELOG, and the GitHub About template (live About + org description updated too)

**Cleanup**
- removed all references to the private generator tooling from public files (`bindings/c/src/lib.rs` header, `CONTRIBUTING.md`, `sync-about.yml`)

Verified locally: `cargo build -p wickra-c --release`, `cmake + ctest` (9/9 pass), and `-Wall -Wextra -Wpedantic` clean on gcc 13.
2026-06-09 02:14:28 +02:00
kingchenc 91e05e3c26 C ABI hub crate (bindings/c) foundation (#222)
## What

Introduces `wickra-c` — a `cdylib` + `staticlib` that exposes the Rust core over a **C ABI**. This is the hub every C-capable language (C, C++, Go, C#, Java, R) links against, instead of re-wiring each indicator natively. The native Python/Node/WASM bindings are untouched; this is purely additive, for ecosystems without first-class Rust tooling.

## Scope (foundation slice)

This PR deliberately validates the **whole pipeline end to end with one indicator (SMA)** before scaling to all 514, so the CI / cross-OS / header-drift mechanics are proven green first.

- Opaque `*mut T` handles; `wickra_<ind>_{new,update,batch,reset,free}`.
- NaN sentinel for warmup / NULL handles; caller-owned batch buffers; every function NULL-safe.
- cbindgen generates and commits `bindings/c/include/wickra.h` with opaque handle typedefs.
- A C smoke example (`examples/c/`) links the header + compiled library and runs (CMake + ctest).
- A `c-abi` CI job builds the library and runs the smoke test on **Linux, macOS and Windows**, plus a header drift check on Linux.

## Notes

- The per-indicator FFI blocks are plain `#[no_mangle]` functions, **not** a macro: cbindgen cannot see macro-generated functions on stable Rust (macro expansion needs nightly), so the blocks are written literally and will be generated mechanically by the ScriptHelpers `capi` wrapper in a follow-up (same model as the committed-but-generated Node `index.js`).
- `bindings/c` cannot inherit the workspace `forbid(unsafe_code)` lint (the C boundary needs raw pointers), so it mirrors every workspace lint and only relaxes `unsafe_code`. The Rust core stays `unsafe`-forbidden.

## Follow-ups (separate PRs)

- ScriptHelpers `capi` generator + wire the scalar family (~235).
- Hand-written blocks for multi-output / custom-input / bars (~279).
- Docs consistency wave (README / docs / webpage: Python·Node·WASM·Rust → +C).
- Release wiring (native-lib matrix + header/lib GH-release assets) — gated.
2026-06-09 02:07:03 +02:00
kingchenc 9d0983b666 ci(sync-about): sync indicator count into the webpage About page (#223)
The `wickra.org/about` page carries the indicator count in the bot-syncable `<N> indicators` token, but `sync-about.yml` only rewrote `index.md` + `.vitepress/config.ts` on the webpage. Add `about.md` to the webpage count step's `sed` file list and its `git add`, so the About page's count self-heals on every push-to-main / tag like the rest of the marketing site.

No-op for the Rust build — workflow file only.
2026-06-08 21:38:42 +02:00
112 changed files with 128581 additions and 122 deletions
+15
View File
@@ -1,3 +1,18 @@
# Shell scripts must keep LF line endings so they run on Linux/macOS CI and
# local shells regardless of the committer's platform autocrlf setting.
*.sh text eol=lf
# The cbindgen-generated C header is committed; pin it to LF so its CI drift
# check (regenerate + `git diff`) never trips on a CRLF normalization.
bindings/c/include/wickra.h text eol=lf
# C# sources (including the generated binding) are pinned to LF so the committed
# files stay stable regardless of the committer's autocrlf setting.
*.cs text eol=lf
# Go sources (including the generated binding) are pinned to LF so gofmt's CI
# check never trips on a CRLF checkout on Windows.
*.go text eol=lf
go.mod text eol=lf
go.sum text eol=lf
+2 -2
View File
@@ -30,9 +30,9 @@ assignees: ""
## Environment
- Wickra version:
- Language / binding: <!-- Rust crate / Python / Node / WASM -->
- Language / binding: <!-- Rust crate / Python / Node / WASM / C ABI / C# (.NET) / Go -->
- OS and architecture:
- Rust / Python / Node version (If relevant):
- Rust / Python / Node / .NET version (If relevant):
## Additional context
@@ -26,7 +26,7 @@ assignees: []
| Binding version | `e.g. python 0.4.2 / node 0.4.2` |
| OS / arch | `e.g. Windows 11 x86_64, Linux glibc` |
| Rust toolchain | `rustc --version` (If building from source) |
| Python / Node version | `python --version` / `node --version` |
| Python / Node / .NET version | `python --version` / `node --version` / `dotnet --version` |
## Minimal reproducer
@@ -25,6 +25,9 @@ assignees: ""
- [ ] Should be exposed in the Python binding
- [ ] Should be exposed in the Node binding
- [ ] Should be exposed in the WASM binding
- [ ] Should be exposed in the C ABI
- [ ] Should be exposed in the C# / .NET binding
- [ ] Should be exposed in the Go binding
## Additional context
@@ -13,7 +13,7 @@ assignees: []
## Affected code path
- Indicator / API: `e.g. EMA.update`
- Binding: `Rust / Python / Node / Wasm`
- Binding: `Rust / Python / Node / Wasm / C ABI / C# (.NET) / Go`
- Hot loop or one-shot call?
## Versions compared
+1 -1
View File
@@ -33,4 +33,4 @@ import wickra as ta
## Environment (Only if relevant)
- Wickra version: `e.g. 0.4.2`
- Binding: `Rust / Python / Node / Wasm`
- Binding: `Rust / Python / Node / Wasm / C ABI / C# (.NET) / Go`
+1 -1
View File
@@ -22,7 +22,7 @@
- [ ] `cargo clippy --workspace --all-targets -- -D warnings` is clean.
- [ ] `cargo test --workspace` passes.
- [ ] New behaviour has tests; bug fixes have a regression test.
- [ ] Public API changes are mirrored in the Python / Node / WASM bindings
- [ ] Public API changes are mirrored in the Python / Node / WASM bindings, and the C ABI + C# + Go bindings are regenerated
and their type stubs (If applicable).
- [ ] The relevant page on the [documentation site](https://docs.wickra.org)
and the `README.md` are updated (If applicable). Docs edits go to a
@@ -23,6 +23,9 @@ Please fill in the sections below. Delete any that don't apply.
- [ ] Python binding (`bindings/python`)
- [ ] Node.js binding (`bindings/node`)
- [ ] WebAssembly binding (`bindings/wasm`)
- [ ] C ABI (`bindings/c`)
- [ ] C# / .NET binding (`bindings/csharp`)
- [ ] Go binding (`bindings/go`)
- [ ] Examples / docs
## Linked issues
+204
View File
@@ -622,6 +622,210 @@ jobs:
working-directory: bindings/node
run: node --test __tests__/
c-abi:
name: C ABI on ${{ matrix.os }}
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable branch, 2026-03-27
- name: Cache cargo
uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2
continue-on-error: true # cache is an optimisation; never block on a stuck/slow restore
timeout-minutes: 6
- name: Install cbindgen
uses: taiki-e/install-action@0fd46367812ee04360509b4169d9f659d6892bb2 # v2.79.15
timeout-minutes: 10 # fail fast on a stuck download instead of hanging the job
with:
tool: cbindgen
- name: Build the C ABI library (cdylib + staticlib)
run: cargo build -p wickra-c --release
- name: Rust unit tests
run: cargo test -p wickra-c
# The generated header is platform-independent, so checking drift on one OS
# is enough — and avoids a spurious CRLF/LF diff on the Windows runner.
- name: Check the committed header is in sync with cbindgen
if: runner.os == 'Linux'
shell: bash
run: |
cbindgen --config bindings/c/cbindgen.toml --crate wickra-c --output bindings/c/include/wickra.h
if ! git diff --quiet -- bindings/c/include/wickra.h; then
echo "::error::bindings/c/include/wickra.h is out of sync — run cbindgen and commit the result"
git --no-pager diff -- bindings/c/include/wickra.h
exit 1
fi
# The real cross-language test: a foreign C consumer links the generated
# header + the compiled library and runs. If this passes on all three OSes,
# every C-capable language can link the same way.
- name: Build and run the C smoke example (CMake + ctest)
shell: bash
run: |
cmake -S examples/c -B examples/c/build
cmake --build examples/c/build --config Release
ctest --test-dir examples/c/build -C Release --output-on-failure
csharp:
name: C# on ${{ matrix.os }}
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable branch, 2026-03-27
- name: Cache cargo
uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2
continue-on-error: true # cache is an optimisation; never block on a stuck/slow restore
timeout-minutes: 6
# The binding links against the C ABI hub at runtime; build it first so the
# DllImportResolver finds target/release/wickra.{dll,so,dylib}. .NET 8 SDK is
# preinstalled on the GitHub runners, so no setup-dotnet step is needed.
- name: Build the C ABI library
run: cargo build -p wickra-c --release
- name: .NET info
run: dotnet --info
- name: Test the C# binding
run: dotnet test bindings/csharp/Wickra.Tests/Wickra.Tests.csproj -c Release
- name: Build the C# examples
shell: bash
run: |
for d in streaming backtest multi_timeframe parallel_assets \
strategy_rsi_mean_reversion strategy_macd_adx strategy_bollinger_squeeze \
fetch_btcusdt live_binance; do
dotnet build "examples/csharp/$d" -c Release
done
# Run only the offline examples (fetch_btcusdt / live_binance need network).
- name: Run the offline C# examples
shell: bash
run: |
for d in streaming backtest multi_timeframe parallel_assets \
strategy_rsi_mean_reversion strategy_macd_adx strategy_bollinger_squeeze; do
dotnet run --project "examples/csharp/$d" -c Release --no-build
done
go:
name: Go on ${{ matrix.os }}
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
# Go is not reliably on PATH on every runner image, so install it
# explicitly. cache: false — the cargo build dominates and the modules
# have no external Go deps worth caching.
- name: Set up Go
id: setup-go
continue-on-error: true
uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5
with:
go-version: "stable"
cache: false
- name: Retry Go setup (CDN flake)
if: steps.setup-go.outcome == 'failure'
shell: bash
run: |
echo "::warning::setup-go failed (likely CDN flake), waiting 30s before retry..."
sleep 30
- name: Set up Go (retry)
if: steps.setup-go.outcome == 'failure'
uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5
with:
go-version: "stable"
cache: false
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable branch, 2026-03-27
- name: Cache cargo
uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2
continue-on-error: true # cache is an optimisation; never block on a stuck/slow restore
timeout-minutes: 6
# The binding links against the C ABI hub via cgo; build it first and stage
# the platform library under bindings/go/lib so cgo's LDFLAGS find it. Go is
# preinstalled on the GitHub runners, so no setup-go step is needed.
- name: Build the C ABI library
run: cargo build -p wickra-c --release
- name: Stage the native library
shell: bash
run: |
mkdir -p bindings/go/lib
case "$RUNNER_OS" in
Linux) cp target/release/libwickra.so bindings/go/lib/ ;;
macOS) cp target/release/libwickra.dylib bindings/go/lib/ ;;
Windows) cp target/release/wickra.dll bindings/go/lib/ ;;
esac
- name: Go info
run: go version
- name: Check gofmt
shell: bash
run: |
unformatted="$(gofmt -l bindings/go examples/go)"
if [ -n "$unformatted" ]; then
echo "gofmt needed on:"; echo "$unformatted"; exit 1
fi
- name: Vet and test the Go binding
shell: bash
# On Windows there is no rpath; the loader resolves wickra.dll via PATH.
run: |
export PATH="$PWD/bindings/go/lib:$PATH"
cd bindings/go
go vet ./...
go test ./...
- name: Build the Go examples
shell: bash
run: |
export PATH="$PWD/bindings/go/lib:$PATH"
cd examples/go
go build ./...
# Run only the offline examples (fetch_btcusdt / live_binance need network).
- name: Run the offline Go examples
shell: bash
run: |
export PATH="$PWD/bindings/go/lib:$PATH"
cd examples/go
for d in streaming backtest multi_timeframe parallel_assets \
strategy_rsi_mean_reversion strategy_macd_adx strategy_bollinger_squeeze; do
go run "./$d"
done
# The cross-library benchmark has moved to a dedicated scheduled workflow
# (.github/workflows/bench.yml) — see audit finding R10. It runs nightly
# at 03:00 UTC and on-demand via `workflow_dispatch`, and is no longer on
+142 -2
View File
@@ -569,9 +569,146 @@ jobs:
# the old "publish, then upload provenance" order would have the provenance
# upload rejected once immutability is enabled.
# --------------------------------------------------------------------------
# --------------------------------------------------------------------------
# C ABI native libraries (bindings/c) — built per target on a native runner
# (no cross toolchain needed) and attached to the GitHub Release as the
# distribution channel. There is no package registry for the C ABI.
# --------------------------------------------------------------------------
c-abi-build:
name: C ABI library (${{ matrix.target }})
strategy:
fail-fast: false
matrix:
include:
- { host: ubuntu-latest, target: x86_64-unknown-linux-gnu }
- { host: ubuntu-24.04-arm, target: aarch64-unknown-linux-gnu }
- { host: macos-latest, target: x86_64-apple-darwin }
- { host: macos-latest, target: aarch64-apple-darwin }
- { host: windows-latest, target: x86_64-pc-windows-msvc }
- { host: windows-11-arm, target: aarch64-pc-windows-msvc }
runs-on: ${{ matrix.host }}
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable branch, 2026-03-27
with:
targets: ${{ matrix.target }}
- uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2
continue-on-error: true # cache is an optimisation; never block on a stuck/slow restore
timeout-minutes: 6
- name: Build the C ABI library (cdylib + staticlib)
run: cargo build -p wickra-c --release --target ${{ matrix.target }}
- name: Package header + libraries
shell: bash
run: |
set -e
dir="wickra-c-${{ matrix.target }}"
mkdir -p "$dir/include" "$dir/lib"
cp bindings/c/include/wickra.h bindings/c/include/wickra.hpp "$dir/include/"
for f in libwickra.so libwickra.a libwickra.dylib wickra.dll wickra.dll.lib wickra.lib; do
src="target/${{ matrix.target }}/release/$f"
[ -f "$src" ] && cp "$src" "$dir/lib/"
done
tar -czf "$dir.tar.gz" "$dir"
echo "packaged $dir:"; ls -lR "$dir"
- name: Upload artifact
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: c-abi-${{ matrix.target }}
path: wickra-c-${{ matrix.target }}.tar.gz
if-no-files-found: error
# Pack and publish the .NET binding to NuGet. Independent of the GitHub-release
# job so a C# hiccup never blocks the C/C++ asset release. Authentication uses
# NuGet Trusted Publishing (OIDC) — no long-lived API key. The 'wickra-release'
# trusted-publishing policy on nuget.org (owner KingchenC, repo wickra-lib/wickra,
# workflow release.yml) exchanges the GitHub OIDC token for a short-lived key.
csharp-publish:
name: Publish to NuGet
needs: c-abi-build
runs-on: ubuntu-latest
environment: release
permissions:
contents: read
id-token: write # request the GitHub OIDC token for trusted publishing
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- name: Download the C ABI native libraries
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
pattern: c-abi-*
path: c-abi-artifacts
- name: Stage native libraries into runtimes/<rid>/native
shell: bash
run: |
set -e
declare -A RID=(
[x86_64-unknown-linux-gnu]=linux-x64
[aarch64-unknown-linux-gnu]=linux-arm64
[x86_64-apple-darwin]=osx-x64
[aarch64-apple-darwin]=osx-arm64
[x86_64-pc-windows-msvc]=win-x64
[aarch64-pc-windows-msvc]=win-arm64
)
base=bindings/csharp/Wickra/runtimes
for target in "${!RID[@]}"; do
archive=$(find c-abi-artifacts -name "wickra-c-$target.tar.gz" | head -1)
if [ -z "$archive" ]; then
echo "::error::missing native artifact for $target"; exit 1
fi
tmp=$(mktemp -d); tar -xzf "$archive" -C "$tmp"
dest="$base/${RID[$target]}/native"; mkdir -p "$dest"
for f in libwickra.so libwickra.dylib wickra.dll; do
src=$(find "$tmp" -name "$f" | head -1)
[ -n "$src" ] && cp "$src" "$dest/"
done
echo "staged ${RID[$target]}:"; ls -l "$dest"
done
- name: Pack
shell: bash
run: |
version="${GITHUB_REF_NAME#v}"
dotnet pack bindings/csharp/Wickra/Wickra.csproj -c Release -p:Version="$version" -o nupkg
# Exchange the GitHub OIDC token for a short-lived (~1h) NuGet API key.
# 'user' is the nuget.org profile name (the package owner), not an email.
- name: NuGet login (OIDC -> temporary API key)
uses: NuGet/login@8d196754b4036150537f80ac539e15c2f1028841 # v1.2.0
id: nuget_login
with:
user: KingchenC
# Pass the temporary key through the environment (not string-interpolated
# into the script) so it cannot be parsed as shell — avoids template injection.
- name: Push to NuGet
shell: bash
env:
NUGET_API_KEY: ${{ steps.nuget_login.outputs.NUGET_API_KEY }}
run: |
dotnet nuget push "nupkg/*.nupkg" --api-key "$NUGET_API_KEY" \
--source https://api.nuget.org/v3/index.json --skip-duplicate
- name: Upload the NuGet package as a build artifact
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: nuget-package
path: nupkg/*.nupkg
if-no-files-found: error
github-release:
name: Attach assets to the draft GitHub Release
needs: [cargo-publish, python-publish, node-publish, wasm-publish]
needs: [cargo-publish, python-publish, node-publish, wasm-publish, c-abi-build]
runs-on: ubuntu-latest
permissions:
contents: write
@@ -644,7 +781,7 @@ jobs:
# the provenance bundle is attached (P24, immutability-ready).
draft: true
body: |
Wickra ${{ github.ref_name }} — streaming-first technical indicators across 4 language registries.
Wickra ${{ github.ref_name }} — streaming-first technical indicators across 4 language registries plus a C ABI.
### Install
@@ -665,6 +802,9 @@ jobs:
darwin-arm64, win32-x64-msvc)
- `wickra-*.tgz` — npm-pack tarballs (main package + per-platform subpackages + WASM)
- `*.crate` — cargo source crates (wickra-core, wickra-data, wickra)
- `wickra-c-<target>.tar.gz` — C ABI: `include/wickra.h` + `wickra.hpp`
and the cdylib/staticlib per target (linux/macos/windows × x64/arm64),
the hub for C / C++ / Go / C# / Java / R
### Auto-generated changelog
+8 -8
View File
@@ -9,7 +9,7 @@ name: Sync indicator count
# 2. GitHub repo "About" description — synced on push to main / v* tag
# 3. Docs site: index.md / overview.md / Indicators-Overview.md
# (wickra-lib/wickra-docs) — synced on push to main / v* tag*
# 4. Marketing site count (wickra-lib/webpage: index.md /
# 4. Marketing site count (wickra-lib/webpage: index.md / about.md /
# .vitepress/config.ts) — push to main / v* tag*
# 5. org profile README count (wickra-lib/.github, profile/README.md)
# — synced on push to main / v* tag*
@@ -42,10 +42,10 @@ name: Sync indicator count
# single source of truth for what the bindings reach.
#
# Design: on PRs this workflow is a READ-ONLY check. The indicator wiring
# (ScriptHelpers/_common.py wire_readme_counter) bumps both README.md and
# docs/README.md inside the author's code commit, so the counter is already
# correct by the time CI runs. If it is not, the check below fails loud and
# asks the author to re-run the wiring — it never pushes a fix-up commit.
# bumps both README.md and docs/README.md inside the author's code commit, so
# the counter is already correct by the time CI runs. If it is not, the check
# below fails loud and asks the author to re-run the wiring — it never pushes a
# fix-up commit.
#
# (An earlier version pushed a GITHUB_TOKEN "sync indicator count" commit to
# the PR head. Because GITHUB_TOKEN pushes trigger no workflows, that commit
@@ -149,7 +149,7 @@ jobs:
# actually live (Cloudflare Pages, P8.1); merging this PR is therefore
# gated on the domain resolving, otherwise the About link would 404.
homepage="https://docs.wickra.org"
desc="Streaming-first technical indicators with a Rust core and Python, Node.js, and WebAssembly bindings. ${n} indicators, O(1) per-tick updates, no system dependencies. Drop-in TA-Lib replacement."
desc="Streaming-first technical indicators with a Rust core and Python, Node.js, WebAssembly, C ABI, .NET, and Go bindings. ${n} indicators, O(1) per-tick updates, no system dependencies. Drop-in TA-Lib replacement."
# Enforce the homepage unconditionally — it is a constant, so this both
# corrects the stale kingchenc URL and self-heals any future drift.
# Same Administration-write permission as --description (no extra scope).
@@ -366,14 +366,14 @@ jobs:
exit 0
fi
cd webpage-count
sed -i -E "s/[0-9]+ (streaming-first )?indicators/${n} \1indicators/g" index.md .vitepress/config.ts
sed -i -E "s/[0-9]+ (streaming-first )?indicators/${n} \1indicators/g" index.md about.md .vitepress/config.ts
if git diff --quiet; then
echo "Webpage indicator count unchanged."
exit 0
fi
git config user.name "wickra-bot"
git config user.email "wickra-bot@users.noreply.github.com"
git add index.md .vitepress/config.ts
git add index.md about.md .vitepress/config.ts
git commit -m "chore: sync indicator count to ${n}"
if ! git push 2>/dev/null; then
echo "::warning::push to wickra-lib/webpage failed — ABOUT_SYNC_TOKEN likely lacks write (findings P10.0a)."
+21 -8
View File
@@ -27,16 +27,28 @@ or replace lives behind a separate crate boundary.
│ no I/O, no deps │ │ optional features │
└──────────────────────┘ └────────────────────┘
(every binding wraps the same core)
┌────────────┴───────────┬─────────────────────┐
│ │ │
┌──▼──────┐ ┌───────▼──────┐ ┌───────▼────────┐
Python │ │ Node │ │ WASM
│ (PyO3) │ │ (napi-rs) │ │ (wasm-bindgen) │
└─────────┘ └──────────────┘ └────────────────┘
every binding wraps the same core
┌────────────┼────────────┬────────────────┐
│ │ │ │
┌──▼───────┐ ┌──▼───────┐ ┌──▼───────────┐ ┌──▼──────────────────┐
│ Python │ │ Node │ │ WASM │ │ C ABI (cbindgen) │
(PyO3) │ │ (napi-rs)│ │(wasm-bindgen)│ │ cdylib + header
└──────────┘ └──────────┘ └──────────────┘ └─────────┬───────────┘
│ linked by
┌──────────▼──────────┐
│ C · C++ · C# · Go │
│ · Java · R │
└─────────────────────┘
```
Python, Node and WASM are *native* Rust bindings (PyO3 / napi-rs /
wasm-bindgen). The C ABI is the *hub* every other C-capable language links
against: it builds to a `cdylib`/`staticlib` plus a generated `wickra.h`, and
downstream languages link that one artifact rather than each re-wrapping the
core. C and C++ link it directly; the **C# / .NET** binding (`bindings/csharp`,
on NuGet) and the **Go** binding (`bindings/go`, cgo) are generated from
`wickra.h`, with Java / R planned the same way.
| Crate | Path | What it owns | Public deps |
|---|---|---|---|
| `wickra-core` | `crates/wickra-core` | every indicator, the `Indicator` trait, `BatchExt`, `Candle`/`Tick` types, `Error` | `thiserror`, `rayon` (parallel batch) |
@@ -45,6 +57,7 @@ or replace lives behind a separate crate boundary.
| `wickra-python` | `bindings/python` | `_wickra` PyO3 module + Python package | `pyo3`, `numpy`, depends on `wickra-core` |
| `wickra-node` | `bindings/node` | NAPI-RS native binding | `napi`, depends on `wickra-core` |
| `wickra-wasm` | `bindings/wasm` | WebAssembly binding | `wasm-bindgen`, depends on `wickra-core` |
| `wickra-c` | `bindings/c` | C ABI hub — `cdylib`/`staticlib` + generated `wickra.h` (cbindgen) | depends on `wickra-core` |
| `wickra-examples` | `examples/rust` | runnable binary examples | depends on `wickra`, `wickra-data` |
The `fuzz/` directory is **excluded** from the workspace (it has its own
+31 -1
View File
@@ -7,6 +7,33 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
## [0.7.7] - 2026-06-09
### Added
- **Go binding (`bindings/go`)** — a cgo binding over the C ABI hub exposing all
514 indicators as idiomatic types with `New<Indicator>` constructors and
`Update`/`Batch`/`Reset`/`Close` methods, generated from `wickra.h`. Handles are
freed by `Close()` with a `runtime.SetFinalizer` backstop. Ships a full example
suite mirroring the C and C# examples; distributed as a subdirectory module
(`go get github.com/wickra-lib/wickra/bindings/go`).
## [0.7.6] - 2026-06-09
### Added
- **C# / .NET binding (`bindings/csharp`)** — the first language stecker on the
C ABI hub. Exposes all 514 indicators as idiomatic `IDisposable` classes via
`[LibraryImport]` source-generated P/Invoke, generated from `wickra.h`. Ships
on NuGet as `Wickra` with prebuilt native libraries for six target triples
(win/linux/osx × x64/arm64), plus a full example suite mirroring the C examples.
## [0.7.5] - 2026-06-09
### Added
- **C ABI (`bindings/c`)** — a `cdylib` + `staticlib` plus a generated
`include/wickra.h` exposing all 514 indicators and 10 bar builders over an
opaque-handle C ABI: the hub any C-capable language (C, C++, Go, C#, Java, R)
links against, complementing the native Python/Node/WASM bindings. Ships a
full example suite (streaming, backtest, multi-timeframe, OpenMP parallel
fan-out, three educational strategies, and Binance fetch/live over `curl`)
mirroring the other bindings, plus an optional `wickra.hpp` C++ RAII wrapper.
## [0.7.4] - 2026-06-08
- **Three-Line Break** — Three-line-break bars (reversal needs N-line break) (`THREE_LINE_BREAK_BARS`).
- **Run** — Run bars (consecutive same-direction tick runs) (`RUN_BARS`).
@@ -1416,7 +1443,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
optional Binance live feed.
- Bindings for Python, Node.js, and WebAssembly.
[Unreleased]: https://github.com/wickra-lib/wickra/compare/v0.7.4...HEAD
[Unreleased]: https://github.com/wickra-lib/wickra/compare/v0.7.7...HEAD
[0.7.7]: https://github.com/wickra-lib/wickra/compare/v0.7.6...v0.7.7
[0.7.6]: https://github.com/wickra-lib/wickra/compare/v0.7.5...v0.7.6
[0.7.5]: https://github.com/wickra-lib/wickra/compare/v0.7.4...v0.7.5
[0.7.4]: https://github.com/wickra-lib/wickra/compare/v0.7.3...v0.7.4
[0.7.3]: https://github.com/wickra-lib/wickra/compare/v0.7.2...v0.7.3
[0.7.2]: https://github.com/wickra-lib/wickra/compare/v0.7.1...v0.7.2
+9 -1
View File
@@ -21,6 +21,9 @@ licensed as above, without any additional terms or conditions.
| `bindings/python` | PyO3 bindings (`wickra` on PyPI). |
| `bindings/node` | napi-rs bindings (`wickra` on npm). |
| `bindings/wasm` | wasm-bindgen bindings (`wickra-wasm` on npm). |
| `bindings/c` | C ABI — `cdylib` + `staticlib` + generated `include/wickra.h`. The hub for C / C++ and any C-capable language. |
| `bindings/csharp` | .NET binding over the C ABI (`Wickra` on NuGet) — `[LibraryImport]` P/Invoke generated from `wickra.h`. |
| `bindings/go` | Go binding over the C ABI via cgo (module tag `bindings/go/vX.Y.Z`) — wrappers generated from `wickra.h`. |
| `examples/` | Runnable examples. |
| `docs/` | Pointer to the documentation site (docs.wickra.org); the docs live in the `wickra-lib/wickra-docs` repo. |
@@ -102,7 +105,12 @@ installed. Dependabot also keeps the `.github/requirements` pins current.
- **Streaming parity.** An indicator's `batch` output must equal the sequence
of `update` calls.
- **Bindings.** A change to a public indicator API must be mirrored across the
Python, Node, and WASM bindings, including their type stubs / `.d.ts`.
Python, Node, and WASM bindings, including their type stubs / `.d.ts`. The C ABI
(`bindings/c`) is generated from the core, so regenerate it from the core and
commit `src/lib.rs` + `include/wickra.h`. The C# binding (`bindings/csharp`) is
generated from `wickra.h`, so regenerate and commit its `Generated/*.g.cs` too.
The Go binding (`bindings/go`) is likewise generated from `wickra.h`, so
regenerate and commit `indicators_gen.go` (`gofmt`-clean).
- **Docs.** Update the relevant page on the
[documentation site](https://docs.wickra.org) and the
`README.md` when behaviour or the public API changes. The docs live in
Generated
+15 -8
View File
@@ -1944,7 +1944,7 @@ dependencies = [
[[package]]
name = "wickra"
version = "0.7.4"
version = "0.7.7"
dependencies = [
"approx",
"criterion",
@@ -1955,7 +1955,7 @@ dependencies = [
[[package]]
name = "wickra-bench"
version = "0.7.4"
version = "0.7.7"
dependencies = [
"criterion",
"kand",
@@ -1965,9 +1965,16 @@ dependencies = [
"yata",
]
[[package]]
name = "wickra-c"
version = "0.7.7"
dependencies = [
"wickra-core",
]
[[package]]
name = "wickra-core"
version = "0.7.4"
version = "0.7.7"
dependencies = [
"approx",
"proptest",
@@ -1977,7 +1984,7 @@ dependencies = [
[[package]]
name = "wickra-data"
version = "0.7.4"
version = "0.7.7"
dependencies = [
"approx",
"csv",
@@ -1994,7 +2001,7 @@ dependencies = [
[[package]]
name = "wickra-examples"
version = "0.7.4"
version = "0.7.7"
dependencies = [
"serde_json",
"tokio",
@@ -2004,7 +2011,7 @@ dependencies = [
[[package]]
name = "wickra-node"
version = "0.7.4"
version = "0.7.7"
dependencies = [
"napi",
"napi-build",
@@ -2014,7 +2021,7 @@ dependencies = [
[[package]]
name = "wickra-python"
version = "0.7.4"
version = "0.7.7"
dependencies = [
"numpy",
"pyo3",
@@ -2023,7 +2030,7 @@ dependencies = [
[[package]]
name = "wickra-wasm"
version = "0.7.4"
version = "0.7.7"
dependencies = [
"console_error_panic_hook",
"js-sys",
+3 -2
View File
@@ -7,13 +7,14 @@ members = [
"bindings/python",
"bindings/wasm",
"bindings/node",
"bindings/c",
"examples/rust",
"crates/wickra-bench",
]
exclude = ["fuzz"]
[workspace.package]
version = "0.7.4"
version = "0.7.7"
authors = ["kingchenc <support@wickra.org>"]
edition = "2021"
rust-version = "1.86"
@@ -25,7 +26,7 @@ keywords = ["finance", "trading", "indicators", "technical-analysis", "ta"]
categories = ["finance", "mathematics", "science"]
[workspace.dependencies]
wickra-core = { path = "crates/wickra-core", version = "0.7.4" }
wickra-core = { path = "crates/wickra-core", version = "0.7.7" }
thiserror = "2"
rayon = "1.10"
+57 -25
View File
@@ -2,24 +2,26 @@
<a href="https://wickra.org"><img src="https://raw.githubusercontent.com/wickra-lib/.github/main/profile/wickra-banner.webp?v=514" alt="Wickra — streaming-first technical indicators" width="100%"></a>
</p>
[![CI](https://github.com/wickra-lib/wickra/actions/workflows/ci.yml/badge.svg)](https://github.com/wickra-lib/wickra/actions/workflows/ci.yml)
[![CodeQL](https://github.com/wickra-lib/wickra/actions/workflows/codeql.yml/badge.svg)](https://github.com/wickra-lib/wickra/actions/workflows/codeql.yml)
[![codecov](https://codecov.io/gh/wickra-lib/wickra/branch/main/graph/badge.svg)](https://codecov.io/gh/wickra-lib/wickra)
[![GitHub release](https://img.shields.io/github/v/release/wickra-lib/wickra?logo=github&color=green)](https://github.com/wickra-lib/wickra/releases/latest)
[![crates.io](https://img.shields.io/crates/v/wickra.svg?logo=rust&color=orange)](https://crates.io/crates/wickra)
[![PyPI](https://img.shields.io/pypi/v/wickra.svg?logo=pypi&color=blue)](https://pypi.org/project/wickra/)
[![npm](https://img.shields.io/npm/v/wickra.svg?logo=npm&color=red)](https://www.npmjs.com/package/wickra)
[![License: MIT OR Apache-2.0](https://img.shields.io/badge/license-MIT_OR_Apache--2.0-blue)](#license)
[![OpenSSF Scorecard](https://api.securityscorecards.dev/projects/github.com/wickra-lib/wickra/badge)](https://scorecard.dev/viewer/?uri=github.com/wickra-lib/wickra)
[![OpenSSF Best Practices](https://www.bestpractices.dev/projects/13094/badge)](https://www.bestpractices.dev/projects/13094)
[![Build provenance](https://img.shields.io/badge/provenance-attested-brightgreen?logo=github)](https://github.com/wickra-lib/wickra/attestations)
[![Docs](https://img.shields.io/badge/docs-docs.wickra.org-0ea5e9?logo=readthedocs&logoColor=white)](https://docs.wickra.org)
[![CI](https://raw.githubusercontent.com/wickra-lib/.github/main/profile/badges/ci.svg)](https://github.com/wickra-lib/wickra/actions/workflows/ci.yml)
[![CodeQL](https://raw.githubusercontent.com/wickra-lib/.github/main/profile/badges/codeql.svg)](https://github.com/wickra-lib/wickra/actions/workflows/codeql.yml)
[![codecov](https://raw.githubusercontent.com/wickra-lib/.github/main/profile/badges/codecov.svg)](https://codecov.io/gh/wickra-lib/wickra)
[![GitHub release](https://raw.githubusercontent.com/wickra-lib/.github/main/profile/badges/release.svg)](https://github.com/wickra-lib/wickra/releases/latest)
[![crates.io](https://raw.githubusercontent.com/wickra-lib/.github/main/profile/badges/crates.svg)](https://crates.io/crates/wickra)
[![PyPI](https://raw.githubusercontent.com/wickra-lib/.github/main/profile/badges/pypi.svg)](https://pypi.org/project/wickra/)
[![npm](https://raw.githubusercontent.com/wickra-lib/.github/main/profile/badges/npm.svg)](https://www.npmjs.com/package/wickra)
[![NuGet](https://raw.githubusercontent.com/wickra-lib/.github/main/profile/badges/nuget.svg)](https://www.nuget.org/packages/Wickra)
[![License: MIT OR Apache-2.0](https://raw.githubusercontent.com/wickra-lib/.github/main/profile/badges/license.svg)](#license)
[![OpenSSF Scorecard](https://raw.githubusercontent.com/wickra-lib/.github/main/profile/badges/scorecard.svg)](https://scorecard.dev/viewer/?uri=github.com/wickra-lib/wickra)
[![OpenSSF Best Practices](https://raw.githubusercontent.com/wickra-lib/.github/main/profile/badges/best-practices.svg)](https://www.bestpractices.dev/projects/13094)
[![Build provenance](https://raw.githubusercontent.com/wickra-lib/.github/main/profile/badges/provenance.svg)](https://github.com/wickra-lib/wickra/attestations)
[![Docs](https://raw.githubusercontent.com/wickra-lib/.github/main/profile/badges/docs.svg)](https://docs.wickra.org)
**Streaming-first technical indicators. Install with `pip install wickra` — no system dependencies.**
Wickra is a multi-language technical-analysis library with a Rust core and
bindings for Python, Node.js, and WebAssembly. Every indicator is a state
machine that updates in O(1) per new data point, so live trading bots and
native bindings for Python, Node.js and WebAssembly, plus a C ABI that C, C++,
C# / .NET, Go and any other C-capable language links against. Every indicator is a
state machine that updates in O(1) per new data point, so live trading bots and
historical backtests share the exact same implementation.
```python
@@ -46,7 +48,10 @@ Full documentation lives at **[docs.wickra.org](https://docs.wickra.org)**:
- **Quickstarts** — [Rust](https://docs.wickra.org/Quickstart-Rust),
[Python](https://docs.wickra.org/Quickstart-Python),
[Node](https://docs.wickra.org/Quickstart-Node),
[WASM](https://docs.wickra.org/Quickstart-WASM).
[WASM](https://docs.wickra.org/Quickstart-WASM),
[C](https://docs.wickra.org/Quickstart-C),
[C#](https://docs.wickra.org/Quickstart-CSharp),
[Go](https://docs.wickra.org/Quickstart-Go).
- **Indicators** — a per-indicator deep dive (formula, parameters, warmup) for
every one of the 514 indicators; start at the
[indicators overview](https://docs.wickra.org/Indicators-Overview).
@@ -71,9 +76,10 @@ times to get there.
breadth, Renko/Kagi/Point&Figure bars, Ehlers DSP cycles, risk/performance
metrics — every single one updating in **O(1) per tick**. TA-Lib ships ~150 and
none of them stream.
- **One Rust core, four first-class targets.** Native **Python · Node.js ·
WebAssembly · Rust** — identical math, identical results, zero per-language
reimplementation and zero GIL bottleneck.
- **One Rust core, five first-class targets.** Native **Python · Node.js ·
WebAssembly · Rust** plus a **C ABI** for C / C++, C# / .NET, Go and any other C-capable language
identical math, identical results, zero per-language reimplementation and zero
GIL bottleneck.
- **Correct by construction, not by hope.** Every `update` validates its input,
runs a real warmup, and returns an `Option` so a single bad tick can't silently
poison state. `batch == streaming` is **bit-exact, fuzzed and 100 %-line-covered
@@ -95,7 +101,8 @@ Every other library forces one of those compromises. Wickra doesn't:
| Library | Install | Streaming | Languages | Indicators | Active |
|------------------|-------------|-------------|-----------------------------|-----------:|--------|
| **★&nbsp;Wickra**| **clean** | **yes, O(1)** | **Python · Node · WASM · Rust** | **514** | **yes** |
| **★&nbsp;Wickra**| **clean** | **yes, O(1)** | **Rust · Python · Node · WASM** | **514** | **yes** |
| | | | **C · C# · Go** | | |
| kand | clean | yes | Python · WASM · Rust | ~60 | yes |
| ta-rs | clean | yes | Rust only | ~30 | stale |
| yata | clean | partial | Rust only | ~35 | yes |
@@ -166,8 +173,9 @@ as one column each. `Doji` is direction-less by default (`+1.0` / `0.0`);
construct it in signed mode (`Doji::new().signed()`, `Doji(signed=True)`,
`new Doji(true)`) for a dragonfly / gravestone `±1` reading.
Adding a new indicator means implementing one trait in Rust; all four bindings
inherit it automatically.
Adding a new indicator means implementing one trait in Rust; every binding
inherits it automatically (the C ABI — and the C# and Go bindings generated from
it — regenerate from the core).
## Languages
@@ -177,12 +185,16 @@ inherit it automatically.
| Node.js (napi-rs) | `npm install wickra` | `examples/node/backtest.js` |
| Browser / WASM | `npm install wickra-wasm` | `examples/wasm/index.html` |
| Rust | `cargo add wickra` | `examples/rust/src/bin/backtest.rs` |
| C / C++ (C ABI) | header + library, see [`bindings/c`](bindings/c) | `examples/c/streaming.c` |
| C# / .NET (C ABI) | `dotnet add package Wickra`, see [`bindings/csharp`](bindings/csharp) | `examples/csharp/streaming` |
| Go (cgo, C ABI) | `go get github.com/wickra-lib/wickra/bindings/go`, see [`bindings/go`](bindings/go) | `examples/go/streaming` |
Each binding ships several runnable examples (streaming, backtest, live feed);
[`examples/README.md`](examples/README.md) is the full cross-language index.
The wickra-core crate is `unsafe`-forbidden, so every binding inherits a
memory-safe implementation.
The wickra-core crate is `unsafe`-forbidden, so the native bindings are
memory-safe end to end. The C ABI runs the same safe core; only its thin FFI
boundary uses `unsafe`, and the caller owns handle lifetimes (`_new` / `_free`).
## Rust API
@@ -244,13 +256,19 @@ wickra/
├── bindings/
│ ├── python/ PyO3 + maturin (publishes on PyPI)
│ ├── node/ napi-rs (publishes on npm)
── wasm/ wasm-bindgen (browsers, bundlers, Node)
── wasm/ wasm-bindgen (browsers, bundlers, Node)
│ ├── c/ C ABI (cdylib + staticlib) + generated include/wickra.h
│ ├── csharp/ .NET binding over the C ABI (publishes on NuGet)
│ └── go/ Go binding over the C ABI via cgo (module tag)
├── examples/ examples/README.md indexes every language
│ ├── data/ real BTCUSDT OHLCV datasets, one per timeframe
│ ├── rust/ Rust workspace member (`wickra-examples`)
│ ├── python/ backtest, live trading, parallel assets, multi-tf
│ ├── node/ streaming, backtest, live trading (load `wickra`)
── wasm/ browser demo for `wickra-wasm`
── wasm/ browser demo for `wickra-wasm`
│ ├── c/ C smoke + streaming, C++ RAII wrapper
│ ├── csharp/ streaming, backtest, strategies (load `Wickra`)
│ └── go/ streaming, backtest, strategies (cgo binding)
└── .github/workflows/ CI and release pipelines
```
@@ -278,6 +296,18 @@ wasm-pack build bindings/wasm --target web --release --features panic-hook
# Node binding (requires @napi-rs/cli)
cd bindings/node && npm install && npm run build && npm test
# C ABI (cdylib + staticlib + generated header)
cargo build -p wickra-c --release
cmake -S examples/c -B examples/c/build -DWICKRA_LIB_DIR="$PWD/target/release"
cmake --build examples/c/build && ctest --test-dir examples/c/build --output-on-failure
# C# / .NET binding (requires the .NET 8 SDK; links the C ABI above)
dotnet test bindings/csharp/Wickra.Tests/Wickra.Tests.csproj
# Go binding (requires a C compiler for cgo; links the C ABI above)
cp target/release/libwickra.so bindings/go/lib/ # .dylib on macOS, wickra.dll on Windows
cd bindings/go && go test ./...
```
## Testing
@@ -297,6 +327,8 @@ Every layer is covered; run the suites with the commands in
values across all indicators.
- `bindings/wasm`: `wasm-bindgen-test` cases for constructors, equivalence,
and reference values.
- `bindings/go`: `go test` cases covering one indicator per FFI archetype
(scalar/batch, multi-output, bars, profile, array input), reset, and lifecycle.
## Contributing
+3 -2
View File
@@ -21,8 +21,9 @@ minor releases; breaking changes are called out in the changelog.
versioning stability for a 1.0 release.
- **Performance.** Keep per-tick updates O(1) and maintain the benchmark suite;
investigate further allocation and cache improvements.
- **Bindings parity.** Keep the Python, Node.js and WebAssembly bindings in
lockstep with the Rust core, including type stubs and platform coverage.
- **Bindings parity.** Keep the Python, Node.js and WebAssembly bindings — plus
the C ABI and the C# / .NET and Go bindings generated from it — in lockstep with the
Rust core, including type stubs and platform coverage.
- **Documentation.** Maintain a deep-dive page per indicator on
<https://docs.wickra.org>, plus quickstarts and cookbook material.
- **Project health.** Maintain test coverage, static and dynamic analysis,
+5 -1
View File
@@ -58,7 +58,11 @@ artifacts, and (4) a healthy dependency supply chain.
- *Memory safety* — the core and all bindings are written in Rust. The crates
forbid or minimise `unsafe`, so the compiler guarantees memory and thread
safety for the indicator logic.
safety for the indicator logic. The one exception is the C ABI
([`bindings/c`](bindings/c)), whose thin FFI shim is necessarily `unsafe`
because it dereferences caller-supplied pointers; it adds no indicator logic,
validates every handle for NULL, and never lets a panic cross the boundary, so
the safe core's guarantees still cover all computation.
- *Input robustness* — every indicator validates its parameters and rejects
non-finite inputs at construction; behaviour on edge cases (flat markets,
warmup, reset) is pinned by unit tests, and the public update paths are
+2 -2
View File
@@ -7,8 +7,8 @@ Thanks for using Wickra! Here is where to get help, depending on what you need.
Most questions are answered in the documentation:
- **Docs site:** <https://docs.wickra.org> — quickstarts for Rust, Python,
Node.js and WebAssembly, a per-indicator reference, warmup periods, the data
layer, and an FAQ.
Node.js, WebAssembly, C, C# and Go, a per-indicator reference, warmup periods, the
data layer, and an FAQ.
- **README:** <https://github.com/wickra-lib/wickra#readme> — installation and a
quick overview.
- **API docs (Rust):** <https://docs.rs/wickra>.
+5 -2
View File
@@ -3,8 +3,10 @@
This document describes Wickra's attack surface and the threats considered,
together with their mitigations. It complements the security assurance case in
[`SECURITY.md`](SECURITY.md). Wickra is a computational technical-analysis
library (a Rust core with Python, Node.js and WebAssembly bindings), not a
network service or trading system; the attack surface is correspondingly small.
library (a Rust core with Python, Node.js and WebAssembly bindings plus a C ABI
and the .NET and Go bindings built on it),
not a network service or trading system; the attack surface is correspondingly
small.
## Assets
@@ -31,6 +33,7 @@ network service or trading system; the attack surface is correspondingly small.
| Threat | Mitigation |
| --- | --- |
| Memory-safety exploit (buffer overflow, UAF) via crafted input | Pure safe Rust; `unsafe` is forbidden/minimised, so the compiler precludes these classes. |
| Misuse of the C ABI FFI boundary (invalid/dangling handle, undersized batch buffer) | The C ABI (`bindings/c`) is the sole `unsafe` surface. Its shim adds no logic, NULL-checks every handle (returning `NaN`/no-op), writes only into caller-sized buffers, and catches panics so none cross the boundary. A caller passing a non-NULL but dangling pointer is undefined behaviour by C's own contract — out of scope, the same as any C library. |
| Denial of service via malformed/degenerate input (NaN, infinities, extreme magnitudes) | Indicators reject non-finite inputs and validate parameters at construction; update paths are exercised by coverage-guided fuzzing and unit tests for edge cases. |
| Silently incorrect results | 100% line coverage on the core crate; reference-value tests against known-good sources; streaming/batch parity tests. |
| Integer overflow / panics | `clippy::pedantic` with `-D warnings`; debug assertions and overflow checks enabled in test/fuzz builds. |
+46
View File
@@ -0,0 +1,46 @@
[package]
name = "wickra-c"
description = "C ABI (cdylib + staticlib) for the Wickra streaming-first technical indicators library — the hub every C-capable language (C, C++, Go, C#, Java, R) links against."
version.workspace = true
authors.workspace = true
edition.workspace = true
rust-version.workspace = true
license.workspace = true
repository.workspace = true
homepage.workspace = true
readme = "README.md"
keywords.workspace = true
categories.workspace = true
publish = false
[lib]
name = "wickra"
crate-type = ["cdylib", "staticlib"]
# The C ABI inherently needs `unsafe` (raw pointers across the FFI boundary,
# `#[export_name]` symbol control). The workspace forbids `unsafe_code`, so this
# crate cannot inherit `workspace = true`; it mirrors every workspace lint and
# only relaxes `unsafe_code` to `allow` (parity with how the proc-macro bindings
# emit their unsafe). The Rust core stays `unsafe`-forbidden — this is the one
# crate where the boundary lives.
[lints.rust]
unsafe_code = "allow"
missing_debug_implementations = "warn"
unreachable_pub = "warn"
unused_must_use = "deny"
[lints.clippy]
all = { level = "warn", priority = -1 }
pedantic = { level = "warn", priority = -1 }
module_name_repetitions = "allow"
must_use_candidate = "allow"
missing_errors_doc = "allow"
missing_panics_doc = "allow"
cast_precision_loss = "allow"
cast_possible_truncation = "allow"
cast_sign_loss = "allow"
similar_names = "allow"
float_cmp = "allow"
[dependencies]
wickra-core = { workspace = true }
+80
View File
@@ -0,0 +1,80 @@
# Wickra — C / C++
[![CI](https://github.com/wickra-lib/wickra/actions/workflows/ci.yml/badge.svg)](https://github.com/wickra-lib/wickra/actions/workflows/ci.yml)
[![codecov](https://codecov.io/gh/wickra-lib/wickra/branch/main/graph/badge.svg)](https://codecov.io/gh/wickra-lib/wickra)
[![GitHub release](https://img.shields.io/github/v/release/wickra-lib/wickra?logo=github&color=green)](https://github.com/wickra-lib/wickra/releases/latest)
[![License: MIT OR Apache-2.0](https://img.shields.io/badge/license-MIT_OR_Apache--2.0-blue)](https://github.com/wickra-lib/wickra#license)
**Streaming-first technical indicators for C and C++. A prebuilt shared/static
library plus a generated `wickra.h` — no system dependencies.**
Wickra is a multi-language technical-analysis library with a Rust core and
bindings for Python, Node.js and WebAssembly, plus a C ABI for C/C++, C#, Go and any
other C-capable language. Every indicator is an O(1)
streaming state machine, so live trading bots and historical backtests share
the exact same implementation. This package is the **C ABI hub**: it compiles the
core to a C-compatible shared/static library plus a generated header, so any
C-capable language (C, C++, Go, C#, Java, R) links against one artifact instead
of re-wrapping every indicator natively.
## Install
Grab the prebuilt header + library for your platform from the
[GitHub releases](https://github.com/wickra-lib/wickra/releases) — each archive
has `wickra.h`, the optional `wickra.hpp` C++ wrapper, and the shared/static
library — or build from source:
```bash
cargo build -p wickra-c --release
# -> target/release/libwickra.{so,dylib} or wickra.dll (+ import lib) + a staticlib
```
Then compile against the header and link the library
(`cc app.c -I include -L lib -lwickra -lm -o app`).
## Quick start
```c
#include "wickra.h"
struct Rsi *rsi = wickra_rsi_new(14); /* NULL on invalid params */
for (size_t i = 0; i < n; ++i) {
double v = wickra_rsi_update(rsi, prices[i]); /* NaN during warmup */
if (v == v && v > 70.0) /* v == v is the NaN check */
printf("overbought\n");
}
wickra_rsi_free(rsi); /* exactly once per _new */
```
Every indicator is an opaque handle with the same five functions —
`_new` / `_update` / `_batch` / `_reset` / `_free`. `update` is O(1); there is no
RAII across the C boundary, so each `_new` needs exactly one `_free`, and every
function is NULL-safe (a NULL handle yields `NaN` or a no-op, never a crash).
Multi-output indicators (MACD, Bollinger, ADX, …) take a pointer to a `#[repr(C)]`
struct and return a `bool`. The optional `wickra.hpp` wraps any handle in a
move-only `wickra::Handle` for exception-safe C++ lifetimes.
## Documentation
The full indicator catalogue, guides, quickstarts, and API reference live in
the main repository and documentation site:
- **Repository & full indicator list:** <https://github.com/wickra-lib/wickra>
- **Docs** (C quickstart, cookbook, TA-Lib migration): <https://docs.wickra.org/Quickstart-C>
- **Runnable examples:** [`examples/c/`](https://github.com/wickra-lib/wickra/tree/main/examples/c)
Wickra ships native bindings for Python, Node.js, WebAssembly and Rust, plus this
C ABI hub that any C-capable language (C, C++, Go, C#, Java, R) links against —
all exposing the same indicators from the shared Rust core.
## Disclaimer
Wickra is an indicator toolkit, not a trading system. The values it computes
are deterministic transforms of the input data — they are not financial advice
and do not predict the market. Any use in a live trading context is at your own
risk. The library is provided **as is**, without warranty of any kind.
## License
Licensed under either of [Apache-2.0](https://github.com/wickra-lib/wickra/blob/main/LICENSE-APACHE)
or [MIT](https://github.com/wickra-lib/wickra/blob/main/LICENSE-MIT) at your option.
+20
View File
@@ -0,0 +1,20 @@
language = "C"
header = "/* Wickra C ABI — generated by cbindgen. Do not edit by hand. */"
include_guard = "WICKRA_H"
pragma_once = true
# Wrap the declarations in `extern "C"` under __cplusplus so the header is usable
# from C++ (the optional wickra.hpp RAII layer and any C++ consumer).
cpp_compat = true
tab_width = 4
# Off: cbindgen copies the Rust struct/fn doc comments verbatim, and some core
# indicator docs contain markdown (e.g. `**1/8**/**7/8**`) whose `*/` would close
# the C block comment early and break the header. Usage docs live in the crate
# README and examples; the header is a pure declaration contract.
documentation = false
[parse]
# Parse wickra-core too so the opaque indicator handle types (Sma, Ema, …) are
# discovered and emitted as forward-declared opaque structs. Their fields are
# never exposed — only `T *` handles cross the boundary.
parse_deps = true
include = ["wickra-core"]
File diff suppressed because it is too large Load Diff
+66
View File
@@ -0,0 +1,66 @@
// Optional C++ convenience layer over the Wickra C ABI (`wickra.h`).
//
// The C ABI hands out raw handles that must be released exactly once with the
// matching `wickra_<ind>_free`. `wickra::Handle` wraps that in a move-only RAII
// owner so the free happens automatically at scope exit:
//
// #include "wickra.hpp"
//
// wickra::Handle<Sma, wickra_sma_free> sma(wickra_sma_new(14));
// if (sma) {
// double v = wickra_sma_update(sma.get(), 42.0); // NaN during warmup
// }
// // sma is freed here
//
// This is header-only and adds no runtime cost beyond the C calls themselves.
#ifndef WICKRA_HPP
#define WICKRA_HPP
#include "wickra.h"
#include <utility>
namespace wickra {
/// Move-only RAII owner of a Wickra handle. `T` is the opaque indicator type and
/// `Free` its `wickra_<ind>_free` function.
template <typename T, void (*Free)(T *)>
class Handle {
public:
explicit Handle(T *ptr) noexcept : ptr_(ptr) {}
~Handle() {
if (ptr_ != nullptr) {
Free(ptr_);
}
}
Handle(const Handle &) = delete;
Handle &operator=(const Handle &) = delete;
Handle(Handle &&other) noexcept : ptr_(std::exchange(other.ptr_, nullptr)) {}
Handle &operator=(Handle &&other) noexcept {
if (this != &other) {
if (ptr_ != nullptr) {
Free(ptr_);
}
ptr_ = std::exchange(other.ptr_, nullptr);
}
return *this;
}
/// The raw handle, for passing to the `wickra_<ind>_*` functions.
T *get() const noexcept { return ptr_; }
/// True if the handle is non-null (construction succeeded).
explicit operator bool() const noexcept { return ptr_ != nullptr; }
private:
T *ptr_;
};
} // namespace wickra
#endif // WICKRA_HPP
+44290
View File
File diff suppressed because it is too large Load Diff
+12
View File
@@ -0,0 +1,12 @@
# .NET build output
bin/
obj/
*.user
# NuGet packaging output
*.nupkg
*.snupkg
# Native libraries staged for packaging (produced by the release pipeline from
# the wickra-c-<triple>.tar.gz assets; never committed to source).
Wickra/runtimes/
+78
View File
@@ -0,0 +1,78 @@
# Wickra — .NET
[![CI](https://github.com/wickra-lib/wickra/actions/workflows/ci.yml/badge.svg)](https://github.com/wickra-lib/wickra/actions/workflows/ci.yml)
[![codecov](https://codecov.io/gh/wickra-lib/wickra/branch/main/graph/badge.svg)](https://codecov.io/gh/wickra-lib/wickra)
[![NuGet](https://img.shields.io/nuget/v/Wickra.svg?logo=nuget&color=blue)](https://www.nuget.org/packages/Wickra)
[![License: MIT OR Apache-2.0](https://img.shields.io/badge/license-MIT_OR_Apache--2.0-blue)](https://github.com/wickra-lib/wickra#license)
**Streaming-first technical indicators for .NET. `dotnet add package Wickra`
prebuilt native library, no system dependencies.**
Wickra is a multi-language technical-analysis library with a Rust core and
bindings for Python, Node.js and WebAssembly, plus a C ABI for C/C++, C#, Go and any
other C-capable language. Every indicator is an O(1)
streaming state machine, so live trading bots and historical backtests share
the exact same implementation. This package is the .NET binding; it consumes the
C ABI hub through `[LibraryImport]` P/Invoke and exposes all 514 streaming-first
indicators as idiomatic `IDisposable` classes.
## Install
```bash
dotnet add package Wickra
```
The native library ships prebuilt per platform (Linux, macOS, Windows — x64 and
arm64) under `runtimes/<rid>/native/`, selected automatically. There is nothing
to compile. Targets .NET 8 and later.
## Quick start
```csharp
using Wickra;
// Batch: run an indicator over a whole series (NaN at warmup positions).
var prices = Enumerable.Range(0, 1000).Select(i => 100.0 + i * 0.1).ToArray();
using var sma = new Sma(20);
double[] values = sma.Batch(prices);
// Streaming: the same indicator, fed tick by tick in O(1).
using var rsi = new Rsi(14);
foreach (var price in liveFeed)
{
var value = rsi.Update(price); // NaN during warmup, no recomputation
if (double.IsFinite(value) && value > 70)
{
Console.WriteLine("overbought");
}
}
```
`Batch(prices)` and feeding the same prices through `Update()` produce identical
values — the equivalence is enforced by the test suite. Multi-output indicators
(MACD, Bollinger, ADX, …) return a nullable `record struct`, `null` while warming up.
## Documentation
The full indicator catalogue, guides, quickstarts, and API reference live in
the main repository and documentation site:
- **Repository & full indicator list:** <https://github.com/wickra-lib/wickra>
- **Docs** (quickstarts, cookbook, TA-Lib migration): <https://docs.wickra.org>
- **Runnable examples:** [`examples/csharp/`](https://github.com/wickra-lib/wickra/tree/main/examples/csharp)
Wickra ships native bindings for Python, Node.js, WebAssembly and Rust, plus a
C ABI hub that any C-capable language (C, C++, Go, C#, Java, R) links against —
all exposing the same indicators from the shared, `unsafe`-forbidden Rust core.
## Disclaimer
Wickra is an indicator toolkit, not a trading system. The values it computes
are deterministic transforms of the input data — they are not financial advice
and do not predict the market. Any use in a live trading context is at your own
risk. The library is provided **as is**, without warranty of any kind.
## License
Licensed under either of [Apache-2.0](https://github.com/wickra-lib/wickra/blob/main/LICENSE-APACHE)
or [MIT](https://github.com/wickra-lib/wickra/blob/main/LICENSE-MIT) at your option.
@@ -0,0 +1,144 @@
using Wickra;
using Xunit;
namespace Wickra.Tests;
/// <summary>
/// One representative per FFI archetype, exercising every marshalling path the
/// generator produces (scalar, candle, pairwise, multi-output, bars, profile,
/// values-profile, array-input). Garbage marshalling surfaces as NaN, wild
/// values, or crashes — so finite/sane assertions are the real check.
/// </summary>
public class ArchetypeTests
{
private static (double open, double high, double low, double close, double volume, long ts) Candle(int i)
{
var close = 100.0 + 10.0 * Math.Sin(i * 0.3);
var open = 100.0 + 10.0 * Math.Sin((i - 1) * 0.3);
var high = Math.Max(open, close) + 1.0;
var low = Math.Min(open, close) - 1.0;
return (open, high, low, close, 1_000.0, i * 60_000L);
}
[Fact]
public void Scalar_Ema_IsFiniteAfterWarmup()
{
using var ema = new Ema(3);
double last = double.NaN;
for (var i = 1; i <= 10; i++)
{
last = ema.Update(i);
}
Assert.True(double.IsFinite(last));
Assert.InRange(last, 1.0, 10.0);
}
[Fact]
public void Candle_Atr_IsFinitePositive()
{
using var atr = new Atr(3);
double last = double.NaN;
for (var i = 0; i < 20; i++)
{
var (o, h, l, c, v, ts) = Candle(i);
last = atr.Update(o, h, l, c, v, ts);
}
Assert.True(double.IsFinite(last));
Assert.True(last > 0.0);
}
[Fact]
public void Pairwise_Beta_IsFinite()
{
using var beta = new Beta(5);
double last = double.NaN;
for (var i = 0; i < 30; i++)
{
var market = 100.0 + 10.0 * Math.Sin(i * 0.5);
var asset = 50.0 + 6.0 * Math.Sin(i * 0.5 + 0.2);
last = beta.Update(market, asset);
}
Assert.True(double.IsFinite(last));
}
[Fact]
public void MultiOutput_Adx_ReturnsFiniteStruct()
{
using var adx = new Adx(5);
AdxOutput? result = null;
for (var i = 0; i < 60; i++)
{
var (o, h, l, c, v, ts) = Candle(i);
result = adx.Update(o, h, l, c, v, ts);
}
Assert.NotNull(result);
Assert.True(double.IsFinite(result!.Value.Adx));
Assert.True(double.IsFinite(result.Value.PlusDi));
Assert.True(double.IsFinite(result.Value.MinusDi));
}
[Fact]
public void Bars_DollarBars_EmitsBars()
{
using var bars = new DollarBars(5_000.0);
var total = 0;
for (var i = 0; i < 200; i++)
{
var (o, h, l, c, v, ts) = Candle(i);
total += bars.Update(o, h, l, c, v, ts).Length;
}
Assert.True(total > 0);
}
[Fact]
public void Profile_VolumeProfile_ReturnsValues()
{
using var profile = new VolumeProfile(20, 8);
VolumeProfileOutputScalars? result = null;
for (var i = 0; i < 60; i++)
{
var (o, h, l, c, v, ts) = Candle(i);
result = profile.Update(o, h, l, c, v, ts);
}
Assert.NotNull(result);
Assert.NotNull(result!.Value.Values);
Assert.True(result.Value.PriceLow <= result.Value.PriceHigh);
}
[Fact]
public void ProfileValues_DayOfWeekProfile_NoCrash()
{
using var profile = new DayOfWeekProfile(0);
double[]? result = null;
for (var i = 0; i < 60; i++)
{
var close = 100.0 + 5.0 * Math.Sin(i * 0.2);
// one day apart so the day-of-week buckets fill
result = profile.Update(close, close + 1, close - 1, close, 1_000.0, i * 86_400_000L);
}
if (result is not null)
{
Assert.All(result, v => Assert.True(double.IsFinite(v)));
}
}
[Fact]
public void ArrayInput_DepthSlope_IsFinite()
{
using var slope = new DepthSlope();
ReadOnlySpan<double> bidPrice = stackalloc double[] { 99.0, 98.0, 97.0 };
ReadOnlySpan<double> bidSize = stackalloc double[] { 10.0, 20.0, 30.0 };
ReadOnlySpan<double> askPrice = stackalloc double[] { 101.0, 102.0, 103.0 };
ReadOnlySpan<double> askSize = stackalloc double[] { 12.0, 22.0, 32.0 };
var result = slope.Update(bidPrice, bidSize, askPrice, askSize);
Assert.True(double.IsFinite(result));
}
}
+51
View File
@@ -0,0 +1,51 @@
using Wickra;
using Xunit;
namespace Wickra.Tests;
public class SmaTests
{
[Fact]
public void StreamingMatchesReference()
{
using var sma = new Sma(3);
Assert.True(double.IsNaN(sma.Update(1)));
Assert.True(double.IsNaN(sma.Update(2)));
Assert.Equal(2.0, sma.Update(3), 9);
Assert.Equal(3.0, sma.Update(4), 9);
Assert.Equal(4.0, sma.Update(5), 9);
}
[Fact]
public void BatchMatchesStreaming()
{
using var sma = new Sma(3);
var output = sma.Batch(new double[] { 1, 2, 3, 4, 5 });
Assert.True(double.IsNaN(output[0]));
Assert.True(double.IsNaN(output[1]));
Assert.Equal(2.0, output[2], 9);
Assert.Equal(3.0, output[3], 9);
Assert.Equal(4.0, output[4], 9);
}
[Fact]
public void ResetClearsState()
{
using var sma = new Sma(3);
sma.Update(1);
sma.Update(2);
sma.Update(3);
sma.Reset();
Assert.True(double.IsNaN(sma.Update(10)));
}
[Fact]
public void ZeroPeriodThrows()
{
// Zero is rejected by the native constructor (returns NULL) -> ArgumentException;
// a negative period is caught earlier by the wrapper guard.
Assert.Throws<ArgumentException>(() => new Sma(0));
Assert.Throws<ArgumentOutOfRangeException>(() => new Sma(-1));
}
}
@@ -0,0 +1,21 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<LangVersion>latest</LangVersion>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.11.1" />
<PackageReference Include="xunit" Version="2.9.2" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Wickra\Wickra.csproj" />
</ItemGroup>
</Project>
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+57
View File
@@ -0,0 +1,57 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<LangVersion>latest</LangVersion>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<RootNamespace>Wickra</RootNamespace>
<AssemblyName>Wickra</AssemblyName>
<!-- NuGet package metadata -->
<PackageId>Wickra</PackageId>
<Version>0.7.7</Version>
<Authors>kingchenc</Authors>
<Description>High-performance streaming technical-analysis indicators (514 indicators) for .NET, backed by the native Rust core via the Wickra C ABI.</Description>
<PackageLicenseExpression>MIT OR Apache-2.0</PackageLicenseExpression>
<PackageProjectUrl>https://github.com/wickra-lib/wickra</PackageProjectUrl>
<RepositoryUrl>https://github.com/wickra-lib/wickra</RepositoryUrl>
<RepositoryType>git</RepositoryType>
<PackageTags>technical-analysis;indicators;trading;finance;streaming;ffi;native</PackageTags>
<PackageReadmeFile>README.md</PackageReadmeFile>
<IncludeSymbols>true</IncludeSymbols>
<SymbolPackageFormat>snupkg</SymbolPackageFormat>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<!-- A managed package carrying per-RID native assets; not built per-RID itself. -->
<IncludeBuildOutput>true</IncludeBuildOutput>
<!-- NU5128: managed package carrying only per-RID native assets.
CS1591: generated members are self-descriptive; hand-written API is documented. -->
<NoWarn>$(NoWarn);NU5128;CS1591</NoWarn>
</PropertyGroup>
<!--
Supported native runtime identifiers. The release pipeline builds the C ABI
per target triple and stages the libraries under
Wickra/runtimes/<rid>/native/ before `dotnet pack`:
win-x64 win-arm64 linux-x64 linux-arm64 osx-x64 osx-arm64
-->
<PropertyGroup>
<WickraRuntimeIdentifiers>win-x64;win-arm64;linux-x64;linux-arm64;osx-x64;osx-arm64</WickraRuntimeIdentifiers>
</PropertyGroup>
<ItemGroup>
<None Include="..\README.md" Pack="true" PackagePath="\" />
</ItemGroup>
<!--
Native libraries are packed under runtimes/<rid>/native/ by the release pipeline,
which unpacks the wickra-c-<triple>.tar.gz assets into the matching RID folders.
For local development and tests the natives are resolved from the cargo target dir
via WickraNative's DllImportResolver.
-->
<ItemGroup>
<None Include="runtimes/**/native/*" Pack="true" PackagePath="runtimes" Condition="Exists('runtimes')" />
</ItemGroup>
</Project>
+26
View File
@@ -0,0 +1,26 @@
using Microsoft.Win32.SafeHandles;
namespace Wickra;
/// <summary>
/// Owns an opaque native indicator handle and releases it via the indicator's
/// <c>_free</c> function. One generic handle type backs every indicator; the
/// correct free routine is captured at construction time.
/// </summary>
internal sealed class WickraHandle : SafeHandleZeroOrMinusOneIsInvalid
{
private readonly Action<nint> _free;
internal WickraHandle(nint handle, Action<nint> free)
: base(ownsHandle: true)
{
_free = free;
SetHandle(handle);
}
protected override bool ReleaseHandle()
{
_free(handle);
return true;
}
}
+87
View File
@@ -0,0 +1,87 @@
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
namespace Wickra;
/// <summary>
/// Native library resolution for the Wickra C ABI.
/// </summary>
/// <remarks>
/// When consumed as a NuGet package the native library ships under
/// <c>runtimes/&lt;rid&gt;/native/</c> and the default runtime resolver finds it
/// automatically. For local development (project reference against a cargo build)
/// the resolver additionally walks up the directory tree to locate
/// <c>target/release</c> or <c>target/debug</c>. Every candidate is validated to
/// actually export the Wickra ABI before it is accepted, so an unrelated library
/// of the same name cannot shadow the real one.
/// </remarks>
internal static class WickraNative
{
/// <summary>The library name passed to <c>[LibraryImport]</c>.</summary>
internal const string LibraryName = "wickra";
// Any exported symbol works as a fingerprint; sma_new exists in every build.
private const string SentinelSymbol = "wickra_sma_new";
[ModuleInitializer]
internal static void Register()
{
NativeLibrary.SetDllImportResolver(typeof(WickraNative).Assembly, Resolve);
}
private static nint Resolve(string libraryName, System.Reflection.Assembly assembly, DllImportSearchPath? searchPath)
{
if (libraryName != LibraryName)
{
return nint.Zero;
}
// 1. Default resolution (NuGet runtimes/ layout, app-local copies). Accept
// only if it is genuinely the Wickra ABI; otherwise discard and fall through.
if (NativeLibrary.TryLoad(libraryName, assembly, searchPath, out var handle))
{
if (Exports(handle))
{
return handle;
}
NativeLibrary.Free(handle);
}
// 2. Development fallback: locate the cargo build output.
var fileName = NativeFileName();
var dir = AppContext.BaseDirectory;
for (var i = 0; i < 16 && dir is not null; i++)
{
foreach (var profile in new[] { "release", "debug" })
{
var candidate = Path.Combine(dir, "target", profile, fileName);
if (File.Exists(candidate) && NativeLibrary.TryLoad(candidate, out var devHandle))
{
if (Exports(devHandle))
{
return devHandle;
}
NativeLibrary.Free(devHandle);
}
}
dir = Path.GetDirectoryName(dir.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar));
}
return nint.Zero;
}
private static bool Exports(nint handle) => NativeLibrary.TryGetExport(handle, SentinelSymbol, out _);
private static string NativeFileName()
{
if (OperatingSystem.IsWindows())
{
return "wickra.dll";
}
return OperatingSystem.IsMacOS() ? "libwickra.dylib" : "libwickra.so";
}
}
+100
View File
@@ -0,0 +1,100 @@
# Wickra — Go
[![CI](https://github.com/wickra-lib/wickra/actions/workflows/ci.yml/badge.svg)](https://github.com/wickra-lib/wickra/actions/workflows/ci.yml)
[![codecov](https://codecov.io/gh/wickra-lib/wickra/branch/main/graph/badge.svg)](https://codecov.io/gh/wickra-lib/wickra)
[![Go Reference](https://pkg.go.dev/badge/github.com/wickra-lib/wickra/bindings/go.svg)](https://pkg.go.dev/github.com/wickra-lib/wickra/bindings/go)
[![License: MIT OR Apache-2.0](https://img.shields.io/badge/license-MIT_OR_Apache--2.0-blue)](https://github.com/wickra-lib/wickra#license)
**Streaming-first technical indicators for Go, over the Wickra C ABI hub via cgo.**
Wickra is a multi-language technical-analysis library with a Rust core and
bindings for Python, Node.js and WebAssembly, plus a C ABI for C/C++, C#, Go and
any other C-capable language. Every indicator is an O(1) streaming state machine,
so live trading bots and historical backtests share the exact same
implementation. This package is the Go binding; it consumes the C ABI hub through
cgo and exposes all 514 streaming-first indicators as idiomatic types.
## Install
```bash
go get github.com/wickra-lib/wickra/bindings/go
```
The binding uses cgo, so a C compiler is required, and it links against the
prebuilt Wickra C ABI library. Build that library from the workspace and stage
it under this package's `lib/` directory:
```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 on PATH at run time)
```
On Linux and macOS the library path is baked in via rpath; on Windows the DLL
must be discoverable at run time (next to the executable or on `PATH`).
## Quick start
```go
package main
import (
"fmt"
wickra "github.com/wickra-lib/wickra/bindings/go"
)
func main() {
// Batch: run an indicator over a whole series (NaN at warmup positions).
prices := make([]float64, 1000)
for i := range prices {
prices[i] = 100.0 + float64(i)*0.1
}
sma, _ := wickra.NewSma(20)
defer sma.Close()
values := sma.Batch(prices)
// Streaming: the same indicator, fed tick by tick in O(1).
rsi, _ := wickra.NewRsi(14)
defer rsi.Close()
for _, price := range prices {
value := rsi.Update(price) // NaN during warmup, no recomputation
if value > 70 {
fmt.Println("overbought")
}
}
_ = values
}
```
`Batch(prices)` and feeding the same prices through `Update()` produce identical
values — the equivalence is enforced by the test suite. Multi-output indicators
(MACD, Bollinger, ADX, …) return `(Output, bool)`, with `false` while warming up.
Every indicator owns a native handle freed by `Close()`; a finalizer is wired as
a backstop, but call `Close()` (e.g. with `defer`) to release memory promptly.
## Documentation
The full indicator catalogue, guides, quickstarts, and API reference live in the
main repository and documentation site:
- **Repository & full indicator list:** <https://github.com/wickra-lib/wickra>
- **Docs** (quickstarts, cookbook, TA-Lib migration): <https://docs.wickra.org>
- **Runnable examples:** [`examples/go/`](https://github.com/wickra-lib/wickra/tree/main/examples/go)
Wickra ships native bindings for Python, Node.js, WebAssembly and Rust, plus a
C ABI hub that any C-capable language (C, C++, C#, Go, Java, R) links against —
all exposing the same indicators from the shared, `unsafe`-forbidden Rust core.
## Disclaimer
Wickra is an indicator toolkit, not a trading system. The values it computes are
deterministic transforms of the input data — they are not financial advice and
do not predict the market. Any use in a live trading context is at your own risk.
The library is provided **as is**, without warranty of any kind.
## License
Licensed under either of [Apache-2.0](https://github.com/wickra-lib/wickra/blob/main/LICENSE-APACHE)
or [MIT](https://github.com/wickra-lib/wickra/blob/main/LICENSE-MIT) at your option.
+3
View File
@@ -0,0 +1,3 @@
module github.com/wickra-lib/wickra/bindings/go
go 1.23
File diff suppressed because it is too large Load Diff
+7
View File
@@ -0,0 +1,7 @@
# Prebuilt Wickra C ABI libraries are provisioned locally / in CI, not committed.
*.so
*.dylib
*.dll
*.a
*.lib
*.exp
+25
View File
@@ -0,0 +1,25 @@
// Package wickra provides idiomatic Go bindings for the Wickra
// technical-analysis library over its C ABI hub.
//
// Each indicator is an opaque-handle type with a New<Indicator> constructor and
// Update/Batch/Reset/Close methods. Handles are freed by Close and, as a
// backstop, by a finalizer; call Close explicitly to release native memory
// promptly. The binding links against the prebuilt Wickra C ABI library
// (libwickra.so/.dylib or wickra.dll) staged under ./lib — see the package
// README for how to provision it.
package wickra
/*
#cgo CFLAGS: -I${SRCDIR}/../c/include
#cgo linux LDFLAGS: -L${SRCDIR}/lib -lwickra -Wl,-rpath,${SRCDIR}/lib
#cgo darwin LDFLAGS: -L${SRCDIR}/lib -lwickra -Wl,-rpath,${SRCDIR}/lib
#cgo windows LDFLAGS: -L${SRCDIR}/lib -l:wickra.dll
#include "wickra.h"
*/
import "C"
import "errors"
// ErrInvalidParams is returned by a New<Indicator> constructor when the native
// constructor rejects the supplied parameters (for example a zero period).
var ErrInvalidParams = errors.New("wickra: invalid indicator parameters")
+151
View File
@@ -0,0 +1,151 @@
package wickra
import (
"math"
"testing"
)
// One indicator per FFI archetype, exercising the full New/Update/Batch/Reset/
// Close surface against the real native library.
func TestScalarKnownValue(t *testing.T) {
s, err := NewSma(3)
if err != nil {
t.Fatalf("NewSma: %v", err)
}
defer s.Close()
var last float64
for _, v := range []float64{1, 2, 3, 4, 5} {
last = s.Update(v)
}
if math.Abs(last-4.0) > 1e-9 {
t.Fatalf("sma(3) last = %v, want 4.0", last)
}
}
func TestScalarBatchMatchesStreaming(t *testing.T) {
input := []float64{1, 2, 3, 4, 5, 6, 7, 8}
stream, _ := NewSma(3)
defer stream.Close()
want := make([]float64, len(input))
for i, v := range input {
want[i] = stream.Update(v)
}
batchInd, _ := NewSma(3)
defer batchInd.Close()
got := batchInd.Batch(input)
if len(got) != len(want) {
t.Fatalf("batch len = %d, want %d", len(got), len(want))
}
for i := range want {
if math.IsNaN(want[i]) && math.IsNaN(got[i]) {
continue
}
if math.Abs(got[i]-want[i]) > 1e-9 {
t.Fatalf("batch[%d] = %v, streaming = %v", i, got[i], want[i])
}
}
}
func TestMultiOutput(t *testing.T) {
m, err := NewMacdIndicator(3, 6, 3)
if err != nil {
t.Fatalf("NewMacdIndicator: %v", err)
}
defer m.Close()
var ok bool
var out MacdOutput
for i := 0; i < 30; i++ {
out, ok = m.Update(100 + float64(i))
}
if !ok {
t.Fatal("macd never produced a value after warmup")
}
if math.IsNaN(out.Macd) {
t.Fatal("macd value is NaN after warmup")
}
}
func TestBars(t *testing.T) {
rb, err := NewRangeBars(2.0)
if err != nil {
t.Fatalf("NewRangeBars: %v", err)
}
defer rb.Close()
total := 0
for _, p := range []float64{100, 101, 103, 104, 99, 96, 102, 108, 95, 110} {
bars := rb.Update(p, p, p, p, 1, 0)
total += len(bars)
}
if total == 0 {
t.Fatal("range bars produced no bars over a 15-point move")
}
}
func TestProfile(t *testing.T) {
vp, err := NewVolumeProfile(10, 24)
if err != nil {
t.Fatalf("NewVolumeProfile: %v", err)
}
defer vp.Close()
var ok bool
var snap VolumeProfileOutputScalars
for i := 0; i < 50; i++ {
price := 100 + 5*math.Sin(float64(i)*0.3)
snap, ok = vp.Update(price, price+1, price-1, price, 1000, int64(i))
}
if !ok {
t.Fatal("volume profile never produced a snapshot")
}
if len(snap.Values) == 0 {
t.Fatal("volume profile returned an empty values buffer")
}
}
func TestArrayInput(t *testing.T) {
ob, err := NewOrderBookImbalanceFull()
if err != nil {
t.Fatalf("NewOrderBookImbalanceFull: %v", err)
}
defer ob.Close()
bidPrice := []float64{99.9, 99.8, 99.7}
bidSize := []float64{5, 3, 2}
askPrice := []float64{100.1, 100.2, 100.3}
askSize := []float64{1, 1, 1}
v := ob.Update(bidPrice, bidSize, askPrice, askSize)
if math.IsNaN(v) {
t.Fatal("order-book imbalance is NaN on a populated book")
}
}
func TestResetReturnsToWarmup(t *testing.T) {
s, _ := NewSma(3)
defer s.Close()
for _, v := range []float64{1, 2, 3} {
s.Update(v)
}
s.Reset()
if got := s.Update(10); !math.IsNaN(got) {
t.Fatalf("after reset first update = %v, want NaN (warmup)", got)
}
}
func TestInvalidParams(t *testing.T) {
if _, err := NewSma(0); err == nil {
t.Fatal("NewSma(0) should return ErrInvalidParams")
}
}
func TestCloseIsIdempotent(t *testing.T) {
s, _ := NewSma(3)
s.Close()
s.Close() // must not panic or double-free
}
+5 -3
View File
@@ -9,7 +9,8 @@
prebuilt native binary, no system dependencies.**
Wickra is a multi-language technical-analysis library with a Rust core and
bindings for Python, Node.js, and WebAssembly. Every indicator is an O(1)
bindings for Python, Node.js and WebAssembly, plus a C ABI for C/C++, C#, Go and any
other C-capable language. Every indicator is an O(1)
streaming state machine, so live trading bots and historical backtests share
the exact same implementation. This package is the Node.js binding (napi-rs);
it exposes 200+ streaming-first indicators across sixteen families.
@@ -55,8 +56,9 @@ the main repository and documentation site:
- **Docs** (quickstarts, cookbook, TA-Lib migration): <https://docs.wickra.org>
- **Runnable examples:** [`examples/node/`](https://github.com/wickra-lib/wickra/tree/main/examples/node)
Wickra ships four bindings Python, Node.js, WebAssembly, and Rust — that all
expose the same indicators from the shared, `unsafe`-forbidden Rust core.
Wickra ships native bindings for Python, Node.js, WebAssembly and Rust, plus a
C ABI hub that any C-capable language (C, C++, Go, C#, Java, R) links against —
all exposing the same indicators from the shared, `unsafe`-forbidden Rust core.
## Disclaimer
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "wickra-darwin-arm64",
"version": "0.7.4",
"version": "0.7.7",
"description": "Native binding for wickra (macOS Apple Silicon). Installed automatically as an optional dependency of wickra on matching platforms.",
"main": "wickra.darwin-arm64.node",
"files": [
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "wickra-darwin-x64",
"version": "0.7.4",
"version": "0.7.7",
"description": "Native binding for wickra (macOS Intel). Installed automatically as an optional dependency of wickra on matching platforms.",
"main": "wickra.darwin-x64.node",
"files": [
@@ -1,6 +1,6 @@
{
"name": "wickra-linux-arm64-gnu",
"version": "0.7.4",
"version": "0.7.7",
"description": "Native binding for wickra (linux arm64 GNU). Installed automatically as an optional dependency of wickra on matching platforms.",
"main": "wickra.linux-arm64-gnu.node",
"files": [
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "wickra-linux-x64-gnu",
"version": "0.7.4",
"version": "0.7.7",
"description": "Native binding for wickra (linux x64 GNU). Installed automatically as an optional dependency of wickra on matching platforms.",
"main": "wickra.linux-x64-gnu.node",
"files": [
@@ -1,6 +1,6 @@
{
"name": "wickra-win32-arm64-msvc",
"version": "0.7.4",
"version": "0.7.7",
"description": "Native binding for wickra (Windows arm64 MSVC). Installed automatically as an optional dependency of wickra on matching platforms.",
"main": "wickra.win32-arm64-msvc.node",
"files": [
@@ -1,6 +1,6 @@
{
"name": "wickra-win32-x64-msvc",
"version": "0.7.4",
"version": "0.7.7",
"description": "Native binding for wickra (Windows x64 MSVC). Installed automatically as an optional dependency of wickra on matching platforms.",
"main": "wickra.win32-x64-msvc.node",
"files": [
+20 -20
View File
@@ -1,12 +1,12 @@
{
"name": "wickra",
"version": "0.7.4",
"version": "0.7.7",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "wickra",
"version": "0.7.4",
"version": "0.7.7",
"license": "MIT OR Apache-2.0",
"devDependencies": {
"@napi-rs/cli": "^2.18.0"
@@ -15,12 +15,12 @@
"node": ">= 18"
},
"optionalDependencies": {
"wickra-darwin-arm64": "0.7.4",
"wickra-darwin-x64": "0.7.4",
"wickra-linux-arm64-gnu": "0.7.4",
"wickra-linux-x64-gnu": "0.7.4",
"wickra-win32-arm64-msvc": "0.7.4",
"wickra-win32-x64-msvc": "0.7.4"
"wickra-darwin-arm64": "0.7.7",
"wickra-darwin-x64": "0.7.7",
"wickra-linux-arm64-gnu": "0.7.7",
"wickra-linux-x64-gnu": "0.7.7",
"wickra-win32-arm64-msvc": "0.7.7",
"wickra-win32-x64-msvc": "0.7.7"
}
},
"node_modules/@napi-rs/cli": {
@@ -41,8 +41,8 @@
}
},
"node_modules/wickra-darwin-arm64": {
"version": "0.7.4",
"resolved": "https://registry.npmjs.org/wickra-darwin-arm64/-/wickra-darwin-arm64-0.7.4.tgz",
"version": "0.7.7",
"resolved": "https://registry.npmjs.org/wickra-darwin-arm64/-/wickra-darwin-arm64-0.7.7.tgz",
"integrity": "sha512-4eZiBR/yGUdr4nzhEUFy2i69XgNx64iI2ax/LPamsThgylC0KpHOZKK19QzJ2d9KbK4C8nMjME5FLuR+4GNEwQ==",
"cpu": [
"arm64"
@@ -57,8 +57,8 @@
}
},
"node_modules/wickra-darwin-x64": {
"version": "0.7.4",
"resolved": "https://registry.npmjs.org/wickra-darwin-x64/-/wickra-darwin-x64-0.7.4.tgz",
"version": "0.7.7",
"resolved": "https://registry.npmjs.org/wickra-darwin-x64/-/wickra-darwin-x64-0.7.7.tgz",
"integrity": "sha512-6hf8zI3QPjTFp4zCpmgUwDvNtu6jHqNUHKD5e55POo0CgA52HkpyxSPtVm8TGTIZDI7kPjlbOdBM8CJ76mmXwA==",
"cpu": [
"x64"
@@ -73,8 +73,8 @@
}
},
"node_modules/wickra-linux-arm64-gnu": {
"version": "0.7.4",
"resolved": "https://registry.npmjs.org/wickra-linux-arm64-gnu/-/wickra-linux-arm64-gnu-0.7.4.tgz",
"version": "0.7.7",
"resolved": "https://registry.npmjs.org/wickra-linux-arm64-gnu/-/wickra-linux-arm64-gnu-0.7.7.tgz",
"integrity": "sha512-kSe6y0xBMSiqdPLXNjwop5WZdHtvdBNKSEBCwZ4hFq33p4apW25/wrlzv9/oDuyD4kuPabJEhCCnFOplh58CUg==",
"cpu": [
"arm64"
@@ -89,8 +89,8 @@
}
},
"node_modules/wickra-linux-x64-gnu": {
"version": "0.7.4",
"resolved": "https://registry.npmjs.org/wickra-linux-x64-gnu/-/wickra-linux-x64-gnu-0.7.4.tgz",
"version": "0.7.7",
"resolved": "https://registry.npmjs.org/wickra-linux-x64-gnu/-/wickra-linux-x64-gnu-0.7.7.tgz",
"integrity": "sha512-tWBWS4qz7hxM4xnpFb59bhf6TaLwXq0Z3jEa/2l7r8PiHA94g8r8S53NRMiT+4yiL5hSWe/nUiC/YXdRrhEZ4g==",
"cpu": [
"x64"
@@ -105,8 +105,8 @@
}
},
"node_modules/wickra-win32-arm64-msvc": {
"version": "0.7.4",
"resolved": "https://registry.npmjs.org/wickra-win32-arm64-msvc/-/wickra-win32-arm64-msvc-0.7.4.tgz",
"version": "0.7.7",
"resolved": "https://registry.npmjs.org/wickra-win32-arm64-msvc/-/wickra-win32-arm64-msvc-0.7.7.tgz",
"integrity": "sha512-EXIckHxAtF75PUGDKRzXyqMe9ldP0JjSdu68WFN6iJfp+McYrGu6h40TEJlQ/oUEIoPqiZB/xhVyo/el5Lg7zw==",
"cpu": [
"arm64"
@@ -121,8 +121,8 @@
}
},
"node_modules/wickra-win32-x64-msvc": {
"version": "0.7.4",
"resolved": "https://registry.npmjs.org/wickra-win32-x64-msvc/-/wickra-win32-x64-msvc-0.7.4.tgz",
"version": "0.7.7",
"resolved": "https://registry.npmjs.org/wickra-win32-x64-msvc/-/wickra-win32-x64-msvc-0.7.7.tgz",
"integrity": "sha512-Yfsqq1Xwp6hdxMyLze411vNdo7BDwI6+lPSe7A9XdqyPecNDbtKwYLpsal2r8EHbNzqM+R8XnuRtUaEQS5VlUQ==",
"cpu": [
"x64"
+7 -7
View File
@@ -1,6 +1,6 @@
{
"name": "wickra",
"version": "0.7.4",
"version": "0.7.7",
"description": "Streaming-first technical indicators: incremental, fast, install-free. Node bindings powered by Rust.",
"author": "kingchenc <support@wickra.org>",
"main": "index.js",
@@ -47,12 +47,12 @@
"node": ">= 18"
},
"optionalDependencies": {
"wickra-linux-x64-gnu": "0.7.4",
"wickra-linux-arm64-gnu": "0.7.4",
"wickra-darwin-x64": "0.7.4",
"wickra-darwin-arm64": "0.7.4",
"wickra-win32-x64-msvc": "0.7.4",
"wickra-win32-arm64-msvc": "0.7.4"
"wickra-linux-x64-gnu": "0.7.7",
"wickra-linux-arm64-gnu": "0.7.7",
"wickra-darwin-x64": "0.7.7",
"wickra-darwin-arm64": "0.7.7",
"wickra-win32-x64-msvc": "0.7.7",
"wickra-win32-arm64-msvc": "0.7.7"
},
"scripts": {
"build": "napi build --platform --release",
+5 -3
View File
@@ -9,7 +9,8 @@
system dependencies, no C build tooling.**
Wickra is a multi-language technical-analysis library with a Rust core and
bindings for Python, Node.js, and WebAssembly. Every indicator is an O(1)
bindings for Python, Node.js and WebAssembly, plus a C ABI for C/C++, C#, Go and any
other C-capable language. Every indicator is an O(1)
streaming state machine, so live trading bots and historical backtests share
the exact same implementation. This package is the Python binding (PyO3); it
exposes 200+ streaming-first indicators across sixteen families.
@@ -54,8 +55,9 @@ the main repository and documentation site:
- **Docs** (quickstarts, cookbook, TA-Lib migration): <https://docs.wickra.org>
- **Runnable examples:** [`examples/python/`](https://github.com/wickra-lib/wickra/tree/main/examples/python)
Wickra ships four bindings Python, Node.js, WebAssembly, and Rust — that all
expose the same indicators from the shared, `unsafe`-forbidden Rust core.
Wickra ships native bindings for Python, Node.js, WebAssembly and Rust, plus a
C ABI hub that any C-capable language (C, C++, Go, C#, Java, R) links against —
all exposing the same indicators from the shared, `unsafe`-forbidden Rust core.
## Disclaimer
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "maturin"
[project]
name = "wickra"
version = "0.7.4"
version = "0.7.7"
description = "Streaming-first technical indicators: incremental, fast, install-free."
readme = "README.md"
license = "MIT OR Apache-2.0"
+5 -3
View File
@@ -9,7 +9,8 @@
wickra-wasm` — pure WebAssembly, runs anywhere a modern JS engine does.**
Wickra is a multi-language technical-analysis library with a Rust core and
bindings for Python, Node.js, and WebAssembly. Every indicator is an O(1)
bindings for Python, Node.js and WebAssembly, plus a C ABI for C/C++, C#, Go and any
other C-capable language. Every indicator is an O(1)
streaming state machine, so live trading dashboards and historical backtests
share the exact same implementation. This package is the WebAssembly binding
(wasm-bindgen, built for the `web` target); it exposes 200+ streaming-first
@@ -54,8 +55,9 @@ the main repository and documentation site:
- **Docs** (quickstarts, cookbook, TA-Lib migration): <https://docs.wickra.org>
- **Runnable browser examples:** [`examples/wasm/`](https://github.com/wickra-lib/wickra/tree/main/examples/wasm)
Wickra ships four bindings Python, Node.js, WebAssembly, and Rust — that all
expose the same indicators from the shared, `unsafe`-forbidden Rust core.
Wickra ships native bindings for Python, Node.js, WebAssembly and Rust, plus a
C ABI hub that any C-capable language (C, C++, Go, C#, Java, R) links against —
all exposing the same indicators from the shared, `unsafe`-forbidden Rust core.
## Disclaimer
+5 -2
View File
@@ -6,8 +6,11 @@ That includes:
- **Quickstarts** for [Rust](https://docs.wickra.org/Quickstart-Rust),
[Python](https://docs.wickra.org/Quickstart-Python),
[Node](https://docs.wickra.org/Quickstart-Node), and
[WASM](https://docs.wickra.org/Quickstart-WASM).
[Node](https://docs.wickra.org/Quickstart-Node),
[WASM](https://docs.wickra.org/Quickstart-WASM),
[C](https://docs.wickra.org/Quickstart-C),
[C#](https://docs.wickra.org/Quickstart-CSharp), and
[Go](https://docs.wickra.org/Quickstart-Go).
- A per-indicator deep dive for every one of the **514 indicators** across
the sixteen families (Moving Averages, Momentum Oscillators, Trend &
Directional, Price Oscillators, Volatility & Bands, Bands & Channels,
+70
View File
@@ -21,6 +21,76 @@ The Rust examples live in the `wickra-examples` workspace member crate.
| `strategy_macd_adx.rs` | Hourly BTCUSDT trend-follower: MACD crossover entries gated by ADX(14) > 20. | `cargo run --release -p wickra-examples --bin strategy_macd_adx` |
| `strategy_bollinger_squeeze.rs` | Daily BTCUSDT Bollinger-squeeze breakout with ATR(14) trailing stop. | `cargo run --release -p wickra-examples --bin strategy_bollinger_squeeze` |
## C / C++ — `examples/c/`
Build the library first (`cargo build -p wickra-c --release`), then build and run
the examples via CMake:
`cmake -S examples/c -B examples/c/build -DWICKRA_LIB_DIR="$PWD/target/release"`
`cmake --build examples/c/build``ctest --test-dir examples/c/build`.
| Example | What it does | CMake target |
| --- | --- | --- |
| `smoke.c` | Links the generated header + library and asserts SMA streaming / batch values across the boundary. | `smoke` |
| `streaming.c` | Feed a synthetic price series through SMA / EMA / RSI / MACD tick by tick. | `streaming` |
| `backtest.c` | Basket of indicators over an OHLCV CSV; defaults to the bundled BTCUSDT daily dataset. | `backtest` |
| `multi_timeframe.c` | Resample the bundled 1-minute CSV to 5m / 15m / 1h / 4h / 1d and print indicators per timeframe. | `multi_timeframe` |
| `parallel_assets.c` | Serial vs OpenMP fan-out over a synthetic panel (one handle per asset), with speedup. | `parallel_assets` |
| `strategy_rsi_mean_reversion.c` | Hourly BTCUSDT mean-reversion using RSI(14) thresholds, with PnL / Sharpe / max-DD summary. | `strategy_rsi_mean_reversion` |
| `strategy_macd_adx.c` | Hourly BTCUSDT trend-follower: MACD crossover entries gated by ADX(14) > 20. | `strategy_macd_adx` |
| `strategy_bollinger_squeeze.c` | Daily BTCUSDT Bollinger-squeeze breakout with ATR(14) stop. | `strategy_bollinger_squeeze` |
| `fetch_btcusdt.c` | Download real BTCUSDT klines from the Binance REST API into `examples/data/` (shells out to `curl`). | `fetch_btcusdt` |
| `live_binance.c` | Poll the Binance REST klines endpoint via `curl` and stream closed candles through RSI(14). | `live_binance` |
| `smoke.cpp` | C++ RAII via `wickra::Handle` from [`wickra.hpp`](../bindings/c/include/wickra.hpp): construct, move, auto-free. | `cpp_smoke` |
The data-driven examples (`backtest`, `multi_timeframe`, `parallel_assets`, the
three `strategy_*`) build against the bundled datasets and run under `ctest`.
`fetch_btcusdt` and `live_binance` reach the network, so they are built but not
run in CI; run them by hand. `parallel_assets` links OpenMP when the toolchain
provides it and falls back to a single-threaded run otherwise.
## C# / .NET — `examples/csharp/`
Build the C ABI library first (`cargo build -p wickra-c --release`), then run any
example with the .NET 8 SDK; the binding resolves the native library automatically.
| Example | What it does | Run |
| --- | --- | --- |
| `streaming` | Feed a synthetic price series through SMA / EMA / RSI / MACD tick by tick. | `dotnet run --project examples/csharp/streaming` |
| `backtest` | Basket of indicators over an OHLCV series (CSV arg or synthetic). | `dotnet run --project examples/csharp/backtest -- <ohlcv.csv>` |
| `multi_timeframe` | Resample a 1-minute series to 5m / 15m and print an indicator per timeframe. | `dotnet run --project examples/csharp/multi_timeframe` |
| `parallel_assets` | SMA(20) batch over a panel, serial vs `Parallel.For`, with speedup. | `dotnet run -c Release --project examples/csharp/parallel_assets` |
| `strategy_rsi_mean_reversion` | RSI(14) mean-reversion with PnL / Sharpe / max-DD summary. | `dotnet run -c Release --project examples/csharp/strategy_rsi_mean_reversion` |
| `strategy_macd_adx` | Trend-follower: MACD crossover entries gated by ADX(14) > 20. | `dotnet run -c Release --project examples/csharp/strategy_macd_adx` |
| `strategy_bollinger_squeeze` | Bollinger-squeeze breakout with an ATR(14) trailing stop. | `dotnet run -c Release --project examples/csharp/strategy_bollinger_squeeze` |
| `fetch_btcusdt` | Download real BTCUSDT klines from the Binance REST API into a CSV. | `dotnet run --project examples/csharp/fetch_btcusdt` |
| `live_binance` | Stream live Binance klines through EMA(20) over a WebSocket. | `dotnet run --project examples/csharp/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.
## 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 |
+82
View File
@@ -0,0 +1,82 @@
cmake_minimum_required(VERSION 3.15)
project(wickra_c_examples C CXX)
# Directory holding the compiled Wickra C library (cargo output), e.g.
# <workspace>/target/release. Override with -DWICKRA_LIB_DIR=/path/to/target/release.
if(NOT DEFINED WICKRA_LIB_DIR)
set(WICKRA_LIB_DIR "${CMAKE_CURRENT_SOURCE_DIR}/../../target/release")
endif()
set(WICKRA_INCLUDE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/../../bindings/c/include")
# Absolute path to the bundled OHLCV datasets, baked into the data-driven
# examples as a compile definition so they run from any working directory (the
# C counterpart of the Rust examples' CARGO_MANIFEST_DIR).
get_filename_component(WICKRA_DATA_DIR "${CMAKE_CURRENT_SOURCE_DIR}/../data" ABSOLUTE)
# OpenMP is optional: parallel_assets links it when present and falls back to a
# single-threaded run otherwise.
find_package(OpenMP QUIET)
# Pick the right link target per platform/toolchain.
# - MSVC links the generated import library (wickra.dll.lib).
# - MinGW/gcc on Windows links the DLL directly.
# - Unix links the shared object / dylib.
if(WIN32)
set(WICKRA_RUNTIME "${WICKRA_LIB_DIR}/wickra.dll")
if(MSVC)
set(WICKRA_LINK_LIB "${WICKRA_LIB_DIR}/wickra.dll.lib")
else()
set(WICKRA_LINK_LIB "${WICKRA_LIB_DIR}/wickra.dll")
endif()
elseif(APPLE)
set(WICKRA_LINK_LIB "${WICKRA_LIB_DIR}/libwickra.dylib")
else()
set(WICKRA_LINK_LIB "${WICKRA_LIB_DIR}/libwickra.so")
endif()
enable_testing()
# Build one example and link it to the Wickra library. On Windows the DLL is
# copied next to the executable so the loader finds it at run time. With
# register_test=TRUE the example is also run as a ctest; network examples pass
# FALSE (they are built only, never run in CI).
function(add_wickra_example name source register_test)
add_executable(${name} ${source})
target_include_directories(${name} PRIVATE "${WICKRA_INCLUDE_DIR}")
target_link_libraries(${name} PRIVATE "${WICKRA_LINK_LIB}")
target_compile_definitions(${name} PRIVATE "WICKRA_DATA_DIR=\"${WICKRA_DATA_DIR}\"")
if(UNIX AND NOT APPLE)
target_link_libraries(${name} PRIVATE m)
endif()
if(WIN32)
add_custom_command(TARGET ${name} POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_if_different
"${WICKRA_RUNTIME}" "$<TARGET_FILE_DIR:${name}>")
endif()
if(register_test)
add_test(NAME ${name} COMMAND ${name})
if(NOT WIN32)
set_tests_properties(${name} PROPERTIES
ENVIRONMENT "LD_LIBRARY_PATH=${WICKRA_LIB_DIR};DYLD_LIBRARY_PATH=${WICKRA_LIB_DIR}")
endif()
endif()
endfunction()
# Offline examples built and run as ctests.
add_wickra_example(smoke smoke.c TRUE) # links the boundary, asserts values
add_wickra_example(streaming streaming.c TRUE) # multi-indicator tick stream
add_wickra_example(cpp_smoke smoke.cpp TRUE) # C++ RAII wrapper (wickra.hpp)
add_wickra_example(backtest backtest.c TRUE) # indicator basket over a CSV
add_wickra_example(multi_timeframe multi_timeframe.c TRUE) # resample + per-TF indicators
add_wickra_example(parallel_assets parallel_assets.c TRUE) # serial vs OpenMP fan-out
add_wickra_example(strategy_rsi_mean_reversion strategy_rsi_mean_reversion.c TRUE)
add_wickra_example(strategy_macd_adx strategy_macd_adx.c TRUE)
add_wickra_example(strategy_bollinger_squeeze strategy_bollinger_squeeze.c TRUE)
if(OpenMP_C_FOUND)
target_link_libraries(parallel_assets PRIVATE OpenMP::OpenMP_C)
endif()
# Network examples built (so they stay compilable) but not run in CI.
add_wickra_example(fetch_btcusdt fetch_btcusdt.c FALSE) # downloads CSVs via curl
add_wickra_example(live_binance live_binance.c FALSE) # polls Binance REST via curl
+90
View File
@@ -0,0 +1,90 @@
# Wickra — C / C++ examples
The Wickra C ABI is a single shared/static library plus a generated header
([`bindings/c/include/wickra.h`](../../bindings/c/include/wickra.h)). Any
C-capable language links against the same artifact; these examples show the
plain-C path.
## Build the library
From the workspace root:
```sh
cargo build -p wickra-c --release
```
This produces, in `target/release/`:
| Platform | Shared library | Link target |
|----------|----------------|-------------|
| Linux | `libwickra.so` | `-lwickra` |
| macOS | `libwickra.dylib` | `-lwickra` |
| Windows (MSVC) | `wickra.dll` | `wickra.dll.lib` (import lib) |
A static library (`libwickra.a` / `wickra.lib`) is emitted alongside.
## Build and run the smoke example
### With CMake (portable, used by CI)
```sh
cmake -S examples/c -B examples/c/build -DWICKRA_LIB_DIR="$PWD/target/release"
cmake --build examples/c/build
ctest --test-dir examples/c/build --output-on-failure
```
### Directly with a compiler
```sh
# Linux / macOS
cc examples/c/smoke.c -I bindings/c/include -L target/release -lwickra -lm -o smoke
LD_LIBRARY_PATH=target/release ./smoke # macOS: DYLD_LIBRARY_PATH
# Windows (MinGW gcc, linking the DLL directly)
gcc examples/c/smoke.c -I bindings/c/include target/release/wickra.dll -lm -o smoke.exe
```
Expected output:
```
OK: wickra C ABI smoke passed (SMA streaming + batch + reset + NULL-safety + free)
```
## The examples
| Example | What it does |
|---------|--------------|
| `smoke.c` | Links the boundary and asserts SMA streaming / batch / reset / NULL-safety values. |
| `streaming.c` | Feeds a synthetic price series through SMA / EMA / RSI / MACD tick by tick. |
| `backtest.c` | Runs an indicator basket over an OHLCV CSV (defaults to the bundled daily dataset). |
| `multi_timeframe.c` | Resamples the bundled 1-minute CSV to 5m / 15m / 1h / 4h / 1d and prints indicators per timeframe. |
| `parallel_assets.c` | Serial vs OpenMP fan-out over a synthetic panel (one handle per asset), with speedup. |
| `strategy_rsi_mean_reversion.c` | Hourly RSI(14) mean-reversion with a PnL / Sharpe / max-drawdown summary. |
| `strategy_macd_adx.c` | Hourly MACD crossover gated by ADX(14) > 20. |
| `strategy_bollinger_squeeze.c` | Daily Bollinger-squeeze breakout with an ATR(14) stop. |
| `fetch_btcusdt.c` | Downloads BTCUSDT klines from the Binance REST API into `examples/data/` (shells out to `curl`). |
| `live_binance.c` | Polls the Binance REST klines endpoint via `curl` and streams closed candles through RSI(14). |
| `smoke.cpp` | C++ RAII via `wickra::Handle` from [`wickra.hpp`](../../bindings/c/include/wickra.hpp). |
`ctest` builds and runs every example except `fetch_btcusdt` and `live_binance`,
which reach the network and are built only — run those two by hand. The C ABI
exposes only the indicators, not the `wickra-data` IO layer, so the examples read
CSV ([`wickra_csv.h`](wickra_csv.h)) and resample themselves; the network ones
shell out to the system `curl` rather than adding an HTTP/TLS dependency.
## Usage shape
Every indicator follows the same five-function pattern over an opaque handle:
```c
#include "wickra.h"
struct Sma *sma = wickra_sma_new(14); /* NULL on invalid params */
double v = wickra_sma_update(sma, 42.0); /* NaN during warmup */
wickra_sma_reset(sma); /* back to fresh state */
wickra_sma_free(sma); /* exactly once per _new */
```
There is no RAII across the C boundary: every `wickra_<ind>_new` must be paired
with exactly one `wickra_<ind>_free`. All functions are NULL-safe (a NULL handle
yields `NaN` / a no-op, never a crash).
+139
View File
@@ -0,0 +1,139 @@
/* Backtest a basket of indicators against an OHLCV CSV with the Wickra C ABI.
*
* The C counterpart of `examples/rust/src/bin/backtest.rs` and
* `examples/node/backtest.js`: load an OHLCV file, run a basket of indicators
* (scalar ones via `_batch`, candle ones streamed bar by bar), and print the
* most recent value of each. Defaults to the bundled BTCUSDT daily dataset.
*
* Build (after `cargo build -p wickra-c --release`):
* cc examples/c/backtest.c -I bindings/c/include -L target/release -lwickra -lm -o backtest
* ./backtest [path/to/ohlcv.csv]
*/
#define WICKRA_CSV_IMPL
#include "wickra.h"
#include "wickra_csv.h"
#include <math.h>
#include <stdio.h>
#include <stdlib.h>
#ifndef WICKRA_DATA_DIR
#define WICKRA_DATA_DIR "../data"
#endif
/* Last finite value of a scalar batch result, or NaN if none warmed up. */
static double last_finite(const double *v, size_t n) {
for (size_t i = n; i-- > 0;) {
if (isfinite(v[i])) {
return v[i];
}
}
return NAN;
}
int main(int argc, char **argv) {
const char *path = (argc > 1) ? argv[1] : WICKRA_DATA_DIR "/btcusdt-1d.csv";
WickraCandle *candles = NULL;
size_t n = wickra_load_csv(path, &candles);
if (n == 0) {
fprintf(stderr, "backtest: no candles read from %s\n", path);
return 1;
}
double *closes = (double *)malloc(n * sizeof(*closes));
double *rsi_out = (double *)malloc(n * sizeof(*rsi_out));
double *ema_out = (double *)malloc(n * sizeof(*ema_out));
struct Rsi *rsi = wickra_rsi_new(14);
struct Ema *ema = wickra_ema_new(20);
struct BollingerBands *bb = wickra_bollinger_bands_new(20, 2.0);
struct MacdIndicator *macd = wickra_macd_indicator_new(12, 26, 9);
struct Atr *atr = wickra_atr_new(14);
struct Adx *adx = wickra_adx_new(14);
struct Obv *obv = wickra_obv_new();
if (closes == NULL || rsi_out == NULL || ema_out == NULL || rsi == NULL ||
ema == NULL || bb == NULL || macd == NULL || atr == NULL || adx == NULL ||
obv == NULL) {
fprintf(stderr, "backtest: allocation failed\n");
return 1;
}
for (size_t i = 0; i < n; ++i) {
closes[i] = candles[i].close;
}
/* Scalar indicators: one batch call over the close series. */
wickra_rsi_batch(rsi, closes, rsi_out, n);
wickra_ema_batch(ema, closes, ema_out, n);
/* Multi-output and candle indicators: streamed bar by bar, keeping the
* last value each one produced. */
WickraBollingerOutput last_bb = {0};
int have_bb = 0;
WickraMacdOutput last_macd = {0};
int have_macd = 0;
WickraAdxOutput last_adx = {0};
int have_adx = 0;
double last_atr = NAN;
double last_obv = NAN;
for (size_t i = 0; i < n; ++i) {
const WickraCandle *c = &candles[i];
WickraBollingerOutput bo;
if (wickra_bollinger_bands_update(bb, c->close, &bo)) {
last_bb = bo;
have_bb = 1;
}
WickraMacdOutput mo;
if (wickra_macd_indicator_update(macd, c->close, &mo)) {
last_macd = mo;
have_macd = 1;
}
double av = wickra_atr_update(atr, c->open, c->high, c->low, c->close,
c->volume, c->timestamp);
if (isfinite(av)) {
last_atr = av;
}
WickraAdxOutput ao;
if (wickra_adx_update(adx, c->open, c->high, c->low, c->close, c->volume,
c->timestamp, &ao)) {
last_adx = ao;
have_adx = 1;
}
double ov = wickra_obv_update(obv, c->open, c->high, c->low, c->close,
c->volume, c->timestamp);
if (isfinite(ov)) {
last_obv = ov;
}
}
printf("backtest summary for %s (%llu bars)\n", path, (unsigned long long)n);
printf(" RSI(14) = %9.4f\n", last_finite(rsi_out, n));
printf(" EMA(20) = %9.4f\n", last_finite(ema_out, n));
if (have_bb) {
printf(" BB(20,2) upper=%9.4f middle=%9.4f lower=%9.4f sd=%8.4f\n",
last_bb.upper, last_bb.middle, last_bb.lower, last_bb.stddev);
}
if (have_macd) {
printf(" MACD macd=%9.4f signal=%9.4f hist=%9.4f\n", last_macd.macd,
last_macd.signal, last_macd.histogram);
}
printf(" ATR(14) = %9.4f\n", last_atr);
if (have_adx) {
printf(" ADX(14) +DI=%6.2f -DI=%6.2f ADX=%6.2f\n", last_adx.plus_di,
last_adx.minus_di, last_adx.adx);
}
printf(" OBV = %14.2f\n", last_obv);
wickra_rsi_free(rsi);
wickra_ema_free(ema);
wickra_bollinger_bands_free(bb);
wickra_macd_indicator_free(macd);
wickra_atr_free(atr);
wickra_adx_free(adx);
wickra_obv_free(obv);
free(closes);
free(rsi_out);
free(ema_out);
free(candles);
return 0;
}
+311
View File
@@ -0,0 +1,311 @@
/* Download real BTCUSDT spot candles from the Binance REST API and write them as
* CSV datasets under examples/data/ the C counterpart of
* `examples/rust/src/bin/fetch_btcusdt.rs` and `examples/node/fetch_btcusdt.js`.
*
* Like the Rust example, HTTPS is handled by shelling out to the system `curl`
* (shipped with Windows 10+, macOS and every Linux distro), so this example adds
* no HTTP/TLS dependency. Each klines response is capped at 1000 rows, so larger
* datasets are paginated backwards through `endTime`. Only fully closed candles
* are kept.
*
* This example talks to the network, so it is built but NOT run as a ctest.
*
* Build (after `cargo build -p wickra-c --release`):
* cc examples/c/fetch_btcusdt.c -I bindings/c/include -L target/release -lwickra -lm -o fetch_btcusdt
* ./fetch_btcusdt
*/
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#ifdef _WIN32
#define POPEN _popen
#define PCLOSE _pclose
#else
#define POPEN popen
#define PCLOSE pclose
#endif
#ifndef WICKRA_DATA_DIR
#define WICKRA_DATA_DIR "../data"
#endif
#define SYMBOL "BTCUSDT"
#define PAGE_LIMIT 1000
typedef struct {
int64_t open_time;
int64_t close_time;
double open, high, low, close, volume;
} Kline;
typedef struct {
const char *interval;
const char *file;
size_t target;
} Dataset;
/* One dataset per timeframe. The monthly file is `btcusdt-1month.csv`, not
* `-1M`, so it does not collide with `-1m` on case-insensitive filesystems. */
static const Dataset DATASETS[] = {
{"1m", "btcusdt-1m.csv", 50000}, {"5m", "btcusdt-5m.csv", 10000},
{"15m", "btcusdt-15m.csv", 10000}, {"1h", "btcusdt-1h.csv", 10000},
{"12h", "btcusdt-12h.csv", 5000}, {"1d", "btcusdt-1d.csv", 5000},
{"1M", "btcusdt-1month.csv", 5000},
};
/* Run `curl <url>` and return its stdout as a malloc'd, NUL-terminated buffer
* (caller frees), or NULL on failure. */
static char *curl_get(const char *url) {
char cmd[512];
snprintf(cmd, sizeof(cmd),
"curl --silent --show-error --fail --max-time 30 \"%s\"", url);
FILE *p = POPEN(cmd, "r");
if (p == NULL) {
fprintf(stderr, "could not run curl (install it / put it on PATH)\n");
return NULL;
}
size_t cap = 1 << 16, len = 0;
char *buf = (char *)malloc(cap);
if (buf == NULL) {
PCLOSE(p);
return NULL;
}
size_t got;
char tmp[8192];
while ((got = fread(tmp, 1, sizeof(tmp), p)) > 0) {
if (len + got + 1 > cap) {
cap *= 2;
char *grown = (char *)realloc(buf, cap);
if (grown == NULL) {
free(buf);
PCLOSE(p);
return NULL;
}
buf = grown;
}
memcpy(buf + len, tmp, got);
len += got;
}
int rc = PCLOSE(p);
buf[len] = '\0';
if (rc != 0 || len == 0) {
fprintf(stderr, "curl failed for %s\n", url);
free(buf);
return NULL;
}
return buf;
}
/* Parse a Binance klines JSON array. Each row is
* [openTime, "open", "high", "low", "close", "volume", closeTime, ...].
* Appends parsed rows to *rows (grown via realloc) and returns the new count. */
static size_t parse_klines(const char *body, Kline **rows, size_t count, size_t *cap) {
int depth = 0;
int in_string = 0;
int field = -1; /* -1 = not inside a row yet */
char tok[64];
size_t tok_len = 0;
Kline cur = {0};
for (const char *s = body; *s; ++s) {
char ch = *s;
if (in_string) {
if (ch == '"') {
in_string = 0;
} else if (tok_len + 1 < sizeof(tok)) {
tok[tok_len++] = ch;
}
continue;
}
switch (ch) {
case '[':
depth++;
if (depth == 2) {
field = 0;
tok_len = 0;
memset(&cur, 0, sizeof(cur));
}
break;
case ']':
if (depth == 2) {
/* close the final field of this row, then emit it */
tok[tok_len] = '\0';
if (field == 6) {
cur.close_time = strtoll(tok, NULL, 10);
}
if (*cap == count) {
*cap = *cap ? *cap * 2 : 1024;
Kline *grown = (Kline *)realloc(*rows, *cap * sizeof(Kline));
if (grown == NULL) {
return count;
}
*rows = grown;
}
(*rows)[count++] = cur;
field = -1;
}
depth--;
break;
case '"':
in_string = 1;
break;
case ',':
if (depth == 2) {
tok[tok_len] = '\0';
switch (field) {
case 0: cur.open_time = strtoll(tok, NULL, 10); break;
case 1: cur.open = strtod(tok, NULL); break;
case 2: cur.high = strtod(tok, NULL); break;
case 3: cur.low = strtod(tok, NULL); break;
case 4: cur.close = strtod(tok, NULL); break;
case 5: cur.volume = strtod(tok, NULL); break;
case 6: cur.close_time = strtoll(tok, NULL, 10); break;
default: break;
}
field++;
tok_len = 0;
}
break;
default:
if (depth == 2 && tok_len + 1 < sizeof(tok)) {
tok[tok_len++] = ch;
}
break;
}
}
return count;
}
static int cmp_open(const void *a, const void *b) {
int64_t x = ((const Kline *)a)->open_time;
int64_t y = ((const Kline *)b)->open_time;
return (x > y) - (x < y);
}
/* Paginate backwards until `target` closed candles are collected. */
static size_t collect(const char *interval, size_t target, int64_t now_ms,
Kline **out) {
Kline *rows = NULL;
size_t count = 0, cap = 0;
int64_t end_time = 0; /* 0 = no endTime cap (most recent page) */
int pages = 0;
for (;;) {
char url[256];
if (end_time > 0) {
snprintf(url, sizeof(url),
"https://api.binance.com/api/v3/klines?symbol=%s&interval=%s"
"&limit=%d&endTime=%lld",
SYMBOL, interval, PAGE_LIMIT, (long long)end_time);
} else {
snprintf(url, sizeof(url),
"https://api.binance.com/api/v3/klines?symbol=%s&interval=%s"
"&limit=%d",
SYMBOL, interval, PAGE_LIMIT);
}
char *body = curl_get(url);
if (body == NULL) {
free(rows);
return 0;
}
Kline *page = NULL;
size_t page_cap = 0;
size_t page_n = parse_klines(body, &page, 0, &page_cap);
free(body);
pages++;
int64_t oldest_open = INT64_MAX;
for (size_t i = 0; i < page_n; ++i) {
if (page[i].open_time < oldest_open) {
oldest_open = page[i].open_time;
}
if (page[i].close_time < now_ms) { /* keep only closed candles */
if (count == cap) {
cap = cap ? cap * 2 : 1024;
Kline *grown = (Kline *)realloc(rows, cap * sizeof(Kline));
if (grown == NULL) {
free(page);
free(rows);
return 0;
}
rows = grown;
}
rows[count++] = page[i];
}
}
fprintf(stderr, "\r %s: collected %llu candles over %d page(s)...",
interval, (unsigned long long)count, pages);
int page_full = page_n >= PAGE_LIMIT;
free(page);
if (count >= target || !page_full || oldest_open == INT64_MAX) {
break;
}
end_time = oldest_open - 1;
}
fprintf(stderr, "\n");
/* Sort ascending, drop duplicate open times, keep the most recent target. */
qsort(rows, count, sizeof(Kline), cmp_open);
size_t uniq = 0;
for (size_t i = 0; i < count; ++i) {
if (uniq == 0 || rows[i].open_time != rows[uniq - 1].open_time) {
rows[uniq++] = rows[i];
}
}
if (uniq > target) {
memmove(rows, rows + (uniq - target), target * sizeof(Kline));
uniq = target;
}
*out = rows;
return uniq;
}
static int write_csv(const char *path, const Kline *rows, size_t n) {
FILE *f = fopen(path, "w");
if (f == NULL) {
return 0;
}
fputs("timestamp,open,high,low,close,volume\n", f);
for (size_t i = 0; i < n; ++i) {
fprintf(f, "%lld,%g,%g,%g,%g,%g\n", (long long)rows[i].open_time,
rows[i].open, rows[i].high, rows[i].low, rows[i].close,
rows[i].volume);
}
fclose(f);
return 1;
}
int main(void) {
int64_t now_ms = (int64_t)time(NULL) * 1000;
printf("Fetching %s klines from Binance into %s\n", SYMBOL, WICKRA_DATA_DIR);
size_t n_datasets = sizeof(DATASETS) / sizeof(DATASETS[0]);
for (size_t d = 0; d < n_datasets; ++d) {
const Dataset *ds = &DATASETS[d];
Kline *rows = NULL;
size_t n = collect(ds->interval, ds->target, now_ms, &rows);
if (n == 0) {
fprintf(stderr, "Binance returned no closed candles for %s\n",
ds->interval);
free(rows);
return 1;
}
char path[256];
snprintf(path, sizeof(path), "%s/%s", WICKRA_DATA_DIR, ds->file);
if (!write_csv(path, rows, n)) {
fprintf(stderr, "could not write %s\n", path);
free(rows);
return 1;
}
printf(" %3s %6llu candles -> %s\n", ds->interval, (unsigned long long)n,
path);
free(rows);
}
printf("Done — %llu datasets written.\n", (unsigned long long)n_datasets);
return 0;
}
+154
View File
@@ -0,0 +1,154 @@
/* Live BTCUSDT indicator with the Wickra C ABI.
*
* The C counterpart of `examples/rust/src/bin/live_binance.rs`,
* `examples/python/live_trading.py` and `examples/node/live_trading.js`. Those
* stream Binance over a WebSocket; the C ABI ships only the indicators and no
* socket layer, so this example polls the Binance REST klines endpoint via the
* system `curl` once per interval and feeds each newly *closed* candle into a
* streaming RSI(14). Same "live feed -> incremental indicator" shape, no extra
* dependency.
*
* This example talks to the network and runs until interrupted (Ctrl+C), so it
* is built but NOT run as a ctest.
*
* Build (after `cargo build -p wickra-c --release`):
* cc examples/c/live_binance.c -I bindings/c/include -L target/release -lwickra -lm -o live_binance
* ./live_binance [SYMBOL]
*/
#include "wickra.h"
#include <math.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#ifdef _WIN32
#include <windows.h>
#define SLEEP_MS(ms) Sleep(ms)
#define POPEN _popen
#define PCLOSE _pclose
#else
#include <time.h>
#define POPEN popen
#define PCLOSE pclose
static void SLEEP_MS(long ms) {
struct timespec ts = {ms / 1000, (ms % 1000) * 1000000L};
nanosleep(&ts, NULL);
}
#endif
/* Run `curl <url>` and return stdout as a malloc'd, NUL-terminated buffer. */
static char *curl_get(const char *url) {
char cmd[512];
snprintf(cmd, sizeof(cmd),
"curl --silent --show-error --fail --max-time 15 \"%s\"", url);
FILE *p = POPEN(cmd, "r");
if (p == NULL) {
return NULL;
}
size_t cap = 1 << 15, len = 0;
char *buf = (char *)malloc(cap);
if (buf == NULL) {
PCLOSE(p);
return NULL;
}
size_t got;
char tmp[4096];
while ((got = fread(tmp, 1, sizeof(tmp), p)) > 0) {
if (len + got + 1 > cap) {
cap *= 2;
char *grown = (char *)realloc(buf, cap);
if (grown == NULL) {
free(buf);
PCLOSE(p);
return NULL;
}
buf = grown;
}
memcpy(buf + len, tmp, got);
len += got;
}
int rc = PCLOSE(p);
buf[len] = '\0';
if (rc != 0 || len == 0) {
free(buf);
return NULL;
}
return buf;
}
/* Extract the first kline's open time and close price from a klines response of
* the form [[openTime,"open","high","low","close",...],...]. Returns 1 on
* success. The first row (limit=2) is the most recent fully closed candle. */
static int first_kline(const char *body, int64_t *open_time, double *close) {
const char *s = strchr(body, '[');
if (s == NULL) {
return 0;
}
s = strchr(s + 1, '['); /* into the first row */
if (s == NULL) {
return 0;
}
s++;
char tok[64];
int field = 0;
while (*s && *s != ']') {
size_t tl = 0;
while (*s && *s != ',' && *s != ']') {
if (*s != '"' && tl + 1 < sizeof(tok)) {
tok[tl++] = *s;
}
s++;
}
tok[tl] = '\0';
if (field == 0) {
*open_time = strtoll(tok, NULL, 10);
} else if (field == 4) {
*close = strtod(tok, NULL);
return 1;
}
field++;
if (*s == ',') {
s++;
}
}
return 0;
}
int main(int argc, char **argv) {
const char *symbol = (argc > 1) ? argv[1] : "BTCUSDT";
char url[256];
snprintf(url, sizeof(url),
"https://api.binance.com/api/v3/klines?symbol=%s&interval=1m&limit=2",
symbol);
struct Rsi *rsi = wickra_rsi_new(14);
if (rsi == NULL) {
fprintf(stderr, "failed to create RSI\n");
return 1;
}
printf("Listening for %s 1m closes (REST poll, Ctrl+C to stop)...\n", symbol);
int64_t last_open = 0;
for (;;) {
char *body = curl_get(url);
if (body != NULL) {
int64_t open_time = 0;
double close = 0.0;
if (first_kline(body, &open_time, &close) && open_time != last_open) {
last_open = open_time;
double v = wickra_rsi_update(rsi, close);
if (isfinite(v)) {
printf("%s close=%.4f rsi=%.2f\n", symbol, close, v);
} else {
printf("%s close=%.4f rsi=...warmup\n", symbol, close);
}
fflush(stdout);
}
free(body);
}
SLEEP_MS(2000);
}
/* Unreachable in normal use (interrupted by Ctrl+C). */
}
+151
View File
@@ -0,0 +1,151 @@
/* Multi-timeframe indicators with the Wickra C ABI.
*
* The C counterpart of `examples/rust/src/bin/multi_timeframe.rs` and
* `examples/python/multi_timeframe.py`: read the bundled 1-minute BTCUSDT CSV
* (or a path on the command line), resample it to 5m / 15m / 1h / 4h / 1d, and
* print the last RSI(14), MACD(12,26,9) histogram and ADX(14) at each timeframe.
*
* The Rust/Python stack resamples through `wickra-data`; the C ABI ships only
* the indicators, so the time-bucket aggregation is done here (open = first,
* high = max, low = min, close = last, volume = sum per bucket).
*
* Build (after `cargo build -p wickra-c --release`):
* cc examples/c/multi_timeframe.c -I bindings/c/include -L target/release -lwickra -lm -o multi_timeframe
* ./multi_timeframe [path/to/1m.csv]
*/
#define WICKRA_CSV_IMPL
#include "wickra.h"
#include "wickra_csv.h"
#include <math.h>
#include <stdio.h>
#include <stdlib.h>
#ifndef WICKRA_DATA_DIR
#define WICKRA_DATA_DIR "../data"
#endif
#define ONE_MINUTE_MS 60000LL
/* Aggregate `in` into fixed-width time buckets of `tf_ms`. Writes the bucketed
* candles into the caller-owned `out` (capacity >= count) and returns how many
* buckets were produced. */
static size_t resample(const WickraCandle *in, size_t count, int64_t tf_ms,
WickraCandle *out) {
size_t produced = 0;
int open_bucket = 0;
int64_t bucket_start = 0;
WickraCandle cur = {0};
for (size_t i = 0; i < count; ++i) {
int64_t b = in[i].timestamp - (in[i].timestamp % tf_ms);
if (!open_bucket || b != bucket_start) {
if (open_bucket) {
out[produced++] = cur;
}
bucket_start = b;
cur = in[i];
cur.timestamp = b;
open_bucket = 1;
} else {
if (in[i].high > cur.high) {
cur.high = in[i].high;
}
if (in[i].low < cur.low) {
cur.low = in[i].low;
}
cur.close = in[i].close;
cur.volume += in[i].volume;
}
}
if (open_bucket) {
out[produced++] = cur;
}
return produced;
}
static void summarize(const char *label, const WickraCandle *candles, size_t n) {
if (n == 0) {
printf(" %-5s (empty)\n", label);
return;
}
struct Rsi *rsi = wickra_rsi_new(14);
struct MacdIndicator *macd = wickra_macd_indicator_new(12, 26, 9);
struct Adx *adx = wickra_adx_new(14);
double last_rsi = NAN;
double last_hist = NAN;
double last_adx = NAN;
for (size_t i = 0; i < n; ++i) {
const WickraCandle *c = &candles[i];
double r = wickra_rsi_update(rsi, c->close);
if (isfinite(r)) {
last_rsi = r;
}
WickraMacdOutput m;
if (wickra_macd_indicator_update(macd, c->close, &m)) {
last_hist = m.histogram;
}
WickraAdxOutput a;
if (wickra_adx_update(adx, c->open, c->high, c->low, c->close, c->volume,
c->timestamp, &a)) {
last_adx = a.adx;
}
}
char b_rsi[16], b_hist[16], b_adx[16];
if (isfinite(last_rsi)) {
snprintf(b_rsi, sizeof(b_rsi), "%6.2f", last_rsi);
} else {
snprintf(b_rsi, sizeof(b_rsi), " --");
}
if (isfinite(last_hist)) {
snprintf(b_hist, sizeof(b_hist), "%+6.2f", last_hist);
} else {
snprintf(b_hist, sizeof(b_hist), " -- ");
}
if (isfinite(last_adx)) {
snprintf(b_adx, sizeof(b_adx), "%6.2f", last_adx);
} else {
snprintf(b_adx, sizeof(b_adx), " --");
}
printf(" %-5s bars=%5llu last_close=%10.2f rsi=%s macd_hist=%s adx=%s\n",
label, (unsigned long long)n, candles[n - 1].close, b_rsi, b_hist, b_adx);
wickra_rsi_free(rsi);
wickra_macd_indicator_free(macd);
wickra_adx_free(adx);
}
int main(int argc, char **argv) {
const char *path = (argc > 1) ? argv[1] : WICKRA_DATA_DIR "/btcusdt-1m.csv";
WickraCandle *ones = NULL;
size_t n = wickra_load_csv(path, &ones);
if (n == 0) {
fprintf(stderr, "multi_timeframe: no candles read from %s\n", path);
return 1;
}
/* Resampling only ever produces fewer candles than the 1m source. */
WickraCandle *buf = (WickraCandle *)malloc(n * sizeof(*buf));
if (buf == NULL) {
fprintf(stderr, "multi_timeframe: allocation failed\n");
free(ones);
return 1;
}
printf("Multi-timeframe view of %s\n", path);
summarize("1m", ones, n);
const struct {
const char *label;
int64_t minutes;
} frames[] = {{"5m", 5}, {"15m", 15}, {"1h", 60}, {"4h", 240}, {"1d", 1440}};
for (size_t f = 0; f < sizeof(frames) / sizeof(frames[0]); ++f) {
size_t m = resample(ones, n, frames[f].minutes * ONE_MINUTE_MS, buf);
summarize(frames[f].label, buf, m);
}
free(buf);
free(ones);
return 0;
}
+156
View File
@@ -0,0 +1,156 @@
/* Parallel multi-asset indicator computation with the Wickra C ABI.
*
* The C counterpart of `examples/rust/src/bin/parallel_assets.rs` (rayon) and
* `examples/python/parallel_assets.py` (the Rust extension drops the GIL). The C
* ABI is the parallelization primitive itself: each `wickra_<ind>_new` handle is
* independent, so the caller fans assets out across threads, one fresh handle per
* asset. Here a serial baseline is compared against an OpenMP `parallel for`.
*
* If the compiler has no OpenMP support the "parallel" pass simply runs the same
* single-threaded loop (speedup ~1x) the result is honest either way.
*
* Build (after `cargo build -p wickra-c --release`):
* cc -fopenmp examples/c/parallel_assets.c -I bindings/c/include -L target/release -lwickra -lm -o parallel_assets
* ./parallel_assets --assets 200 --bars 5000 --indicator sma
*/
#include "wickra.h"
#include <math.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#ifdef _OPENMP
#include <omp.h>
#endif
/* Wall-clock seconds (not CPU time, so threaded speedup is measured correctly). */
static double now_seconds(void) {
#ifdef _OPENMP
return omp_get_wtime();
#else
struct timespec ts;
timespec_get(&ts, TIME_UTC);
return (double)ts.tv_sec + (double)ts.tv_nsec * 1e-9;
#endif
}
typedef enum { IND_SMA, IND_RSI } Which;
/* Deterministic synthetic (assets, bars) panel, flat row-major. Each asset uses
* an independent LCG seed so the series are uncorrelated but reproducible. */
static void synthesize_panel(double *panel, size_t assets, size_t bars) {
for (size_t a = 0; a < assets; ++a) {
double price = 100.0;
uint32_t state = (uint32_t)(1234567u + a) * 2654435761u;
for (size_t b = 0; b < bars; ++b) {
state = (state * 1103515245u + 12345u) & 0x7FFFFFFFu;
double r = (double)state / (double)0x7FFFFFFFu;
price += (r - 0.5) * 0.4;
panel[a * bars + b] = price;
}
}
}
/* Run one asset's series through a fresh handle into its output slice. */
static void run_one(Which which, const double *prices, double *out, size_t bars) {
if (which == IND_SMA) {
struct Sma *h = wickra_sma_new(14);
wickra_sma_batch(h, prices, out, bars);
wickra_sma_free(h);
} else {
struct Rsi *h = wickra_rsi_new(14);
wickra_rsi_batch(h, prices, out, bars);
wickra_rsi_free(h);
}
}
int main(int argc, char **argv) {
size_t assets = 200;
size_t bars = 5000;
Which which = IND_SMA;
for (int i = 1; i < argc; ++i) {
if (strcmp(argv[i], "--assets") == 0 && i + 1 < argc) {
assets = (size_t)strtoul(argv[++i], NULL, 10);
} else if (strcmp(argv[i], "--bars") == 0 && i + 1 < argc) {
bars = (size_t)strtoul(argv[++i], NULL, 10);
} else if (strcmp(argv[i], "--indicator") == 0 && i + 1 < argc) {
++i;
if (strcmp(argv[i], "sma") == 0) {
which = IND_SMA;
} else if (strcmp(argv[i], "rsi") == 0) {
which = IND_RSI;
} else {
fprintf(stderr, "--indicator: expected 'sma' or 'rsi'\n");
return 1;
}
} else {
fprintf(stderr, "usage: parallel_assets [--assets N] [--bars N] "
"[--indicator sma|rsi]\n");
return 1;
}
}
if (assets == 0 || bars == 0) {
fprintf(stderr, "--assets and --bars must be positive\n");
return 1;
}
const char *ind_name = (which == IND_SMA) ? "sma" : "rsi";
printf("Generating %llux%llu synthetic panel...\n", (unsigned long long)assets,
(unsigned long long)bars);
double *panel = (double *)malloc(assets * bars * sizeof(*panel));
double *serial = (double *)malloc(assets * bars * sizeof(*serial));
double *parallel = (double *)malloc(assets * bars * sizeof(*parallel));
if (panel == NULL || serial == NULL || parallel == NULL) {
fprintf(stderr, "allocation failed\n");
return 1;
}
synthesize_panel(panel, assets, bars);
double t0 = now_seconds();
for (size_t a = 0; a < assets; ++a) {
run_one(which, &panel[a * bars], &serial[a * bars], bars);
}
double t_serial = now_seconds() - t0;
printf("Serial: %8.3f s (%llu assets, indicator=%s)\n", t_serial,
(unsigned long long)assets, ind_name);
/* The loop variable is declared outside the `for` and the bound is a plain
* variable: MSVC's OpenMP 2.0 rejects an in-init declaration or a cast in
* the condition (error C3015). */
long a;
long asset_count = (long)assets;
t0 = now_seconds();
#ifdef _OPENMP
#pragma omp parallel for schedule(static)
#endif
for (a = 0; a < asset_count; ++a) {
run_one(which, &panel[(size_t)a * bars], &parallel[(size_t)a * bars], bars);
}
double t_parallel = now_seconds() - t0;
double denom = t_parallel > 1e-9 ? t_parallel : 1e-9;
#ifdef _OPENMP
printf("Parallel: %8.3f s (OpenMP, %d threads, speedup ~%.2fx)\n", t_parallel,
omp_get_max_threads(), t_serial / denom);
#else
printf("Parallel: %8.3f s (no OpenMP at build time — serial, speedup ~%.2fx)\n",
t_parallel, t_serial / denom);
#endif
/* The parallel run must reproduce the serial results exactly. */
for (size_t i = 0; i < assets * bars; ++i) {
int both_nan = isnan(serial[i]) && isnan(parallel[i]);
if (!both_nan && serial[i] != parallel[i]) {
fprintf(stderr, "mismatch at %llu: serial=%g parallel=%g\n",
(unsigned long long)i, serial[i], parallel[i]);
return 1;
}
}
printf("Parallel results match serial results — OK.\n");
free(panel);
free(serial);
free(parallel);
return 0;
}
+64
View File
@@ -0,0 +1,64 @@
/* Smoke test for the Wickra C ABI.
*
* This is the one test the Rust unit tests structurally cannot do: it links a
* foreign C consumer against the generated `wickra.h` + the compiled library and
* exercises the real FFI boundary (symbol export, header correctness, opaque
* handle, pointer ownership, `_free`). If this passes, every C-capable language
* (C, C++, Go, C#, Java, R) can link the same way.
*
* Build (from the workspace root, after `cargo build -p wickra-c --release`):
* cc examples/c/smoke.c -I bindings/c/include target/release/<lib> -lm -o smoke
*/
#include "wickra.h"
#include <math.h>
#include <stdio.h>
static int near(double a, double b) { return fabs(a - b) < 1e-9; }
int main(void) {
struct Sma *sma = wickra_sma_new(3);
if (sma == NULL) {
printf("FAIL: wickra_sma_new returned NULL\n");
return 1;
}
/* SMA(3): first two outputs are warmup (NaN), then the trailing mean. */
double in[5] = {1.0, 2.0, 3.0, 4.0, 5.0};
double r0 = wickra_sma_update(sma, in[0]); /* NaN (1/3) */
double r1 = wickra_sma_update(sma, in[1]); /* NaN (2/3) */
double r2 = wickra_sma_update(sma, in[2]); /* 2.0 (1+2+3)/3 */
double r3 = wickra_sma_update(sma, in[3]); /* 3.0 (2+3+4)/3 */
if (!isnan(r0) || !isnan(r1)) {
printf("FAIL: warmup not NaN (%f %f)\n", r0, r1);
return 1;
}
if (!near(r2, 2.0) || !near(r3, 3.0)) {
printf("FAIL: streaming values (%f %f), expected (2.0 3.0)\n", r2, r3);
return 1;
}
/* Batch over a reset instance must reproduce the streaming result. */
wickra_sma_reset(sma);
double out[5];
wickra_sma_batch(sma, in, out, 5);
if (!isnan(out[0]) || !isnan(out[1]) ||
!near(out[2], 2.0) || !near(out[3], 3.0) || !near(out[4], 4.0)) {
printf("FAIL: batch mismatch (%f %f %f %f %f)\n",
out[0], out[1], out[2], out[3], out[4]);
return 1;
}
/* NULL handle is a defined no-op / NaN, never a crash. */
if (!isnan(wickra_sma_update(NULL, 1.0))) {
printf("FAIL: NULL update did not return NaN\n");
return 1;
}
wickra_sma_reset(NULL);
wickra_sma_free(NULL);
wickra_sma_free(sma);
printf("OK: wickra C ABI smoke passed (SMA streaming + batch + reset + NULL-safety + free)\n");
return 0;
}
+36
View File
@@ -0,0 +1,36 @@
// C++ smoke test for the Wickra C ABI via the optional RAII wrapper (`wickra.hpp`).
//
// Validates that the header compiles as C++ and that `wickra::Handle` constructs,
// moves, and frees correctly across the boundary.
#include "wickra.hpp"
#include <cmath>
#include <cstdio>
#include <utility>
int main() {
wickra::Handle<Sma, wickra_sma_free> sma(wickra_sma_new(3));
if (!sma) {
std::puts("FAIL: wickra_sma_new returned null");
return 1;
}
(void)wickra_sma_update(sma.get(), 1.0);
(void)wickra_sma_update(sma.get(), 2.0);
double value = wickra_sma_update(sma.get(), 3.0);
if (std::fabs(value - 2.0) > 1e-9) {
std::printf("FAIL: SMA(3) value %.6f, expected 2.0\n", value);
return 1;
}
// Move transfers ownership; the moved-from handle must not double-free.
wickra::Handle<Sma, wickra_sma_free> moved(std::move(sma));
if (static_cast<bool>(sma) || !static_cast<bool>(moved)) {
std::puts("FAIL: move semantics");
return 1;
}
std::puts("OK: wickra C++ RAII smoke passed (Handle construct + move + auto-free)");
return 0;
}
+137
View File
@@ -0,0 +1,137 @@
/* Strategy example: Bollinger-Squeeze breakout with ATR stop (Wickra C ABI).
*
* Enters long when the Bollinger Bandwidth has just printed a fresh 6-month low
* (the squeeze) and price closes above the upper band (the release). Exits when
* price closes below entry minus 2*ATR(14), or when the upper band trails back
* below the entry price (the squeeze has played out). 0.1% fees per trade. The
* C counterpart of `examples/rust/src/bin/strategy_bollinger_squeeze.rs`.
*
* Educational example. NOT a live trading recommendation. Uses the checked-in
* `examples/data/btcusdt-1d.csv` dataset because daily bars give an
* interpretable "6-month low" lookback (~180 bars).
*
* Build (after `cargo build -p wickra-c --release`):
* cc examples/c/strategy_bollinger_squeeze.c -I bindings/c/include -L target/release -lwickra -lm -o strat_bb
*/
#define WICKRA_CSV_IMPL
#define WICKRA_STRATEGY_IMPL
#include "wickra.h"
#include "wickra_csv.h"
#include "wickra_strategy.h"
#include <math.h>
#include <stdio.h>
#include <stdlib.h>
#ifndef WICKRA_DATA_DIR
#define WICKRA_DATA_DIR "../data"
#endif
#define FEE 0.001
#define BB_PERIOD 20
#define BB_K 2.0
#define ATR_PERIOD 14
#define ATR_STOP_MULT 2.0
#define SQUEEZE_LOOKBACK 180 /* ~6 months of daily bars */
int main(int argc, char **argv) {
const char *path = (argc > 1) ? argv[1] : WICKRA_DATA_DIR "/btcusdt-1d.csv";
WickraCandle *candles = NULL;
size_t n = wickra_load_csv(path, &candles);
if (n < SQUEEZE_LOOKBACK + BB_PERIOD) {
fprintf(stderr, "dataset has only %llu bars; need at least %d\n",
(unsigned long long)n, SQUEEZE_LOOKBACK + BB_PERIOD);
free(candles);
return 1;
}
struct BollingerBands *bb = wickra_bollinger_bands_new(BB_PERIOD, BB_K);
struct Atr *atr = wickra_atr_new(ATR_PERIOD);
double *trades = (double *)malloc(n * sizeof(*trades));
double *equity_curve = (double *)malloc(n * sizeof(*equity_curve));
/* Circular buffer of recent bandwidth values for the squeeze lookback. */
double bw_window[SQUEEZE_LOOKBACK];
size_t bw_len = 0, bw_head = 0;
if (bb == NULL || atr == NULL || trades == NULL || equity_curve == NULL) {
fprintf(stderr, "allocation failed\n");
return 1;
}
int in_position = 0;
double entry_price = 0.0, stop_level = 0.0;
size_t n_trades = 0;
double equity = 1.0;
for (size_t i = 0; i < n; ++i) {
const WickraCandle *c = &candles[i];
double price = c->close;
WickraBollingerOutput b;
int bb_ready = wickra_bollinger_bands_update(bb, price, &b);
double a = wickra_atr_update(atr, c->open, c->high, c->low, c->close,
c->volume, c->timestamp);
equity_curve[i] = in_position ? equity * (price / entry_price) : equity;
if (!bb_ready || !isfinite(a)) {
continue;
}
double bandwidth =
fabs(b.middle) > 1e-15 ? (b.upper - b.lower) / b.middle : NAN;
if (isfinite(bandwidth)) {
if (bw_len == SQUEEZE_LOOKBACK) {
bw_window[bw_head] = bandwidth;
bw_head = (bw_head + 1) % SQUEEZE_LOOKBACK;
} else {
bw_window[bw_len++] = bandwidth;
}
}
if (bw_len < SQUEEZE_LOOKBACK || !isfinite(bandwidth)) {
continue;
}
double min_bw = INFINITY;
for (size_t k = 0; k < bw_len; ++k) {
if (bw_window[k] < min_bw) {
min_bw = bw_window[k];
}
}
if (in_position) {
int stop_hit = price < stop_level;
int upper_collapse = b.upper < entry_price;
if (stop_hit || upper_collapse) {
double trade_ret = price / entry_price - 1.0;
trades[n_trades++] = trade_ret;
equity *= (1.0 + trade_ret) * (1.0 - FEE);
in_position = 0;
}
} else {
int is_new_low = fabs(bandwidth - min_bw) < 1e-12;
int breakout = price > b.upper;
if (is_new_low && breakout) {
entry_price = price;
stop_level = price - ATR_STOP_MULT * a;
equity *= 1.0 - FEE;
in_position = 1;
}
}
}
if (in_position) {
double trade_ret = candles[n - 1].close / entry_price - 1.0;
trades[n_trades++] = trade_ret;
equity *= (1.0 + trade_ret) * (1.0 - FEE);
}
wickra_print_summary("Bollinger Squeeze Breakout (1d, BTCUSDT)", candles[0].close,
candles[n - 1].close, n, trades, n_trades, equity,
equity_curve, n);
wickra_bollinger_bands_free(bb);
wickra_atr_free(atr);
free(trades);
free(equity_curve);
free(candles);
return 0;
}
+107
View File
@@ -0,0 +1,107 @@
/* Strategy example: MACD crossover with ADX trend-strength filter (Wickra C ABI).
*
* Long-only trend follower. Entries fire when the MACD line crosses above the
* signal line while ADX(14) > 20 (a market with at least mild directional
* strength); exits on the opposite MACD crossover regardless of ADX. 0.1% fees
* per trade. The C counterpart of `examples/rust/src/bin/strategy_macd_adx.rs`.
*
* The ADX filter is the point: pure MACD on sideways markets chops in and out;
* gating entries on directional strength cuts the worst losing streak.
*
* Educational example. NOT a live trading recommendation. Uses the checked-in
* `examples/data/btcusdt-1h.csv` dataset.
*
* Build (after `cargo build -p wickra-c --release`):
* cc examples/c/strategy_macd_adx.c -I bindings/c/include -L target/release -lwickra -lm -o strat_macd
*/
#define WICKRA_CSV_IMPL
#define WICKRA_STRATEGY_IMPL
#include "wickra.h"
#include "wickra_csv.h"
#include "wickra_strategy.h"
#include <math.h>
#include <stdio.h>
#include <stdlib.h>
#ifndef WICKRA_DATA_DIR
#define WICKRA_DATA_DIR "../data"
#endif
#define FEE 0.001
#define ADX_FLOOR 20.0
int main(int argc, char **argv) {
const char *path = (argc > 1) ? argv[1] : WICKRA_DATA_DIR "/btcusdt-1h.csv";
WickraCandle *candles = NULL;
size_t n = wickra_load_csv(path, &candles);
if (n == 0) {
fprintf(stderr, "CSV is empty: %s\n", path);
return 1;
}
struct MacdIndicator *macd = wickra_macd_indicator_new(12, 26, 9);
struct Adx *adx = wickra_adx_new(14);
double *trades = (double *)malloc(n * sizeof(*trades));
double *equity_curve = (double *)malloc(n * sizeof(*equity_curve));
if (macd == NULL || adx == NULL || trades == NULL || equity_curve == NULL) {
fprintf(stderr, "allocation failed\n");
return 1;
}
int in_position = 0;
double entry_price = 0.0;
size_t n_trades = 0;
double equity = 1.0;
/* Previous histogram sign to detect MACD-line crossovers: -1 unset, 0/1 sign. */
int prev_hist_sign = -1;
for (size_t i = 0; i < n; ++i) {
const WickraCandle *c = &candles[i];
double price = c->close;
WickraMacdOutput m;
int macd_ready = wickra_macd_indicator_update(macd, price, &m);
WickraAdxOutput a;
int adx_ready = wickra_adx_update(adx, c->open, c->high, c->low, c->close,
c->volume, c->timestamp, &a);
equity_curve[i] = in_position ? equity * (price / entry_price) : equity;
if (!macd_ready || !adx_ready) {
continue;
}
int hist_sign = m.histogram > 0.0 ? 1 : 0;
int cross_up = prev_hist_sign == 0 && hist_sign == 1;
int cross_down = prev_hist_sign == 1 && hist_sign == 0;
prev_hist_sign = hist_sign;
if (!in_position && cross_up && a.adx > ADX_FLOOR) {
entry_price = price;
equity *= 1.0 - FEE;
in_position = 1;
} else if (in_position && cross_down) {
double trade_ret = price / entry_price - 1.0;
trades[n_trades++] = trade_ret;
equity *= (1.0 + trade_ret) * (1.0 - FEE);
in_position = 0;
}
}
if (in_position) {
double trade_ret = candles[n - 1].close / entry_price - 1.0;
trades[n_trades++] = trade_ret;
equity *= (1.0 + trade_ret) * (1.0 - FEE);
}
wickra_print_summary("MACD + ADX Trend Filter (1h, BTCUSDT)", candles[0].close,
candles[n - 1].close, n, trades, n_trades, equity,
equity_curve, n);
wickra_macd_indicator_free(macd);
wickra_adx_free(adx);
free(trades);
free(equity_curve);
free(candles);
return 0;
}
+95
View File
@@ -0,0 +1,95 @@
/* Strategy example: RSI mean-reversion on hourly BTCUSDT data (Wickra C ABI).
*
* Goes long when RSI(14) crosses below 30 (oversold), exits when RSI crosses
* above 70 (overbought). Position is binary (full-in / full-out), fees are 0.1%
* per trade (Binance maker tier), no stop-loss. The C counterpart of
* `examples/rust/src/bin/strategy_rsi_mean_reversion.rs`.
*
* Educational example. NOT a recommended trading strategy the point is to
* show how a Wickra streaming indicator wires into a signal -> fill -> PnL ->
* equity loop. Uses the checked-in `examples/data/btcusdt-1h.csv` dataset.
*
* Build (after `cargo build -p wickra-c --release`):
* cc examples/c/strategy_rsi_mean_reversion.c -I bindings/c/include -L target/release -lwickra -lm -o strat_rsi
*/
#define WICKRA_CSV_IMPL
#define WICKRA_STRATEGY_IMPL
#include "wickra.h"
#include "wickra_csv.h"
#include "wickra_strategy.h"
#include <math.h>
#include <stdio.h>
#include <stdlib.h>
#ifndef WICKRA_DATA_DIR
#define WICKRA_DATA_DIR "../data"
#endif
#define FEE 0.001
#define RSI_PERIOD 14
#define OVERSOLD 30.0
#define OVERBOUGHT 70.0
int main(int argc, char **argv) {
const char *path = (argc > 1) ? argv[1] : WICKRA_DATA_DIR "/btcusdt-1h.csv";
WickraCandle *candles = NULL;
size_t n = wickra_load_csv(path, &candles);
if (n < RSI_PERIOD * 4) {
fprintf(stderr, "dataset too small: %llu\n", (unsigned long long)n);
free(candles);
return 1;
}
struct Rsi *rsi = wickra_rsi_new(RSI_PERIOD);
double *trades = (double *)malloc(n * sizeof(*trades));
double *equity_curve = (double *)malloc(n * sizeof(*equity_curve));
if (rsi == NULL || trades == NULL || equity_curve == NULL) {
fprintf(stderr, "allocation failed\n");
return 1;
}
int in_position = 0;
double entry_price = 0.0;
size_t n_trades = 0;
double equity = 1.0;
for (size_t i = 0; i < n; ++i) {
double price = candles[i].close;
double r = wickra_rsi_update(rsi, price);
/* Mark-to-market so the equity curve moves bar-by-bar between trades. */
equity_curve[i] = in_position ? equity * (price / entry_price) : equity;
if (!isfinite(r)) {
continue;
}
if (!in_position && r < OVERSOLD) {
entry_price = price;
equity *= 1.0 - FEE;
in_position = 1;
} else if (in_position && r > OVERBOUGHT) {
double trade_ret = price / entry_price - 1.0;
trades[n_trades++] = trade_ret;
equity *= (1.0 + trade_ret) * (1.0 - FEE);
in_position = 0;
}
}
/* Close any still-open trade at the last bar so metrics include it. */
if (in_position) {
double trade_ret = candles[n - 1].close / entry_price - 1.0;
trades[n_trades++] = trade_ret;
equity *= (1.0 + trade_ret) * (1.0 - FEE);
}
wickra_print_summary("RSI Mean-Reversion (1h, BTCUSDT)", candles[0].close,
candles[n - 1].close, n, trades, n_trades, equity,
equity_curve, n);
wickra_rsi_free(rsi);
free(trades);
free(equity_curve);
free(candles);
return 0;
}
+110
View File
@@ -0,0 +1,110 @@
/* Streaming indicators with the Wickra C ABI.
*
* Feeds a synthetic price series through several indicators tick by tick the
* same O(1)-per-update model a live trading bot would use and prints a status
* line once every indicator has warmed up. The C counterpart of
* `examples/rust/src/bin/streaming.rs`, `examples/python/streaming.py` and
* `examples/node/streaming.js`, using the same seeded LCG so a side-by-side run
* produces visibly comparable streams.
*
* Build (after `cargo build -p wickra-c --release`):
* cc examples/c/streaming.c -I bindings/c/include -L target/release -lwickra -lm -o streaming
*/
#include "wickra.h"
#include <math.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define DEFAULT_TICKS 120
/* Deterministic synthetic series matching the sibling examples' seeded LCG. */
static void make_series(double *prices, size_t n) {
uint64_t seed = 1234567u;
for (size_t t = 0; t < n; ++t) {
seed = (seed * 1103515245u + 12345u) & 0x7FFFFFFFu;
double rnd = (double)seed / (double)0x7FFFFFFFu;
double tf = (double)t;
prices[t] = 100.0 + tf * 0.05 + sin(tf * 0.07) * 8.0 + cos(tf * 0.21) * 3.0 +
(rnd - 0.5);
}
}
/* Format an indicator value, rendering warmup (NaN) as a dashed placeholder. */
static void fmt(char *buf, size_t buflen, double v) {
if (isfinite(v)) {
snprintf(buf, buflen, "%7.2f", v);
} else {
snprintf(buf, buflen, " -- ");
}
}
int main(int argc, char **argv) {
size_t ticks = DEFAULT_TICKS;
if (argc == 3 && strcmp(argv[1], "--ticks") == 0) {
long parsed = strtol(argv[2], NULL, 10);
if (parsed <= 0) {
fprintf(stderr, "--ticks must be positive\n");
return 1;
}
ticks = (size_t)parsed;
} else if (argc != 1) {
fprintf(stderr, "usage: streaming [--ticks N]\n");
return 1;
}
struct Sma *sma = wickra_sma_new(20);
struct Ema *ema = wickra_ema_new(20);
struct Rsi *rsi = wickra_rsi_new(14);
struct MacdIndicator *macd = wickra_macd_indicator_new(12, 26, 9);
double *prices = (double *)malloc(ticks * sizeof(*prices));
if (sma == NULL || ema == NULL || rsi == NULL || macd == NULL || prices == NULL) {
fprintf(stderr, "allocation failed\n");
return 1;
}
make_series(prices, ticks);
printf("Wickra streaming indicator demo (C)\n\n");
size_t signals = 0;
for (size_t t = 0; t < ticks; ++t) {
double price = prices[t];
double sv = wickra_sma_update(sma, price);
double ev = wickra_ema_update(ema, price);
double rv = wickra_rsi_update(rsi, price);
WickraMacdOutput m;
int macd_ready = wickra_macd_indicator_update(macd, price, &m);
/* Only act once every indicator has produced a value. */
if (!isfinite(sv) || !isfinite(ev) || !isfinite(rv) || !macd_ready) {
continue;
}
int overbought = rv > 70.0 && m.histogram < 0.0;
int oversold = rv < 30.0 && m.histogram > 0.0;
const char *tag = overbought ? "SELL?" : (oversold ? "BUY? " : " ");
if (overbought || oversold) {
signals++;
}
char b_price[16], b_sma[16], b_ema[16], b_rsi[16], b_hist[16];
fmt(b_price, sizeof(b_price), price);
fmt(b_sma, sizeof(b_sma), sv);
fmt(b_ema, sizeof(b_ema), ev);
fmt(b_rsi, sizeof(b_rsi), rv);
fmt(b_hist, sizeof(b_hist), m.histogram);
printf("t=%3llu price=%s sma=%s ema=%s rsi=%s macd_hist=%s %s\n",
(unsigned long long)t, b_price, b_sma, b_ema, b_rsi, b_hist, tag);
}
printf("\nDone — %llu candidate signal(s) over %llu ticks.\n",
(unsigned long long)signals, (unsigned long long)ticks);
wickra_sma_free(sma);
wickra_ema_free(ema);
wickra_rsi_free(rsi);
wickra_macd_indicator_free(macd);
free(prices);
return 0;
}
+90
View File
@@ -0,0 +1,90 @@
/* Shared OHLCV CSV loader for the Wickra C examples.
*
* The C ABI exposes only the indicators, not the wickra-data IO layer, so the
* examples read CSV themselves. This header-only helper is the C counterpart of
* `wickra_data::csv::CandleReader` used by the Rust examples: it parses the
* standard `timestamp,open,high,low,close,volume` files shipped under
* `examples/data/`.
*
* Header-only: define WICKRA_CSV_IMPL in exactly one translation unit (each
* example is a single .c file, so it just defines it before including this).
*/
#ifndef WICKRA_CSV_H
#define WICKRA_CSV_H
#include <stddef.h>
#include <stdint.h>
typedef struct WickraCandle {
int64_t timestamp;
double open;
double high;
double low;
double close;
double volume;
} WickraCandle;
/* Load an OHLCV CSV into a malloc'd array. Returns the candle count and stores
* the array in *out (caller frees with free()). Returns 0 and leaves *out NULL
* on any error (missing file, no parseable rows). A leading header line whose
* first field is non-numeric is skipped. */
size_t wickra_load_csv(const char *path, WickraCandle **out);
#ifdef WICKRA_CSV_IMPL
#include <stdio.h>
#include <stdlib.h>
size_t wickra_load_csv(const char *path, WickraCandle **out) {
*out = NULL;
FILE *f = fopen(path, "r");
if (f == NULL) {
fprintf(stderr, "wickra_load_csv: cannot open %s\n", path);
return 0;
}
size_t cap = 1024;
size_t n = 0;
WickraCandle *rows = (WickraCandle *)malloc(cap * sizeof(*rows));
if (rows == NULL) {
fclose(f);
return 0;
}
char line[512];
while (fgets(line, (int)sizeof(line), f) != NULL) {
WickraCandle c;
long long ts = 0;
/* sscanf returns the number of fields successfully matched. A header
* row ("timestamp,...") matches 0 and is skipped. */
int matched = sscanf(line, "%lld,%lf,%lf,%lf,%lf,%lf", &ts, &c.open,
&c.high, &c.low, &c.close, &c.volume);
if (matched != 6) {
continue;
}
c.timestamp = (int64_t)ts;
if (n == cap) {
cap *= 2;
WickraCandle *grown = (WickraCandle *)realloc(rows, cap * sizeof(*rows));
if (grown == NULL) {
free(rows);
fclose(f);
return 0;
}
rows = grown;
}
rows[n++] = c;
}
fclose(f);
if (n == 0) {
free(rows);
return 0;
}
*out = rows;
return n;
}
#endif /* WICKRA_CSV_IMPL */
#endif /* WICKRA_CSV_H */
+92
View File
@@ -0,0 +1,92 @@
/* Shared equity-curve summary for the Wickra C strategy examples.
*
* The Rust strategy examples repeat their `print_summary` per file; in C the
* presentation is factored into this header so each strategy .c file stays
* focused on its signal logic. Pure reporting no indicator state.
*
* Header-only: define WICKRA_STRATEGY_IMPL in exactly one translation unit.
*/
#ifndef WICKRA_STRATEGY_H
#define WICKRA_STRATEGY_H
#include <stddef.h>
/* Print a one-screen summary of a strategy run: returns vs buy & hold, trade
* win/loss counts, max drawdown, per-trade Sharpe, best/worst trade. */
void wickra_print_summary(const char *name, double first_price, double last_price,
size_t bars, const double *closed_trades, size_t n_trades,
double final_equity, const double *equity_curve,
size_t n_curve);
#ifdef WICKRA_STRATEGY_IMPL
#include <math.h>
#include <stdio.h>
void wickra_print_summary(const char *name, double first_price, double last_price,
size_t bars, const double *closed_trades, size_t n_trades,
double final_equity, const double *equity_curve,
size_t n_curve) {
double buy_hold = last_price / first_price;
double strat_return = final_equity - 1.0;
double bh_return = buy_hold - 1.0;
size_t wins = 0, losses = 0;
double best = -INFINITY, worst = INFINITY;
double sum_ret = 0.0, sum_sq = 0.0;
for (size_t i = 0; i < n_trades; ++i) {
double r = closed_trades[i];
if (r > 0.0) {
wins++;
} else if (r < 0.0) {
losses++;
}
if (r > best) {
best = r;
}
if (r < worst) {
worst = r;
}
sum_ret += r;
sum_sq += r * r;
}
double n = (double)n_trades;
double mean_ret = n > 0.0 ? sum_ret / n : 0.0;
double var_ret = n > 1.0 ? (sum_sq - n * mean_ret * mean_ret) / (n - 1.0) : 0.0;
double sharpe = var_ret > 0.0 ? mean_ret / sqrt(var_ret) : 0.0;
if (n_trades == 0) {
best = 0.0;
worst = 0.0;
}
double peak = n_curve > 0 ? equity_curve[0] : 1.0;
double max_dd = 0.0;
for (size_t i = 0; i < n_curve; ++i) {
if (equity_curve[i] > peak) {
peak = equity_curve[i];
}
double dd = (peak - equity_curve[i]) / peak;
if (dd > max_dd) {
max_dd = dd;
}
}
printf("=== %s ===\n", name);
printf("Bars: %llu\n", (unsigned long long)bars);
printf("Trades: %llu (W%llu / L%llu)\n", (unsigned long long)n_trades,
(unsigned long long)wins, (unsigned long long)losses);
printf("Strategy return: %+.2f%%\n", strat_return * 100.0);
printf("Buy & Hold return: %+.2f%%\n", bh_return * 100.0);
printf("Excess over BH: %+.2f%%\n", (strat_return - bh_return) * 100.0);
printf("Max drawdown: %.2f%%\n", max_dd * 100.0);
printf("Per-trade Sharpe: %.2f (mean %+.4f, stddev %.4f)\n", sharpe,
mean_ret, sqrt(var_ret));
printf("Best / worst trade: %+.2f%% / %+.2f%%\n", best * 100.0, worst * 100.0);
printf("\n");
printf("NOTE: Educational example — fees, slippage, funding costs and tax "
"effects are simplified or omitted. Past performance is not indicative "
"of future results.\n");
}
#endif /* WICKRA_STRATEGY_IMPL */
#endif /* WICKRA_STRATEGY_H */
+7
View File
@@ -0,0 +1,7 @@
# .NET build output
bin/
obj/
*.user
# Data fetched at runtime by fetch_btcusdt
**/data/
+19
View File
@@ -0,0 +1,19 @@
<Project>
<!-- Shared settings + references for every C# example. Each example project
only declares <OutputType>Exe</OutputType>. -->
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<LangVersion>latest</LangVersion>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\bindings\csharp\Wickra\Wickra.csproj" />
<Compile Include="..\_common\MarketData.cs" Link="_common\MarketData.cs" />
<Compile Include="..\_common\Backtest.cs" Link="_common\Backtest.cs" />
</ItemGroup>
</Project>
+29
View File
@@ -0,0 +1,29 @@
# Wickra examples — C# / .NET
Runnable .NET examples for the [Wickra .NET binding](../../bindings/csharp).
Each example is a small console project that references the `Wickra` project and
resolves the native library automatically (from `target/release` during local
development, or the NuGet `runtimes/` layout when packaged).
Build the native library first, then run any example:
```bash
cargo build -p wickra-c --release
dotnet run --project examples/csharp/streaming
```
| Example | What it does | Run |
| --- | --- | --- |
| `streaming` | Feed a synthetic price series through SMA / EMA / RSI / MACD tick by tick. | `dotnet run --project examples/csharp/streaming` |
| `backtest` | Compute a basket of indicators over an OHLCV series and print a summary. | `dotnet run --project examples/csharp/backtest -- <ohlcv.csv>` |
| `multi_timeframe` | Resample a 1-minute series into 5m / 15m and print an indicator per timeframe. | `dotnet run --project examples/csharp/multi_timeframe` |
| `parallel_assets` | SMA(20) batch over a panel of assets, serial vs `Parallel.For`, with speedup. | `dotnet run -c Release --project examples/csharp/parallel_assets -- 200 5000` |
| `strategy_rsi_mean_reversion` | RSI(14) mean-reversion with a PnL / Sharpe / max-DD summary. | `dotnet run -c Release --project examples/csharp/strategy_rsi_mean_reversion` |
| `strategy_macd_adx` | MACD crossover entries gated by ADX(14) > 20. | `dotnet run -c Release --project examples/csharp/strategy_macd_adx` |
| `strategy_bollinger_squeeze` | Bollinger-squeeze breakout with an ATR(14) trailing stop. | `dotnet run -c Release --project examples/csharp/strategy_bollinger_squeeze` |
| `fetch_btcusdt` | Download real BTCUSDT klines from the Binance REST API into a CSV. | `dotnet run --project examples/csharp/fetch_btcusdt` |
| `live_binance` | Stream live Binance klines through EMA(20) over a WebSocket. | `dotnet run --project examples/csharp/live_binance` |
`fetch_btcusdt` and `live_binance` require network access; the rest run offline
on deterministic synthetic data. Shared helpers (synthetic data, CSV loader,
equity summary) live in [`_common/`](_common).
+45
View File
@@ -0,0 +1,45 @@
namespace Wickra.Examples;
/// <summary>Summary statistics for a long-only equity curve.</summary>
public sealed record EquityResult(double TotalReturnPct, double Sharpe, double MaxDrawdownPct, int Trades, double FinalEquity);
/// <summary>
/// Minimal long-only backtest helper: turn a stream of per-bar fractional
/// returns into a PnL / Sharpe / max-drawdown summary. The strategy examples
/// produce the returns; this aggregates them.
/// </summary>
public static class Backtest
{
/// <param name="periodReturns">Per-bar fractional returns (0.01 == +1%).</param>
/// <param name="trades">Number of position entries.</param>
/// <param name="periodsPerYear">Annualisation factor for the Sharpe ratio.</param>
public static EquityResult Summarize(IReadOnlyList<double> periodReturns, int trades, double periodsPerYear = 252.0)
{
double equity = 1.0, peak = 1.0, maxDrawdown = 0.0;
foreach (var r in periodReturns)
{
equity *= 1.0 + r;
peak = Math.Max(peak, equity);
if (peak > 0)
{
maxDrawdown = Math.Max(maxDrawdown, (peak - equity) / peak);
}
}
var mean = periodReturns.Count > 0 ? periodReturns.Average() : 0.0;
var variance = periodReturns.Count > 1
? periodReturns.Sum(x => (x - mean) * (x - mean)) / (periodReturns.Count - 1)
: 0.0;
var stdDev = Math.Sqrt(variance);
var sharpe = stdDev > 1e-12 ? mean / stdDev * Math.Sqrt(periodsPerYear) : 0.0;
return new EquityResult((equity - 1.0) * 100.0, sharpe, maxDrawdown * 100.0, trades, equity);
}
/// <summary>Prints a one-line summary.</summary>
public static void Print(string name, EquityResult r)
{
Console.WriteLine(
$"{name,-26} return={r.TotalReturnPct,8:F2}% sharpe={r.Sharpe,6:F2} maxDD={r.MaxDrawdownPct,6:F2}% trades={r.Trades}");
}
}
+78
View File
@@ -0,0 +1,78 @@
namespace Wickra.Examples;
/// <summary>One OHLCV bar with a millisecond timestamp.</summary>
public readonly record struct Bar(double Open, double High, double Low, double Close, double Volume, long Timestamp);
/// <summary>
/// Deterministic synthetic market data plus a small OHLCV CSV loader, shared by
/// the offline examples so they run without network access.
/// </summary>
public static class MarketData
{
/// <summary>A reproducible price path (trend + two cycles), no randomness.</summary>
public static double[] SyntheticPrices(int count, double start = 100.0)
{
var prices = new double[count];
for (var i = 0; i < count; i++)
{
prices[i] = start + 12.0 * Math.Sin(i * 0.05) + 5.0 * Math.Sin(i * 0.013) + i * 0.01;
}
return prices;
}
/// <summary>A reproducible OHLCV series derived from <see cref="SyntheticPrices"/>.</summary>
public static Bar[] SyntheticCandles(int count, long startTimestamp = 0, long stepMs = 3_600_000)
{
var prices = SyntheticPrices(count + 1);
var bars = new Bar[count];
for (var i = 0; i < count; i++)
{
var open = prices[i];
var close = prices[i + 1];
var high = Math.Max(open, close) + 0.5 + Math.Abs(Math.Sin(i * 0.7));
var low = Math.Min(open, close) - 0.5 - Math.Abs(Math.Cos(i * 0.7));
var volume = 1_000.0 + 500.0 * (1.0 + Math.Sin(i * 0.1));
bars[i] = new Bar(open, high, low, close, volume, startTimestamp + i * stepMs);
}
return bars;
}
/// <summary>
/// Loads an OHLCV CSV. Accepts rows of <c>timestamp,open,high,low,close,volume</c>
/// or <c>open,high,low,close,volume</c>; a non-numeric first row is treated as a header.
/// </summary>
public static Bar[] LoadOhlcvCsv(string path)
{
var bars = new List<Bar>();
foreach (var rawLine in File.ReadLines(path))
{
var line = rawLine.Trim();
if (line.Length == 0)
{
continue;
}
var cols = line.Split(',');
if (!double.TryParse(cols[0], System.Globalization.CultureInfo.InvariantCulture, out _) &&
!long.TryParse(cols[0], out _))
{
continue; // header row
}
double F(int i) => double.Parse(cols[i], System.Globalization.CultureInfo.InvariantCulture);
if (cols.Length >= 6)
{
bars.Add(new Bar(F(1), F(2), F(3), F(4), F(5), long.Parse(cols[0])));
}
else
{
bars.Add(new Bar(F(0), F(1), F(2), F(3), F(4), bars.Count));
}
}
return bars.ToArray();
}
}
+33
View File
@@ -0,0 +1,33 @@
using Wickra;
using Wickra.Examples;
// 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.
var source = args.Length > 0 ? args[0] : "synthetic";
Bar[] bars = args.Length > 0 ? MarketData.LoadOhlcvCsv(args[0]) : MarketData.SyntheticCandles(1000);
Console.WriteLine($"Backtest over {bars.Length} bars ({source}):");
using var sma = new Sma(20);
using var ema = new Ema(50);
using var rsi = new Rsi(14);
using var atr = new Atr(14);
double lastSma = 0, lastEma = 0, lastRsi = 0, lastAtr = 0;
var oversold = 0;
foreach (var b in 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 (double.IsFinite(lastRsi) && lastRsi < 30.0)
{
oversold++;
}
}
Console.WriteLine($" SMA(20) last = {lastSma:F4}");
Console.WriteLine($" EMA(50) last = {lastEma:F4}");
Console.WriteLine($" RSI(14) last = {lastRsi:F4} ({oversold} oversold bars)");
Console.WriteLine($" ATR(14) last = {lastAtr:F4}");
+5
View File
@@ -0,0 +1,5 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
</PropertyGroup>
</Project>
+33
View File
@@ -0,0 +1,33 @@
using System.Globalization;
using System.Text.Json;
// 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).
const string url = "https://api.binance.com/api/v3/klines?symbol=BTCUSDT&interval=1h&limit=500";
using var http = new HttpClient();
Console.WriteLine($"Fetching {url}");
var json = await http.GetStringAsync(url);
using var doc = JsonDocument.Parse(json);
var dir = Path.Combine(AppContext.BaseDirectory, "data");
Directory.CreateDirectory(dir);
var path = Path.Combine(dir, "btcusdt_1h.csv");
using var writer = new StreamWriter(path);
writer.WriteLine("timestamp,open,high,low,close,volume");
var count = 0;
foreach (var kline in doc.RootElement.EnumerateArray())
{
// Binance kline array: [openTime, open, high, low, close, volume, ...]
var ts = kline[0].GetInt64();
var o = kline[1].GetString();
var h = kline[2].GetString();
var l = kline[3].GetString();
var c = kline[4].GetString();
var v = kline[5].GetString();
writer.WriteLine(string.Create(CultureInfo.InvariantCulture, $"{ts},{o},{h},{l},{c},{v}"));
count++;
}
Console.WriteLine($"Wrote {count} klines to {path}");
@@ -0,0 +1,5 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
</PropertyGroup>
</Project>
+40
View File
@@ -0,0 +1,40 @@
using System.Globalization;
using System.Net.WebSockets;
using System.Text;
using System.Text.Json;
using Wickra;
// 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.
var uri = new Uri("wss://stream.binance.com:9443/ws/btcusdt@kline_1m");
Console.WriteLine($"Connecting to {uri} (up to 60s)...");
using var ws = new ClientWebSocket();
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(60));
using var ema = new Ema(20);
var buffer = new byte[8192];
try
{
await ws.ConnectAsync(uri, cts.Token);
while (ws.State == WebSocketState.Open && !cts.IsCancellationRequested)
{
var result = await ws.ReceiveAsync(buffer, cts.Token);
if (result.MessageType == WebSocketMessageType.Close)
{
break;
}
using var doc = JsonDocument.Parse(Encoding.UTF8.GetString(buffer, 0, result.Count));
if (doc.RootElement.TryGetProperty("k", out var k))
{
var close = double.Parse(k.GetProperty("c").GetString()!, CultureInfo.InvariantCulture);
var value = ema.Update(close);
Console.WriteLine($"close={close:F2} EMA(20)={value:F2}");
}
}
}
catch (OperationCanceledException)
{
Console.WriteLine("Done (time limit reached).");
}
@@ -0,0 +1,5 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
</PropertyGroup>
</Project>
@@ -0,0 +1,44 @@
using Wickra;
using Wickra.Examples;
// Resample a 1-minute series into higher timeframes and run an indicator per timeframe.
var oneMinute = MarketData.SyntheticCandles(1200, startTimestamp: 0, stepMs: 60_000);
Console.WriteLine("EMA(20) of close across timeframes (resampled from 1-minute bars):");
foreach (var factor in new[] { 1, 5, 15 })
{
var bars = Resample(oneMinute, factor);
using var ema = new Ema(20);
double last = 0;
foreach (var b in bars)
{
last = ema.Update(b.Close);
}
Console.WriteLine($" {factor,2}m: {bars.Length,5} bars EMA(20) last = {last:F4}");
}
static Bar[] Resample(Bar[] source, int factor)
{
if (factor <= 1)
{
return source;
}
var output = new List<Bar>();
for (var i = 0; i < source.Length; i += factor)
{
var end = Math.Min(i + factor, source.Length);
double high = double.MinValue, low = double.MaxValue, volume = 0;
for (var j = i; j < end; j++)
{
high = Math.Max(high, source[j].High);
low = Math.Min(low, source[j].Low);
volume += source[j].Volume;
}
output.Add(new Bar(source[i].Open, high, low, source[end - 1].Close, volume, source[i].Timestamp));
}
return output.ToArray();
}
@@ -0,0 +1,5 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
</PropertyGroup>
</Project>
@@ -0,0 +1,47 @@
using System.Diagnostics;
using Wickra;
using Wickra.Examples;
// Run SMA(20) batch over a panel of assets, serial vs Parallel.For, and report the speedup.
var assets = args.Length > 0 ? int.Parse(args[0]) : 500;
var bars = args.Length > 1 ? int.Parse(args[1]) : 20_000;
var panel = new double[assets][];
for (var a = 0; a < assets; a++)
{
panel[a] = MarketData.SyntheticPrices(bars, start: 50.0 + a * 0.1);
}
// Warm up the JIT and thread pool so the comparison is fair.
using (var warm = new Sma(20))
{
warm.Batch(panel[0]);
}
var sink = 0.0;
var sw = Stopwatch.StartNew();
for (var a = 0; a < assets; a++)
{
using var sma = new Sma(20);
var result = sma.Batch(panel[a]);
sink += result[^1];
}
sw.Stop();
var serialMs = sw.Elapsed.TotalMilliseconds;
var lasts = new double[assets];
sw.Restart();
Parallel.For(0, assets, a =>
{
using var sma = new Sma(20);
var result = sma.Batch(panel[a]);
lasts[a] = result[^1];
});
sw.Stop();
var parallelMs = sw.Elapsed.TotalMilliseconds;
Console.WriteLine($"{assets} assets x {bars} bars, SMA(20) batch:");
Console.WriteLine($" serial {serialMs,8:F1} ms");
Console.WriteLine($" parallel {parallelMs,8:F1} ms ({serialMs / Math.Max(parallelMs, 1e-9):F1}x speedup)");
GC.KeepAlive(sink);
@@ -0,0 +1,5 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
</PropertyGroup>
</Project>
@@ -0,0 +1,46 @@
using Wickra;
using Wickra.Examples;
// Breakout: when Bollinger bandwidth is tight (a "squeeze") and price closes above the
// upper band, go long with an ATR(14) trailing stop.
var bars = args.Length > 0 ? MarketData.LoadOhlcvCsv(args[0]) : MarketData.SyntheticCandles(2000);
using var bollinger = new BollingerBands(20, 2.0);
using var atr = new Atr(14);
var returns = new List<double>();
var trades = 0;
var inPosition = false;
var entry = 0.0;
var stop = 0.0;
foreach (var b in bars)
{
var bands = bollinger.Update(b.Close);
var atrValue = atr.Update(b.Open, b.High, b.Low, b.Close, b.Volume, b.Timestamp);
if (bands is not { } band || !double.IsFinite(atrValue))
{
continue;
}
var bandwidth = band.Middle != 0.0 ? (band.Upper - band.Lower) / band.Middle : double.MaxValue;
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.Add((b.Close - entry) / entry);
inPosition = false;
}
}
}
Backtest.Print("Bollinger squeeze", Backtest.Summarize(returns, trades));
@@ -0,0 +1,5 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
</PropertyGroup>
</Project>
@@ -0,0 +1,42 @@
using Wickra;
using Wickra.Examples;
// 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.
var bars = args.Length > 0 ? MarketData.LoadOhlcvCsv(args[0]) : MarketData.SyntheticCandles(2000);
using var macd = new MacdIndicator(12, 26, 9);
using var adx = new Adx(14);
var returns = new List<double>();
var trades = 0;
var inPosition = false;
var entry = 0.0;
var prevHistogram = double.NaN;
foreach (var b in bars)
{
var m = macd.Update(b.Close);
var a = adx.Update(b.Open, b.High, b.Low, b.Close, b.Volume, b.Timestamp);
if (m is not { } macdValue || a is not { } adxValue)
{
continue;
}
var trending = adxValue.Adx > 20.0;
if (!inPosition && trending && double.IsFinite(prevHistogram) && prevHistogram <= 0.0 && macdValue.Histogram > 0.0)
{
inPosition = true;
entry = b.Close;
trades++;
}
else if (inPosition && macdValue.Histogram < 0.0)
{
returns.Add((b.Close - entry) / entry);
inPosition = false;
}
prevHistogram = macdValue.Histogram;
}
Backtest.Print("MACD + ADX trend", Backtest.Summarize(returns, trades));
@@ -0,0 +1,5 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
</PropertyGroup>
</Project>
@@ -0,0 +1,34 @@
using Wickra;
using Wickra.Examples;
// Mean reversion: go long when RSI(14) drops below 30, exit when it recovers above 50.
var bars = args.Length > 0 ? MarketData.LoadOhlcvCsv(args[0]) : MarketData.SyntheticCandles(2000);
using var rsi = new Rsi(14);
var returns = new List<double>();
var trades = 0;
var inPosition = false;
var entry = 0.0;
foreach (var b in bars)
{
var value = rsi.Update(b.Close);
if (!double.IsFinite(value))
{
continue;
}
if (!inPosition && value < 30.0)
{
inPosition = true;
entry = b.Close;
trades++;
}
else if (inPosition && value > 50.0)
{
returns.Add((b.Close - entry) / entry);
inPosition = false;
}
}
Backtest.Print("RSI mean-reversion", Backtest.Summarize(returns, trades));
@@ -0,0 +1,5 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
</PropertyGroup>
</Project>
+29
View File
@@ -0,0 +1,29 @@
using Wickra;
using Wickra.Examples;
// Feed a synthetic price series through several indicators tick by tick (O(1) each).
var prices = MarketData.SyntheticPrices(500);
using var sma = new Sma(20);
using var ema = new Ema(20);
using var rsi = new Rsi(14);
using var macd = new MacdIndicator(12, 26, 9);
double lastSma = 0, lastEma = 0, lastRsi = 0;
MacdOutput? lastMacd = null;
foreach (var price in prices)
{
lastSma = sma.Update(price);
lastEma = ema.Update(price);
lastRsi = rsi.Update(price);
lastMacd = macd.Update(price);
}
Console.WriteLine($"Streamed {prices.Length} prices through SMA(20), EMA(20), RSI(14), MACD(12,26,9):");
Console.WriteLine($" SMA = {lastSma:F4}");
Console.WriteLine($" EMA = {lastEma:F4}");
Console.WriteLine($" RSI = {lastRsi:F4}");
if (lastMacd is { } m)
{
Console.WriteLine($" MACD = {m.Macd:F4} signal={m.Signal:F4} hist={m.Histogram:F4}");
}
@@ -0,0 +1,5 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
</PropertyGroup>
</Project>
+14
View File
@@ -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
+38
View File
@@ -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.
+56
View File
@@ -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)
}
+58
View File
@@ -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)
}
+9
View File
@@ -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

Some files were not shown because too many files have changed in this diff Show More