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:
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)`
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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
@@ -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,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
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
[](https://github.com/wickra-lib/wickra/actions/workflows/ci.yml)
|
||||
[](https://github.com/wickra-lib/wickra/actions/workflows/codeql.yml)
|
||||
[](https://codecov.io/gh/wickra-lib/wickra)
|
||||
[](https://github.com/wickra-lib/wickra/releases/latest)
|
||||
[](https://crates.io/crates/wickra)
|
||||
[](https://pypi.org/project/wickra/)
|
||||
[](https://www.npmjs.com/package/wickra)
|
||||
[](#license)
|
||||
[](https://scorecard.dev/viewer/?uri=github.com/wickra-lib/wickra)
|
||||
[](https://www.bestpractices.dev/projects/13094)
|
||||
[](https://github.com/wickra-lib/wickra/attestations)
|
||||
[](https://docs.wickra.org)
|
||||
[](https://github.com/wickra-lib/wickra/actions/workflows/ci.yml)
|
||||
[](https://github.com/wickra-lib/wickra/actions/workflows/codeql.yml)
|
||||
[](https://codecov.io/gh/wickra-lib/wickra)
|
||||
[](https://github.com/wickra-lib/wickra/releases/latest)
|
||||
[](https://crates.io/crates/wickra)
|
||||
[](https://pypi.org/project/wickra/)
|
||||
[](https://www.npmjs.com/package/wickra)
|
||||
[](https://www.nuget.org/packages/Wickra)
|
||||
[](#license)
|
||||
[](https://scorecard.dev/viewer/?uri=github.com/wickra-lib/wickra)
|
||||
[](https://www.bestpractices.dev/projects/13094)
|
||||
[](https://github.com/wickra-lib/wickra/attestations)
|
||||
[](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 |
|
||||
|------------------|-------------|-------------|-----------------------------|-----------:|--------|
|
||||
| **★ Wickra**| **clean** | **yes, O(1)** | **Python · Node · WASM · Rust · C** | **514** | **yes** |
|
||||
| **★ 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
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
[](https://github.com/wickra-lib/wickra/actions/workflows/ci.yml)
|
||||
[](https://codecov.io/gh/wickra-lib/wickra)
|
||||
[](https://github.com/wickra-lib/wickra/releases/latest)
|
||||
[](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.
|
||||
|
||||
@@ -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/
|
||||
@@ -0,0 +1,78 @@
|
||||
# Wickra — .NET
|
||||
|
||||
[](https://github.com/wickra-lib/wickra/actions/workflows/ci.yml)
|
||||
[](https://codecov.io/gh/wickra-lib/wickra)
|
||||
[](https://www.nuget.org/packages/Wickra)
|
||||
[](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));
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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>
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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/<rid>/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";
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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,
|
||||
|
||||
@@ -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 |
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
# .NET build output
|
||||
bin/
|
||||
obj/
|
||||
*.user
|
||||
|
||||
# Data fetched at runtime by fetch_btcusdt
|
||||
**/data/
|
||||
@@ -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>
|
||||
@@ -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).
|
||||
@@ -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}");
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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}");
|
||||
@@ -0,0 +1,5 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user