Compare commits

..
8 Commits
Author SHA1 Message Date
kingchencandGitHub 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
kingchencandGitHub 23d636fd97 Add the Go binding over the C ABI hub (#228)
Adds a Go binding (`bindings/go`) over the C ABI hub — the second language stecker after C#.

## What's here
- **`bindings/go`** — a cgo binding exposing all 514 indicators as idiomatic Go types with `New<Indicator>` constructors and `Update`/`Batch`/`Reset`/`Close` methods. The wrappers in `indicators_gen.go` are generated from `bindings/c/include/wickra.h` (same archetype taxonomy as the C# generator: scalar/batch, multi-output, bars, profile, profile-values, array-input). Opaque handles are freed by `Close()` with a `runtime.SetFinalizer` backstop; pointer arguments are caller-owned, panics never cross the boundary.
- **`examples/go`** — the full example suite mirroring C/C#: streaming, backtest, multi_timeframe, parallel_assets (goroutine fan-out), three strategies, and `fetch_btcusdt`/`live_binance`.
- **CI** — a `go` job builds the C ABI library, stages it, and runs `gofmt`/`go vet`/`go test` plus the offline examples on Linux, macOS and Windows.
- **Docs** — Go added to the README languages table, project layout, building/testing, CONTRIBUTING binding table + regenerate note, ARCHITECTURE, examples index, issue/PR templates, the About-description template, and the other binding READMEs.

## Linking / distribution
The binding links the prebuilt C ABI library via cgo (`libwickra.so`/`.dylib`/`wickra.dll` staged under `bindings/go/lib`, gitignored). The native libraries are already shipped per target triple by the existing `c-abi-build` release job; distribution is via the subdirectory module tag `bindings/go/vX.Y.Z` (gated), so `release.yml` needs no new publish job.

No Rust crate or `Cargo.toml` change — the Go module is standalone and additive.

Not for merge yet (gated, per request).
2026-06-09 17:33:37 +02:00
kingchencandGitHub 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
kingchencandGitHub 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
kingchencandGitHub 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
kingchencandGitHub 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
kingchencandGitHub 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
kingchencandGitHub 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 # 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. # local shells regardless of the committer's platform autocrlf setting.
*.sh text eol=lf *.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 ## Environment
- Wickra version: - Wickra version:
- Language / binding: <!-- Rust crate / Python / Node / WASM --> - Language / binding: <!-- Rust crate / Python / Node / WASM / C ABI / C# (.NET) / Go -->
- OS and architecture: - OS and architecture:
- Rust / Python / Node version (If relevant): - Rust / Python / Node / .NET version (If relevant):
## Additional context ## Additional context
@@ -26,7 +26,7 @@ assignees: []
| Binding version | `e.g. python 0.4.2 / node 0.4.2` | | Binding version | `e.g. python 0.4.2 / node 0.4.2` |
| OS / arch | `e.g. Windows 11 x86_64, Linux glibc` | | OS / arch | `e.g. Windows 11 x86_64, Linux glibc` |
| Rust toolchain | `rustc --version` (If building from source) | | 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 ## Minimal reproducer
@@ -25,6 +25,9 @@ assignees: ""
- [ ] Should be exposed in the Python binding - [ ] Should be exposed in the Python binding
- [ ] Should be exposed in the Node binding - [ ] Should be exposed in the Node binding
- [ ] Should be exposed in the WASM 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 ## Additional context
@@ -13,7 +13,7 @@ assignees: []
## Affected code path ## Affected code path
- Indicator / API: `e.g. EMA.update` - 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? - Hot loop or one-shot call?
## Versions compared ## Versions compared
+1 -1
View File
@@ -33,4 +33,4 @@ import wickra as ta
## Environment (Only if relevant) ## Environment (Only if relevant)
- Wickra version: `e.g. 0.4.2` - 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 clippy --workspace --all-targets -- -D warnings` is clean.
- [ ] `cargo test --workspace` passes. - [ ] `cargo test --workspace` passes.
- [ ] New behaviour has tests; bug fixes have a regression test. - [ ] 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). and their type stubs (If applicable).
- [ ] The relevant page on the [documentation site](https://docs.wickra.org) - [ ] The relevant page on the [documentation site](https://docs.wickra.org)
and the `README.md` are updated (If applicable). Docs edits go to a 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`) - [ ] Python binding (`bindings/python`)
- [ ] Node.js binding (`bindings/node`) - [ ] Node.js binding (`bindings/node`)
- [ ] WebAssembly binding (`bindings/wasm`) - [ ] WebAssembly binding (`bindings/wasm`)
- [ ] C ABI (`bindings/c`)
- [ ] C# / .NET binding (`bindings/csharp`)
- [ ] Go binding (`bindings/go`)
- [ ] Examples / docs - [ ] Examples / docs
## Linked issues ## Linked issues
+204
View File
@@ -622,6 +622,210 @@ jobs:
working-directory: bindings/node working-directory: bindings/node
run: node --test __tests__/ 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 # The cross-library benchmark has moved to a dedicated scheduled workflow
# (.github/workflows/bench.yml) — see audit finding R10. It runs nightly # (.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 # 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 # the old "publish, then upload provenance" order would have the provenance
# upload rejected once immutability is enabled. # 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: github-release:
name: Attach assets to the draft 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 runs-on: ubuntu-latest
permissions: permissions:
contents: write contents: write
@@ -644,7 +781,7 @@ jobs:
# the provenance bundle is attached (P24, immutability-ready). # the provenance bundle is attached (P24, immutability-ready).
draft: true draft: true
body: | 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 ### Install
@@ -665,6 +802,9 @@ jobs:
darwin-arm64, win32-x64-msvc) darwin-arm64, win32-x64-msvc)
- `wickra-*.tgz` — npm-pack tarballs (main package + per-platform subpackages + WASM) - `wickra-*.tgz` — npm-pack tarballs (main package + per-platform subpackages + WASM)
- `*.crate` — cargo source crates (wickra-core, wickra-data, wickra) - `*.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 ### 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 # 2. GitHub repo "About" description — synced on push to main / v* tag
# 3. Docs site: index.md / overview.md / Indicators-Overview.md # 3. Docs site: index.md / overview.md / Indicators-Overview.md
# (wickra-lib/wickra-docs) — synced on push to main / v* tag* # (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* # .vitepress/config.ts) — push to main / v* tag*
# 5. org profile README count (wickra-lib/.github, profile/README.md) # 5. org profile README count (wickra-lib/.github, profile/README.md)
# — synced on push to main / v* tag* # — synced on push to main / v* tag*
@@ -42,10 +42,10 @@ name: Sync indicator count
# single source of truth for what the bindings reach. # single source of truth for what the bindings reach.
# #
# Design: on PRs this workflow is a READ-ONLY check. The indicator wiring # Design: on PRs this workflow is a READ-ONLY check. The indicator wiring
# (ScriptHelpers/_common.py wire_readme_counter) bumps both README.md and # bumps both README.md and docs/README.md inside the author's code commit, so
# docs/README.md inside the author's code commit, so the counter is already # the counter is already correct by the time CI runs. If it is not, the check
# correct by the time CI runs. If it is not, the check below fails loud and # below fails loud and asks the author to re-run the wiring — it never pushes a
# asks the author to re-run the wiring — it never pushes a fix-up commit. # fix-up commit.
# #
# (An earlier version pushed a GITHUB_TOKEN "sync indicator count" commit to # (An earlier version pushed a GITHUB_TOKEN "sync indicator count" commit to
# the PR head. Because GITHUB_TOKEN pushes trigger no workflows, that commit # 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 # actually live (Cloudflare Pages, P8.1); merging this PR is therefore
# gated on the domain resolving, otherwise the About link would 404. # gated on the domain resolving, otherwise the About link would 404.
homepage="https://docs.wickra.org" 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 # Enforce the homepage unconditionally — it is a constant, so this both
# corrects the stale kingchenc URL and self-heals any future drift. # corrects the stale kingchenc URL and self-heals any future drift.
# Same Administration-write permission as --description (no extra scope). # Same Administration-write permission as --description (no extra scope).
@@ -366,14 +366,14 @@ jobs:
exit 0 exit 0
fi fi
cd webpage-count 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 if git diff --quiet; then
echo "Webpage indicator count unchanged." echo "Webpage indicator count unchanged."
exit 0 exit 0
fi fi
git config user.name "wickra-bot" git config user.name "wickra-bot"
git config user.email "wickra-bot@users.noreply.github.com" 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}" git commit -m "chore: sync indicator count to ${n}"
if ! git push 2>/dev/null; then if ! git push 2>/dev/null; then
echo "::warning::push to wickra-lib/webpage failed — ABOUT_SYNC_TOKEN likely lacks write (findings P10.0a)." 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 │ │ no I/O, no deps │ │ optional features │
└──────────────────────┘ └────────────────────┘ └──────────────────────┘ └────────────────────┘
(every binding wraps the same core) every binding wraps the same core
┌────────────┼────────────┬────────────────┐
┌────────────┴───────────┬─────────────────────┐ │ │ │ │
│ │ │ ┌──▼───────┐ ┌──▼───────┐ ┌──▼───────────┐ ┌──▼──────────────────┐
┌──▼──────┐ ┌───────▼──────┐ ┌───────▼────────┐ │ Python │ │ Node │ │ WASM │ │ C ABI (cbindgen) │
Python │ │ Node │ │ WASM (PyO3) │ │ (napi-rs)│ │(wasm-bindgen)│ │ cdylib + header
│ (PyO3) │ │ (napi-rs) │ │ (wasm-bindgen) │ └──────────┘ └──────────┘ └──────────────┘ └─────────┬───────────┘
└─────────┘ └──────────────┘ └────────────────┘ │ 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 | | 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) | | `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-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-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-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` | | `wickra-examples` | `examples/rust` | runnable binary examples | depends on `wickra`, `wickra-data` |
The `fuzz/` directory is **excluded** from the workspace (it has its own 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] ## [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 ## [0.7.4] - 2026-06-08
- **Three-Line Break** — Three-line-break bars (reversal needs N-line break) (`THREE_LINE_BREAK_BARS`). - **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`). - **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. optional Binance live feed.
- Bindings for Python, Node.js, and WebAssembly. - 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.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.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 [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/python` | PyO3 bindings (`wickra` on PyPI). |
| `bindings/node` | napi-rs bindings (`wickra` on npm). | | `bindings/node` | napi-rs bindings (`wickra` on npm). |
| `bindings/wasm` | wasm-bindgen bindings (`wickra-wasm` 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. | | `examples/` | Runnable examples. |
| `docs/` | Pointer to the documentation site (docs.wickra.org); the docs live in the `wickra-lib/wickra-docs` repo. | | `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 - **Streaming parity.** An indicator's `batch` output must equal the sequence
of `update` calls. of `update` calls.
- **Bindings.** A change to a public indicator API must be mirrored across the - **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 - **Docs.** Update the relevant page on the
[documentation site](https://docs.wickra.org) and the [documentation site](https://docs.wickra.org) and the
`README.md` when behaviour or the public API changes. The docs live in `README.md` when behaviour or the public API changes. The docs live in
Generated
+15 -8
View File
@@ -1944,7 +1944,7 @@ dependencies = [
[[package]] [[package]]
name = "wickra" name = "wickra"
version = "0.7.4" version = "0.7.7"
dependencies = [ dependencies = [
"approx", "approx",
"criterion", "criterion",
@@ -1955,7 +1955,7 @@ dependencies = [
[[package]] [[package]]
name = "wickra-bench" name = "wickra-bench"
version = "0.7.4" version = "0.7.7"
dependencies = [ dependencies = [
"criterion", "criterion",
"kand", "kand",
@@ -1965,9 +1965,16 @@ dependencies = [
"yata", "yata",
] ]
[[package]]
name = "wickra-c"
version = "0.7.7"
dependencies = [
"wickra-core",
]
[[package]] [[package]]
name = "wickra-core" name = "wickra-core"
version = "0.7.4" version = "0.7.7"
dependencies = [ dependencies = [
"approx", "approx",
"proptest", "proptest",
@@ -1977,7 +1984,7 @@ dependencies = [
[[package]] [[package]]
name = "wickra-data" name = "wickra-data"
version = "0.7.4" version = "0.7.7"
dependencies = [ dependencies = [
"approx", "approx",
"csv", "csv",
@@ -1994,7 +2001,7 @@ dependencies = [
[[package]] [[package]]
name = "wickra-examples" name = "wickra-examples"
version = "0.7.4" version = "0.7.7"
dependencies = [ dependencies = [
"serde_json", "serde_json",
"tokio", "tokio",
@@ -2004,7 +2011,7 @@ dependencies = [
[[package]] [[package]]
name = "wickra-node" name = "wickra-node"
version = "0.7.4" version = "0.7.7"
dependencies = [ dependencies = [
"napi", "napi",
"napi-build", "napi-build",
@@ -2014,7 +2021,7 @@ dependencies = [
[[package]] [[package]]
name = "wickra-python" name = "wickra-python"
version = "0.7.4" version = "0.7.7"
dependencies = [ dependencies = [
"numpy", "numpy",
"pyo3", "pyo3",
@@ -2023,7 +2030,7 @@ dependencies = [
[[package]] [[package]]
name = "wickra-wasm" name = "wickra-wasm"
version = "0.7.4" version = "0.7.7"
dependencies = [ dependencies = [
"console_error_panic_hook", "console_error_panic_hook",
"js-sys", "js-sys",
+3 -2
View File
@@ -7,13 +7,14 @@ members = [
"bindings/python", "bindings/python",
"bindings/wasm", "bindings/wasm",
"bindings/node", "bindings/node",
"bindings/c",
"examples/rust", "examples/rust",
"crates/wickra-bench", "crates/wickra-bench",
] ]
exclude = ["fuzz"] exclude = ["fuzz"]
[workspace.package] [workspace.package]
version = "0.7.4" version = "0.7.7"
authors = ["kingchenc <support@wickra.org>"] authors = ["kingchenc <support@wickra.org>"]
edition = "2021" edition = "2021"
rust-version = "1.86" rust-version = "1.86"
@@ -25,7 +26,7 @@ keywords = ["finance", "trading", "indicators", "technical-analysis", "ta"]
categories = ["finance", "mathematics", "science"] categories = ["finance", "mathematics", "science"]
[workspace.dependencies] [workspace.dependencies]
wickra-core = { path = "crates/wickra-core", version = "0.7.4" } wickra-core = { path = "crates/wickra-core", version = "0.7.7" }
thiserror = "2" thiserror = "2"
rayon = "1.10" 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> <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> </p>
[![CI](https://github.com/wickra-lib/wickra/actions/workflows/ci.yml/badge.svg)](https://github.com/wickra-lib/wickra/actions/workflows/ci.yml) [![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://github.com/wickra-lib/wickra/actions/workflows/codeql.yml/badge.svg)](https://github.com/wickra-lib/wickra/actions/workflows/codeql.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://codecov.io/gh/wickra-lib/wickra/branch/main/graph/badge.svg)](https://codecov.io/gh/wickra-lib/wickra) [![codecov](https://raw.githubusercontent.com/wickra-lib/.github/main/profile/badges/codecov.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) [![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://img.shields.io/crates/v/wickra.svg?logo=rust&color=orange)](https://crates.io/crates/wickra) [![crates.io](https://raw.githubusercontent.com/wickra-lib/.github/main/profile/badges/crates.svg)](https://crates.io/crates/wickra)
[![PyPI](https://img.shields.io/pypi/v/wickra.svg?logo=pypi&color=blue)](https://pypi.org/project/wickra/) [![PyPI](https://raw.githubusercontent.com/wickra-lib/.github/main/profile/badges/pypi.svg)](https://pypi.org/project/wickra/)
[![npm](https://img.shields.io/npm/v/wickra.svg?logo=npm&color=red)](https://www.npmjs.com/package/wickra) [![npm](https://raw.githubusercontent.com/wickra-lib/.github/main/profile/badges/npm.svg)](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) [![NuGet](https://raw.githubusercontent.com/wickra-lib/.github/main/profile/badges/nuget.svg)](https://www.nuget.org/packages/Wickra)
[![OpenSSF Scorecard](https://api.securityscorecards.dev/projects/github.com/wickra-lib/wickra/badge)](https://scorecard.dev/viewer/?uri=github.com/wickra-lib/wickra) [![License: MIT OR Apache-2.0](https://raw.githubusercontent.com/wickra-lib/.github/main/profile/badges/license.svg)](#license)
[![OpenSSF Best Practices](https://www.bestpractices.dev/projects/13094/badge)](https://www.bestpractices.dev/projects/13094) [![OpenSSF Scorecard](https://raw.githubusercontent.com/wickra-lib/.github/main/profile/badges/scorecard.svg)](https://scorecard.dev/viewer/?uri=github.com/wickra-lib/wickra)
[![Build provenance](https://img.shields.io/badge/provenance-attested-brightgreen?logo=github)](https://github.com/wickra-lib/wickra/attestations) [![OpenSSF Best Practices](https://raw.githubusercontent.com/wickra-lib/.github/main/profile/badges/best-practices.svg)](https://www.bestpractices.dev/projects/13094)
[![Docs](https://img.shields.io/badge/docs-docs.wickra.org-0ea5e9?logo=readthedocs&logoColor=white)](https://docs.wickra.org) [![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.** **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 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 native bindings for Python, Node.js and WebAssembly, plus a C ABI that C, C++,
machine that updates in O(1) per new data point, so live trading bots and 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. historical backtests share the exact same implementation.
```python ```python
@@ -46,7 +48,10 @@ Full documentation lives at **[docs.wickra.org](https://docs.wickra.org)**:
- **Quickstarts** — [Rust](https://docs.wickra.org/Quickstart-Rust), - **Quickstarts** — [Rust](https://docs.wickra.org/Quickstart-Rust),
[Python](https://docs.wickra.org/Quickstart-Python), [Python](https://docs.wickra.org/Quickstart-Python),
[Node](https://docs.wickra.org/Quickstart-Node), [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 - **Indicators** — a per-indicator deep dive (formula, parameters, warmup) for
every one of the 514 indicators; start at the every one of the 514 indicators; start at the
[indicators overview](https://docs.wickra.org/Indicators-Overview). [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 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 metrics — every single one updating in **O(1) per tick**. TA-Lib ships ~150 and
none of them stream. none of them stream.
- **One Rust core, four first-class targets.** Native **Python · Node.js · - **One Rust core, five first-class targets.** Native **Python · Node.js ·
WebAssembly · Rust** — identical math, identical results, zero per-language WebAssembly · Rust** plus a **C ABI** for C / C++, C# / .NET, Go and any other C-capable language
reimplementation and zero GIL bottleneck. identical math, identical results, zero per-language reimplementation and zero
GIL bottleneck.
- **Correct by construction, not by hope.** Every `update` validates its input, - **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 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 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 | | 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 | | kand | clean | yes | Python · WASM · Rust | ~60 | yes |
| ta-rs | clean | yes | Rust only | ~30 | stale | | ta-rs | clean | yes | Rust only | ~30 | stale |
| yata | clean | partial | Rust only | ~35 | yes | | 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)`, construct it in signed mode (`Doji::new().signed()`, `Doji(signed=True)`,
`new Doji(true)`) for a dragonfly / gravestone `±1` reading. `new Doji(true)`) for a dragonfly / gravestone `±1` reading.
Adding a new indicator means implementing one trait in Rust; all four bindings Adding a new indicator means implementing one trait in Rust; every binding
inherit it automatically. inherits it automatically (the C ABI — and the C# and Go bindings generated from
it — regenerate from the core).
## Languages ## Languages
@@ -177,12 +185,16 @@ inherit it automatically.
| Node.js (napi-rs) | `npm install wickra` | `examples/node/backtest.js` | | Node.js (napi-rs) | `npm install wickra` | `examples/node/backtest.js` |
| Browser / WASM | `npm install wickra-wasm` | `examples/wasm/index.html` | | Browser / WASM | `npm install wickra-wasm` | `examples/wasm/index.html` |
| Rust | `cargo add wickra` | `examples/rust/src/bin/backtest.rs` | | 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); Each binding ships several runnable examples (streaming, backtest, live feed);
[`examples/README.md`](examples/README.md) is the full cross-language index. [`examples/README.md`](examples/README.md) is the full cross-language index.
The wickra-core crate is `unsafe`-forbidden, so every binding inherits a The wickra-core crate is `unsafe`-forbidden, so the native bindings are
memory-safe implementation. 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 ## Rust API
@@ -244,13 +256,19 @@ wickra/
├── bindings/ ├── bindings/
│ ├── python/ PyO3 + maturin (publishes on PyPI) │ ├── python/ PyO3 + maturin (publishes on PyPI)
│ ├── node/ napi-rs (publishes on npm) │ ├── 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 ├── examples/ examples/README.md indexes every language
│ ├── data/ real BTCUSDT OHLCV datasets, one per timeframe │ ├── data/ real BTCUSDT OHLCV datasets, one per timeframe
│ ├── rust/ Rust workspace member (`wickra-examples`) │ ├── rust/ Rust workspace member (`wickra-examples`)
│ ├── python/ backtest, live trading, parallel assets, multi-tf │ ├── python/ backtest, live trading, parallel assets, multi-tf
│ ├── node/ streaming, backtest, live trading (load `wickra`) │ ├── 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 └── .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) # Node binding (requires @napi-rs/cli)
cd bindings/node && npm install && npm run build && npm test 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 ## Testing
@@ -297,6 +327,8 @@ Every layer is covered; run the suites with the commands in
values across all indicators. values across all indicators.
- `bindings/wasm`: `wasm-bindgen-test` cases for constructors, equivalence, - `bindings/wasm`: `wasm-bindgen-test` cases for constructors, equivalence,
and reference values. 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 ## 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. versioning stability for a 1.0 release.
- **Performance.** Keep per-tick updates O(1) and maintain the benchmark suite; - **Performance.** Keep per-tick updates O(1) and maintain the benchmark suite;
investigate further allocation and cache improvements. investigate further allocation and cache improvements.
- **Bindings parity.** Keep the Python, Node.js and WebAssembly bindings in - **Bindings parity.** Keep the Python, Node.js and WebAssembly bindings — plus
lockstep with the Rust core, including type stubs and platform coverage. 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 - **Documentation.** Maintain a deep-dive page per indicator on
<https://docs.wickra.org>, plus quickstarts and cookbook material. <https://docs.wickra.org>, plus quickstarts and cookbook material.
- **Project health.** Maintain test coverage, static and dynamic analysis, - **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 - *Memory safety* — the core and all bindings are written in Rust. The crates
forbid or minimise `unsafe`, so the compiler guarantees memory and thread 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 - *Input robustness* — every indicator validates its parameters and rejects
non-finite inputs at construction; behaviour on edge cases (flat markets, non-finite inputs at construction; behaviour on edge cases (flat markets,
warmup, reset) is pinned by unit tests, and the public update paths are 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: Most questions are answered in the documentation:
- **Docs site:** <https://docs.wickra.org> — quickstarts for Rust, Python, - **Docs site:** <https://docs.wickra.org> — quickstarts for Rust, Python,
Node.js and WebAssembly, a per-indicator reference, warmup periods, the data Node.js, WebAssembly, C, C# and Go, a per-indicator reference, warmup periods, the
layer, and an FAQ. data layer, and an FAQ.
- **README:** <https://github.com/wickra-lib/wickra#readme> — installation and a - **README:** <https://github.com/wickra-lib/wickra#readme> — installation and a
quick overview. quick overview.
- **API docs (Rust):** <https://docs.rs/wickra>. - **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, This document describes Wickra's attack surface and the threats considered,
together with their mitigations. It complements the security assurance case in together with their mitigations. It complements the security assurance case in
[`SECURITY.md`](SECURITY.md). Wickra is a computational technical-analysis [`SECURITY.md`](SECURITY.md). Wickra is a computational technical-analysis
library (a Rust core with Python, Node.js and WebAssembly bindings), not a library (a Rust core with Python, Node.js and WebAssembly bindings plus a C ABI
network service or trading system; the attack surface is correspondingly small. and the .NET and Go bindings built on it),
not a network service or trading system; the attack surface is correspondingly
small.
## Assets ## Assets
@@ -31,6 +33,7 @@ network service or trading system; the attack surface is correspondingly small.
| Threat | Mitigation | | Threat | Mitigation |
| --- | --- | | --- | --- |
| Memory-safety exploit (buffer overflow, UAF) via crafted input | Pure safe Rust; `unsafe` is forbidden/minimised, so the compiler precludes these classes. | | 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. | | 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. | | 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. | | 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.** prebuilt native binary, no system dependencies.**
Wickra is a multi-language technical-analysis library with a Rust core and 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 streaming state machine, so live trading bots and historical backtests share
the exact same implementation. This package is the Node.js binding (napi-rs); the exact same implementation. This package is the Node.js binding (napi-rs);
it exposes 200+ streaming-first indicators across sixteen families. 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> - **Docs** (quickstarts, cookbook, TA-Lib migration): <https://docs.wickra.org>
- **Runnable examples:** [`examples/node/`](https://github.com/wickra-lib/wickra/tree/main/examples/node) - **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 Wickra ships native bindings for Python, Node.js, WebAssembly and Rust, plus a
expose the same indicators from the shared, `unsafe`-forbidden Rust core. 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 ## Disclaimer
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "wickra-darwin-arm64", "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.", "description": "Native binding for wickra (macOS Apple Silicon). Installed automatically as an optional dependency of wickra on matching platforms.",
"main": "wickra.darwin-arm64.node", "main": "wickra.darwin-arm64.node",
"files": [ "files": [
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "wickra-darwin-x64", "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.", "description": "Native binding for wickra (macOS Intel). Installed automatically as an optional dependency of wickra on matching platforms.",
"main": "wickra.darwin-x64.node", "main": "wickra.darwin-x64.node",
"files": [ "files": [
@@ -1,6 +1,6 @@
{ {
"name": "wickra-linux-arm64-gnu", "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.", "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", "main": "wickra.linux-arm64-gnu.node",
"files": [ "files": [
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "wickra-linux-x64-gnu", "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.", "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", "main": "wickra.linux-x64-gnu.node",
"files": [ "files": [
@@ -1,6 +1,6 @@
{ {
"name": "wickra-win32-arm64-msvc", "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.", "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", "main": "wickra.win32-arm64-msvc.node",
"files": [ "files": [
@@ -1,6 +1,6 @@
{ {
"name": "wickra-win32-x64-msvc", "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.", "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", "main": "wickra.win32-x64-msvc.node",
"files": [ "files": [
+20 -20
View File
@@ -1,12 +1,12 @@
{ {
"name": "wickra", "name": "wickra",
"version": "0.7.4", "version": "0.7.7",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "wickra", "name": "wickra",
"version": "0.7.4", "version": "0.7.7",
"license": "MIT OR Apache-2.0", "license": "MIT OR Apache-2.0",
"devDependencies": { "devDependencies": {
"@napi-rs/cli": "^2.18.0" "@napi-rs/cli": "^2.18.0"
@@ -15,12 +15,12 @@
"node": ">= 18" "node": ">= 18"
}, },
"optionalDependencies": { "optionalDependencies": {
"wickra-darwin-arm64": "0.7.4", "wickra-darwin-arm64": "0.7.7",
"wickra-darwin-x64": "0.7.4", "wickra-darwin-x64": "0.7.7",
"wickra-linux-arm64-gnu": "0.7.4", "wickra-linux-arm64-gnu": "0.7.7",
"wickra-linux-x64-gnu": "0.7.4", "wickra-linux-x64-gnu": "0.7.7",
"wickra-win32-arm64-msvc": "0.7.4", "wickra-win32-arm64-msvc": "0.7.7",
"wickra-win32-x64-msvc": "0.7.4" "wickra-win32-x64-msvc": "0.7.7"
} }
}, },
"node_modules/@napi-rs/cli": { "node_modules/@napi-rs/cli": {
@@ -41,8 +41,8 @@
} }
}, },
"node_modules/wickra-darwin-arm64": { "node_modules/wickra-darwin-arm64": {
"version": "0.7.4", "version": "0.7.7",
"resolved": "https://registry.npmjs.org/wickra-darwin-arm64/-/wickra-darwin-arm64-0.7.4.tgz", "resolved": "https://registry.npmjs.org/wickra-darwin-arm64/-/wickra-darwin-arm64-0.7.7.tgz",
"integrity": "sha512-4eZiBR/yGUdr4nzhEUFy2i69XgNx64iI2ax/LPamsThgylC0KpHOZKK19QzJ2d9KbK4C8nMjME5FLuR+4GNEwQ==", "integrity": "sha512-4eZiBR/yGUdr4nzhEUFy2i69XgNx64iI2ax/LPamsThgylC0KpHOZKK19QzJ2d9KbK4C8nMjME5FLuR+4GNEwQ==",
"cpu": [ "cpu": [
"arm64" "arm64"
@@ -57,8 +57,8 @@
} }
}, },
"node_modules/wickra-darwin-x64": { "node_modules/wickra-darwin-x64": {
"version": "0.7.4", "version": "0.7.7",
"resolved": "https://registry.npmjs.org/wickra-darwin-x64/-/wickra-darwin-x64-0.7.4.tgz", "resolved": "https://registry.npmjs.org/wickra-darwin-x64/-/wickra-darwin-x64-0.7.7.tgz",
"integrity": "sha512-6hf8zI3QPjTFp4zCpmgUwDvNtu6jHqNUHKD5e55POo0CgA52HkpyxSPtVm8TGTIZDI7kPjlbOdBM8CJ76mmXwA==", "integrity": "sha512-6hf8zI3QPjTFp4zCpmgUwDvNtu6jHqNUHKD5e55POo0CgA52HkpyxSPtVm8TGTIZDI7kPjlbOdBM8CJ76mmXwA==",
"cpu": [ "cpu": [
"x64" "x64"
@@ -73,8 +73,8 @@
} }
}, },
"node_modules/wickra-linux-arm64-gnu": { "node_modules/wickra-linux-arm64-gnu": {
"version": "0.7.4", "version": "0.7.7",
"resolved": "https://registry.npmjs.org/wickra-linux-arm64-gnu/-/wickra-linux-arm64-gnu-0.7.4.tgz", "resolved": "https://registry.npmjs.org/wickra-linux-arm64-gnu/-/wickra-linux-arm64-gnu-0.7.7.tgz",
"integrity": "sha512-kSe6y0xBMSiqdPLXNjwop5WZdHtvdBNKSEBCwZ4hFq33p4apW25/wrlzv9/oDuyD4kuPabJEhCCnFOplh58CUg==", "integrity": "sha512-kSe6y0xBMSiqdPLXNjwop5WZdHtvdBNKSEBCwZ4hFq33p4apW25/wrlzv9/oDuyD4kuPabJEhCCnFOplh58CUg==",
"cpu": [ "cpu": [
"arm64" "arm64"
@@ -89,8 +89,8 @@
} }
}, },
"node_modules/wickra-linux-x64-gnu": { "node_modules/wickra-linux-x64-gnu": {
"version": "0.7.4", "version": "0.7.7",
"resolved": "https://registry.npmjs.org/wickra-linux-x64-gnu/-/wickra-linux-x64-gnu-0.7.4.tgz", "resolved": "https://registry.npmjs.org/wickra-linux-x64-gnu/-/wickra-linux-x64-gnu-0.7.7.tgz",
"integrity": "sha512-tWBWS4qz7hxM4xnpFb59bhf6TaLwXq0Z3jEa/2l7r8PiHA94g8r8S53NRMiT+4yiL5hSWe/nUiC/YXdRrhEZ4g==", "integrity": "sha512-tWBWS4qz7hxM4xnpFb59bhf6TaLwXq0Z3jEa/2l7r8PiHA94g8r8S53NRMiT+4yiL5hSWe/nUiC/YXdRrhEZ4g==",
"cpu": [ "cpu": [
"x64" "x64"
@@ -105,8 +105,8 @@
} }
}, },
"node_modules/wickra-win32-arm64-msvc": { "node_modules/wickra-win32-arm64-msvc": {
"version": "0.7.4", "version": "0.7.7",
"resolved": "https://registry.npmjs.org/wickra-win32-arm64-msvc/-/wickra-win32-arm64-msvc-0.7.4.tgz", "resolved": "https://registry.npmjs.org/wickra-win32-arm64-msvc/-/wickra-win32-arm64-msvc-0.7.7.tgz",
"integrity": "sha512-EXIckHxAtF75PUGDKRzXyqMe9ldP0JjSdu68WFN6iJfp+McYrGu6h40TEJlQ/oUEIoPqiZB/xhVyo/el5Lg7zw==", "integrity": "sha512-EXIckHxAtF75PUGDKRzXyqMe9ldP0JjSdu68WFN6iJfp+McYrGu6h40TEJlQ/oUEIoPqiZB/xhVyo/el5Lg7zw==",
"cpu": [ "cpu": [
"arm64" "arm64"
@@ -121,8 +121,8 @@
} }
}, },
"node_modules/wickra-win32-x64-msvc": { "node_modules/wickra-win32-x64-msvc": {
"version": "0.7.4", "version": "0.7.7",
"resolved": "https://registry.npmjs.org/wickra-win32-x64-msvc/-/wickra-win32-x64-msvc-0.7.4.tgz", "resolved": "https://registry.npmjs.org/wickra-win32-x64-msvc/-/wickra-win32-x64-msvc-0.7.7.tgz",
"integrity": "sha512-Yfsqq1Xwp6hdxMyLze411vNdo7BDwI6+lPSe7A9XdqyPecNDbtKwYLpsal2r8EHbNzqM+R8XnuRtUaEQS5VlUQ==", "integrity": "sha512-Yfsqq1Xwp6hdxMyLze411vNdo7BDwI6+lPSe7A9XdqyPecNDbtKwYLpsal2r8EHbNzqM+R8XnuRtUaEQS5VlUQ==",
"cpu": [ "cpu": [
"x64" "x64"
+7 -7
View File
@@ -1,6 +1,6 @@
{ {
"name": "wickra", "name": "wickra",
"version": "0.7.4", "version": "0.7.7",
"description": "Streaming-first technical indicators: incremental, fast, install-free. Node bindings powered by Rust.", "description": "Streaming-first technical indicators: incremental, fast, install-free. Node bindings powered by Rust.",
"author": "kingchenc <support@wickra.org>", "author": "kingchenc <support@wickra.org>",
"main": "index.js", "main": "index.js",
@@ -47,12 +47,12 @@
"node": ">= 18" "node": ">= 18"
}, },
"optionalDependencies": { "optionalDependencies": {
"wickra-linux-x64-gnu": "0.7.4", "wickra-linux-x64-gnu": "0.7.7",
"wickra-linux-arm64-gnu": "0.7.4", "wickra-linux-arm64-gnu": "0.7.7",
"wickra-darwin-x64": "0.7.4", "wickra-darwin-x64": "0.7.7",
"wickra-darwin-arm64": "0.7.4", "wickra-darwin-arm64": "0.7.7",
"wickra-win32-x64-msvc": "0.7.4", "wickra-win32-x64-msvc": "0.7.7",
"wickra-win32-arm64-msvc": "0.7.4" "wickra-win32-arm64-msvc": "0.7.7"
}, },
"scripts": { "scripts": {
"build": "napi build --platform --release", "build": "napi build --platform --release",
+5 -3
View File
@@ -9,7 +9,8 @@
system dependencies, no C build tooling.** system dependencies, no C build tooling.**
Wickra is a multi-language technical-analysis library with a Rust core and 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 streaming state machine, so live trading bots and historical backtests share
the exact same implementation. This package is the Python binding (PyO3); it the exact same implementation. This package is the Python binding (PyO3); it
exposes 200+ streaming-first indicators across sixteen families. 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> - **Docs** (quickstarts, cookbook, TA-Lib migration): <https://docs.wickra.org>
- **Runnable examples:** [`examples/python/`](https://github.com/wickra-lib/wickra/tree/main/examples/python) - **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 Wickra ships native bindings for Python, Node.js, WebAssembly and Rust, plus a
expose the same indicators from the shared, `unsafe`-forbidden Rust core. 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 ## Disclaimer
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "maturin"
[project] [project]
name = "wickra" name = "wickra"
version = "0.7.4" version = "0.7.7"
description = "Streaming-first technical indicators: incremental, fast, install-free." description = "Streaming-first technical indicators: incremental, fast, install-free."
readme = "README.md" readme = "README.md"
license = "MIT OR Apache-2.0" 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-wasm` — pure WebAssembly, runs anywhere a modern JS engine does.**
Wickra is a multi-language technical-analysis library with a Rust core and 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 streaming state machine, so live trading dashboards and historical backtests
share the exact same implementation. This package is the WebAssembly binding share the exact same implementation. This package is the WebAssembly binding
(wasm-bindgen, built for the `web` target); it exposes 200+ streaming-first (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> - **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) - **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 Wickra ships native bindings for Python, Node.js, WebAssembly and Rust, plus a
expose the same indicators from the shared, `unsafe`-forbidden Rust core. 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 ## Disclaimer
+5 -2
View File
@@ -6,8 +6,11 @@ That includes:
- **Quickstarts** for [Rust](https://docs.wickra.org/Quickstart-Rust), - **Quickstarts** for [Rust](https://docs.wickra.org/Quickstart-Rust),
[Python](https://docs.wickra.org/Quickstart-Python), [Python](https://docs.wickra.org/Quickstart-Python),
[Node](https://docs.wickra.org/Quickstart-Node), and [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), and
[Go](https://docs.wickra.org/Quickstart-Go).
- A per-indicator deep dive for every one of the **514 indicators** across - A per-indicator deep dive for every one of the **514 indicators** across
the sixteen families (Moving Averages, Momentum Oscillators, Trend & the sixteen families (Moving Averages, Momentum Oscillators, Trend &
Directional, Price Oscillators, Volatility & Bands, Bands & Channels, 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_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` | | `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/` ## Python — `examples/python/`
| Example | What it does | Run | | 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