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.
This commit is contained in:
kingchenc
2026-06-09 14:32:05 +02:00
committed by GitHub
parent 4caaa1db97
commit 91f6f67257
59 changed files with 38713 additions and 87 deletions
+4
View File
@@ -6,3 +6,7 @@
# 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
+2 -2
View File
@@ -30,9 +30,9 @@ assignees: ""
## Environment
- Wickra version:
- Language / binding: <!-- Rust crate / Python / Node / WASM / C ABI -->
- Language / binding: <!-- Rust crate / Python / Node / WASM / C ABI / C# (.NET) -->
- OS and architecture:
- Rust / Python / Node version (If relevant):
- Rust / Python / Node / .NET version (If relevant):
## Additional context
@@ -26,7 +26,7 @@ assignees: []
| Binding version | `e.g. python 0.4.2 / node 0.4.2` |
| OS / arch | `e.g. Windows 11 x86_64, Linux glibc` |
| Rust toolchain | `rustc --version` (If building from source) |
| Python / Node version | `python --version` / `node --version` |
| Python / Node / .NET version | `python --version` / `node --version` / `dotnet --version` |
## Minimal reproducer
@@ -25,6 +25,8 @@ assignees: ""
- [ ] Should be exposed in the Python binding
- [ ] Should be exposed in the Node binding
- [ ] Should be exposed in the WASM binding
- [ ] Should be exposed in the C ABI
- [ ] Should be exposed in the C# / .NET binding
## Additional context
@@ -13,7 +13,7 @@ assignees: []
## Affected code path
- Indicator / API: `e.g. EMA.update`
- Binding: `Rust / Python / Node / Wasm / C ABI`
- Binding: `Rust / Python / Node / Wasm / C ABI / C# (.NET)`
- Hot loop or one-shot call?
## Versions compared
+1 -1
View File
@@ -33,4 +33,4 @@ import wickra as ta
## Environment (Only if relevant)
- Wickra version: `e.g. 0.4.2`
- Binding: `Rust / Python / Node / Wasm / C ABI`
- Binding: `Rust / Python / Node / Wasm / C ABI / C# (.NET)`
+1 -1
View File
@@ -22,7 +22,7 @@
- [ ] `cargo clippy --workspace --all-targets -- -D warnings` is clean.
- [ ] `cargo test --workspace` passes.
- [ ] New behaviour has tests; bug fixes have a regression test.
- [ ] Public API changes are mirrored in the Python / Node / WASM bindings and the C ABI is regenerated
- [ ] Public API changes are mirrored in the Python / Node / WASM bindings, and the C ABI + C# binding are regenerated
and their type stubs (If applicable).
- [ ] The relevant page on the [documentation site](https://docs.wickra.org)
and the `README.md` are updated (If applicable). Docs edits go to a
@@ -23,6 +23,8 @@ Please fill in the sections below. Delete any that don't apply.
- [ ] Python binding (`bindings/python`)
- [ ] Node.js binding (`bindings/node`)
- [ ] WebAssembly binding (`bindings/wasm`)
- [ ] C ABI (`bindings/c`)
- [ ] C# / .NET binding (`bindings/csharp`)
- [ ] Examples / docs
## Linked issues
+50
View File
@@ -677,6 +677,56 @@ jobs:
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
# The cross-library benchmark has moved to a dedicated scheduled workflow
# (.github/workflows/bench.yml) — see audit finding R10. It runs nightly
# at 03:00 UTC and on-demand via `workflow_dispatch`, and is no longer on
+82
View File
@@ -624,6 +624,88 @@ jobs:
path: wickra-c-${{ matrix.target }}.tar.gz
if-no-files-found: error
# Pack and publish the .NET binding to NuGet. Independent of the GitHub-release
# job so a C# hiccup never blocks the C/C++ asset release. Authentication uses
# NuGet Trusted Publishing (OIDC) — no long-lived API key. The 'wickra-release'
# trusted-publishing policy on nuget.org (owner KingchenC, repo wickra-lib/wickra,
# workflow release.yml) exchanges the GitHub OIDC token for a short-lived key.
csharp-publish:
name: Publish to NuGet
needs: c-abi-build
runs-on: ubuntu-latest
environment: release
permissions:
contents: read
id-token: write # request the GitHub OIDC token for trusted publishing
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- name: Download the C ABI native libraries
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
pattern: c-abi-*
path: c-abi-artifacts
- name: Stage native libraries into runtimes/<rid>/native
shell: bash
run: |
set -e
declare -A RID=(
[x86_64-unknown-linux-gnu]=linux-x64
[aarch64-unknown-linux-gnu]=linux-arm64
[x86_64-apple-darwin]=osx-x64
[aarch64-apple-darwin]=osx-arm64
[x86_64-pc-windows-msvc]=win-x64
[aarch64-pc-windows-msvc]=win-arm64
)
base=bindings/csharp/Wickra/runtimes
for target in "${!RID[@]}"; do
archive=$(find c-abi-artifacts -name "wickra-c-$target.tar.gz" | head -1)
if [ -z "$archive" ]; then
echo "::error::missing native artifact for $target"; exit 1
fi
tmp=$(mktemp -d); tar -xzf "$archive" -C "$tmp"
dest="$base/${RID[$target]}/native"; mkdir -p "$dest"
for f in libwickra.so libwickra.dylib wickra.dll; do
src=$(find "$tmp" -name "$f" | head -1)
[ -n "$src" ] && cp "$src" "$dest/"
done
echo "staged ${RID[$target]}:"; ls -l "$dest"
done
- name: Pack
shell: bash
run: |
version="${GITHUB_REF_NAME#v}"
dotnet pack bindings/csharp/Wickra/Wickra.csproj -c Release -p:Version="$version" -o nupkg
# Exchange the GitHub OIDC token for a short-lived (~1h) NuGet API key.
# 'user' is the nuget.org profile name (the package owner), not an email.
- name: NuGet login (OIDC -> temporary API key)
uses: NuGet/login@8d196754b4036150537f80ac539e15c2f1028841 # v1.2.0
id: nuget_login
with:
user: KingchenC
# Pass the temporary key through the environment (not string-interpolated
# into the script) so it cannot be parsed as shell — avoids template injection.
- name: Push to NuGet
shell: bash
env:
NUGET_API_KEY: ${{ steps.nuget_login.outputs.NUGET_API_KEY }}
run: |
dotnet nuget push "nupkg/*.nupkg" --api-key "$NUGET_API_KEY" \
--source https://api.nuget.org/v3/index.json --skip-duplicate
- name: Upload the NuGet package as a build artifact
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: nuget-package
path: nupkg/*.nupkg
if-no-files-found: error
github-release:
name: Attach assets to the draft GitHub Release
needs: [cargo-publish, python-publish, node-publish, wasm-publish, c-abi-build]
+1 -1
View File
@@ -149,7 +149,7 @@ jobs:
# actually live (Cloudflare Pages, P8.1); merging this PR is therefore
# gated on the domain resolving, otherwise the About link would 404.
homepage="https://docs.wickra.org"
desc="Streaming-first technical indicators with a Rust core and Python, Node.js, WebAssembly, and C ABI 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, and .NET bindings. ${n} indicators, O(1) per-tick updates, no system dependencies. Drop-in TA-Lib replacement."
# Enforce the homepage unconditionally — it is a constant, so this both
# corrects the stale kingchenc URL and self-heals any future drift.
# Same Administration-write permission as --description (no extra scope).
+15 -10
View File
@@ -27,21 +27,26 @@ or replace lives behind a separate crate boundary.
│ no I/O, no deps │ │ optional features │
└──────────────────────┘ └────────────────────┘
(every binding wraps the same core)
┌───────────┬────────────┴┬──────────────┬──────────────────────┐
│ │ │ │ │
┌──▼─────┐ ┌───▼────┐ ┌──────▼──────┐ ┌─────▼──────────────┐
Python │ │ Node │ │ WASM │ │ C ABI (cbindgen)
│ (PyO3) │ │(napi-rs)│ │(wasm-bindgen)│ │ cdylib + header │
└────────┘ └────────┘ └─────────────┘ └────────────────────┘
every binding wraps the same core
┌────────────┼────────────┬────────────────┐
│ │ │ │
┌──▼───────┐ ┌──▼───────┐ ┌──▼───────────┐ ┌──▼──────────────────┐
│ Python │ │ Node │ │ WASM │ │ C ABI (cbindgen) │
(PyO3) │ │ (napi-rs)│ │(wasm-bindgen)│ │ cdylib + header
└──────────┘ └──────────┘ └──────────────┘ └─────────┬───────────┘
│ linked by
┌──────────▼──────────┐
│ C · C++ · C# · Go │
│ · Java · R │
└─────────────────────┘
```
Python, Node and WASM are *native* Rust bindings (PyO3 / napi-rs /
wasm-bindgen). The C ABI is the *hub* every other C-capable language links
against: it builds to a `cdylib`/`staticlib` plus a generated `wickra.h`, and
C / C++ / Go / C# / Java / R consume that one artifact rather than each
re-wrapping the core.
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) is generated from `wickra.h`, with Go / Java / R planned the same way.
| Crate | Path | What it owns | Public deps |
|---|---|---|---|
+6
View File
@@ -6,6 +6,12 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased]
### 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
+3 -1
View File
@@ -22,6 +22,7 @@ licensed as above, without any additional terms or conditions.
| `bindings/node` | napi-rs bindings (`wickra` on npm). |
| `bindings/wasm` | wasm-bindgen bindings (`wickra-wasm` on npm). |
| `bindings/c` | C ABI — `cdylib` + `staticlib` + generated `include/wickra.h`. The hub for C / C++ and any C-capable language. |
| `bindings/csharp` | .NET binding over the C ABI (`Wickra` on NuGet) — `[LibraryImport]` P/Invoke generated from `wickra.h`. |
| `examples/` | Runnable examples. |
| `docs/` | Pointer to the documentation site (docs.wickra.org); the docs live in the `wickra-lib/wickra-docs` repo. |
@@ -105,7 +106,8 @@ installed. Dependabot also keeps the `.github/requirements` pins current.
- **Bindings.** A change to a public indicator API must be mirrored across the
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`.
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.
- **Docs.** Update the relevant page on the
[documentation site](https://docs.wickra.org) and the
`README.md` when behaviour or the public API changes. The docs live in
+32 -21
View File
@@ -2,24 +2,25 @@
<a href="https://wickra.org"><img src="https://raw.githubusercontent.com/wickra-lib/.github/main/profile/wickra-banner.webp?v=514" alt="Wickra — streaming-first technical indicators" width="100%"></a>
</p>
[![CI](https://github.com/wickra-lib/wickra/actions/workflows/ci.yml/badge.svg)](https://github.com/wickra-lib/wickra/actions/workflows/ci.yml)
[![CodeQL](https://github.com/wickra-lib/wickra/actions/workflows/codeql.yml/badge.svg)](https://github.com/wickra-lib/wickra/actions/workflows/codeql.yml)
[![codecov](https://codecov.io/gh/wickra-lib/wickra/branch/main/graph/badge.svg)](https://codecov.io/gh/wickra-lib/wickra)
[![GitHub release](https://img.shields.io/github/v/release/wickra-lib/wickra?logo=github&color=green)](https://github.com/wickra-lib/wickra/releases/latest)
[![crates.io](https://img.shields.io/crates/v/wickra.svg?logo=rust&color=orange)](https://crates.io/crates/wickra)
[![PyPI](https://img.shields.io/pypi/v/wickra.svg?logo=pypi&color=blue)](https://pypi.org/project/wickra/)
[![npm](https://img.shields.io/npm/v/wickra.svg?logo=npm&color=red)](https://www.npmjs.com/package/wickra)
[![License: MIT OR Apache-2.0](https://img.shields.io/badge/license-MIT_OR_Apache--2.0-blue)](#license)
[![OpenSSF Scorecard](https://api.securityscorecards.dev/projects/github.com/wickra-lib/wickra/badge)](https://scorecard.dev/viewer/?uri=github.com/wickra-lib/wickra)
[![OpenSSF Best Practices](https://www.bestpractices.dev/projects/13094/badge)](https://www.bestpractices.dev/projects/13094)
[![Build provenance](https://img.shields.io/badge/provenance-attested-brightgreen?logo=github)](https://github.com/wickra-lib/wickra/attestations)
[![Docs](https://img.shields.io/badge/docs-docs.wickra.org-0ea5e9?logo=readthedocs&logoColor=white)](https://docs.wickra.org)
[![CI](https://raw.githubusercontent.com/wickra-lib/.github/main/profile/badges/ci.svg)](https://github.com/wickra-lib/wickra/actions/workflows/ci.yml)
[![CodeQL](https://raw.githubusercontent.com/wickra-lib/.github/main/profile/badges/codeql.svg)](https://github.com/wickra-lib/wickra/actions/workflows/codeql.yml)
[![codecov](https://raw.githubusercontent.com/wickra-lib/.github/main/profile/badges/codecov.svg)](https://codecov.io/gh/wickra-lib/wickra)
[![GitHub release](https://raw.githubusercontent.com/wickra-lib/.github/main/profile/badges/release.svg)](https://github.com/wickra-lib/wickra/releases/latest)
[![crates.io](https://raw.githubusercontent.com/wickra-lib/.github/main/profile/badges/crates.svg)](https://crates.io/crates/wickra)
[![PyPI](https://raw.githubusercontent.com/wickra-lib/.github/main/profile/badges/pypi.svg)](https://pypi.org/project/wickra/)
[![npm](https://raw.githubusercontent.com/wickra-lib/.github/main/profile/badges/npm.svg)](https://www.npmjs.com/package/wickra)
[![NuGet](https://raw.githubusercontent.com/wickra-lib/.github/main/profile/badges/nuget.svg)](https://www.nuget.org/packages/Wickra)
[![License: MIT OR Apache-2.0](https://raw.githubusercontent.com/wickra-lib/.github/main/profile/badges/license.svg)](#license)
[![OpenSSF Scorecard](https://raw.githubusercontent.com/wickra-lib/.github/main/profile/badges/scorecard.svg)](https://scorecard.dev/viewer/?uri=github.com/wickra-lib/wickra)
[![OpenSSF Best Practices](https://raw.githubusercontent.com/wickra-lib/.github/main/profile/badges/best-practices.svg)](https://www.bestpractices.dev/projects/13094)
[![Build provenance](https://raw.githubusercontent.com/wickra-lib/.github/main/profile/badges/provenance.svg)](https://github.com/wickra-lib/wickra/attestations)
[![Docs](https://raw.githubusercontent.com/wickra-lib/.github/main/profile/badges/docs.svg)](https://docs.wickra.org)
**Streaming-first technical indicators. Install with `pip install wickra` — no system dependencies.**
Wickra is a multi-language technical-analysis library with a Rust core and
native bindings for Python, Node.js and WebAssembly, plus a C ABI that any
C-capable language (C, C++, and beyond) links against. Every indicator is a
native bindings for Python, Node.js and WebAssembly, plus a C ABI that C, C++,
C# / .NET 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.
@@ -47,7 +48,9 @@ Full documentation lives at **[docs.wickra.org](https://docs.wickra.org)**:
- **Quickstarts** — [Rust](https://docs.wickra.org/Quickstart-Rust),
[Python](https://docs.wickra.org/Quickstart-Python),
[Node](https://docs.wickra.org/Quickstart-Node),
[WASM](https://docs.wickra.org/Quickstart-WASM).
[WASM](https://docs.wickra.org/Quickstart-WASM),
[C](https://docs.wickra.org/Quickstart-C),
[C#](https://docs.wickra.org/Quickstart-CSharp).
- **Indicators** — a per-indicator deep dive (formula, parameters, warmup) for
every one of the 514 indicators; start at the
[indicators overview](https://docs.wickra.org/Indicators-Overview).
@@ -73,7 +76,7 @@ times to get there.
metrics — every single one updating in **O(1) per tick**. TA-Lib ships ~150 and
none of them stream.
- **One Rust core, five first-class targets.** Native **Python · Node.js ·
WebAssembly · Rust** plus a **C ABI** for C / C++ and any C-capable language —
WebAssembly · Rust** plus a **C ABI** for C / C++, C# / .NET and any other C-capable language —
identical math, identical results, zero per-language reimplementation and zero
GIL bottleneck.
- **Correct by construction, not by hope.** Every `update` validates its input,
@@ -97,7 +100,8 @@ Every other library forces one of those compromises. Wickra doesn't:
| Library | Install | Streaming | Languages | Indicators | Active |
|------------------|-------------|-------------|-----------------------------|-----------:|--------|
| **★&nbsp;Wickra**| **clean** | **yes, O(1)** | **Python · Node · WASM · Rust · C** | **514** | **yes** |
| **★&nbsp;Wickra**| **clean** | **yes, O(1)** | **Rust · Python · Node · WASM** | **514** | **yes** |
| | | | **C · C#** | | |
| kand | clean | yes | Python · WASM · Rust | ~60 | yes |
| ta-rs | clean | yes | Rust only | ~30 | stale |
| yata | clean | partial | Rust only | ~35 | yes |
@@ -168,8 +172,9 @@ as one column each. `Doji` is direction-less by default (`+1.0` / `0.0`);
construct it in signed mode (`Doji::new().signed()`, `Doji(signed=True)`,
`new Doji(true)`) for a dragonfly / gravestone `±1` reading.
Adding a new indicator means implementing one trait in Rust; all five bindings
inherit it automatically (the C ABI is generated from the core).
Adding a new indicator means implementing one trait in Rust; every binding
inherits it automatically (the C ABI — and the C# binding generated from it —
regenerate from the core).
## Languages
@@ -180,6 +185,7 @@ inherit it automatically (the C ABI is generated from the core).
| Browser / WASM | `npm install wickra-wasm` | `examples/wasm/index.html` |
| Rust | `cargo add wickra` | `examples/rust/src/bin/backtest.rs` |
| C / C++ (C ABI) | header + library, see [`bindings/c`](bindings/c) | `examples/c/streaming.c` |
| C# / .NET (C ABI) | `dotnet add package Wickra`, see [`bindings/csharp`](bindings/csharp) | `examples/csharp/streaming` |
Each binding ships several runnable examples (streaming, backtest, live feed);
[`examples/README.md`](examples/README.md) is the full cross-language index.
@@ -249,14 +255,16 @@ wickra/
│ ├── python/ PyO3 + maturin (publishes on PyPI)
│ ├── node/ napi-rs (publishes on npm)
│ ├── wasm/ wasm-bindgen (browsers, bundlers, Node)
── c/ C ABI (cdylib + staticlib) + generated include/wickra.h
── c/ C ABI (cdylib + staticlib) + generated include/wickra.h
│ └── csharp/ .NET binding over the C ABI (publishes on NuGet)
├── examples/ examples/README.md indexes every language
│ ├── data/ real BTCUSDT OHLCV datasets, one per timeframe
│ ├── rust/ Rust workspace member (`wickra-examples`)
│ ├── python/ backtest, live trading, parallel assets, multi-tf
│ ├── node/ streaming, backtest, live trading (load `wickra`)
│ ├── wasm/ browser demo for `wickra-wasm`
── c/ C smoke + streaming, C++ RAII wrapper
── c/ C smoke + streaming, C++ RAII wrapper
│ └── csharp/ streaming, backtest, strategies (load `Wickra`)
└── .github/workflows/ CI and release pipelines
```
@@ -289,6 +297,9 @@ cd bindings/node && npm install && npm run build && npm test
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
```
## Testing
+3 -2
View File
@@ -21,8 +21,9 @@ minor releases; breaking changes are called out in the changelog.
versioning stability for a 1.0 release.
- **Performance.** Keep per-tick updates O(1) and maintain the benchmark suite;
investigate further allocation and cache improvements.
- **Bindings parity.** Keep the Python, Node.js and WebAssembly bindings in
lockstep with the Rust core, including type stubs and platform coverage.
- **Bindings parity.** Keep the Python, Node.js and WebAssembly bindings — plus
the C ABI and the C# / .NET binding generated from it — in lockstep with the
Rust core, including type stubs and platform coverage.
- **Documentation.** Maintain a deep-dive page per indicator on
<https://docs.wickra.org>, plus quickstarts and cookbook material.
- **Project health.** Maintain test coverage, static and dynamic analysis,
+2 -2
View File
@@ -7,8 +7,8 @@ Thanks for using Wickra! Here is where to get help, depending on what you need.
Most questions are answered in the documentation:
- **Docs site:** <https://docs.wickra.org> — quickstarts for Rust, Python,
Node.js and WebAssembly, a per-indicator reference, warmup periods, the data
layer, and an FAQ.
Node.js, WebAssembly, C and C#, a per-indicator reference, warmup periods, the
data layer, and an FAQ.
- **README:** <https://github.com/wickra-lib/wickra#readme> — installation and a
quick overview.
- **API docs (Rust):** <https://docs.rs/wickra>.
+2 -1
View File
@@ -3,7 +3,8 @@
This document describes Wickra's attack surface and the threats considered,
together with their mitigations. It complements the security assurance case in
[`SECURITY.md`](SECURITY.md). Wickra is a computational technical-analysis
library (a Rust core with Python, Node.js and WebAssembly bindings plus a C ABI),
library (a Rust core with Python, Node.js and WebAssembly bindings plus a C ABI
and the .NET binding built on it),
not a network service or trading system; the attack surface is correspondingly
small.
+57 -36
View File
@@ -1,59 +1,80 @@
# wickra-c
# Wickra — C / C++
C ABI for [Wickra](https://github.com/wickra-lib/wickra) — streaming-first
technical indicators with a Rust core. This crate is the **hub**: it compiles the
[![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# 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-wiring every indicator natively.
of re-wrapping every indicator natively.
The native Python, Node, and WebAssembly bindings are unaffected — this is
additive, for the ecosystems without first-class Rust tooling.
## Install
## Artifacts
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:
```sh
```bash
cargo build -p wickra-c --release
# -> target/release/libwickra.{so,dylib} or wickra.dll (+ import lib) + a staticlib
```
- `target/release/libwickra.{so,dylib}` / `wickra.dll` (+ `wickra.dll.lib` import lib on Windows)
- `target/release/libwickra.a` / `wickra.lib` (static)
- [`include/wickra.h`](include/wickra.h) — generated by cbindgen, committed.
Then compile against the header and link the library
(`cc app.c -I include -L lib -lwickra -lm -o app`).
## API shape
Each indicator is exposed as five `extern "C"` functions over an opaque handle:
## Quick start
```c
struct Sma *wickra_sma_new(uintptr_t period); /* NULL on bad params */
double wickra_sma_update(struct Sma *h, double value); /* NaN during warmup */
void wickra_sma_batch(struct Sma *h, const double *in, double *out, uintptr_t n);
void wickra_sma_reset(struct Sma *h);
void wickra_sma_free(struct Sma *h); /* exactly once per _new */
#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 */
```
Conventions:
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.
- **Opaque handles.** `wickra_<ind>_new` returns a `T *` you must release with
exactly one `wickra_<ind>_free`. There is no RAII across the boundary.
- **NaN sentinel.** Scalar outputs return `NaN` while warming up or on a `NULL`
handle, mirroring the other bindings — no error codes for the common path.
- **Caller-owned batch buffers.** `wickra_<ind>_batch` writes one output per
input into a buffer you provide; nothing is allocated across the boundary.
- **NULL-safe.** Every function tolerates a `NULL` handle without crashing.
## Documentation
## Header regeneration
The full indicator catalogue, guides, quickstarts, and API reference live in
the main repository and documentation site:
The header is generated and committed; CI checks it is in sync:
- **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)
```sh
cbindgen --config bindings/c/cbindgen.toml --crate wickra-c --output bindings/c/include/wickra.h
```
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.
## Examples
## Disclaimer
Runnable C examples (build via CMake or a direct compiler invocation) live in
[`examples/c`](../../examples/c).
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
`MIT OR Apache-2.0`, the same as the rest of Wickra.
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.
+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# 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.5</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";
}
}
+1 -1
View File
@@ -9,7 +9,7 @@
prebuilt native binary, no system dependencies.**
Wickra is a multi-language technical-analysis library with a Rust core and
bindings for Python, Node.js and WebAssembly, plus a C ABI for C/C++ and any
bindings for Python, Node.js and WebAssembly, plus a C ABI for C/C++, C# and any
other C-capable language. Every indicator is an O(1)
streaming state machine, so live trading bots and historical backtests share
the exact same implementation. This package is the Node.js binding (napi-rs);
+1 -1
View File
@@ -9,7 +9,7 @@
system dependencies, no C build tooling.**
Wickra is a multi-language technical-analysis library with a Rust core and
bindings for Python, Node.js and WebAssembly, plus a C ABI for C/C++ and any
bindings for Python, Node.js and WebAssembly, plus a C ABI for C/C++, C# and any
other C-capable language. Every indicator is an O(1)
streaming state machine, so live trading bots and historical backtests share
the exact same implementation. This package is the Python binding (PyO3); it
+1 -1
View File
@@ -9,7 +9,7 @@
wickra-wasm` — pure WebAssembly, runs anywhere a modern JS engine does.**
Wickra is a multi-language technical-analysis library with a Rust core and
bindings for Python, Node.js and WebAssembly, plus a C ABI for C/C++ and any
bindings for Python, Node.js and WebAssembly, plus a C ABI for C/C++, C# and any
other C-capable language. Every indicator is an O(1)
streaming state machine, so live trading dashboards and historical backtests
share the exact same implementation. This package is the WebAssembly binding
+4 -2
View File
@@ -6,8 +6,10 @@ That includes:
- **Quickstarts** for [Rust](https://docs.wickra.org/Quickstart-Rust),
[Python](https://docs.wickra.org/Quickstart-Python),
[Node](https://docs.wickra.org/Quickstart-Node), and
[WASM](https://docs.wickra.org/Quickstart-WASM).
[Node](https://docs.wickra.org/Quickstart-Node),
[WASM](https://docs.wickra.org/Quickstart-WASM),
[C](https://docs.wickra.org/Quickstart-C), and
[C#](https://docs.wickra.org/Quickstart-CSharp).
- A per-indicator deep dive for every one of the **514 indicators** across
the sixteen families (Moving Averages, Momentum Oscillators, Trend &
Directional, Price Oscillators, Volatility & Bands, Bands & Channels,
+21
View File
@@ -48,6 +48,27 @@ three `strategy_*`) build against the bundled datasets and run under `ctest`.
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.
## Python — `examples/python/`
| Example | What it does | Run |
+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>
+1 -1
View File
@@ -61,4 +61,4 @@ indicators tracks those.
- [Quickstart: WASM](https://docs.wickra.org/Quickstart-WASM) — module-load
flow, `wasm-pack` targets, and the streaming API.
- [examples/README.md](../README.md) — cross-language index, including
the Rust, Python and Node siblings of every demo above.
the Rust, Python, Node, C and C# siblings of every demo above.
-1
View File
@@ -34,7 +34,6 @@ ci_workflow = "ci.yml"
# resurfaces in source, the audit workflow fails.
forbidden = [
"kingchenc/wickra",
"kingchencp@gmail.com",
]
# Paths that are exempt from the forbidden-substring scan. Historical