Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| fce26cf881 | |||
| 91f6f67257 | |||
| 4caaa1db97 | |||
| 12681e4b1b | |||
| 91e05e3c26 | |||
| 9d0983b666 |
@@ -1,3 +1,12 @@
|
||||
# Shell scripts must keep LF line endings so they run on Linux/macOS CI and
|
||||
# local shells regardless of the committer's platform autocrlf setting.
|
||||
*.sh text eol=lf
|
||||
|
||||
# The cbindgen-generated C header is committed; pin it to LF so its CI drift
|
||||
# check (regenerate + `git diff`) never trips on a CRLF normalization.
|
||||
bindings/c/include/wickra.h text eol=lf
|
||||
|
||||
# C# sources (including the generated binding) are pinned to LF so the committed
|
||||
# files stay stable regardless of the committer's autocrlf setting.
|
||||
*.cs text eol=lf
|
||||
|
||||
|
||||
@@ -30,9 +30,9 @@ assignees: ""
|
||||
## Environment
|
||||
|
||||
- Wickra version:
|
||||
- Language / binding: <!-- Rust crate / Python / Node / WASM -->
|
||||
- Language / binding: <!-- Rust crate / Python / Node / WASM / C ABI / C# (.NET) -->
|
||||
- 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`
|
||||
- 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`
|
||||
- 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
|
||||
- [ ] 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
|
||||
|
||||
@@ -622,6 +622,111 @@ jobs:
|
||||
working-directory: bindings/node
|
||||
run: node --test __tests__/
|
||||
|
||||
c-abi:
|
||||
name: C ABI on ${{ matrix.os }}
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [ubuntu-latest, macos-latest, windows-latest]
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Install Rust toolchain
|
||||
uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable branch, 2026-03-27
|
||||
|
||||
- name: Cache cargo
|
||||
uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2
|
||||
continue-on-error: true # cache is an optimisation; never block on a stuck/slow restore
|
||||
timeout-minutes: 6
|
||||
|
||||
- name: Install cbindgen
|
||||
uses: taiki-e/install-action@0fd46367812ee04360509b4169d9f659d6892bb2 # v2.79.15
|
||||
timeout-minutes: 10 # fail fast on a stuck download instead of hanging the job
|
||||
with:
|
||||
tool: cbindgen
|
||||
|
||||
- name: Build the C ABI library (cdylib + staticlib)
|
||||
run: cargo build -p wickra-c --release
|
||||
|
||||
- name: Rust unit tests
|
||||
run: cargo test -p wickra-c
|
||||
|
||||
# The generated header is platform-independent, so checking drift on one OS
|
||||
# is enough — and avoids a spurious CRLF/LF diff on the Windows runner.
|
||||
- name: Check the committed header is in sync with cbindgen
|
||||
if: runner.os == 'Linux'
|
||||
shell: bash
|
||||
run: |
|
||||
cbindgen --config bindings/c/cbindgen.toml --crate wickra-c --output bindings/c/include/wickra.h
|
||||
if ! git diff --quiet -- bindings/c/include/wickra.h; then
|
||||
echo "::error::bindings/c/include/wickra.h is out of sync — run cbindgen and commit the result"
|
||||
git --no-pager diff -- bindings/c/include/wickra.h
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# The real cross-language test: a foreign C consumer links the generated
|
||||
# header + the compiled library and runs. If this passes on all three OSes,
|
||||
# every C-capable language can link the same way.
|
||||
- name: Build and run the C smoke example (CMake + ctest)
|
||||
shell: bash
|
||||
run: |
|
||||
cmake -S examples/c -B examples/c/build
|
||||
cmake --build examples/c/build --config Release
|
||||
ctest --test-dir examples/c/build -C Release --output-on-failure
|
||||
|
||||
csharp:
|
||||
name: C# on ${{ matrix.os }}
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [ubuntu-latest, macos-latest, windows-latest]
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Install Rust toolchain
|
||||
uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable branch, 2026-03-27
|
||||
|
||||
- name: Cache cargo
|
||||
uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2
|
||||
continue-on-error: true # cache is an optimisation; never block on a stuck/slow restore
|
||||
timeout-minutes: 6
|
||||
|
||||
# The binding links against the C ABI hub at runtime; build it first so the
|
||||
# DllImportResolver finds target/release/wickra.{dll,so,dylib}. .NET 8 SDK is
|
||||
# preinstalled on the GitHub runners, so no setup-dotnet step is needed.
|
||||
- name: Build the C ABI library
|
||||
run: cargo build -p wickra-c --release
|
||||
|
||||
- name: .NET info
|
||||
run: dotnet --info
|
||||
|
||||
- name: Test the C# binding
|
||||
run: dotnet test bindings/csharp/Wickra.Tests/Wickra.Tests.csproj -c Release
|
||||
|
||||
- name: Build the C# examples
|
||||
shell: bash
|
||||
run: |
|
||||
for d in streaming backtest multi_timeframe parallel_assets \
|
||||
strategy_rsi_mean_reversion strategy_macd_adx strategy_bollinger_squeeze \
|
||||
fetch_btcusdt live_binance; do
|
||||
dotnet build "examples/csharp/$d" -c Release
|
||||
done
|
||||
|
||||
# Run only the offline examples (fetch_btcusdt / live_binance need network).
|
||||
- name: Run the offline C# examples
|
||||
shell: bash
|
||||
run: |
|
||||
for d in streaming backtest multi_timeframe parallel_assets \
|
||||
strategy_rsi_mean_reversion strategy_macd_adx strategy_bollinger_squeeze; do
|
||||
dotnet run --project "examples/csharp/$d" -c Release --no-build
|
||||
done
|
||||
|
||||
# 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
|
||||
|
||||
@@ -569,9 +569,146 @@ jobs:
|
||||
# the old "publish, then upload provenance" order would have the provenance
|
||||
# upload rejected once immutability is enabled.
|
||||
# --------------------------------------------------------------------------
|
||||
# --------------------------------------------------------------------------
|
||||
# C ABI native libraries (bindings/c) — built per target on a native runner
|
||||
# (no cross toolchain needed) and attached to the GitHub Release as the
|
||||
# distribution channel. There is no package registry for the C ABI.
|
||||
# --------------------------------------------------------------------------
|
||||
c-abi-build:
|
||||
name: C ABI library (${{ matrix.target }})
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- { host: ubuntu-latest, target: x86_64-unknown-linux-gnu }
|
||||
- { host: ubuntu-24.04-arm, target: aarch64-unknown-linux-gnu }
|
||||
- { host: macos-latest, target: x86_64-apple-darwin }
|
||||
- { host: macos-latest, target: aarch64-apple-darwin }
|
||||
- { host: windows-latest, target: x86_64-pc-windows-msvc }
|
||||
- { host: windows-11-arm, target: aarch64-pc-windows-msvc }
|
||||
runs-on: ${{ matrix.host }}
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable branch, 2026-03-27
|
||||
with:
|
||||
targets: ${{ matrix.target }}
|
||||
|
||||
- uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2
|
||||
continue-on-error: true # cache is an optimisation; never block on a stuck/slow restore
|
||||
timeout-minutes: 6
|
||||
|
||||
- name: Build the C ABI library (cdylib + staticlib)
|
||||
run: cargo build -p wickra-c --release --target ${{ matrix.target }}
|
||||
|
||||
- name: Package header + libraries
|
||||
shell: bash
|
||||
run: |
|
||||
set -e
|
||||
dir="wickra-c-${{ matrix.target }}"
|
||||
mkdir -p "$dir/include" "$dir/lib"
|
||||
cp bindings/c/include/wickra.h bindings/c/include/wickra.hpp "$dir/include/"
|
||||
for f in libwickra.so libwickra.a libwickra.dylib wickra.dll wickra.dll.lib wickra.lib; do
|
||||
src="target/${{ matrix.target }}/release/$f"
|
||||
[ -f "$src" ] && cp "$src" "$dir/lib/"
|
||||
done
|
||||
tar -czf "$dir.tar.gz" "$dir"
|
||||
echo "packaged $dir:"; ls -lR "$dir"
|
||||
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: c-abi-${{ matrix.target }}
|
||||
path: wickra-c-${{ matrix.target }}.tar.gz
|
||||
if-no-files-found: error
|
||||
|
||||
# Pack and publish the .NET binding to NuGet. Independent of the GitHub-release
|
||||
# job so a C# hiccup never blocks the C/C++ asset release. Authentication uses
|
||||
# NuGet Trusted Publishing (OIDC) — no long-lived API key. The 'wickra-release'
|
||||
# trusted-publishing policy on nuget.org (owner KingchenC, repo wickra-lib/wickra,
|
||||
# workflow release.yml) exchanges the GitHub OIDC token for a short-lived key.
|
||||
csharp-publish:
|
||||
name: Publish to NuGet
|
||||
needs: c-abi-build
|
||||
runs-on: ubuntu-latest
|
||||
environment: release
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: write # request the GitHub OIDC token for trusted publishing
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Download the C ABI native libraries
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
with:
|
||||
pattern: c-abi-*
|
||||
path: c-abi-artifacts
|
||||
|
||||
- name: Stage native libraries into runtimes/<rid>/native
|
||||
shell: bash
|
||||
run: |
|
||||
set -e
|
||||
declare -A RID=(
|
||||
[x86_64-unknown-linux-gnu]=linux-x64
|
||||
[aarch64-unknown-linux-gnu]=linux-arm64
|
||||
[x86_64-apple-darwin]=osx-x64
|
||||
[aarch64-apple-darwin]=osx-arm64
|
||||
[x86_64-pc-windows-msvc]=win-x64
|
||||
[aarch64-pc-windows-msvc]=win-arm64
|
||||
)
|
||||
base=bindings/csharp/Wickra/runtimes
|
||||
for target in "${!RID[@]}"; do
|
||||
archive=$(find c-abi-artifacts -name "wickra-c-$target.tar.gz" | head -1)
|
||||
if [ -z "$archive" ]; then
|
||||
echo "::error::missing native artifact for $target"; exit 1
|
||||
fi
|
||||
tmp=$(mktemp -d); tar -xzf "$archive" -C "$tmp"
|
||||
dest="$base/${RID[$target]}/native"; mkdir -p "$dest"
|
||||
for f in libwickra.so libwickra.dylib wickra.dll; do
|
||||
src=$(find "$tmp" -name "$f" | head -1)
|
||||
[ -n "$src" ] && cp "$src" "$dest/"
|
||||
done
|
||||
echo "staged ${RID[$target]}:"; ls -l "$dest"
|
||||
done
|
||||
|
||||
- name: Pack
|
||||
shell: bash
|
||||
run: |
|
||||
version="${GITHUB_REF_NAME#v}"
|
||||
dotnet pack bindings/csharp/Wickra/Wickra.csproj -c Release -p:Version="$version" -o nupkg
|
||||
|
||||
# Exchange the GitHub OIDC token for a short-lived (~1h) NuGet API key.
|
||||
# 'user' is the nuget.org profile name (the package owner), not an email.
|
||||
- name: NuGet login (OIDC -> temporary API key)
|
||||
uses: NuGet/login@8d196754b4036150537f80ac539e15c2f1028841 # v1.2.0
|
||||
id: nuget_login
|
||||
with:
|
||||
user: KingchenC
|
||||
|
||||
# Pass the temporary key through the environment (not string-interpolated
|
||||
# into the script) so it cannot be parsed as shell — avoids template injection.
|
||||
- name: Push to NuGet
|
||||
shell: bash
|
||||
env:
|
||||
NUGET_API_KEY: ${{ steps.nuget_login.outputs.NUGET_API_KEY }}
|
||||
run: |
|
||||
dotnet nuget push "nupkg/*.nupkg" --api-key "$NUGET_API_KEY" \
|
||||
--source https://api.nuget.org/v3/index.json --skip-duplicate
|
||||
|
||||
- name: Upload the NuGet package as a build artifact
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: nuget-package
|
||||
path: nupkg/*.nupkg
|
||||
if-no-files-found: error
|
||||
|
||||
github-release:
|
||||
name: Attach assets to the draft GitHub Release
|
||||
needs: [cargo-publish, python-publish, node-publish, wasm-publish]
|
||||
needs: [cargo-publish, python-publish, node-publish, wasm-publish, c-abi-build]
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
@@ -644,7 +781,7 @@ jobs:
|
||||
# the provenance bundle is attached (P24, immutability-ready).
|
||||
draft: true
|
||||
body: |
|
||||
Wickra ${{ github.ref_name }} — streaming-first technical indicators across 4 language registries.
|
||||
Wickra ${{ github.ref_name }} — streaming-first technical indicators across 4 language registries plus a C ABI.
|
||||
|
||||
### Install
|
||||
|
||||
@@ -665,6 +802,9 @@ jobs:
|
||||
darwin-arm64, win32-x64-msvc)
|
||||
- `wickra-*.tgz` — npm-pack tarballs (main package + per-platform subpackages + WASM)
|
||||
- `*.crate` — cargo source crates (wickra-core, wickra-data, wickra)
|
||||
- `wickra-c-<target>.tar.gz` — C ABI: `include/wickra.h` + `wickra.hpp`
|
||||
and the cdylib/staticlib per target (linux/macos/windows × x64/arm64),
|
||||
the hub for C / C++ / Go / C# / Java / R
|
||||
|
||||
### Auto-generated changelog
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ name: Sync indicator count
|
||||
# 2. GitHub repo "About" description — synced on push to main / v* tag
|
||||
# 3. Docs site: index.md / overview.md / Indicators-Overview.md
|
||||
# (wickra-lib/wickra-docs) — synced on push to main / v* tag*
|
||||
# 4. Marketing site count (wickra-lib/webpage: index.md /
|
||||
# 4. Marketing site count (wickra-lib/webpage: index.md / about.md /
|
||||
# .vitepress/config.ts) — push to main / v* tag*
|
||||
# 5. org profile README count (wickra-lib/.github, profile/README.md)
|
||||
# — synced on push to main / v* tag*
|
||||
@@ -42,10 +42,10 @@ name: Sync indicator count
|
||||
# single source of truth for what the bindings reach.
|
||||
#
|
||||
# Design: on PRs this workflow is a READ-ONLY check. The indicator wiring
|
||||
# (ScriptHelpers/_common.py wire_readme_counter) bumps both README.md and
|
||||
# docs/README.md inside the author's code commit, so the counter is already
|
||||
# correct by the time CI runs. If it is not, the check below fails loud and
|
||||
# asks the author to re-run the wiring — it never pushes a fix-up commit.
|
||||
# bumps both README.md and docs/README.md inside the author's code commit, so
|
||||
# the counter is already correct by the time CI runs. If it is not, the check
|
||||
# below fails loud and asks the author to re-run the wiring — it never pushes a
|
||||
# fix-up commit.
|
||||
#
|
||||
# (An earlier version pushed a GITHUB_TOKEN "sync indicator count" commit to
|
||||
# the PR head. Because GITHUB_TOKEN pushes trigger no workflows, that commit
|
||||
@@ -149,7 +149,7 @@ jobs:
|
||||
# actually live (Cloudflare Pages, P8.1); merging this PR is therefore
|
||||
# gated on the domain resolving, otherwise the About link would 404.
|
||||
homepage="https://docs.wickra.org"
|
||||
desc="Streaming-first technical indicators with a Rust core and Python, Node.js, and WebAssembly bindings. ${n} indicators, O(1) per-tick updates, no system dependencies. Drop-in TA-Lib replacement."
|
||||
desc="Streaming-first technical indicators with a Rust core and Python, Node.js, WebAssembly, C ABI, 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).
|
||||
@@ -366,14 +366,14 @@ jobs:
|
||||
exit 0
|
||||
fi
|
||||
cd webpage-count
|
||||
sed -i -E "s/[0-9]+ (streaming-first )?indicators/${n} \1indicators/g" index.md .vitepress/config.ts
|
||||
sed -i -E "s/[0-9]+ (streaming-first )?indicators/${n} \1indicators/g" index.md about.md .vitepress/config.ts
|
||||
if git diff --quiet; then
|
||||
echo "Webpage indicator count unchanged."
|
||||
exit 0
|
||||
fi
|
||||
git config user.name "wickra-bot"
|
||||
git config user.email "wickra-bot@users.noreply.github.com"
|
||||
git add index.md .vitepress/config.ts
|
||||
git add index.md about.md .vitepress/config.ts
|
||||
git commit -m "chore: sync indicator count to ${n}"
|
||||
if ! git push 2>/dev/null; then
|
||||
echo "::warning::push to wickra-lib/webpage failed — ABOUT_SYNC_TOKEN likely lacks write (findings P10.0a)."
|
||||
|
||||
+20
-8
@@ -27,16 +27,27 @@ or replace lives behind a separate crate boundary.
|
||||
│ no I/O, no deps │ │ optional features │
|
||||
└──────────────────────┘ └────────────────────┘
|
||||
▲
|
||||
│ (every binding wraps the same core)
|
||||
│
|
||||
┌────────────┴───────────┬─────────────────────┐
|
||||
│ │ │
|
||||
┌──▼──────┐ ┌───────▼──────┐ ┌───────▼────────┐
|
||||
│ Python │ │ Node │ │ WASM │
|
||||
│ (PyO3) │ │ (napi-rs) │ │ (wasm-bindgen) │
|
||||
└─────────┘ └──────────────┘ └────────────────┘
|
||||
│ every binding wraps the same core
|
||||
┌────────────┼────────────┬────────────────┐
|
||||
│ │ │ │
|
||||
┌──▼───────┐ ┌──▼───────┐ ┌──▼───────────┐ ┌──▼──────────────────┐
|
||||
│ Python │ │ Node │ │ WASM │ │ C ABI (cbindgen) │
|
||||
│ (PyO3) │ │ (napi-rs)│ │(wasm-bindgen)│ │ cdylib + header │
|
||||
└──────────┘ └──────────┘ └──────────────┘ └─────────┬───────────┘
|
||||
│ linked by
|
||||
┌──────────▼──────────┐
|
||||
│ C · C++ · C# · Go │
|
||||
│ · Java · R │
|
||||
└─────────────────────┘
|
||||
```
|
||||
|
||||
Python, Node and WASM are *native* Rust bindings (PyO3 / napi-rs /
|
||||
wasm-bindgen). The C ABI is the *hub* every other C-capable language links
|
||||
against: it builds to a `cdylib`/`staticlib` plus a generated `wickra.h`, and
|
||||
downstream languages link that one artifact rather than each re-wrapping the
|
||||
core. C and C++ link it directly; the **C# / .NET** binding (`bindings/csharp`,
|
||||
on NuGet) is generated from `wickra.h`, with Go / Java / R planned the same way.
|
||||
|
||||
| Crate | Path | What it owns | Public deps |
|
||||
|---|---|---|---|
|
||||
| `wickra-core` | `crates/wickra-core` | every indicator, the `Indicator` trait, `BatchExt`, `Candle`/`Tick` types, `Error` | `thiserror`, `rayon` (parallel batch) |
|
||||
@@ -45,6 +56,7 @@ or replace lives behind a separate crate boundary.
|
||||
| `wickra-python` | `bindings/python` | `_wickra` PyO3 module + Python package | `pyo3`, `numpy`, depends on `wickra-core` |
|
||||
| `wickra-node` | `bindings/node` | NAPI-RS native binding | `napi`, depends on `wickra-core` |
|
||||
| `wickra-wasm` | `bindings/wasm` | WebAssembly binding | `wasm-bindgen`, depends on `wickra-core` |
|
||||
| `wickra-c` | `bindings/c` | C ABI hub — `cdylib`/`staticlib` + generated `wickra.h` (cbindgen) | depends on `wickra-core` |
|
||||
| `wickra-examples` | `examples/rust` | runnable binary examples | depends on `wickra`, `wickra-data` |
|
||||
|
||||
The `fuzz/` directory is **excluded** from the workspace (it has its own
|
||||
|
||||
+21
-1
@@ -7,6 +7,24 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [0.7.6] - 2026-06-09
|
||||
### Added
|
||||
- **C# / .NET binding (`bindings/csharp`)** — the first language stecker on the
|
||||
C ABI hub. Exposes all 514 indicators as idiomatic `IDisposable` classes via
|
||||
`[LibraryImport]` source-generated P/Invoke, generated from `wickra.h`. Ships
|
||||
on NuGet as `Wickra` with prebuilt native libraries for six target triples
|
||||
(win/linux/osx × x64/arm64), plus a full example suite mirroring the C examples.
|
||||
|
||||
## [0.7.5] - 2026-06-09
|
||||
### Added
|
||||
- **C ABI (`bindings/c`)** — a `cdylib` + `staticlib` plus a generated
|
||||
`include/wickra.h` exposing all 514 indicators and 10 bar builders over an
|
||||
opaque-handle C ABI: the hub any C-capable language (C, C++, Go, C#, Java, R)
|
||||
links against, complementing the native Python/Node/WASM bindings. Ships a
|
||||
full example suite (streaming, backtest, multi-timeframe, OpenMP parallel
|
||||
fan-out, three educational strategies, and Binance fetch/live over `curl`)
|
||||
mirroring the other bindings, plus an optional `wickra.hpp` C++ RAII wrapper.
|
||||
|
||||
## [0.7.4] - 2026-06-08
|
||||
- **Three-Line Break** — Three-line-break bars (reversal needs N-line break) (`THREE_LINE_BREAK_BARS`).
|
||||
- **Run** — Run bars (consecutive same-direction tick runs) (`RUN_BARS`).
|
||||
@@ -1416,7 +1434,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
optional Binance live feed.
|
||||
- Bindings for Python, Node.js, and WebAssembly.
|
||||
|
||||
[Unreleased]: https://github.com/wickra-lib/wickra/compare/v0.7.4...HEAD
|
||||
[Unreleased]: https://github.com/wickra-lib/wickra/compare/v0.7.6...HEAD
|
||||
[0.7.6]: https://github.com/wickra-lib/wickra/compare/v0.7.5...v0.7.6
|
||||
[0.7.5]: https://github.com/wickra-lib/wickra/compare/v0.7.4...v0.7.5
|
||||
[0.7.4]: https://github.com/wickra-lib/wickra/compare/v0.7.3...v0.7.4
|
||||
[0.7.3]: https://github.com/wickra-lib/wickra/compare/v0.7.2...v0.7.3
|
||||
[0.7.2]: https://github.com/wickra-lib/wickra/compare/v0.7.1...v0.7.2
|
||||
|
||||
+6
-1
@@ -21,6 +21,8 @@ licensed as above, without any additional terms or conditions.
|
||||
| `bindings/python` | PyO3 bindings (`wickra` on PyPI). |
|
||||
| `bindings/node` | napi-rs bindings (`wickra` on npm). |
|
||||
| `bindings/wasm` | wasm-bindgen bindings (`wickra-wasm` on npm). |
|
||||
| `bindings/c` | C ABI — `cdylib` + `staticlib` + generated `include/wickra.h`. The hub for C / C++ and any C-capable language. |
|
||||
| `bindings/csharp` | .NET binding over the C ABI (`Wickra` on NuGet) — `[LibraryImport]` P/Invoke generated from `wickra.h`. |
|
||||
| `examples/` | Runnable examples. |
|
||||
| `docs/` | Pointer to the documentation site (docs.wickra.org); the docs live in the `wickra-lib/wickra-docs` repo. |
|
||||
|
||||
@@ -102,7 +104,10 @@ installed. Dependabot also keeps the `.github/requirements` pins current.
|
||||
- **Streaming parity.** An indicator's `batch` output must equal the sequence
|
||||
of `update` calls.
|
||||
- **Bindings.** A change to a public indicator API must be mirrored across the
|
||||
Python, Node, and WASM bindings, including their type stubs / `.d.ts`.
|
||||
Python, Node, and WASM bindings, including their type stubs / `.d.ts`. The C ABI
|
||||
(`bindings/c`) is generated from the core, so regenerate it from the core and
|
||||
commit `src/lib.rs` + `include/wickra.h`. The C# binding (`bindings/csharp`) is
|
||||
generated from `wickra.h`, so regenerate and commit its `Generated/*.g.cs` too.
|
||||
- **Docs.** Update the relevant page on the
|
||||
[documentation site](https://docs.wickra.org) and the
|
||||
`README.md` when behaviour or the public API changes. The docs live in
|
||||
|
||||
Generated
+15
-8
@@ -1944,7 +1944,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wickra"
|
||||
version = "0.7.4"
|
||||
version = "0.7.6"
|
||||
dependencies = [
|
||||
"approx",
|
||||
"criterion",
|
||||
@@ -1955,7 +1955,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wickra-bench"
|
||||
version = "0.7.4"
|
||||
version = "0.7.6"
|
||||
dependencies = [
|
||||
"criterion",
|
||||
"kand",
|
||||
@@ -1965,9 +1965,16 @@ dependencies = [
|
||||
"yata",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wickra-c"
|
||||
version = "0.7.6"
|
||||
dependencies = [
|
||||
"wickra-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wickra-core"
|
||||
version = "0.7.4"
|
||||
version = "0.7.6"
|
||||
dependencies = [
|
||||
"approx",
|
||||
"proptest",
|
||||
@@ -1977,7 +1984,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wickra-data"
|
||||
version = "0.7.4"
|
||||
version = "0.7.6"
|
||||
dependencies = [
|
||||
"approx",
|
||||
"csv",
|
||||
@@ -1994,7 +2001,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wickra-examples"
|
||||
version = "0.7.4"
|
||||
version = "0.7.6"
|
||||
dependencies = [
|
||||
"serde_json",
|
||||
"tokio",
|
||||
@@ -2004,7 +2011,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wickra-node"
|
||||
version = "0.7.4"
|
||||
version = "0.7.6"
|
||||
dependencies = [
|
||||
"napi",
|
||||
"napi-build",
|
||||
@@ -2014,7 +2021,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wickra-python"
|
||||
version = "0.7.4"
|
||||
version = "0.7.6"
|
||||
dependencies = [
|
||||
"numpy",
|
||||
"pyo3",
|
||||
@@ -2023,7 +2030,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wickra-wasm"
|
||||
version = "0.7.4"
|
||||
version = "0.7.6"
|
||||
dependencies = [
|
||||
"console_error_panic_hook",
|
||||
"js-sys",
|
||||
|
||||
+3
-2
@@ -7,13 +7,14 @@ members = [
|
||||
"bindings/python",
|
||||
"bindings/wasm",
|
||||
"bindings/node",
|
||||
"bindings/c",
|
||||
"examples/rust",
|
||||
"crates/wickra-bench",
|
||||
]
|
||||
exclude = ["fuzz"]
|
||||
|
||||
[workspace.package]
|
||||
version = "0.7.4"
|
||||
version = "0.7.6"
|
||||
authors = ["kingchenc <support@wickra.org>"]
|
||||
edition = "2021"
|
||||
rust-version = "1.86"
|
||||
@@ -25,7 +26,7 @@ keywords = ["finance", "trading", "indicators", "technical-analysis", "ta"]
|
||||
categories = ["finance", "mathematics", "science"]
|
||||
|
||||
[workspace.dependencies]
|
||||
wickra-core = { path = "crates/wickra-core", version = "0.7.4" }
|
||||
wickra-core = { path = "crates/wickra-core", version = "0.7.6" }
|
||||
|
||||
thiserror = "2"
|
||||
rayon = "1.10"
|
||||
|
||||
@@ -2,24 +2,26 @@
|
||||
<a href="https://wickra.org"><img src="https://raw.githubusercontent.com/wickra-lib/.github/main/profile/wickra-banner.webp?v=514" alt="Wickra — streaming-first technical indicators" width="100%"></a>
|
||||
</p>
|
||||
|
||||
[](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
|
||||
bindings for Python, Node.js, and WebAssembly. Every indicator is a state
|
||||
machine that updates in O(1) per new data point, so live trading bots and
|
||||
native bindings for Python, Node.js and WebAssembly, plus a C ABI that C, C++,
|
||||
C# / .NET and any other C-capable language links against. Every indicator is a
|
||||
state machine that updates in O(1) per new data point, so live trading bots and
|
||||
historical backtests share the exact same implementation.
|
||||
|
||||
```python
|
||||
@@ -46,7 +48,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).
|
||||
@@ -71,9 +75,10 @@ times to get there.
|
||||
breadth, Renko/Kagi/Point&Figure bars, Ehlers DSP cycles, risk/performance
|
||||
metrics — every single one updating in **O(1) per tick**. TA-Lib ships ~150 and
|
||||
none of them stream.
|
||||
- **One Rust core, four first-class targets.** Native **Python · Node.js ·
|
||||
WebAssembly · Rust** — identical math, identical results, zero per-language
|
||||
reimplementation and zero GIL bottleneck.
|
||||
- **One Rust core, five first-class targets.** Native **Python · Node.js ·
|
||||
WebAssembly · Rust** plus a **C ABI** for C / C++, C# / .NET and any other C-capable language —
|
||||
identical math, identical results, zero per-language reimplementation and zero
|
||||
GIL bottleneck.
|
||||
- **Correct by construction, not by hope.** Every `update` validates its input,
|
||||
runs a real warmup, and returns an `Option` so a single bad tick can't silently
|
||||
poison state. `batch == streaming` is **bit-exact, fuzzed and 100 %-line-covered
|
||||
@@ -95,7 +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** | **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 |
|
||||
@@ -166,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 four bindings
|
||||
inherit it automatically.
|
||||
Adding a new indicator means implementing one trait in Rust; every binding
|
||||
inherits it automatically (the C ABI — and the C# binding generated from it —
|
||||
regenerate from the core).
|
||||
|
||||
## Languages
|
||||
|
||||
@@ -177,12 +184,15 @@ inherit it automatically.
|
||||
| Node.js (napi-rs) | `npm install wickra` | `examples/node/backtest.js` |
|
||||
| Browser / WASM | `npm install wickra-wasm` | `examples/wasm/index.html` |
|
||||
| Rust | `cargo add wickra` | `examples/rust/src/bin/backtest.rs` |
|
||||
| C / C++ (C ABI) | header + library, see [`bindings/c`](bindings/c) | `examples/c/streaming.c` |
|
||||
| C# / .NET (C ABI) | `dotnet add package Wickra`, see [`bindings/csharp`](bindings/csharp) | `examples/csharp/streaming` |
|
||||
|
||||
Each binding ships several runnable examples (streaming, backtest, live feed);
|
||||
[`examples/README.md`](examples/README.md) is the full cross-language index.
|
||||
|
||||
The wickra-core crate is `unsafe`-forbidden, so every binding inherits a
|
||||
memory-safe implementation.
|
||||
The wickra-core crate is `unsafe`-forbidden, so the native bindings are
|
||||
memory-safe end to end. The C ABI runs the same safe core; only its thin FFI
|
||||
boundary uses `unsafe`, and the caller owns handle lifetimes (`_new` / `_free`).
|
||||
|
||||
## Rust API
|
||||
|
||||
@@ -244,13 +254,17 @@ wickra/
|
||||
├── bindings/
|
||||
│ ├── python/ PyO3 + maturin (publishes on PyPI)
|
||||
│ ├── node/ napi-rs (publishes on npm)
|
||||
│ └── wasm/ wasm-bindgen (browsers, bundlers, Node)
|
||||
│ ├── wasm/ wasm-bindgen (browsers, bundlers, Node)
|
||||
│ ├── c/ C ABI (cdylib + staticlib) + generated include/wickra.h
|
||||
│ └── csharp/ .NET binding over the C ABI (publishes on NuGet)
|
||||
├── examples/ examples/README.md indexes every language
|
||||
│ ├── data/ real BTCUSDT OHLCV datasets, one per timeframe
|
||||
│ ├── rust/ Rust workspace member (`wickra-examples`)
|
||||
│ ├── python/ backtest, live trading, parallel assets, multi-tf
|
||||
│ ├── node/ streaming, backtest, live trading (load `wickra`)
|
||||
│ └── wasm/ browser demo for `wickra-wasm`
|
||||
│ ├── wasm/ browser demo for `wickra-wasm`
|
||||
│ ├── c/ C smoke + streaming, C++ RAII wrapper
|
||||
│ └── csharp/ streaming, backtest, strategies (load `Wickra`)
|
||||
└── .github/workflows/ CI and release pipelines
|
||||
```
|
||||
|
||||
@@ -278,6 +292,14 @@ wasm-pack build bindings/wasm --target web --release --features panic-hook
|
||||
|
||||
# Node binding (requires @napi-rs/cli)
|
||||
cd bindings/node && npm install && npm run build && npm test
|
||||
|
||||
# C ABI (cdylib + staticlib + generated header)
|
||||
cargo build -p wickra-c --release
|
||||
cmake -S examples/c -B examples/c/build -DWICKRA_LIB_DIR="$PWD/target/release"
|
||||
cmake --build examples/c/build && ctest --test-dir examples/c/build --output-on-failure
|
||||
|
||||
# C# / .NET binding (requires the .NET 8 SDK; links the C ABI above)
|
||||
dotnet test bindings/csharp/Wickra.Tests/Wickra.Tests.csproj
|
||||
```
|
||||
|
||||
## 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,
|
||||
|
||||
+5
-1
@@ -58,7 +58,11 @@ artifacts, and (4) a healthy dependency supply chain.
|
||||
|
||||
- *Memory safety* — the core and all bindings are written in Rust. The crates
|
||||
forbid or minimise `unsafe`, so the compiler guarantees memory and thread
|
||||
safety for the indicator logic.
|
||||
safety for the indicator logic. The one exception is the C ABI
|
||||
([`bindings/c`](bindings/c)), whose thin FFI shim is necessarily `unsafe`
|
||||
because it dereferences caller-supplied pointers; it adds no indicator logic,
|
||||
validates every handle for NULL, and never lets a panic cross the boundary, so
|
||||
the safe core's guarantees still cover all computation.
|
||||
- *Input robustness* — every indicator validates its parameters and rejects
|
||||
non-finite inputs at construction; behaviour on edge cases (flat markets,
|
||||
warmup, reset) is pinned by unit tests, and the public update paths are
|
||||
|
||||
+2
-2
@@ -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>.
|
||||
|
||||
+5
-2
@@ -3,8 +3,10 @@
|
||||
This document describes Wickra's attack surface and the threats considered,
|
||||
together with their mitigations. It complements the security assurance case in
|
||||
[`SECURITY.md`](SECURITY.md). Wickra is a computational technical-analysis
|
||||
library (a Rust core with Python, Node.js and WebAssembly bindings), not a
|
||||
network service or trading system; the attack surface is correspondingly small.
|
||||
library (a Rust core with Python, Node.js and WebAssembly bindings plus a C ABI
|
||||
and the .NET binding built on it),
|
||||
not a network service or trading system; the attack surface is correspondingly
|
||||
small.
|
||||
|
||||
## Assets
|
||||
|
||||
@@ -31,6 +33,7 @@ network service or trading system; the attack surface is correspondingly small.
|
||||
| Threat | Mitigation |
|
||||
| --- | --- |
|
||||
| Memory-safety exploit (buffer overflow, UAF) via crafted input | Pure safe Rust; `unsafe` is forbidden/minimised, so the compiler precludes these classes. |
|
||||
| Misuse of the C ABI FFI boundary (invalid/dangling handle, undersized batch buffer) | The C ABI (`bindings/c`) is the sole `unsafe` surface. Its shim adds no logic, NULL-checks every handle (returning `NaN`/no-op), writes only into caller-sized buffers, and catches panics so none cross the boundary. A caller passing a non-NULL but dangling pointer is undefined behaviour by C's own contract — out of scope, the same as any C library. |
|
||||
| Denial of service via malformed/degenerate input (NaN, infinities, extreme magnitudes) | Indicators reject non-finite inputs and validate parameters at construction; update paths are exercised by coverage-guided fuzzing and unit tests for edge cases. |
|
||||
| Silently incorrect results | 100% line coverage on the core crate; reference-value tests against known-good sources; streaming/batch parity tests. |
|
||||
| Integer overflow / panics | `clippy::pedantic` with `-D warnings`; debug assertions and overflow checks enabled in test/fuzz builds. |
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
[package]
|
||||
name = "wickra-c"
|
||||
description = "C ABI (cdylib + staticlib) for the Wickra streaming-first technical indicators library — the hub every C-capable language (C, C++, Go, C#, Java, R) links against."
|
||||
version.workspace = true
|
||||
authors.workspace = true
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
license.workspace = true
|
||||
repository.workspace = true
|
||||
homepage.workspace = true
|
||||
readme = "README.md"
|
||||
keywords.workspace = true
|
||||
categories.workspace = true
|
||||
publish = false
|
||||
|
||||
[lib]
|
||||
name = "wickra"
|
||||
crate-type = ["cdylib", "staticlib"]
|
||||
|
||||
# The C ABI inherently needs `unsafe` (raw pointers across the FFI boundary,
|
||||
# `#[export_name]` symbol control). The workspace forbids `unsafe_code`, so this
|
||||
# crate cannot inherit `workspace = true`; it mirrors every workspace lint and
|
||||
# only relaxes `unsafe_code` to `allow` (parity with how the proc-macro bindings
|
||||
# emit their unsafe). The Rust core stays `unsafe`-forbidden — this is the one
|
||||
# crate where the boundary lives.
|
||||
[lints.rust]
|
||||
unsafe_code = "allow"
|
||||
missing_debug_implementations = "warn"
|
||||
unreachable_pub = "warn"
|
||||
unused_must_use = "deny"
|
||||
|
||||
[lints.clippy]
|
||||
all = { level = "warn", priority = -1 }
|
||||
pedantic = { level = "warn", priority = -1 }
|
||||
module_name_repetitions = "allow"
|
||||
must_use_candidate = "allow"
|
||||
missing_errors_doc = "allow"
|
||||
missing_panics_doc = "allow"
|
||||
cast_precision_loss = "allow"
|
||||
cast_possible_truncation = "allow"
|
||||
cast_sign_loss = "allow"
|
||||
similar_names = "allow"
|
||||
float_cmp = "allow"
|
||||
|
||||
[dependencies]
|
||||
wickra-core = { workspace = true }
|
||||
@@ -0,0 +1,80 @@
|
||||
# Wickra — C / C++
|
||||
|
||||
[](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-wrapping every indicator natively.
|
||||
|
||||
## Install
|
||||
|
||||
Grab the prebuilt header + library for your platform from the
|
||||
[GitHub releases](https://github.com/wickra-lib/wickra/releases) — each archive
|
||||
has `wickra.h`, the optional `wickra.hpp` C++ wrapper, and the shared/static
|
||||
library — or build from source:
|
||||
|
||||
```bash
|
||||
cargo build -p wickra-c --release
|
||||
# -> target/release/libwickra.{so,dylib} or wickra.dll (+ import lib) + a staticlib
|
||||
```
|
||||
|
||||
Then compile against the header and link the library
|
||||
(`cc app.c -I include -L lib -lwickra -lm -o app`).
|
||||
|
||||
## Quick start
|
||||
|
||||
```c
|
||||
#include "wickra.h"
|
||||
|
||||
struct Rsi *rsi = wickra_rsi_new(14); /* NULL on invalid params */
|
||||
for (size_t i = 0; i < n; ++i) {
|
||||
double v = wickra_rsi_update(rsi, prices[i]); /* NaN during warmup */
|
||||
if (v == v && v > 70.0) /* v == v is the NaN check */
|
||||
printf("overbought\n");
|
||||
}
|
||||
wickra_rsi_free(rsi); /* exactly once per _new */
|
||||
```
|
||||
|
||||
Every indicator is an opaque handle with the same five functions —
|
||||
`_new` / `_update` / `_batch` / `_reset` / `_free`. `update` is O(1); there is no
|
||||
RAII across the C boundary, so each `_new` needs exactly one `_free`, and every
|
||||
function is NULL-safe (a NULL handle yields `NaN` or a no-op, never a crash).
|
||||
Multi-output indicators (MACD, Bollinger, ADX, …) take a pointer to a `#[repr(C)]`
|
||||
struct and return a `bool`. The optional `wickra.hpp` wraps any handle in a
|
||||
move-only `wickra::Handle` for exception-safe C++ lifetimes.
|
||||
|
||||
## Documentation
|
||||
|
||||
The full indicator catalogue, guides, quickstarts, and API reference live in
|
||||
the main repository and documentation site:
|
||||
|
||||
- **Repository & full indicator list:** <https://github.com/wickra-lib/wickra>
|
||||
- **Docs** (C quickstart, cookbook, TA-Lib migration): <https://docs.wickra.org/Quickstart-C>
|
||||
- **Runnable examples:** [`examples/c/`](https://github.com/wickra-lib/wickra/tree/main/examples/c)
|
||||
|
||||
Wickra ships native bindings for Python, Node.js, WebAssembly and Rust, plus this
|
||||
C ABI hub that any C-capable language (C, C++, Go, C#, Java, R) links against —
|
||||
all exposing the same indicators from the shared Rust core.
|
||||
|
||||
## Disclaimer
|
||||
|
||||
Wickra is an indicator toolkit, not a trading system. The values it computes
|
||||
are deterministic transforms of the input data — they are not financial advice
|
||||
and do not predict the market. Any use in a live trading context is at your own
|
||||
risk. The library is provided **as is**, without warranty of any kind.
|
||||
|
||||
## License
|
||||
|
||||
Licensed under either of [Apache-2.0](https://github.com/wickra-lib/wickra/blob/main/LICENSE-APACHE)
|
||||
or [MIT](https://github.com/wickra-lib/wickra/blob/main/LICENSE-MIT) at your option.
|
||||
@@ -0,0 +1,20 @@
|
||||
language = "C"
|
||||
header = "/* Wickra C ABI — generated by cbindgen. Do not edit by hand. */"
|
||||
include_guard = "WICKRA_H"
|
||||
pragma_once = true
|
||||
# Wrap the declarations in `extern "C"` under __cplusplus so the header is usable
|
||||
# from C++ (the optional wickra.hpp RAII layer and any C++ consumer).
|
||||
cpp_compat = true
|
||||
tab_width = 4
|
||||
# Off: cbindgen copies the Rust struct/fn doc comments verbatim, and some core
|
||||
# indicator docs contain markdown (e.g. `**1/8**/**7/8**`) whose `*/` would close
|
||||
# the C block comment early and break the header. Usage docs live in the crate
|
||||
# README and examples; the header is a pure declaration contract.
|
||||
documentation = false
|
||||
|
||||
[parse]
|
||||
# Parse wickra-core too so the opaque indicator handle types (Sma, Ema, …) are
|
||||
# discovered and emitted as forward-declared opaque structs. Their fields are
|
||||
# never exposed — only `T *` handles cross the boundary.
|
||||
parse_deps = true
|
||||
include = ["wickra-core"]
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,66 @@
|
||||
// Optional C++ convenience layer over the Wickra C ABI (`wickra.h`).
|
||||
//
|
||||
// The C ABI hands out raw handles that must be released exactly once with the
|
||||
// matching `wickra_<ind>_free`. `wickra::Handle` wraps that in a move-only RAII
|
||||
// owner so the free happens automatically at scope exit:
|
||||
//
|
||||
// #include "wickra.hpp"
|
||||
//
|
||||
// wickra::Handle<Sma, wickra_sma_free> sma(wickra_sma_new(14));
|
||||
// if (sma) {
|
||||
// double v = wickra_sma_update(sma.get(), 42.0); // NaN during warmup
|
||||
// }
|
||||
// // sma is freed here
|
||||
//
|
||||
// This is header-only and adds no runtime cost beyond the C calls themselves.
|
||||
|
||||
#ifndef WICKRA_HPP
|
||||
#define WICKRA_HPP
|
||||
|
||||
#include "wickra.h"
|
||||
|
||||
#include <utility>
|
||||
|
||||
namespace wickra {
|
||||
|
||||
/// Move-only RAII owner of a Wickra handle. `T` is the opaque indicator type and
|
||||
/// `Free` its `wickra_<ind>_free` function.
|
||||
template <typename T, void (*Free)(T *)>
|
||||
class Handle {
|
||||
public:
|
||||
explicit Handle(T *ptr) noexcept : ptr_(ptr) {}
|
||||
|
||||
~Handle() {
|
||||
if (ptr_ != nullptr) {
|
||||
Free(ptr_);
|
||||
}
|
||||
}
|
||||
|
||||
Handle(const Handle &) = delete;
|
||||
Handle &operator=(const Handle &) = delete;
|
||||
|
||||
Handle(Handle &&other) noexcept : ptr_(std::exchange(other.ptr_, nullptr)) {}
|
||||
|
||||
Handle &operator=(Handle &&other) noexcept {
|
||||
if (this != &other) {
|
||||
if (ptr_ != nullptr) {
|
||||
Free(ptr_);
|
||||
}
|
||||
ptr_ = std::exchange(other.ptr_, nullptr);
|
||||
}
|
||||
return *this;
|
||||
}
|
||||
|
||||
/// The raw handle, for passing to the `wickra_<ind>_*` functions.
|
||||
T *get() const noexcept { return ptr_; }
|
||||
|
||||
/// True if the handle is non-null (construction succeeded).
|
||||
explicit operator bool() const noexcept { return ptr_ != nullptr; }
|
||||
|
||||
private:
|
||||
T *ptr_;
|
||||
};
|
||||
|
||||
} // namespace wickra
|
||||
|
||||
#endif // WICKRA_HPP
|
||||
+44290
File diff suppressed because it is too large
Load Diff
@@ -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.6</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,8 @@
|
||||
prebuilt native binary, no system dependencies.**
|
||||
|
||||
Wickra is a multi-language technical-analysis library with a Rust core and
|
||||
bindings for Python, Node.js, and WebAssembly. Every indicator is an O(1)
|
||||
bindings for Python, Node.js and WebAssembly, plus a C ABI for C/C++, C# and any
|
||||
other C-capable language. Every indicator is an O(1)
|
||||
streaming state machine, so live trading bots and historical backtests share
|
||||
the exact same implementation. This package is the Node.js binding (napi-rs);
|
||||
it exposes 200+ streaming-first indicators across sixteen families.
|
||||
@@ -55,8 +56,9 @@ the main repository and documentation site:
|
||||
- **Docs** (quickstarts, cookbook, TA-Lib migration): <https://docs.wickra.org>
|
||||
- **Runnable examples:** [`examples/node/`](https://github.com/wickra-lib/wickra/tree/main/examples/node)
|
||||
|
||||
Wickra ships four bindings — Python, Node.js, WebAssembly, and Rust — that all
|
||||
expose the same indicators from the shared, `unsafe`-forbidden Rust core.
|
||||
Wickra ships native bindings for Python, Node.js, WebAssembly and Rust, plus a
|
||||
C ABI hub that any C-capable language (C, C++, Go, C#, Java, R) links against —
|
||||
all exposing the same indicators from the shared, `unsafe`-forbidden Rust core.
|
||||
|
||||
## Disclaimer
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "wickra-darwin-arm64",
|
||||
"version": "0.7.4",
|
||||
"version": "0.7.6",
|
||||
"description": "Native binding for wickra (macOS Apple Silicon). Installed automatically as an optional dependency of wickra on matching platforms.",
|
||||
"main": "wickra.darwin-arm64.node",
|
||||
"files": [
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "wickra-darwin-x64",
|
||||
"version": "0.7.4",
|
||||
"version": "0.7.6",
|
||||
"description": "Native binding for wickra (macOS Intel). Installed automatically as an optional dependency of wickra on matching platforms.",
|
||||
"main": "wickra.darwin-x64.node",
|
||||
"files": [
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "wickra-linux-arm64-gnu",
|
||||
"version": "0.7.4",
|
||||
"version": "0.7.6",
|
||||
"description": "Native binding for wickra (linux arm64 GNU). Installed automatically as an optional dependency of wickra on matching platforms.",
|
||||
"main": "wickra.linux-arm64-gnu.node",
|
||||
"files": [
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "wickra-linux-x64-gnu",
|
||||
"version": "0.7.4",
|
||||
"version": "0.7.6",
|
||||
"description": "Native binding for wickra (linux x64 GNU). Installed automatically as an optional dependency of wickra on matching platforms.",
|
||||
"main": "wickra.linux-x64-gnu.node",
|
||||
"files": [
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "wickra-win32-arm64-msvc",
|
||||
"version": "0.7.4",
|
||||
"version": "0.7.6",
|
||||
"description": "Native binding for wickra (Windows arm64 MSVC). Installed automatically as an optional dependency of wickra on matching platforms.",
|
||||
"main": "wickra.win32-arm64-msvc.node",
|
||||
"files": [
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "wickra-win32-x64-msvc",
|
||||
"version": "0.7.4",
|
||||
"version": "0.7.6",
|
||||
"description": "Native binding for wickra (Windows x64 MSVC). Installed automatically as an optional dependency of wickra on matching platforms.",
|
||||
"main": "wickra.win32-x64-msvc.node",
|
||||
"files": [
|
||||
|
||||
Generated
+20
-20
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "wickra",
|
||||
"version": "0.7.4",
|
||||
"version": "0.7.6",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "wickra",
|
||||
"version": "0.7.4",
|
||||
"version": "0.7.6",
|
||||
"license": "MIT OR Apache-2.0",
|
||||
"devDependencies": {
|
||||
"@napi-rs/cli": "^2.18.0"
|
||||
@@ -15,12 +15,12 @@
|
||||
"node": ">= 18"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"wickra-darwin-arm64": "0.7.4",
|
||||
"wickra-darwin-x64": "0.7.4",
|
||||
"wickra-linux-arm64-gnu": "0.7.4",
|
||||
"wickra-linux-x64-gnu": "0.7.4",
|
||||
"wickra-win32-arm64-msvc": "0.7.4",
|
||||
"wickra-win32-x64-msvc": "0.7.4"
|
||||
"wickra-darwin-arm64": "0.7.6",
|
||||
"wickra-darwin-x64": "0.7.6",
|
||||
"wickra-linux-arm64-gnu": "0.7.6",
|
||||
"wickra-linux-x64-gnu": "0.7.6",
|
||||
"wickra-win32-arm64-msvc": "0.7.6",
|
||||
"wickra-win32-x64-msvc": "0.7.6"
|
||||
}
|
||||
},
|
||||
"node_modules/@napi-rs/cli": {
|
||||
@@ -41,8 +41,8 @@
|
||||
}
|
||||
},
|
||||
"node_modules/wickra-darwin-arm64": {
|
||||
"version": "0.7.4",
|
||||
"resolved": "https://registry.npmjs.org/wickra-darwin-arm64/-/wickra-darwin-arm64-0.7.4.tgz",
|
||||
"version": "0.7.6",
|
||||
"resolved": "https://registry.npmjs.org/wickra-darwin-arm64/-/wickra-darwin-arm64-0.7.6.tgz",
|
||||
"integrity": "sha512-4eZiBR/yGUdr4nzhEUFy2i69XgNx64iI2ax/LPamsThgylC0KpHOZKK19QzJ2d9KbK4C8nMjME5FLuR+4GNEwQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
@@ -57,8 +57,8 @@
|
||||
}
|
||||
},
|
||||
"node_modules/wickra-darwin-x64": {
|
||||
"version": "0.7.4",
|
||||
"resolved": "https://registry.npmjs.org/wickra-darwin-x64/-/wickra-darwin-x64-0.7.4.tgz",
|
||||
"version": "0.7.6",
|
||||
"resolved": "https://registry.npmjs.org/wickra-darwin-x64/-/wickra-darwin-x64-0.7.6.tgz",
|
||||
"integrity": "sha512-6hf8zI3QPjTFp4zCpmgUwDvNtu6jHqNUHKD5e55POo0CgA52HkpyxSPtVm8TGTIZDI7kPjlbOdBM8CJ76mmXwA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
@@ -73,8 +73,8 @@
|
||||
}
|
||||
},
|
||||
"node_modules/wickra-linux-arm64-gnu": {
|
||||
"version": "0.7.4",
|
||||
"resolved": "https://registry.npmjs.org/wickra-linux-arm64-gnu/-/wickra-linux-arm64-gnu-0.7.4.tgz",
|
||||
"version": "0.7.6",
|
||||
"resolved": "https://registry.npmjs.org/wickra-linux-arm64-gnu/-/wickra-linux-arm64-gnu-0.7.6.tgz",
|
||||
"integrity": "sha512-kSe6y0xBMSiqdPLXNjwop5WZdHtvdBNKSEBCwZ4hFq33p4apW25/wrlzv9/oDuyD4kuPabJEhCCnFOplh58CUg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
@@ -89,8 +89,8 @@
|
||||
}
|
||||
},
|
||||
"node_modules/wickra-linux-x64-gnu": {
|
||||
"version": "0.7.4",
|
||||
"resolved": "https://registry.npmjs.org/wickra-linux-x64-gnu/-/wickra-linux-x64-gnu-0.7.4.tgz",
|
||||
"version": "0.7.6",
|
||||
"resolved": "https://registry.npmjs.org/wickra-linux-x64-gnu/-/wickra-linux-x64-gnu-0.7.6.tgz",
|
||||
"integrity": "sha512-tWBWS4qz7hxM4xnpFb59bhf6TaLwXq0Z3jEa/2l7r8PiHA94g8r8S53NRMiT+4yiL5hSWe/nUiC/YXdRrhEZ4g==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
@@ -105,8 +105,8 @@
|
||||
}
|
||||
},
|
||||
"node_modules/wickra-win32-arm64-msvc": {
|
||||
"version": "0.7.4",
|
||||
"resolved": "https://registry.npmjs.org/wickra-win32-arm64-msvc/-/wickra-win32-arm64-msvc-0.7.4.tgz",
|
||||
"version": "0.7.6",
|
||||
"resolved": "https://registry.npmjs.org/wickra-win32-arm64-msvc/-/wickra-win32-arm64-msvc-0.7.6.tgz",
|
||||
"integrity": "sha512-EXIckHxAtF75PUGDKRzXyqMe9ldP0JjSdu68WFN6iJfp+McYrGu6h40TEJlQ/oUEIoPqiZB/xhVyo/el5Lg7zw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
@@ -121,8 +121,8 @@
|
||||
}
|
||||
},
|
||||
"node_modules/wickra-win32-x64-msvc": {
|
||||
"version": "0.7.4",
|
||||
"resolved": "https://registry.npmjs.org/wickra-win32-x64-msvc/-/wickra-win32-x64-msvc-0.7.4.tgz",
|
||||
"version": "0.7.6",
|
||||
"resolved": "https://registry.npmjs.org/wickra-win32-x64-msvc/-/wickra-win32-x64-msvc-0.7.6.tgz",
|
||||
"integrity": "sha512-Yfsqq1Xwp6hdxMyLze411vNdo7BDwI6+lPSe7A9XdqyPecNDbtKwYLpsal2r8EHbNzqM+R8XnuRtUaEQS5VlUQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "wickra",
|
||||
"version": "0.7.4",
|
||||
"version": "0.7.6",
|
||||
"description": "Streaming-first technical indicators: incremental, fast, install-free. Node bindings powered by Rust.",
|
||||
"author": "kingchenc <support@wickra.org>",
|
||||
"main": "index.js",
|
||||
@@ -47,12 +47,12 @@
|
||||
"node": ">= 18"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"wickra-linux-x64-gnu": "0.7.4",
|
||||
"wickra-linux-arm64-gnu": "0.7.4",
|
||||
"wickra-darwin-x64": "0.7.4",
|
||||
"wickra-darwin-arm64": "0.7.4",
|
||||
"wickra-win32-x64-msvc": "0.7.4",
|
||||
"wickra-win32-arm64-msvc": "0.7.4"
|
||||
"wickra-linux-x64-gnu": "0.7.6",
|
||||
"wickra-linux-arm64-gnu": "0.7.6",
|
||||
"wickra-darwin-x64": "0.7.6",
|
||||
"wickra-darwin-arm64": "0.7.6",
|
||||
"wickra-win32-x64-msvc": "0.7.6",
|
||||
"wickra-win32-arm64-msvc": "0.7.6"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "napi build --platform --release",
|
||||
|
||||
@@ -9,7 +9,8 @@
|
||||
system dependencies, no C build tooling.**
|
||||
|
||||
Wickra is a multi-language technical-analysis library with a Rust core and
|
||||
bindings for Python, Node.js, and WebAssembly. Every indicator is an O(1)
|
||||
bindings for Python, Node.js and WebAssembly, plus a C ABI for C/C++, C# and any
|
||||
other C-capable language. Every indicator is an O(1)
|
||||
streaming state machine, so live trading bots and historical backtests share
|
||||
the exact same implementation. This package is the Python binding (PyO3); it
|
||||
exposes 200+ streaming-first indicators across sixteen families.
|
||||
@@ -54,8 +55,9 @@ the main repository and documentation site:
|
||||
- **Docs** (quickstarts, cookbook, TA-Lib migration): <https://docs.wickra.org>
|
||||
- **Runnable examples:** [`examples/python/`](https://github.com/wickra-lib/wickra/tree/main/examples/python)
|
||||
|
||||
Wickra ships four bindings — Python, Node.js, WebAssembly, and Rust — that all
|
||||
expose the same indicators from the shared, `unsafe`-forbidden Rust core.
|
||||
Wickra ships native bindings for Python, Node.js, WebAssembly and Rust, plus a
|
||||
C ABI hub that any C-capable language (C, C++, Go, C#, Java, R) links against —
|
||||
all exposing the same indicators from the shared, `unsafe`-forbidden Rust core.
|
||||
|
||||
## Disclaimer
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ build-backend = "maturin"
|
||||
|
||||
[project]
|
||||
name = "wickra"
|
||||
version = "0.7.4"
|
||||
version = "0.7.6"
|
||||
description = "Streaming-first technical indicators: incremental, fast, install-free."
|
||||
readme = "README.md"
|
||||
license = "MIT OR Apache-2.0"
|
||||
|
||||
@@ -9,7 +9,8 @@
|
||||
wickra-wasm` — pure WebAssembly, runs anywhere a modern JS engine does.**
|
||||
|
||||
Wickra is a multi-language technical-analysis library with a Rust core and
|
||||
bindings for Python, Node.js, and WebAssembly. Every indicator is an O(1)
|
||||
bindings for Python, Node.js and WebAssembly, plus a C ABI for C/C++, C# and any
|
||||
other C-capable language. Every indicator is an O(1)
|
||||
streaming state machine, so live trading dashboards and historical backtests
|
||||
share the exact same implementation. This package is the WebAssembly binding
|
||||
(wasm-bindgen, built for the `web` target); it exposes 200+ streaming-first
|
||||
@@ -54,8 +55,9 @@ the main repository and documentation site:
|
||||
- **Docs** (quickstarts, cookbook, TA-Lib migration): <https://docs.wickra.org>
|
||||
- **Runnable browser examples:** [`examples/wasm/`](https://github.com/wickra-lib/wickra/tree/main/examples/wasm)
|
||||
|
||||
Wickra ships four bindings — Python, Node.js, WebAssembly, and Rust — that all
|
||||
expose the same indicators from the shared, `unsafe`-forbidden Rust core.
|
||||
Wickra ships native bindings for Python, Node.js, WebAssembly and Rust, plus a
|
||||
C ABI hub that any C-capable language (C, C++, Go, C#, Java, R) links against —
|
||||
all exposing the same indicators from the shared, `unsafe`-forbidden Rust core.
|
||||
|
||||
## Disclaimer
|
||||
|
||||
|
||||
+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,
|
||||
|
||||
@@ -21,6 +21,54 @@ The Rust examples live in the `wickra-examples` workspace member crate.
|
||||
| `strategy_macd_adx.rs` | Hourly BTCUSDT trend-follower: MACD crossover entries gated by ADX(14) > 20. | `cargo run --release -p wickra-examples --bin strategy_macd_adx` |
|
||||
| `strategy_bollinger_squeeze.rs` | Daily BTCUSDT Bollinger-squeeze breakout with ATR(14) trailing stop. | `cargo run --release -p wickra-examples --bin strategy_bollinger_squeeze` |
|
||||
|
||||
## C / C++ — `examples/c/`
|
||||
|
||||
Build the library first (`cargo build -p wickra-c --release`), then build and run
|
||||
the examples via CMake:
|
||||
`cmake -S examples/c -B examples/c/build -DWICKRA_LIB_DIR="$PWD/target/release"` →
|
||||
`cmake --build examples/c/build` → `ctest --test-dir examples/c/build`.
|
||||
|
||||
| Example | What it does | CMake target |
|
||||
| --- | --- | --- |
|
||||
| `smoke.c` | Links the generated header + library and asserts SMA streaming / batch values across the boundary. | `smoke` |
|
||||
| `streaming.c` | Feed a synthetic price series through SMA / EMA / RSI / MACD tick by tick. | `streaming` |
|
||||
| `backtest.c` | Basket of indicators over an OHLCV CSV; defaults to the bundled BTCUSDT daily dataset. | `backtest` |
|
||||
| `multi_timeframe.c` | Resample the bundled 1-minute CSV to 5m / 15m / 1h / 4h / 1d and print indicators per timeframe. | `multi_timeframe` |
|
||||
| `parallel_assets.c` | Serial vs OpenMP fan-out over a synthetic panel (one handle per asset), with speedup. | `parallel_assets` |
|
||||
| `strategy_rsi_mean_reversion.c` | Hourly BTCUSDT mean-reversion using RSI(14) thresholds, with PnL / Sharpe / max-DD summary. | `strategy_rsi_mean_reversion` |
|
||||
| `strategy_macd_adx.c` | Hourly BTCUSDT trend-follower: MACD crossover entries gated by ADX(14) > 20. | `strategy_macd_adx` |
|
||||
| `strategy_bollinger_squeeze.c` | Daily BTCUSDT Bollinger-squeeze breakout with ATR(14) stop. | `strategy_bollinger_squeeze` |
|
||||
| `fetch_btcusdt.c` | Download real BTCUSDT klines from the Binance REST API into `examples/data/` (shells out to `curl`). | `fetch_btcusdt` |
|
||||
| `live_binance.c` | Poll the Binance REST klines endpoint via `curl` and stream closed candles through RSI(14). | `live_binance` |
|
||||
| `smoke.cpp` | C++ RAII via `wickra::Handle` from [`wickra.hpp`](../bindings/c/include/wickra.hpp): construct, move, auto-free. | `cpp_smoke` |
|
||||
|
||||
The data-driven examples (`backtest`, `multi_timeframe`, `parallel_assets`, the
|
||||
three `strategy_*`) build against the bundled datasets and run under `ctest`.
|
||||
`fetch_btcusdt` and `live_binance` reach the network, so they are built but not
|
||||
run in CI; run them by hand. `parallel_assets` links OpenMP when the toolchain
|
||||
provides it and falls back to a single-threaded run otherwise.
|
||||
|
||||
## C# / .NET — `examples/csharp/`
|
||||
|
||||
Build the C ABI library first (`cargo build -p wickra-c --release`), then run any
|
||||
example with the .NET 8 SDK; the binding resolves the native library automatically.
|
||||
|
||||
| Example | What it does | Run |
|
||||
| --- | --- | --- |
|
||||
| `streaming` | Feed a synthetic price series through SMA / EMA / RSI / MACD tick by tick. | `dotnet run --project examples/csharp/streaming` |
|
||||
| `backtest` | Basket of indicators over an OHLCV series (CSV arg or synthetic). | `dotnet run --project examples/csharp/backtest -- <ohlcv.csv>` |
|
||||
| `multi_timeframe` | Resample a 1-minute series to 5m / 15m and print an indicator per timeframe. | `dotnet run --project examples/csharp/multi_timeframe` |
|
||||
| `parallel_assets` | SMA(20) batch over a panel, serial vs `Parallel.For`, with speedup. | `dotnet run -c Release --project examples/csharp/parallel_assets` |
|
||||
| `strategy_rsi_mean_reversion` | RSI(14) mean-reversion with PnL / Sharpe / max-DD summary. | `dotnet run -c Release --project examples/csharp/strategy_rsi_mean_reversion` |
|
||||
| `strategy_macd_adx` | Trend-follower: MACD crossover entries gated by ADX(14) > 20. | `dotnet run -c Release --project examples/csharp/strategy_macd_adx` |
|
||||
| `strategy_bollinger_squeeze` | Bollinger-squeeze breakout with an ATR(14) trailing stop. | `dotnet run -c Release --project examples/csharp/strategy_bollinger_squeeze` |
|
||||
| `fetch_btcusdt` | Download real BTCUSDT klines from the Binance REST API into a CSV. | `dotnet run --project examples/csharp/fetch_btcusdt` |
|
||||
| `live_binance` | Stream live Binance klines through EMA(20) over a WebSocket. | `dotnet run --project examples/csharp/live_binance` |
|
||||
|
||||
The offline examples run on deterministic synthetic data (and under CI on all
|
||||
three OSes); `fetch_btcusdt` and `live_binance` reach the network and are built
|
||||
but not run in CI.
|
||||
|
||||
## Python — `examples/python/`
|
||||
|
||||
| Example | What it does | Run |
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
cmake_minimum_required(VERSION 3.15)
|
||||
project(wickra_c_examples C CXX)
|
||||
|
||||
# Directory holding the compiled Wickra C library (cargo output), e.g.
|
||||
# <workspace>/target/release. Override with -DWICKRA_LIB_DIR=/path/to/target/release.
|
||||
if(NOT DEFINED WICKRA_LIB_DIR)
|
||||
set(WICKRA_LIB_DIR "${CMAKE_CURRENT_SOURCE_DIR}/../../target/release")
|
||||
endif()
|
||||
set(WICKRA_INCLUDE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/../../bindings/c/include")
|
||||
|
||||
# Absolute path to the bundled OHLCV datasets, baked into the data-driven
|
||||
# examples as a compile definition so they run from any working directory (the
|
||||
# C counterpart of the Rust examples' CARGO_MANIFEST_DIR).
|
||||
get_filename_component(WICKRA_DATA_DIR "${CMAKE_CURRENT_SOURCE_DIR}/../data" ABSOLUTE)
|
||||
|
||||
# OpenMP is optional: parallel_assets links it when present and falls back to a
|
||||
# single-threaded run otherwise.
|
||||
find_package(OpenMP QUIET)
|
||||
|
||||
# Pick the right link target per platform/toolchain.
|
||||
# - MSVC links the generated import library (wickra.dll.lib).
|
||||
# - MinGW/gcc on Windows links the DLL directly.
|
||||
# - Unix links the shared object / dylib.
|
||||
if(WIN32)
|
||||
set(WICKRA_RUNTIME "${WICKRA_LIB_DIR}/wickra.dll")
|
||||
if(MSVC)
|
||||
set(WICKRA_LINK_LIB "${WICKRA_LIB_DIR}/wickra.dll.lib")
|
||||
else()
|
||||
set(WICKRA_LINK_LIB "${WICKRA_LIB_DIR}/wickra.dll")
|
||||
endif()
|
||||
elseif(APPLE)
|
||||
set(WICKRA_LINK_LIB "${WICKRA_LIB_DIR}/libwickra.dylib")
|
||||
else()
|
||||
set(WICKRA_LINK_LIB "${WICKRA_LIB_DIR}/libwickra.so")
|
||||
endif()
|
||||
|
||||
enable_testing()
|
||||
|
||||
# Build one example and link it to the Wickra library. On Windows the DLL is
|
||||
# copied next to the executable so the loader finds it at run time. With
|
||||
# register_test=TRUE the example is also run as a ctest; network examples pass
|
||||
# FALSE (they are built only, never run in CI).
|
||||
function(add_wickra_example name source register_test)
|
||||
add_executable(${name} ${source})
|
||||
target_include_directories(${name} PRIVATE "${WICKRA_INCLUDE_DIR}")
|
||||
target_link_libraries(${name} PRIVATE "${WICKRA_LINK_LIB}")
|
||||
target_compile_definitions(${name} PRIVATE "WICKRA_DATA_DIR=\"${WICKRA_DATA_DIR}\"")
|
||||
if(UNIX AND NOT APPLE)
|
||||
target_link_libraries(${name} PRIVATE m)
|
||||
endif()
|
||||
if(WIN32)
|
||||
add_custom_command(TARGET ${name} POST_BUILD
|
||||
COMMAND ${CMAKE_COMMAND} -E copy_if_different
|
||||
"${WICKRA_RUNTIME}" "$<TARGET_FILE_DIR:${name}>")
|
||||
endif()
|
||||
if(register_test)
|
||||
add_test(NAME ${name} COMMAND ${name})
|
||||
if(NOT WIN32)
|
||||
set_tests_properties(${name} PROPERTIES
|
||||
ENVIRONMENT "LD_LIBRARY_PATH=${WICKRA_LIB_DIR};DYLD_LIBRARY_PATH=${WICKRA_LIB_DIR}")
|
||||
endif()
|
||||
endif()
|
||||
endfunction()
|
||||
|
||||
# Offline examples — built and run as ctests.
|
||||
add_wickra_example(smoke smoke.c TRUE) # links the boundary, asserts values
|
||||
add_wickra_example(streaming streaming.c TRUE) # multi-indicator tick stream
|
||||
add_wickra_example(cpp_smoke smoke.cpp TRUE) # C++ RAII wrapper (wickra.hpp)
|
||||
add_wickra_example(backtest backtest.c TRUE) # indicator basket over a CSV
|
||||
add_wickra_example(multi_timeframe multi_timeframe.c TRUE) # resample + per-TF indicators
|
||||
add_wickra_example(parallel_assets parallel_assets.c TRUE) # serial vs OpenMP fan-out
|
||||
add_wickra_example(strategy_rsi_mean_reversion strategy_rsi_mean_reversion.c TRUE)
|
||||
add_wickra_example(strategy_macd_adx strategy_macd_adx.c TRUE)
|
||||
add_wickra_example(strategy_bollinger_squeeze strategy_bollinger_squeeze.c TRUE)
|
||||
|
||||
if(OpenMP_C_FOUND)
|
||||
target_link_libraries(parallel_assets PRIVATE OpenMP::OpenMP_C)
|
||||
endif()
|
||||
|
||||
# Network examples — built (so they stay compilable) but not run in CI.
|
||||
add_wickra_example(fetch_btcusdt fetch_btcusdt.c FALSE) # downloads CSVs via curl
|
||||
add_wickra_example(live_binance live_binance.c FALSE) # polls Binance REST via curl
|
||||
@@ -0,0 +1,90 @@
|
||||
# Wickra — C / C++ examples
|
||||
|
||||
The Wickra C ABI is a single shared/static library plus a generated header
|
||||
([`bindings/c/include/wickra.h`](../../bindings/c/include/wickra.h)). Any
|
||||
C-capable language links against the same artifact; these examples show the
|
||||
plain-C path.
|
||||
|
||||
## Build the library
|
||||
|
||||
From the workspace root:
|
||||
|
||||
```sh
|
||||
cargo build -p wickra-c --release
|
||||
```
|
||||
|
||||
This produces, in `target/release/`:
|
||||
|
||||
| Platform | Shared library | Link target |
|
||||
|----------|----------------|-------------|
|
||||
| Linux | `libwickra.so` | `-lwickra` |
|
||||
| macOS | `libwickra.dylib` | `-lwickra` |
|
||||
| Windows (MSVC) | `wickra.dll` | `wickra.dll.lib` (import lib) |
|
||||
|
||||
A static library (`libwickra.a` / `wickra.lib`) is emitted alongside.
|
||||
|
||||
## Build and run the smoke example
|
||||
|
||||
### With CMake (portable, used by CI)
|
||||
|
||||
```sh
|
||||
cmake -S examples/c -B examples/c/build -DWICKRA_LIB_DIR="$PWD/target/release"
|
||||
cmake --build examples/c/build
|
||||
ctest --test-dir examples/c/build --output-on-failure
|
||||
```
|
||||
|
||||
### Directly with a compiler
|
||||
|
||||
```sh
|
||||
# Linux / macOS
|
||||
cc examples/c/smoke.c -I bindings/c/include -L target/release -lwickra -lm -o smoke
|
||||
LD_LIBRARY_PATH=target/release ./smoke # macOS: DYLD_LIBRARY_PATH
|
||||
|
||||
# Windows (MinGW gcc, linking the DLL directly)
|
||||
gcc examples/c/smoke.c -I bindings/c/include target/release/wickra.dll -lm -o smoke.exe
|
||||
```
|
||||
|
||||
Expected output:
|
||||
|
||||
```
|
||||
OK: wickra C ABI smoke passed (SMA streaming + batch + reset + NULL-safety + free)
|
||||
```
|
||||
|
||||
## The examples
|
||||
|
||||
| Example | What it does |
|
||||
|---------|--------------|
|
||||
| `smoke.c` | Links the boundary and asserts SMA streaming / batch / reset / NULL-safety values. |
|
||||
| `streaming.c` | Feeds a synthetic price series through SMA / EMA / RSI / MACD tick by tick. |
|
||||
| `backtest.c` | Runs an indicator basket over an OHLCV CSV (defaults to the bundled daily dataset). |
|
||||
| `multi_timeframe.c` | Resamples the bundled 1-minute CSV to 5m / 15m / 1h / 4h / 1d and prints indicators per timeframe. |
|
||||
| `parallel_assets.c` | Serial vs OpenMP fan-out over a synthetic panel (one handle per asset), with speedup. |
|
||||
| `strategy_rsi_mean_reversion.c` | Hourly RSI(14) mean-reversion with a PnL / Sharpe / max-drawdown summary. |
|
||||
| `strategy_macd_adx.c` | Hourly MACD crossover gated by ADX(14) > 20. |
|
||||
| `strategy_bollinger_squeeze.c` | Daily Bollinger-squeeze breakout with an ATR(14) stop. |
|
||||
| `fetch_btcusdt.c` | Downloads BTCUSDT klines from the Binance REST API into `examples/data/` (shells out to `curl`). |
|
||||
| `live_binance.c` | Polls the Binance REST klines endpoint via `curl` and streams closed candles through RSI(14). |
|
||||
| `smoke.cpp` | C++ RAII via `wickra::Handle` from [`wickra.hpp`](../../bindings/c/include/wickra.hpp). |
|
||||
|
||||
`ctest` builds and runs every example except `fetch_btcusdt` and `live_binance`,
|
||||
which reach the network and are built only — run those two by hand. The C ABI
|
||||
exposes only the indicators, not the `wickra-data` IO layer, so the examples read
|
||||
CSV ([`wickra_csv.h`](wickra_csv.h)) and resample themselves; the network ones
|
||||
shell out to the system `curl` rather than adding an HTTP/TLS dependency.
|
||||
|
||||
## Usage shape
|
||||
|
||||
Every indicator follows the same five-function pattern over an opaque handle:
|
||||
|
||||
```c
|
||||
#include "wickra.h"
|
||||
|
||||
struct Sma *sma = wickra_sma_new(14); /* NULL on invalid params */
|
||||
double v = wickra_sma_update(sma, 42.0); /* NaN during warmup */
|
||||
wickra_sma_reset(sma); /* back to fresh state */
|
||||
wickra_sma_free(sma); /* exactly once per _new */
|
||||
```
|
||||
|
||||
There is no RAII across the C boundary: every `wickra_<ind>_new` must be paired
|
||||
with exactly one `wickra_<ind>_free`. All functions are NULL-safe (a NULL handle
|
||||
yields `NaN` / a no-op, never a crash).
|
||||
@@ -0,0 +1,139 @@
|
||||
/* Backtest a basket of indicators against an OHLCV CSV with the Wickra C ABI.
|
||||
*
|
||||
* The C counterpart of `examples/rust/src/bin/backtest.rs` and
|
||||
* `examples/node/backtest.js`: load an OHLCV file, run a basket of indicators
|
||||
* (scalar ones via `_batch`, candle ones streamed bar by bar), and print the
|
||||
* most recent value of each. Defaults to the bundled BTCUSDT daily dataset.
|
||||
*
|
||||
* Build (after `cargo build -p wickra-c --release`):
|
||||
* cc examples/c/backtest.c -I bindings/c/include -L target/release -lwickra -lm -o backtest
|
||||
* ./backtest [path/to/ohlcv.csv]
|
||||
*/
|
||||
|
||||
#define WICKRA_CSV_IMPL
|
||||
#include "wickra.h"
|
||||
#include "wickra_csv.h"
|
||||
#include <math.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
#ifndef WICKRA_DATA_DIR
|
||||
#define WICKRA_DATA_DIR "../data"
|
||||
#endif
|
||||
|
||||
/* Last finite value of a scalar batch result, or NaN if none warmed up. */
|
||||
static double last_finite(const double *v, size_t n) {
|
||||
for (size_t i = n; i-- > 0;) {
|
||||
if (isfinite(v[i])) {
|
||||
return v[i];
|
||||
}
|
||||
}
|
||||
return NAN;
|
||||
}
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
const char *path = (argc > 1) ? argv[1] : WICKRA_DATA_DIR "/btcusdt-1d.csv";
|
||||
|
||||
WickraCandle *candles = NULL;
|
||||
size_t n = wickra_load_csv(path, &candles);
|
||||
if (n == 0) {
|
||||
fprintf(stderr, "backtest: no candles read from %s\n", path);
|
||||
return 1;
|
||||
}
|
||||
|
||||
double *closes = (double *)malloc(n * sizeof(*closes));
|
||||
double *rsi_out = (double *)malloc(n * sizeof(*rsi_out));
|
||||
double *ema_out = (double *)malloc(n * sizeof(*ema_out));
|
||||
struct Rsi *rsi = wickra_rsi_new(14);
|
||||
struct Ema *ema = wickra_ema_new(20);
|
||||
struct BollingerBands *bb = wickra_bollinger_bands_new(20, 2.0);
|
||||
struct MacdIndicator *macd = wickra_macd_indicator_new(12, 26, 9);
|
||||
struct Atr *atr = wickra_atr_new(14);
|
||||
struct Adx *adx = wickra_adx_new(14);
|
||||
struct Obv *obv = wickra_obv_new();
|
||||
if (closes == NULL || rsi_out == NULL || ema_out == NULL || rsi == NULL ||
|
||||
ema == NULL || bb == NULL || macd == NULL || atr == NULL || adx == NULL ||
|
||||
obv == NULL) {
|
||||
fprintf(stderr, "backtest: allocation failed\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
for (size_t i = 0; i < n; ++i) {
|
||||
closes[i] = candles[i].close;
|
||||
}
|
||||
|
||||
/* Scalar indicators: one batch call over the close series. */
|
||||
wickra_rsi_batch(rsi, closes, rsi_out, n);
|
||||
wickra_ema_batch(ema, closes, ema_out, n);
|
||||
|
||||
/* Multi-output and candle indicators: streamed bar by bar, keeping the
|
||||
* last value each one produced. */
|
||||
WickraBollingerOutput last_bb = {0};
|
||||
int have_bb = 0;
|
||||
WickraMacdOutput last_macd = {0};
|
||||
int have_macd = 0;
|
||||
WickraAdxOutput last_adx = {0};
|
||||
int have_adx = 0;
|
||||
double last_atr = NAN;
|
||||
double last_obv = NAN;
|
||||
for (size_t i = 0; i < n; ++i) {
|
||||
const WickraCandle *c = &candles[i];
|
||||
WickraBollingerOutput bo;
|
||||
if (wickra_bollinger_bands_update(bb, c->close, &bo)) {
|
||||
last_bb = bo;
|
||||
have_bb = 1;
|
||||
}
|
||||
WickraMacdOutput mo;
|
||||
if (wickra_macd_indicator_update(macd, c->close, &mo)) {
|
||||
last_macd = mo;
|
||||
have_macd = 1;
|
||||
}
|
||||
double av = wickra_atr_update(atr, c->open, c->high, c->low, c->close,
|
||||
c->volume, c->timestamp);
|
||||
if (isfinite(av)) {
|
||||
last_atr = av;
|
||||
}
|
||||
WickraAdxOutput ao;
|
||||
if (wickra_adx_update(adx, c->open, c->high, c->low, c->close, c->volume,
|
||||
c->timestamp, &ao)) {
|
||||
last_adx = ao;
|
||||
have_adx = 1;
|
||||
}
|
||||
double ov = wickra_obv_update(obv, c->open, c->high, c->low, c->close,
|
||||
c->volume, c->timestamp);
|
||||
if (isfinite(ov)) {
|
||||
last_obv = ov;
|
||||
}
|
||||
}
|
||||
|
||||
printf("backtest summary for %s (%llu bars)\n", path, (unsigned long long)n);
|
||||
printf(" RSI(14) = %9.4f\n", last_finite(rsi_out, n));
|
||||
printf(" EMA(20) = %9.4f\n", last_finite(ema_out, n));
|
||||
if (have_bb) {
|
||||
printf(" BB(20,2) upper=%9.4f middle=%9.4f lower=%9.4f sd=%8.4f\n",
|
||||
last_bb.upper, last_bb.middle, last_bb.lower, last_bb.stddev);
|
||||
}
|
||||
if (have_macd) {
|
||||
printf(" MACD macd=%9.4f signal=%9.4f hist=%9.4f\n", last_macd.macd,
|
||||
last_macd.signal, last_macd.histogram);
|
||||
}
|
||||
printf(" ATR(14) = %9.4f\n", last_atr);
|
||||
if (have_adx) {
|
||||
printf(" ADX(14) +DI=%6.2f -DI=%6.2f ADX=%6.2f\n", last_adx.plus_di,
|
||||
last_adx.minus_di, last_adx.adx);
|
||||
}
|
||||
printf(" OBV = %14.2f\n", last_obv);
|
||||
|
||||
wickra_rsi_free(rsi);
|
||||
wickra_ema_free(ema);
|
||||
wickra_bollinger_bands_free(bb);
|
||||
wickra_macd_indicator_free(macd);
|
||||
wickra_atr_free(atr);
|
||||
wickra_adx_free(adx);
|
||||
wickra_obv_free(obv);
|
||||
free(closes);
|
||||
free(rsi_out);
|
||||
free(ema_out);
|
||||
free(candles);
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,311 @@
|
||||
/* Download real BTCUSDT spot candles from the Binance REST API and write them as
|
||||
* CSV datasets under examples/data/ — the C counterpart of
|
||||
* `examples/rust/src/bin/fetch_btcusdt.rs` and `examples/node/fetch_btcusdt.js`.
|
||||
*
|
||||
* Like the Rust example, HTTPS is handled by shelling out to the system `curl`
|
||||
* (shipped with Windows 10+, macOS and every Linux distro), so this example adds
|
||||
* no HTTP/TLS dependency. Each klines response is capped at 1000 rows, so larger
|
||||
* datasets are paginated backwards through `endTime`. Only fully closed candles
|
||||
* are kept.
|
||||
*
|
||||
* This example talks to the network, so it is built but NOT run as a ctest.
|
||||
*
|
||||
* Build (after `cargo build -p wickra-c --release`):
|
||||
* cc examples/c/fetch_btcusdt.c -I bindings/c/include -L target/release -lwickra -lm -o fetch_btcusdt
|
||||
* ./fetch_btcusdt
|
||||
*/
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <time.h>
|
||||
|
||||
#ifdef _WIN32
|
||||
#define POPEN _popen
|
||||
#define PCLOSE _pclose
|
||||
#else
|
||||
#define POPEN popen
|
||||
#define PCLOSE pclose
|
||||
#endif
|
||||
|
||||
#ifndef WICKRA_DATA_DIR
|
||||
#define WICKRA_DATA_DIR "../data"
|
||||
#endif
|
||||
|
||||
#define SYMBOL "BTCUSDT"
|
||||
#define PAGE_LIMIT 1000
|
||||
|
||||
typedef struct {
|
||||
int64_t open_time;
|
||||
int64_t close_time;
|
||||
double open, high, low, close, volume;
|
||||
} Kline;
|
||||
|
||||
typedef struct {
|
||||
const char *interval;
|
||||
const char *file;
|
||||
size_t target;
|
||||
} Dataset;
|
||||
|
||||
/* One dataset per timeframe. The monthly file is `btcusdt-1month.csv`, not
|
||||
* `-1M`, so it does not collide with `-1m` on case-insensitive filesystems. */
|
||||
static const Dataset DATASETS[] = {
|
||||
{"1m", "btcusdt-1m.csv", 50000}, {"5m", "btcusdt-5m.csv", 10000},
|
||||
{"15m", "btcusdt-15m.csv", 10000}, {"1h", "btcusdt-1h.csv", 10000},
|
||||
{"12h", "btcusdt-12h.csv", 5000}, {"1d", "btcusdt-1d.csv", 5000},
|
||||
{"1M", "btcusdt-1month.csv", 5000},
|
||||
};
|
||||
|
||||
/* Run `curl <url>` and return its stdout as a malloc'd, NUL-terminated buffer
|
||||
* (caller frees), or NULL on failure. */
|
||||
static char *curl_get(const char *url) {
|
||||
char cmd[512];
|
||||
snprintf(cmd, sizeof(cmd),
|
||||
"curl --silent --show-error --fail --max-time 30 \"%s\"", url);
|
||||
FILE *p = POPEN(cmd, "r");
|
||||
if (p == NULL) {
|
||||
fprintf(stderr, "could not run curl (install it / put it on PATH)\n");
|
||||
return NULL;
|
||||
}
|
||||
size_t cap = 1 << 16, len = 0;
|
||||
char *buf = (char *)malloc(cap);
|
||||
if (buf == NULL) {
|
||||
PCLOSE(p);
|
||||
return NULL;
|
||||
}
|
||||
size_t got;
|
||||
char tmp[8192];
|
||||
while ((got = fread(tmp, 1, sizeof(tmp), p)) > 0) {
|
||||
if (len + got + 1 > cap) {
|
||||
cap *= 2;
|
||||
char *grown = (char *)realloc(buf, cap);
|
||||
if (grown == NULL) {
|
||||
free(buf);
|
||||
PCLOSE(p);
|
||||
return NULL;
|
||||
}
|
||||
buf = grown;
|
||||
}
|
||||
memcpy(buf + len, tmp, got);
|
||||
len += got;
|
||||
}
|
||||
int rc = PCLOSE(p);
|
||||
buf[len] = '\0';
|
||||
if (rc != 0 || len == 0) {
|
||||
fprintf(stderr, "curl failed for %s\n", url);
|
||||
free(buf);
|
||||
return NULL;
|
||||
}
|
||||
return buf;
|
||||
}
|
||||
|
||||
/* Parse a Binance klines JSON array. Each row is
|
||||
* [openTime, "open", "high", "low", "close", "volume", closeTime, ...].
|
||||
* Appends parsed rows to *rows (grown via realloc) and returns the new count. */
|
||||
static size_t parse_klines(const char *body, Kline **rows, size_t count, size_t *cap) {
|
||||
int depth = 0;
|
||||
int in_string = 0;
|
||||
int field = -1; /* -1 = not inside a row yet */
|
||||
char tok[64];
|
||||
size_t tok_len = 0;
|
||||
Kline cur = {0};
|
||||
|
||||
for (const char *s = body; *s; ++s) {
|
||||
char ch = *s;
|
||||
if (in_string) {
|
||||
if (ch == '"') {
|
||||
in_string = 0;
|
||||
} else if (tok_len + 1 < sizeof(tok)) {
|
||||
tok[tok_len++] = ch;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
switch (ch) {
|
||||
case '[':
|
||||
depth++;
|
||||
if (depth == 2) {
|
||||
field = 0;
|
||||
tok_len = 0;
|
||||
memset(&cur, 0, sizeof(cur));
|
||||
}
|
||||
break;
|
||||
case ']':
|
||||
if (depth == 2) {
|
||||
/* close the final field of this row, then emit it */
|
||||
tok[tok_len] = '\0';
|
||||
if (field == 6) {
|
||||
cur.close_time = strtoll(tok, NULL, 10);
|
||||
}
|
||||
if (*cap == count) {
|
||||
*cap = *cap ? *cap * 2 : 1024;
|
||||
Kline *grown = (Kline *)realloc(*rows, *cap * sizeof(Kline));
|
||||
if (grown == NULL) {
|
||||
return count;
|
||||
}
|
||||
*rows = grown;
|
||||
}
|
||||
(*rows)[count++] = cur;
|
||||
field = -1;
|
||||
}
|
||||
depth--;
|
||||
break;
|
||||
case '"':
|
||||
in_string = 1;
|
||||
break;
|
||||
case ',':
|
||||
if (depth == 2) {
|
||||
tok[tok_len] = '\0';
|
||||
switch (field) {
|
||||
case 0: cur.open_time = strtoll(tok, NULL, 10); break;
|
||||
case 1: cur.open = strtod(tok, NULL); break;
|
||||
case 2: cur.high = strtod(tok, NULL); break;
|
||||
case 3: cur.low = strtod(tok, NULL); break;
|
||||
case 4: cur.close = strtod(tok, NULL); break;
|
||||
case 5: cur.volume = strtod(tok, NULL); break;
|
||||
case 6: cur.close_time = strtoll(tok, NULL, 10); break;
|
||||
default: break;
|
||||
}
|
||||
field++;
|
||||
tok_len = 0;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
if (depth == 2 && tok_len + 1 < sizeof(tok)) {
|
||||
tok[tok_len++] = ch;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
static int cmp_open(const void *a, const void *b) {
|
||||
int64_t x = ((const Kline *)a)->open_time;
|
||||
int64_t y = ((const Kline *)b)->open_time;
|
||||
return (x > y) - (x < y);
|
||||
}
|
||||
|
||||
/* Paginate backwards until `target` closed candles are collected. */
|
||||
static size_t collect(const char *interval, size_t target, int64_t now_ms,
|
||||
Kline **out) {
|
||||
Kline *rows = NULL;
|
||||
size_t count = 0, cap = 0;
|
||||
int64_t end_time = 0; /* 0 = no endTime cap (most recent page) */
|
||||
int pages = 0;
|
||||
|
||||
for (;;) {
|
||||
char url[256];
|
||||
if (end_time > 0) {
|
||||
snprintf(url, sizeof(url),
|
||||
"https://api.binance.com/api/v3/klines?symbol=%s&interval=%s"
|
||||
"&limit=%d&endTime=%lld",
|
||||
SYMBOL, interval, PAGE_LIMIT, (long long)end_time);
|
||||
} else {
|
||||
snprintf(url, sizeof(url),
|
||||
"https://api.binance.com/api/v3/klines?symbol=%s&interval=%s"
|
||||
"&limit=%d",
|
||||
SYMBOL, interval, PAGE_LIMIT);
|
||||
}
|
||||
char *body = curl_get(url);
|
||||
if (body == NULL) {
|
||||
free(rows);
|
||||
return 0;
|
||||
}
|
||||
Kline *page = NULL;
|
||||
size_t page_cap = 0;
|
||||
size_t page_n = parse_klines(body, &page, 0, &page_cap);
|
||||
free(body);
|
||||
pages++;
|
||||
|
||||
int64_t oldest_open = INT64_MAX;
|
||||
for (size_t i = 0; i < page_n; ++i) {
|
||||
if (page[i].open_time < oldest_open) {
|
||||
oldest_open = page[i].open_time;
|
||||
}
|
||||
if (page[i].close_time < now_ms) { /* keep only closed candles */
|
||||
if (count == cap) {
|
||||
cap = cap ? cap * 2 : 1024;
|
||||
Kline *grown = (Kline *)realloc(rows, cap * sizeof(Kline));
|
||||
if (grown == NULL) {
|
||||
free(page);
|
||||
free(rows);
|
||||
return 0;
|
||||
}
|
||||
rows = grown;
|
||||
}
|
||||
rows[count++] = page[i];
|
||||
}
|
||||
}
|
||||
fprintf(stderr, "\r %s: collected %llu candles over %d page(s)...",
|
||||
interval, (unsigned long long)count, pages);
|
||||
|
||||
int page_full = page_n >= PAGE_LIMIT;
|
||||
free(page);
|
||||
if (count >= target || !page_full || oldest_open == INT64_MAX) {
|
||||
break;
|
||||
}
|
||||
end_time = oldest_open - 1;
|
||||
}
|
||||
fprintf(stderr, "\n");
|
||||
|
||||
/* Sort ascending, drop duplicate open times, keep the most recent target. */
|
||||
qsort(rows, count, sizeof(Kline), cmp_open);
|
||||
size_t uniq = 0;
|
||||
for (size_t i = 0; i < count; ++i) {
|
||||
if (uniq == 0 || rows[i].open_time != rows[uniq - 1].open_time) {
|
||||
rows[uniq++] = rows[i];
|
||||
}
|
||||
}
|
||||
if (uniq > target) {
|
||||
memmove(rows, rows + (uniq - target), target * sizeof(Kline));
|
||||
uniq = target;
|
||||
}
|
||||
*out = rows;
|
||||
return uniq;
|
||||
}
|
||||
|
||||
static int write_csv(const char *path, const Kline *rows, size_t n) {
|
||||
FILE *f = fopen(path, "w");
|
||||
if (f == NULL) {
|
||||
return 0;
|
||||
}
|
||||
fputs("timestamp,open,high,low,close,volume\n", f);
|
||||
for (size_t i = 0; i < n; ++i) {
|
||||
fprintf(f, "%lld,%g,%g,%g,%g,%g\n", (long long)rows[i].open_time,
|
||||
rows[i].open, rows[i].high, rows[i].low, rows[i].close,
|
||||
rows[i].volume);
|
||||
}
|
||||
fclose(f);
|
||||
return 1;
|
||||
}
|
||||
|
||||
int main(void) {
|
||||
int64_t now_ms = (int64_t)time(NULL) * 1000;
|
||||
|
||||
printf("Fetching %s klines from Binance into %s\n", SYMBOL, WICKRA_DATA_DIR);
|
||||
size_t n_datasets = sizeof(DATASETS) / sizeof(DATASETS[0]);
|
||||
for (size_t d = 0; d < n_datasets; ++d) {
|
||||
const Dataset *ds = &DATASETS[d];
|
||||
Kline *rows = NULL;
|
||||
size_t n = collect(ds->interval, ds->target, now_ms, &rows);
|
||||
if (n == 0) {
|
||||
fprintf(stderr, "Binance returned no closed candles for %s\n",
|
||||
ds->interval);
|
||||
free(rows);
|
||||
return 1;
|
||||
}
|
||||
char path[256];
|
||||
snprintf(path, sizeof(path), "%s/%s", WICKRA_DATA_DIR, ds->file);
|
||||
if (!write_csv(path, rows, n)) {
|
||||
fprintf(stderr, "could not write %s\n", path);
|
||||
free(rows);
|
||||
return 1;
|
||||
}
|
||||
printf(" %3s %6llu candles -> %s\n", ds->interval, (unsigned long long)n,
|
||||
path);
|
||||
free(rows);
|
||||
}
|
||||
printf("Done — %llu datasets written.\n", (unsigned long long)n_datasets);
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
/* Live BTCUSDT indicator with the Wickra C ABI.
|
||||
*
|
||||
* The C counterpart of `examples/rust/src/bin/live_binance.rs`,
|
||||
* `examples/python/live_trading.py` and `examples/node/live_trading.js`. Those
|
||||
* stream Binance over a WebSocket; the C ABI ships only the indicators and no
|
||||
* socket layer, so this example polls the Binance REST klines endpoint via the
|
||||
* system `curl` once per interval and feeds each newly *closed* candle into a
|
||||
* streaming RSI(14). Same "live feed -> incremental indicator" shape, no extra
|
||||
* dependency.
|
||||
*
|
||||
* This example talks to the network and runs until interrupted (Ctrl+C), so it
|
||||
* is built but NOT run as a ctest.
|
||||
*
|
||||
* Build (after `cargo build -p wickra-c --release`):
|
||||
* cc examples/c/live_binance.c -I bindings/c/include -L target/release -lwickra -lm -o live_binance
|
||||
* ./live_binance [SYMBOL]
|
||||
*/
|
||||
|
||||
#include "wickra.h"
|
||||
#include <math.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#ifdef _WIN32
|
||||
#include <windows.h>
|
||||
#define SLEEP_MS(ms) Sleep(ms)
|
||||
#define POPEN _popen
|
||||
#define PCLOSE _pclose
|
||||
#else
|
||||
#include <time.h>
|
||||
#define POPEN popen
|
||||
#define PCLOSE pclose
|
||||
static void SLEEP_MS(long ms) {
|
||||
struct timespec ts = {ms / 1000, (ms % 1000) * 1000000L};
|
||||
nanosleep(&ts, NULL);
|
||||
}
|
||||
#endif
|
||||
|
||||
/* Run `curl <url>` and return stdout as a malloc'd, NUL-terminated buffer. */
|
||||
static char *curl_get(const char *url) {
|
||||
char cmd[512];
|
||||
snprintf(cmd, sizeof(cmd),
|
||||
"curl --silent --show-error --fail --max-time 15 \"%s\"", url);
|
||||
FILE *p = POPEN(cmd, "r");
|
||||
if (p == NULL) {
|
||||
return NULL;
|
||||
}
|
||||
size_t cap = 1 << 15, len = 0;
|
||||
char *buf = (char *)malloc(cap);
|
||||
if (buf == NULL) {
|
||||
PCLOSE(p);
|
||||
return NULL;
|
||||
}
|
||||
size_t got;
|
||||
char tmp[4096];
|
||||
while ((got = fread(tmp, 1, sizeof(tmp), p)) > 0) {
|
||||
if (len + got + 1 > cap) {
|
||||
cap *= 2;
|
||||
char *grown = (char *)realloc(buf, cap);
|
||||
if (grown == NULL) {
|
||||
free(buf);
|
||||
PCLOSE(p);
|
||||
return NULL;
|
||||
}
|
||||
buf = grown;
|
||||
}
|
||||
memcpy(buf + len, tmp, got);
|
||||
len += got;
|
||||
}
|
||||
int rc = PCLOSE(p);
|
||||
buf[len] = '\0';
|
||||
if (rc != 0 || len == 0) {
|
||||
free(buf);
|
||||
return NULL;
|
||||
}
|
||||
return buf;
|
||||
}
|
||||
|
||||
/* Extract the first kline's open time and close price from a klines response of
|
||||
* the form [[openTime,"open","high","low","close",...],...]. Returns 1 on
|
||||
* success. The first row (limit=2) is the most recent fully closed candle. */
|
||||
static int first_kline(const char *body, int64_t *open_time, double *close) {
|
||||
const char *s = strchr(body, '[');
|
||||
if (s == NULL) {
|
||||
return 0;
|
||||
}
|
||||
s = strchr(s + 1, '['); /* into the first row */
|
||||
if (s == NULL) {
|
||||
return 0;
|
||||
}
|
||||
s++;
|
||||
char tok[64];
|
||||
int field = 0;
|
||||
while (*s && *s != ']') {
|
||||
size_t tl = 0;
|
||||
while (*s && *s != ',' && *s != ']') {
|
||||
if (*s != '"' && tl + 1 < sizeof(tok)) {
|
||||
tok[tl++] = *s;
|
||||
}
|
||||
s++;
|
||||
}
|
||||
tok[tl] = '\0';
|
||||
if (field == 0) {
|
||||
*open_time = strtoll(tok, NULL, 10);
|
||||
} else if (field == 4) {
|
||||
*close = strtod(tok, NULL);
|
||||
return 1;
|
||||
}
|
||||
field++;
|
||||
if (*s == ',') {
|
||||
s++;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
const char *symbol = (argc > 1) ? argv[1] : "BTCUSDT";
|
||||
char url[256];
|
||||
snprintf(url, sizeof(url),
|
||||
"https://api.binance.com/api/v3/klines?symbol=%s&interval=1m&limit=2",
|
||||
symbol);
|
||||
|
||||
struct Rsi *rsi = wickra_rsi_new(14);
|
||||
if (rsi == NULL) {
|
||||
fprintf(stderr, "failed to create RSI\n");
|
||||
return 1;
|
||||
}
|
||||
printf("Listening for %s 1m closes (REST poll, Ctrl+C to stop)...\n", symbol);
|
||||
|
||||
int64_t last_open = 0;
|
||||
for (;;) {
|
||||
char *body = curl_get(url);
|
||||
if (body != NULL) {
|
||||
int64_t open_time = 0;
|
||||
double close = 0.0;
|
||||
if (first_kline(body, &open_time, &close) && open_time != last_open) {
|
||||
last_open = open_time;
|
||||
double v = wickra_rsi_update(rsi, close);
|
||||
if (isfinite(v)) {
|
||||
printf("%s close=%.4f rsi=%.2f\n", symbol, close, v);
|
||||
} else {
|
||||
printf("%s close=%.4f rsi=...warmup\n", symbol, close);
|
||||
}
|
||||
fflush(stdout);
|
||||
}
|
||||
free(body);
|
||||
}
|
||||
SLEEP_MS(2000);
|
||||
}
|
||||
/* Unreachable in normal use (interrupted by Ctrl+C). */
|
||||
}
|
||||
@@ -0,0 +1,151 @@
|
||||
/* Multi-timeframe indicators with the Wickra C ABI.
|
||||
*
|
||||
* The C counterpart of `examples/rust/src/bin/multi_timeframe.rs` and
|
||||
* `examples/python/multi_timeframe.py`: read the bundled 1-minute BTCUSDT CSV
|
||||
* (or a path on the command line), resample it to 5m / 15m / 1h / 4h / 1d, and
|
||||
* print the last RSI(14), MACD(12,26,9) histogram and ADX(14) at each timeframe.
|
||||
*
|
||||
* The Rust/Python stack resamples through `wickra-data`; the C ABI ships only
|
||||
* the indicators, so the time-bucket aggregation is done here (open = first,
|
||||
* high = max, low = min, close = last, volume = sum per bucket).
|
||||
*
|
||||
* Build (after `cargo build -p wickra-c --release`):
|
||||
* cc examples/c/multi_timeframe.c -I bindings/c/include -L target/release -lwickra -lm -o multi_timeframe
|
||||
* ./multi_timeframe [path/to/1m.csv]
|
||||
*/
|
||||
|
||||
#define WICKRA_CSV_IMPL
|
||||
#include "wickra.h"
|
||||
#include "wickra_csv.h"
|
||||
#include <math.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
#ifndef WICKRA_DATA_DIR
|
||||
#define WICKRA_DATA_DIR "../data"
|
||||
#endif
|
||||
|
||||
#define ONE_MINUTE_MS 60000LL
|
||||
|
||||
/* Aggregate `in` into fixed-width time buckets of `tf_ms`. Writes the bucketed
|
||||
* candles into the caller-owned `out` (capacity >= count) and returns how many
|
||||
* buckets were produced. */
|
||||
static size_t resample(const WickraCandle *in, size_t count, int64_t tf_ms,
|
||||
WickraCandle *out) {
|
||||
size_t produced = 0;
|
||||
int open_bucket = 0;
|
||||
int64_t bucket_start = 0;
|
||||
WickraCandle cur = {0};
|
||||
for (size_t i = 0; i < count; ++i) {
|
||||
int64_t b = in[i].timestamp - (in[i].timestamp % tf_ms);
|
||||
if (!open_bucket || b != bucket_start) {
|
||||
if (open_bucket) {
|
||||
out[produced++] = cur;
|
||||
}
|
||||
bucket_start = b;
|
||||
cur = in[i];
|
||||
cur.timestamp = b;
|
||||
open_bucket = 1;
|
||||
} else {
|
||||
if (in[i].high > cur.high) {
|
||||
cur.high = in[i].high;
|
||||
}
|
||||
if (in[i].low < cur.low) {
|
||||
cur.low = in[i].low;
|
||||
}
|
||||
cur.close = in[i].close;
|
||||
cur.volume += in[i].volume;
|
||||
}
|
||||
}
|
||||
if (open_bucket) {
|
||||
out[produced++] = cur;
|
||||
}
|
||||
return produced;
|
||||
}
|
||||
|
||||
static void summarize(const char *label, const WickraCandle *candles, size_t n) {
|
||||
if (n == 0) {
|
||||
printf(" %-5s (empty)\n", label);
|
||||
return;
|
||||
}
|
||||
struct Rsi *rsi = wickra_rsi_new(14);
|
||||
struct MacdIndicator *macd = wickra_macd_indicator_new(12, 26, 9);
|
||||
struct Adx *adx = wickra_adx_new(14);
|
||||
|
||||
double last_rsi = NAN;
|
||||
double last_hist = NAN;
|
||||
double last_adx = NAN;
|
||||
for (size_t i = 0; i < n; ++i) {
|
||||
const WickraCandle *c = &candles[i];
|
||||
double r = wickra_rsi_update(rsi, c->close);
|
||||
if (isfinite(r)) {
|
||||
last_rsi = r;
|
||||
}
|
||||
WickraMacdOutput m;
|
||||
if (wickra_macd_indicator_update(macd, c->close, &m)) {
|
||||
last_hist = m.histogram;
|
||||
}
|
||||
WickraAdxOutput a;
|
||||
if (wickra_adx_update(adx, c->open, c->high, c->low, c->close, c->volume,
|
||||
c->timestamp, &a)) {
|
||||
last_adx = a.adx;
|
||||
}
|
||||
}
|
||||
|
||||
char b_rsi[16], b_hist[16], b_adx[16];
|
||||
if (isfinite(last_rsi)) {
|
||||
snprintf(b_rsi, sizeof(b_rsi), "%6.2f", last_rsi);
|
||||
} else {
|
||||
snprintf(b_rsi, sizeof(b_rsi), " --");
|
||||
}
|
||||
if (isfinite(last_hist)) {
|
||||
snprintf(b_hist, sizeof(b_hist), "%+6.2f", last_hist);
|
||||
} else {
|
||||
snprintf(b_hist, sizeof(b_hist), " -- ");
|
||||
}
|
||||
if (isfinite(last_adx)) {
|
||||
snprintf(b_adx, sizeof(b_adx), "%6.2f", last_adx);
|
||||
} else {
|
||||
snprintf(b_adx, sizeof(b_adx), " --");
|
||||
}
|
||||
printf(" %-5s bars=%5llu last_close=%10.2f rsi=%s macd_hist=%s adx=%s\n",
|
||||
label, (unsigned long long)n, candles[n - 1].close, b_rsi, b_hist, b_adx);
|
||||
|
||||
wickra_rsi_free(rsi);
|
||||
wickra_macd_indicator_free(macd);
|
||||
wickra_adx_free(adx);
|
||||
}
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
const char *path = (argc > 1) ? argv[1] : WICKRA_DATA_DIR "/btcusdt-1m.csv";
|
||||
|
||||
WickraCandle *ones = NULL;
|
||||
size_t n = wickra_load_csv(path, &ones);
|
||||
if (n == 0) {
|
||||
fprintf(stderr, "multi_timeframe: no candles read from %s\n", path);
|
||||
return 1;
|
||||
}
|
||||
/* Resampling only ever produces fewer candles than the 1m source. */
|
||||
WickraCandle *buf = (WickraCandle *)malloc(n * sizeof(*buf));
|
||||
if (buf == NULL) {
|
||||
fprintf(stderr, "multi_timeframe: allocation failed\n");
|
||||
free(ones);
|
||||
return 1;
|
||||
}
|
||||
|
||||
printf("Multi-timeframe view of %s\n", path);
|
||||
summarize("1m", ones, n);
|
||||
|
||||
const struct {
|
||||
const char *label;
|
||||
int64_t minutes;
|
||||
} frames[] = {{"5m", 5}, {"15m", 15}, {"1h", 60}, {"4h", 240}, {"1d", 1440}};
|
||||
for (size_t f = 0; f < sizeof(frames) / sizeof(frames[0]); ++f) {
|
||||
size_t m = resample(ones, n, frames[f].minutes * ONE_MINUTE_MS, buf);
|
||||
summarize(frames[f].label, buf, m);
|
||||
}
|
||||
|
||||
free(buf);
|
||||
free(ones);
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,156 @@
|
||||
/* Parallel multi-asset indicator computation with the Wickra C ABI.
|
||||
*
|
||||
* The C counterpart of `examples/rust/src/bin/parallel_assets.rs` (rayon) and
|
||||
* `examples/python/parallel_assets.py` (the Rust extension drops the GIL). The C
|
||||
* ABI is the parallelization primitive itself: each `wickra_<ind>_new` handle is
|
||||
* independent, so the caller fans assets out across threads, one fresh handle per
|
||||
* asset. Here a serial baseline is compared against an OpenMP `parallel for`.
|
||||
*
|
||||
* If the compiler has no OpenMP support the "parallel" pass simply runs the same
|
||||
* single-threaded loop (speedup ~1x) — the result is honest either way.
|
||||
*
|
||||
* Build (after `cargo build -p wickra-c --release`):
|
||||
* cc -fopenmp examples/c/parallel_assets.c -I bindings/c/include -L target/release -lwickra -lm -o parallel_assets
|
||||
* ./parallel_assets --assets 200 --bars 5000 --indicator sma
|
||||
*/
|
||||
|
||||
#include "wickra.h"
|
||||
#include <math.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <time.h>
|
||||
|
||||
#ifdef _OPENMP
|
||||
#include <omp.h>
|
||||
#endif
|
||||
|
||||
/* Wall-clock seconds (not CPU time, so threaded speedup is measured correctly). */
|
||||
static double now_seconds(void) {
|
||||
#ifdef _OPENMP
|
||||
return omp_get_wtime();
|
||||
#else
|
||||
struct timespec ts;
|
||||
timespec_get(&ts, TIME_UTC);
|
||||
return (double)ts.tv_sec + (double)ts.tv_nsec * 1e-9;
|
||||
#endif
|
||||
}
|
||||
|
||||
typedef enum { IND_SMA, IND_RSI } Which;
|
||||
|
||||
/* Deterministic synthetic (assets, bars) panel, flat row-major. Each asset uses
|
||||
* an independent LCG seed so the series are uncorrelated but reproducible. */
|
||||
static void synthesize_panel(double *panel, size_t assets, size_t bars) {
|
||||
for (size_t a = 0; a < assets; ++a) {
|
||||
double price = 100.0;
|
||||
uint32_t state = (uint32_t)(1234567u + a) * 2654435761u;
|
||||
for (size_t b = 0; b < bars; ++b) {
|
||||
state = (state * 1103515245u + 12345u) & 0x7FFFFFFFu;
|
||||
double r = (double)state / (double)0x7FFFFFFFu;
|
||||
price += (r - 0.5) * 0.4;
|
||||
panel[a * bars + b] = price;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Run one asset's series through a fresh handle into its output slice. */
|
||||
static void run_one(Which which, const double *prices, double *out, size_t bars) {
|
||||
if (which == IND_SMA) {
|
||||
struct Sma *h = wickra_sma_new(14);
|
||||
wickra_sma_batch(h, prices, out, bars);
|
||||
wickra_sma_free(h);
|
||||
} else {
|
||||
struct Rsi *h = wickra_rsi_new(14);
|
||||
wickra_rsi_batch(h, prices, out, bars);
|
||||
wickra_rsi_free(h);
|
||||
}
|
||||
}
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
size_t assets = 200;
|
||||
size_t bars = 5000;
|
||||
Which which = IND_SMA;
|
||||
for (int i = 1; i < argc; ++i) {
|
||||
if (strcmp(argv[i], "--assets") == 0 && i + 1 < argc) {
|
||||
assets = (size_t)strtoul(argv[++i], NULL, 10);
|
||||
} else if (strcmp(argv[i], "--bars") == 0 && i + 1 < argc) {
|
||||
bars = (size_t)strtoul(argv[++i], NULL, 10);
|
||||
} else if (strcmp(argv[i], "--indicator") == 0 && i + 1 < argc) {
|
||||
++i;
|
||||
if (strcmp(argv[i], "sma") == 0) {
|
||||
which = IND_SMA;
|
||||
} else if (strcmp(argv[i], "rsi") == 0) {
|
||||
which = IND_RSI;
|
||||
} else {
|
||||
fprintf(stderr, "--indicator: expected 'sma' or 'rsi'\n");
|
||||
return 1;
|
||||
}
|
||||
} else {
|
||||
fprintf(stderr, "usage: parallel_assets [--assets N] [--bars N] "
|
||||
"[--indicator sma|rsi]\n");
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
if (assets == 0 || bars == 0) {
|
||||
fprintf(stderr, "--assets and --bars must be positive\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
const char *ind_name = (which == IND_SMA) ? "sma" : "rsi";
|
||||
printf("Generating %llux%llu synthetic panel...\n", (unsigned long long)assets,
|
||||
(unsigned long long)bars);
|
||||
double *panel = (double *)malloc(assets * bars * sizeof(*panel));
|
||||
double *serial = (double *)malloc(assets * bars * sizeof(*serial));
|
||||
double *parallel = (double *)malloc(assets * bars * sizeof(*parallel));
|
||||
if (panel == NULL || serial == NULL || parallel == NULL) {
|
||||
fprintf(stderr, "allocation failed\n");
|
||||
return 1;
|
||||
}
|
||||
synthesize_panel(panel, assets, bars);
|
||||
|
||||
double t0 = now_seconds();
|
||||
for (size_t a = 0; a < assets; ++a) {
|
||||
run_one(which, &panel[a * bars], &serial[a * bars], bars);
|
||||
}
|
||||
double t_serial = now_seconds() - t0;
|
||||
printf("Serial: %8.3f s (%llu assets, indicator=%s)\n", t_serial,
|
||||
(unsigned long long)assets, ind_name);
|
||||
|
||||
/* The loop variable is declared outside the `for` and the bound is a plain
|
||||
* variable: MSVC's OpenMP 2.0 rejects an in-init declaration or a cast in
|
||||
* the condition (error C3015). */
|
||||
long a;
|
||||
long asset_count = (long)assets;
|
||||
t0 = now_seconds();
|
||||
#ifdef _OPENMP
|
||||
#pragma omp parallel for schedule(static)
|
||||
#endif
|
||||
for (a = 0; a < asset_count; ++a) {
|
||||
run_one(which, &panel[(size_t)a * bars], ¶llel[(size_t)a * bars], bars);
|
||||
}
|
||||
double t_parallel = now_seconds() - t0;
|
||||
double denom = t_parallel > 1e-9 ? t_parallel : 1e-9;
|
||||
#ifdef _OPENMP
|
||||
printf("Parallel: %8.3f s (OpenMP, %d threads, speedup ~%.2fx)\n", t_parallel,
|
||||
omp_get_max_threads(), t_serial / denom);
|
||||
#else
|
||||
printf("Parallel: %8.3f s (no OpenMP at build time — serial, speedup ~%.2fx)\n",
|
||||
t_parallel, t_serial / denom);
|
||||
#endif
|
||||
|
||||
/* The parallel run must reproduce the serial results exactly. */
|
||||
for (size_t i = 0; i < assets * bars; ++i) {
|
||||
int both_nan = isnan(serial[i]) && isnan(parallel[i]);
|
||||
if (!both_nan && serial[i] != parallel[i]) {
|
||||
fprintf(stderr, "mismatch at %llu: serial=%g parallel=%g\n",
|
||||
(unsigned long long)i, serial[i], parallel[i]);
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
printf("Parallel results match serial results — OK.\n");
|
||||
|
||||
free(panel);
|
||||
free(serial);
|
||||
free(parallel);
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
/* Smoke test for the Wickra C ABI.
|
||||
*
|
||||
* This is the one test the Rust unit tests structurally cannot do: it links a
|
||||
* foreign C consumer against the generated `wickra.h` + the compiled library and
|
||||
* exercises the real FFI boundary (symbol export, header correctness, opaque
|
||||
* handle, pointer ownership, `_free`). If this passes, every C-capable language
|
||||
* (C, C++, Go, C#, Java, R) can link the same way.
|
||||
*
|
||||
* Build (from the workspace root, after `cargo build -p wickra-c --release`):
|
||||
* cc examples/c/smoke.c -I bindings/c/include target/release/<lib> -lm -o smoke
|
||||
*/
|
||||
|
||||
#include "wickra.h"
|
||||
#include <math.h>
|
||||
#include <stdio.h>
|
||||
|
||||
static int near(double a, double b) { return fabs(a - b) < 1e-9; }
|
||||
|
||||
int main(void) {
|
||||
struct Sma *sma = wickra_sma_new(3);
|
||||
if (sma == NULL) {
|
||||
printf("FAIL: wickra_sma_new returned NULL\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* SMA(3): first two outputs are warmup (NaN), then the trailing mean. */
|
||||
double in[5] = {1.0, 2.0, 3.0, 4.0, 5.0};
|
||||
double r0 = wickra_sma_update(sma, in[0]); /* NaN (1/3) */
|
||||
double r1 = wickra_sma_update(sma, in[1]); /* NaN (2/3) */
|
||||
double r2 = wickra_sma_update(sma, in[2]); /* 2.0 (1+2+3)/3 */
|
||||
double r3 = wickra_sma_update(sma, in[3]); /* 3.0 (2+3+4)/3 */
|
||||
|
||||
if (!isnan(r0) || !isnan(r1)) {
|
||||
printf("FAIL: warmup not NaN (%f %f)\n", r0, r1);
|
||||
return 1;
|
||||
}
|
||||
if (!near(r2, 2.0) || !near(r3, 3.0)) {
|
||||
printf("FAIL: streaming values (%f %f), expected (2.0 3.0)\n", r2, r3);
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* Batch over a reset instance must reproduce the streaming result. */
|
||||
wickra_sma_reset(sma);
|
||||
double out[5];
|
||||
wickra_sma_batch(sma, in, out, 5);
|
||||
if (!isnan(out[0]) || !isnan(out[1]) ||
|
||||
!near(out[2], 2.0) || !near(out[3], 3.0) || !near(out[4], 4.0)) {
|
||||
printf("FAIL: batch mismatch (%f %f %f %f %f)\n",
|
||||
out[0], out[1], out[2], out[3], out[4]);
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* NULL handle is a defined no-op / NaN, never a crash. */
|
||||
if (!isnan(wickra_sma_update(NULL, 1.0))) {
|
||||
printf("FAIL: NULL update did not return NaN\n");
|
||||
return 1;
|
||||
}
|
||||
wickra_sma_reset(NULL);
|
||||
wickra_sma_free(NULL);
|
||||
|
||||
wickra_sma_free(sma);
|
||||
printf("OK: wickra C ABI smoke passed (SMA streaming + batch + reset + NULL-safety + free)\n");
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
// C++ smoke test for the Wickra C ABI via the optional RAII wrapper (`wickra.hpp`).
|
||||
//
|
||||
// Validates that the header compiles as C++ and that `wickra::Handle` constructs,
|
||||
// moves, and frees correctly across the boundary.
|
||||
|
||||
#include "wickra.hpp"
|
||||
|
||||
#include <cmath>
|
||||
#include <cstdio>
|
||||
#include <utility>
|
||||
|
||||
int main() {
|
||||
wickra::Handle<Sma, wickra_sma_free> sma(wickra_sma_new(3));
|
||||
if (!sma) {
|
||||
std::puts("FAIL: wickra_sma_new returned null");
|
||||
return 1;
|
||||
}
|
||||
|
||||
(void)wickra_sma_update(sma.get(), 1.0);
|
||||
(void)wickra_sma_update(sma.get(), 2.0);
|
||||
double value = wickra_sma_update(sma.get(), 3.0);
|
||||
if (std::fabs(value - 2.0) > 1e-9) {
|
||||
std::printf("FAIL: SMA(3) value %.6f, expected 2.0\n", value);
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Move transfers ownership; the moved-from handle must not double-free.
|
||||
wickra::Handle<Sma, wickra_sma_free> moved(std::move(sma));
|
||||
if (static_cast<bool>(sma) || !static_cast<bool>(moved)) {
|
||||
std::puts("FAIL: move semantics");
|
||||
return 1;
|
||||
}
|
||||
|
||||
std::puts("OK: wickra C++ RAII smoke passed (Handle construct + move + auto-free)");
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
/* Strategy example: Bollinger-Squeeze breakout with ATR stop (Wickra C ABI).
|
||||
*
|
||||
* Enters long when the Bollinger Bandwidth has just printed a fresh 6-month low
|
||||
* (the squeeze) and price closes above the upper band (the release). Exits when
|
||||
* price closes below entry minus 2*ATR(14), or when the upper band trails back
|
||||
* below the entry price (the squeeze has played out). 0.1% fees per trade. The
|
||||
* C counterpart of `examples/rust/src/bin/strategy_bollinger_squeeze.rs`.
|
||||
*
|
||||
* Educational example. NOT a live trading recommendation. Uses the checked-in
|
||||
* `examples/data/btcusdt-1d.csv` dataset because daily bars give an
|
||||
* interpretable "6-month low" lookback (~180 bars).
|
||||
*
|
||||
* Build (after `cargo build -p wickra-c --release`):
|
||||
* cc examples/c/strategy_bollinger_squeeze.c -I bindings/c/include -L target/release -lwickra -lm -o strat_bb
|
||||
*/
|
||||
|
||||
#define WICKRA_CSV_IMPL
|
||||
#define WICKRA_STRATEGY_IMPL
|
||||
#include "wickra.h"
|
||||
#include "wickra_csv.h"
|
||||
#include "wickra_strategy.h"
|
||||
#include <math.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
#ifndef WICKRA_DATA_DIR
|
||||
#define WICKRA_DATA_DIR "../data"
|
||||
#endif
|
||||
|
||||
#define FEE 0.001
|
||||
#define BB_PERIOD 20
|
||||
#define BB_K 2.0
|
||||
#define ATR_PERIOD 14
|
||||
#define ATR_STOP_MULT 2.0
|
||||
#define SQUEEZE_LOOKBACK 180 /* ~6 months of daily bars */
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
const char *path = (argc > 1) ? argv[1] : WICKRA_DATA_DIR "/btcusdt-1d.csv";
|
||||
|
||||
WickraCandle *candles = NULL;
|
||||
size_t n = wickra_load_csv(path, &candles);
|
||||
if (n < SQUEEZE_LOOKBACK + BB_PERIOD) {
|
||||
fprintf(stderr, "dataset has only %llu bars; need at least %d\n",
|
||||
(unsigned long long)n, SQUEEZE_LOOKBACK + BB_PERIOD);
|
||||
free(candles);
|
||||
return 1;
|
||||
}
|
||||
|
||||
struct BollingerBands *bb = wickra_bollinger_bands_new(BB_PERIOD, BB_K);
|
||||
struct Atr *atr = wickra_atr_new(ATR_PERIOD);
|
||||
double *trades = (double *)malloc(n * sizeof(*trades));
|
||||
double *equity_curve = (double *)malloc(n * sizeof(*equity_curve));
|
||||
/* Circular buffer of recent bandwidth values for the squeeze lookback. */
|
||||
double bw_window[SQUEEZE_LOOKBACK];
|
||||
size_t bw_len = 0, bw_head = 0;
|
||||
if (bb == NULL || atr == NULL || trades == NULL || equity_curve == NULL) {
|
||||
fprintf(stderr, "allocation failed\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
int in_position = 0;
|
||||
double entry_price = 0.0, stop_level = 0.0;
|
||||
size_t n_trades = 0;
|
||||
double equity = 1.0;
|
||||
|
||||
for (size_t i = 0; i < n; ++i) {
|
||||
const WickraCandle *c = &candles[i];
|
||||
double price = c->close;
|
||||
WickraBollingerOutput b;
|
||||
int bb_ready = wickra_bollinger_bands_update(bb, price, &b);
|
||||
double a = wickra_atr_update(atr, c->open, c->high, c->low, c->close,
|
||||
c->volume, c->timestamp);
|
||||
|
||||
equity_curve[i] = in_position ? equity * (price / entry_price) : equity;
|
||||
|
||||
if (!bb_ready || !isfinite(a)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
double bandwidth =
|
||||
fabs(b.middle) > 1e-15 ? (b.upper - b.lower) / b.middle : NAN;
|
||||
if (isfinite(bandwidth)) {
|
||||
if (bw_len == SQUEEZE_LOOKBACK) {
|
||||
bw_window[bw_head] = bandwidth;
|
||||
bw_head = (bw_head + 1) % SQUEEZE_LOOKBACK;
|
||||
} else {
|
||||
bw_window[bw_len++] = bandwidth;
|
||||
}
|
||||
}
|
||||
|
||||
if (bw_len < SQUEEZE_LOOKBACK || !isfinite(bandwidth)) {
|
||||
continue;
|
||||
}
|
||||
double min_bw = INFINITY;
|
||||
for (size_t k = 0; k < bw_len; ++k) {
|
||||
if (bw_window[k] < min_bw) {
|
||||
min_bw = bw_window[k];
|
||||
}
|
||||
}
|
||||
|
||||
if (in_position) {
|
||||
int stop_hit = price < stop_level;
|
||||
int upper_collapse = b.upper < entry_price;
|
||||
if (stop_hit || upper_collapse) {
|
||||
double trade_ret = price / entry_price - 1.0;
|
||||
trades[n_trades++] = trade_ret;
|
||||
equity *= (1.0 + trade_ret) * (1.0 - FEE);
|
||||
in_position = 0;
|
||||
}
|
||||
} else {
|
||||
int is_new_low = fabs(bandwidth - min_bw) < 1e-12;
|
||||
int breakout = price > b.upper;
|
||||
if (is_new_low && breakout) {
|
||||
entry_price = price;
|
||||
stop_level = price - ATR_STOP_MULT * a;
|
||||
equity *= 1.0 - FEE;
|
||||
in_position = 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (in_position) {
|
||||
double trade_ret = candles[n - 1].close / entry_price - 1.0;
|
||||
trades[n_trades++] = trade_ret;
|
||||
equity *= (1.0 + trade_ret) * (1.0 - FEE);
|
||||
}
|
||||
|
||||
wickra_print_summary("Bollinger Squeeze Breakout (1d, BTCUSDT)", candles[0].close,
|
||||
candles[n - 1].close, n, trades, n_trades, equity,
|
||||
equity_curve, n);
|
||||
|
||||
wickra_bollinger_bands_free(bb);
|
||||
wickra_atr_free(atr);
|
||||
free(trades);
|
||||
free(equity_curve);
|
||||
free(candles);
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
/* Strategy example: MACD crossover with ADX trend-strength filter (Wickra C ABI).
|
||||
*
|
||||
* Long-only trend follower. Entries fire when the MACD line crosses above the
|
||||
* signal line while ADX(14) > 20 (a market with at least mild directional
|
||||
* strength); exits on the opposite MACD crossover regardless of ADX. 0.1% fees
|
||||
* per trade. The C counterpart of `examples/rust/src/bin/strategy_macd_adx.rs`.
|
||||
*
|
||||
* The ADX filter is the point: pure MACD on sideways markets chops in and out;
|
||||
* gating entries on directional strength cuts the worst losing streak.
|
||||
*
|
||||
* Educational example. NOT a live trading recommendation. Uses the checked-in
|
||||
* `examples/data/btcusdt-1h.csv` dataset.
|
||||
*
|
||||
* Build (after `cargo build -p wickra-c --release`):
|
||||
* cc examples/c/strategy_macd_adx.c -I bindings/c/include -L target/release -lwickra -lm -o strat_macd
|
||||
*/
|
||||
|
||||
#define WICKRA_CSV_IMPL
|
||||
#define WICKRA_STRATEGY_IMPL
|
||||
#include "wickra.h"
|
||||
#include "wickra_csv.h"
|
||||
#include "wickra_strategy.h"
|
||||
#include <math.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
#ifndef WICKRA_DATA_DIR
|
||||
#define WICKRA_DATA_DIR "../data"
|
||||
#endif
|
||||
|
||||
#define FEE 0.001
|
||||
#define ADX_FLOOR 20.0
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
const char *path = (argc > 1) ? argv[1] : WICKRA_DATA_DIR "/btcusdt-1h.csv";
|
||||
|
||||
WickraCandle *candles = NULL;
|
||||
size_t n = wickra_load_csv(path, &candles);
|
||||
if (n == 0) {
|
||||
fprintf(stderr, "CSV is empty: %s\n", path);
|
||||
return 1;
|
||||
}
|
||||
|
||||
struct MacdIndicator *macd = wickra_macd_indicator_new(12, 26, 9);
|
||||
struct Adx *adx = wickra_adx_new(14);
|
||||
double *trades = (double *)malloc(n * sizeof(*trades));
|
||||
double *equity_curve = (double *)malloc(n * sizeof(*equity_curve));
|
||||
if (macd == NULL || adx == NULL || trades == NULL || equity_curve == NULL) {
|
||||
fprintf(stderr, "allocation failed\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
int in_position = 0;
|
||||
double entry_price = 0.0;
|
||||
size_t n_trades = 0;
|
||||
double equity = 1.0;
|
||||
/* Previous histogram sign to detect MACD-line crossovers: -1 unset, 0/1 sign. */
|
||||
int prev_hist_sign = -1;
|
||||
|
||||
for (size_t i = 0; i < n; ++i) {
|
||||
const WickraCandle *c = &candles[i];
|
||||
double price = c->close;
|
||||
WickraMacdOutput m;
|
||||
int macd_ready = wickra_macd_indicator_update(macd, price, &m);
|
||||
WickraAdxOutput a;
|
||||
int adx_ready = wickra_adx_update(adx, c->open, c->high, c->low, c->close,
|
||||
c->volume, c->timestamp, &a);
|
||||
|
||||
equity_curve[i] = in_position ? equity * (price / entry_price) : equity;
|
||||
|
||||
if (!macd_ready || !adx_ready) {
|
||||
continue;
|
||||
}
|
||||
|
||||
int hist_sign = m.histogram > 0.0 ? 1 : 0;
|
||||
int cross_up = prev_hist_sign == 0 && hist_sign == 1;
|
||||
int cross_down = prev_hist_sign == 1 && hist_sign == 0;
|
||||
prev_hist_sign = hist_sign;
|
||||
|
||||
if (!in_position && cross_up && a.adx > ADX_FLOOR) {
|
||||
entry_price = price;
|
||||
equity *= 1.0 - FEE;
|
||||
in_position = 1;
|
||||
} else if (in_position && cross_down) {
|
||||
double trade_ret = price / entry_price - 1.0;
|
||||
trades[n_trades++] = trade_ret;
|
||||
equity *= (1.0 + trade_ret) * (1.0 - FEE);
|
||||
in_position = 0;
|
||||
}
|
||||
}
|
||||
if (in_position) {
|
||||
double trade_ret = candles[n - 1].close / entry_price - 1.0;
|
||||
trades[n_trades++] = trade_ret;
|
||||
equity *= (1.0 + trade_ret) * (1.0 - FEE);
|
||||
}
|
||||
|
||||
wickra_print_summary("MACD + ADX Trend Filter (1h, BTCUSDT)", candles[0].close,
|
||||
candles[n - 1].close, n, trades, n_trades, equity,
|
||||
equity_curve, n);
|
||||
|
||||
wickra_macd_indicator_free(macd);
|
||||
wickra_adx_free(adx);
|
||||
free(trades);
|
||||
free(equity_curve);
|
||||
free(candles);
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
/* Strategy example: RSI mean-reversion on hourly BTCUSDT data (Wickra C ABI).
|
||||
*
|
||||
* Goes long when RSI(14) crosses below 30 (oversold), exits when RSI crosses
|
||||
* above 70 (overbought). Position is binary (full-in / full-out), fees are 0.1%
|
||||
* per trade (Binance maker tier), no stop-loss. The C counterpart of
|
||||
* `examples/rust/src/bin/strategy_rsi_mean_reversion.rs`.
|
||||
*
|
||||
* Educational example. NOT a recommended trading strategy — the point is to
|
||||
* show how a Wickra streaming indicator wires into a signal -> fill -> PnL ->
|
||||
* equity loop. Uses the checked-in `examples/data/btcusdt-1h.csv` dataset.
|
||||
*
|
||||
* Build (after `cargo build -p wickra-c --release`):
|
||||
* cc examples/c/strategy_rsi_mean_reversion.c -I bindings/c/include -L target/release -lwickra -lm -o strat_rsi
|
||||
*/
|
||||
|
||||
#define WICKRA_CSV_IMPL
|
||||
#define WICKRA_STRATEGY_IMPL
|
||||
#include "wickra.h"
|
||||
#include "wickra_csv.h"
|
||||
#include "wickra_strategy.h"
|
||||
#include <math.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
#ifndef WICKRA_DATA_DIR
|
||||
#define WICKRA_DATA_DIR "../data"
|
||||
#endif
|
||||
|
||||
#define FEE 0.001
|
||||
#define RSI_PERIOD 14
|
||||
#define OVERSOLD 30.0
|
||||
#define OVERBOUGHT 70.0
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
const char *path = (argc > 1) ? argv[1] : WICKRA_DATA_DIR "/btcusdt-1h.csv";
|
||||
|
||||
WickraCandle *candles = NULL;
|
||||
size_t n = wickra_load_csv(path, &candles);
|
||||
if (n < RSI_PERIOD * 4) {
|
||||
fprintf(stderr, "dataset too small: %llu\n", (unsigned long long)n);
|
||||
free(candles);
|
||||
return 1;
|
||||
}
|
||||
|
||||
struct Rsi *rsi = wickra_rsi_new(RSI_PERIOD);
|
||||
double *trades = (double *)malloc(n * sizeof(*trades));
|
||||
double *equity_curve = (double *)malloc(n * sizeof(*equity_curve));
|
||||
if (rsi == NULL || trades == NULL || equity_curve == NULL) {
|
||||
fprintf(stderr, "allocation failed\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
int in_position = 0;
|
||||
double entry_price = 0.0;
|
||||
size_t n_trades = 0;
|
||||
double equity = 1.0;
|
||||
|
||||
for (size_t i = 0; i < n; ++i) {
|
||||
double price = candles[i].close;
|
||||
double r = wickra_rsi_update(rsi, price);
|
||||
|
||||
/* Mark-to-market so the equity curve moves bar-by-bar between trades. */
|
||||
equity_curve[i] = in_position ? equity * (price / entry_price) : equity;
|
||||
|
||||
if (!isfinite(r)) {
|
||||
continue;
|
||||
}
|
||||
if (!in_position && r < OVERSOLD) {
|
||||
entry_price = price;
|
||||
equity *= 1.0 - FEE;
|
||||
in_position = 1;
|
||||
} else if (in_position && r > OVERBOUGHT) {
|
||||
double trade_ret = price / entry_price - 1.0;
|
||||
trades[n_trades++] = trade_ret;
|
||||
equity *= (1.0 + trade_ret) * (1.0 - FEE);
|
||||
in_position = 0;
|
||||
}
|
||||
}
|
||||
/* Close any still-open trade at the last bar so metrics include it. */
|
||||
if (in_position) {
|
||||
double trade_ret = candles[n - 1].close / entry_price - 1.0;
|
||||
trades[n_trades++] = trade_ret;
|
||||
equity *= (1.0 + trade_ret) * (1.0 - FEE);
|
||||
}
|
||||
|
||||
wickra_print_summary("RSI Mean-Reversion (1h, BTCUSDT)", candles[0].close,
|
||||
candles[n - 1].close, n, trades, n_trades, equity,
|
||||
equity_curve, n);
|
||||
|
||||
wickra_rsi_free(rsi);
|
||||
free(trades);
|
||||
free(equity_curve);
|
||||
free(candles);
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
/* Streaming indicators with the Wickra C ABI.
|
||||
*
|
||||
* Feeds a synthetic price series through several indicators tick by tick — the
|
||||
* same O(1)-per-update model a live trading bot would use — and prints a status
|
||||
* line once every indicator has warmed up. The C counterpart of
|
||||
* `examples/rust/src/bin/streaming.rs`, `examples/python/streaming.py` and
|
||||
* `examples/node/streaming.js`, using the same seeded LCG so a side-by-side run
|
||||
* produces visibly comparable streams.
|
||||
*
|
||||
* Build (after `cargo build -p wickra-c --release`):
|
||||
* cc examples/c/streaming.c -I bindings/c/include -L target/release -lwickra -lm -o streaming
|
||||
*/
|
||||
|
||||
#include "wickra.h"
|
||||
#include <math.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#define DEFAULT_TICKS 120
|
||||
|
||||
/* Deterministic synthetic series matching the sibling examples' seeded LCG. */
|
||||
static void make_series(double *prices, size_t n) {
|
||||
uint64_t seed = 1234567u;
|
||||
for (size_t t = 0; t < n; ++t) {
|
||||
seed = (seed * 1103515245u + 12345u) & 0x7FFFFFFFu;
|
||||
double rnd = (double)seed / (double)0x7FFFFFFFu;
|
||||
double tf = (double)t;
|
||||
prices[t] = 100.0 + tf * 0.05 + sin(tf * 0.07) * 8.0 + cos(tf * 0.21) * 3.0 +
|
||||
(rnd - 0.5);
|
||||
}
|
||||
}
|
||||
|
||||
/* Format an indicator value, rendering warmup (NaN) as a dashed placeholder. */
|
||||
static void fmt(char *buf, size_t buflen, double v) {
|
||||
if (isfinite(v)) {
|
||||
snprintf(buf, buflen, "%7.2f", v);
|
||||
} else {
|
||||
snprintf(buf, buflen, " -- ");
|
||||
}
|
||||
}
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
size_t ticks = DEFAULT_TICKS;
|
||||
if (argc == 3 && strcmp(argv[1], "--ticks") == 0) {
|
||||
long parsed = strtol(argv[2], NULL, 10);
|
||||
if (parsed <= 0) {
|
||||
fprintf(stderr, "--ticks must be positive\n");
|
||||
return 1;
|
||||
}
|
||||
ticks = (size_t)parsed;
|
||||
} else if (argc != 1) {
|
||||
fprintf(stderr, "usage: streaming [--ticks N]\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
struct Sma *sma = wickra_sma_new(20);
|
||||
struct Ema *ema = wickra_ema_new(20);
|
||||
struct Rsi *rsi = wickra_rsi_new(14);
|
||||
struct MacdIndicator *macd = wickra_macd_indicator_new(12, 26, 9);
|
||||
double *prices = (double *)malloc(ticks * sizeof(*prices));
|
||||
if (sma == NULL || ema == NULL || rsi == NULL || macd == NULL || prices == NULL) {
|
||||
fprintf(stderr, "allocation failed\n");
|
||||
return 1;
|
||||
}
|
||||
make_series(prices, ticks);
|
||||
|
||||
printf("Wickra streaming indicator demo (C)\n\n");
|
||||
|
||||
size_t signals = 0;
|
||||
for (size_t t = 0; t < ticks; ++t) {
|
||||
double price = prices[t];
|
||||
double sv = wickra_sma_update(sma, price);
|
||||
double ev = wickra_ema_update(ema, price);
|
||||
double rv = wickra_rsi_update(rsi, price);
|
||||
WickraMacdOutput m;
|
||||
int macd_ready = wickra_macd_indicator_update(macd, price, &m);
|
||||
|
||||
/* Only act once every indicator has produced a value. */
|
||||
if (!isfinite(sv) || !isfinite(ev) || !isfinite(rv) || !macd_ready) {
|
||||
continue;
|
||||
}
|
||||
|
||||
int overbought = rv > 70.0 && m.histogram < 0.0;
|
||||
int oversold = rv < 30.0 && m.histogram > 0.0;
|
||||
const char *tag = overbought ? "SELL?" : (oversold ? "BUY? " : " ");
|
||||
if (overbought || oversold) {
|
||||
signals++;
|
||||
}
|
||||
|
||||
char b_price[16], b_sma[16], b_ema[16], b_rsi[16], b_hist[16];
|
||||
fmt(b_price, sizeof(b_price), price);
|
||||
fmt(b_sma, sizeof(b_sma), sv);
|
||||
fmt(b_ema, sizeof(b_ema), ev);
|
||||
fmt(b_rsi, sizeof(b_rsi), rv);
|
||||
fmt(b_hist, sizeof(b_hist), m.histogram);
|
||||
printf("t=%3llu price=%s sma=%s ema=%s rsi=%s macd_hist=%s %s\n",
|
||||
(unsigned long long)t, b_price, b_sma, b_ema, b_rsi, b_hist, tag);
|
||||
}
|
||||
|
||||
printf("\nDone — %llu candidate signal(s) over %llu ticks.\n",
|
||||
(unsigned long long)signals, (unsigned long long)ticks);
|
||||
|
||||
wickra_sma_free(sma);
|
||||
wickra_ema_free(ema);
|
||||
wickra_rsi_free(rsi);
|
||||
wickra_macd_indicator_free(macd);
|
||||
free(prices);
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
/* Shared OHLCV CSV loader for the Wickra C examples.
|
||||
*
|
||||
* The C ABI exposes only the indicators, not the wickra-data IO layer, so the
|
||||
* examples read CSV themselves. This header-only helper is the C counterpart of
|
||||
* `wickra_data::csv::CandleReader` used by the Rust examples: it parses the
|
||||
* standard `timestamp,open,high,low,close,volume` files shipped under
|
||||
* `examples/data/`.
|
||||
*
|
||||
* Header-only: define WICKRA_CSV_IMPL in exactly one translation unit (each
|
||||
* example is a single .c file, so it just defines it before including this).
|
||||
*/
|
||||
#ifndef WICKRA_CSV_H
|
||||
#define WICKRA_CSV_H
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
typedef struct WickraCandle {
|
||||
int64_t timestamp;
|
||||
double open;
|
||||
double high;
|
||||
double low;
|
||||
double close;
|
||||
double volume;
|
||||
} WickraCandle;
|
||||
|
||||
/* Load an OHLCV CSV into a malloc'd array. Returns the candle count and stores
|
||||
* the array in *out (caller frees with free()). Returns 0 and leaves *out NULL
|
||||
* on any error (missing file, no parseable rows). A leading header line whose
|
||||
* first field is non-numeric is skipped. */
|
||||
size_t wickra_load_csv(const char *path, WickraCandle **out);
|
||||
|
||||
#ifdef WICKRA_CSV_IMPL
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
size_t wickra_load_csv(const char *path, WickraCandle **out) {
|
||||
*out = NULL;
|
||||
FILE *f = fopen(path, "r");
|
||||
if (f == NULL) {
|
||||
fprintf(stderr, "wickra_load_csv: cannot open %s\n", path);
|
||||
return 0;
|
||||
}
|
||||
|
||||
size_t cap = 1024;
|
||||
size_t n = 0;
|
||||
WickraCandle *rows = (WickraCandle *)malloc(cap * sizeof(*rows));
|
||||
if (rows == NULL) {
|
||||
fclose(f);
|
||||
return 0;
|
||||
}
|
||||
|
||||
char line[512];
|
||||
while (fgets(line, (int)sizeof(line), f) != NULL) {
|
||||
WickraCandle c;
|
||||
long long ts = 0;
|
||||
/* sscanf returns the number of fields successfully matched. A header
|
||||
* row ("timestamp,...") matches 0 and is skipped. */
|
||||
int matched = sscanf(line, "%lld,%lf,%lf,%lf,%lf,%lf", &ts, &c.open,
|
||||
&c.high, &c.low, &c.close, &c.volume);
|
||||
if (matched != 6) {
|
||||
continue;
|
||||
}
|
||||
c.timestamp = (int64_t)ts;
|
||||
|
||||
if (n == cap) {
|
||||
cap *= 2;
|
||||
WickraCandle *grown = (WickraCandle *)realloc(rows, cap * sizeof(*rows));
|
||||
if (grown == NULL) {
|
||||
free(rows);
|
||||
fclose(f);
|
||||
return 0;
|
||||
}
|
||||
rows = grown;
|
||||
}
|
||||
rows[n++] = c;
|
||||
}
|
||||
fclose(f);
|
||||
|
||||
if (n == 0) {
|
||||
free(rows);
|
||||
return 0;
|
||||
}
|
||||
*out = rows;
|
||||
return n;
|
||||
}
|
||||
|
||||
#endif /* WICKRA_CSV_IMPL */
|
||||
#endif /* WICKRA_CSV_H */
|
||||
@@ -0,0 +1,92 @@
|
||||
/* Shared equity-curve summary for the Wickra C strategy examples.
|
||||
*
|
||||
* The Rust strategy examples repeat their `print_summary` per file; in C the
|
||||
* presentation is factored into this header so each strategy .c file stays
|
||||
* focused on its signal logic. Pure reporting — no indicator state.
|
||||
*
|
||||
* Header-only: define WICKRA_STRATEGY_IMPL in exactly one translation unit.
|
||||
*/
|
||||
#ifndef WICKRA_STRATEGY_H
|
||||
#define WICKRA_STRATEGY_H
|
||||
|
||||
#include <stddef.h>
|
||||
|
||||
/* Print a one-screen summary of a strategy run: returns vs buy & hold, trade
|
||||
* win/loss counts, max drawdown, per-trade Sharpe, best/worst trade. */
|
||||
void wickra_print_summary(const char *name, double first_price, double last_price,
|
||||
size_t bars, const double *closed_trades, size_t n_trades,
|
||||
double final_equity, const double *equity_curve,
|
||||
size_t n_curve);
|
||||
|
||||
#ifdef WICKRA_STRATEGY_IMPL
|
||||
|
||||
#include <math.h>
|
||||
#include <stdio.h>
|
||||
|
||||
void wickra_print_summary(const char *name, double first_price, double last_price,
|
||||
size_t bars, const double *closed_trades, size_t n_trades,
|
||||
double final_equity, const double *equity_curve,
|
||||
size_t n_curve) {
|
||||
double buy_hold = last_price / first_price;
|
||||
double strat_return = final_equity - 1.0;
|
||||
double bh_return = buy_hold - 1.0;
|
||||
|
||||
size_t wins = 0, losses = 0;
|
||||
double best = -INFINITY, worst = INFINITY;
|
||||
double sum_ret = 0.0, sum_sq = 0.0;
|
||||
for (size_t i = 0; i < n_trades; ++i) {
|
||||
double r = closed_trades[i];
|
||||
if (r > 0.0) {
|
||||
wins++;
|
||||
} else if (r < 0.0) {
|
||||
losses++;
|
||||
}
|
||||
if (r > best) {
|
||||
best = r;
|
||||
}
|
||||
if (r < worst) {
|
||||
worst = r;
|
||||
}
|
||||
sum_ret += r;
|
||||
sum_sq += r * r;
|
||||
}
|
||||
double n = (double)n_trades;
|
||||
double mean_ret = n > 0.0 ? sum_ret / n : 0.0;
|
||||
double var_ret = n > 1.0 ? (sum_sq - n * mean_ret * mean_ret) / (n - 1.0) : 0.0;
|
||||
double sharpe = var_ret > 0.0 ? mean_ret / sqrt(var_ret) : 0.0;
|
||||
if (n_trades == 0) {
|
||||
best = 0.0;
|
||||
worst = 0.0;
|
||||
}
|
||||
|
||||
double peak = n_curve > 0 ? equity_curve[0] : 1.0;
|
||||
double max_dd = 0.0;
|
||||
for (size_t i = 0; i < n_curve; ++i) {
|
||||
if (equity_curve[i] > peak) {
|
||||
peak = equity_curve[i];
|
||||
}
|
||||
double dd = (peak - equity_curve[i]) / peak;
|
||||
if (dd > max_dd) {
|
||||
max_dd = dd;
|
||||
}
|
||||
}
|
||||
|
||||
printf("=== %s ===\n", name);
|
||||
printf("Bars: %llu\n", (unsigned long long)bars);
|
||||
printf("Trades: %llu (W%llu / L%llu)\n", (unsigned long long)n_trades,
|
||||
(unsigned long long)wins, (unsigned long long)losses);
|
||||
printf("Strategy return: %+.2f%%\n", strat_return * 100.0);
|
||||
printf("Buy & Hold return: %+.2f%%\n", bh_return * 100.0);
|
||||
printf("Excess over BH: %+.2f%%\n", (strat_return - bh_return) * 100.0);
|
||||
printf("Max drawdown: %.2f%%\n", max_dd * 100.0);
|
||||
printf("Per-trade Sharpe: %.2f (mean %+.4f, stddev %.4f)\n", sharpe,
|
||||
mean_ret, sqrt(var_ret));
|
||||
printf("Best / worst trade: %+.2f%% / %+.2f%%\n", best * 100.0, worst * 100.0);
|
||||
printf("\n");
|
||||
printf("NOTE: Educational example — fees, slippage, funding costs and tax "
|
||||
"effects are simplified or omitted. Past performance is not indicative "
|
||||
"of future results.\n");
|
||||
}
|
||||
|
||||
#endif /* WICKRA_STRATEGY_IMPL */
|
||||
#endif /* WICKRA_STRATEGY_H */
|
||||
@@ -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>
|
||||
Generated
+7
-7
@@ -17,7 +17,7 @@
|
||||
},
|
||||
"../../bindings/node": {
|
||||
"name": "wickra",
|
||||
"version": "0.7.4",
|
||||
"version": "0.7.6",
|
||||
"license": "MIT OR Apache-2.0",
|
||||
"devDependencies": {
|
||||
"@napi-rs/cli": "^2.18.0"
|
||||
@@ -26,12 +26,12 @@
|
||||
"node": ">= 18"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"wickra-darwin-arm64": "0.7.4",
|
||||
"wickra-darwin-x64": "0.7.4",
|
||||
"wickra-linux-arm64-gnu": "0.7.4",
|
||||
"wickra-linux-x64-gnu": "0.7.4",
|
||||
"wickra-win32-arm64-msvc": "0.7.4",
|
||||
"wickra-win32-x64-msvc": "0.7.4"
|
||||
"wickra-darwin-arm64": "0.7.6",
|
||||
"wickra-darwin-x64": "0.7.6",
|
||||
"wickra-linux-arm64-gnu": "0.7.6",
|
||||
"wickra-linux-x64-gnu": "0.7.6",
|
||||
"wickra-win32-arm64-msvc": "0.7.6",
|
||||
"wickra-win32-x64-msvc": "0.7.6"
|
||||
}
|
||||
},
|
||||
"node_modules/wickra": {
|
||||
|
||||
@@ -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