Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d0061c73b8 | |||
| 23d636fd97 | |||
| fce26cf881 | |||
| 91f6f67257 | |||
| 4caaa1db97 | |||
| 12681e4b1b | |||
| 91e05e3c26 | |||
| 9d0983b666 | |||
| 13c8250488 | |||
| e5305ffa94 | |||
| 46be7a54ea | |||
| bca61322b5 |
@@ -1,3 +1,18 @@
|
||||
# Shell scripts must keep LF line endings so they run on Linux/macOS CI and
|
||||
# local shells regardless of the committer's platform autocrlf setting.
|
||||
*.sh text eol=lf
|
||||
|
||||
# The cbindgen-generated C header is committed; pin it to LF so its CI drift
|
||||
# check (regenerate + `git diff`) never trips on a CRLF normalization.
|
||||
bindings/c/include/wickra.h text eol=lf
|
||||
|
||||
# C# sources (including the generated binding) are pinned to LF so the committed
|
||||
# files stay stable regardless of the committer's autocrlf setting.
|
||||
*.cs text eol=lf
|
||||
|
||||
# Go sources (including the generated binding) are pinned to LF so gofmt's CI
|
||||
# check never trips on a CRLF checkout on Windows.
|
||||
*.go text eol=lf
|
||||
go.mod text eol=lf
|
||||
go.sum text eol=lf
|
||||
|
||||
|
||||
@@ -30,9 +30,9 @@ assignees: ""
|
||||
## Environment
|
||||
|
||||
- Wickra version:
|
||||
- Language / binding: <!-- Rust crate / Python / Node / WASM -->
|
||||
- Language / binding: <!-- Rust crate / Python / Node / WASM / C ABI / C# (.NET) / Go -->
|
||||
- OS and architecture:
|
||||
- Rust / Python / Node version (If relevant):
|
||||
- Rust / Python / Node / .NET version (If relevant):
|
||||
|
||||
## Additional context
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ assignees: []
|
||||
| Binding version | `e.g. python 0.4.2 / node 0.4.2` |
|
||||
| OS / arch | `e.g. Windows 11 x86_64, Linux glibc` |
|
||||
| Rust toolchain | `rustc --version` (If building from source) |
|
||||
| Python / Node version | `python --version` / `node --version` |
|
||||
| Python / Node / .NET version | `python --version` / `node --version` / `dotnet --version` |
|
||||
|
||||
## Minimal reproducer
|
||||
|
||||
|
||||
@@ -25,6 +25,9 @@ assignees: ""
|
||||
- [ ] Should be exposed in the Python binding
|
||||
- [ ] Should be exposed in the Node binding
|
||||
- [ ] Should be exposed in the WASM binding
|
||||
- [ ] Should be exposed in the C ABI
|
||||
- [ ] Should be exposed in the C# / .NET binding
|
||||
- [ ] Should be exposed in the Go binding
|
||||
|
||||
## Additional context
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ assignees: []
|
||||
## Affected code path
|
||||
|
||||
- Indicator / API: `e.g. EMA.update`
|
||||
- Binding: `Rust / Python / Node / Wasm`
|
||||
- Binding: `Rust / Python / Node / Wasm / C ABI / C# (.NET) / Go`
|
||||
- Hot loop or one-shot call?
|
||||
|
||||
## Versions compared
|
||||
|
||||
@@ -33,4 +33,4 @@ import wickra as ta
|
||||
## Environment (Only if relevant)
|
||||
|
||||
- Wickra version: `e.g. 0.4.2`
|
||||
- Binding: `Rust / Python / Node / Wasm`
|
||||
- Binding: `Rust / Python / Node / Wasm / C ABI / C# (.NET) / Go`
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
- [ ] `cargo clippy --workspace --all-targets -- -D warnings` is clean.
|
||||
- [ ] `cargo test --workspace` passes.
|
||||
- [ ] New behaviour has tests; bug fixes have a regression test.
|
||||
- [ ] Public API changes are mirrored in the Python / Node / WASM bindings
|
||||
- [ ] Public API changes are mirrored in the Python / Node / WASM bindings, and the C ABI + C# + Go bindings are regenerated
|
||||
and their type stubs (If applicable).
|
||||
- [ ] The relevant page on the [documentation site](https://docs.wickra.org)
|
||||
and the `README.md` are updated (If applicable). Docs edits go to a
|
||||
|
||||
@@ -23,6 +23,9 @@ Please fill in the sections below. Delete any that don't apply.
|
||||
- [ ] Python binding (`bindings/python`)
|
||||
- [ ] Node.js binding (`bindings/node`)
|
||||
- [ ] WebAssembly binding (`bindings/wasm`)
|
||||
- [ ] C ABI (`bindings/c`)
|
||||
- [ ] C# / .NET binding (`bindings/csharp`)
|
||||
- [ ] Go binding (`bindings/go`)
|
||||
- [ ] Examples / docs
|
||||
|
||||
## Linked issues
|
||||
|
||||
@@ -622,6 +622,210 @@ jobs:
|
||||
working-directory: bindings/node
|
||||
run: node --test __tests__/
|
||||
|
||||
c-abi:
|
||||
name: C ABI on ${{ matrix.os }}
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [ubuntu-latest, macos-latest, windows-latest]
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Install Rust toolchain
|
||||
uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable branch, 2026-03-27
|
||||
|
||||
- name: Cache cargo
|
||||
uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2
|
||||
continue-on-error: true # cache is an optimisation; never block on a stuck/slow restore
|
||||
timeout-minutes: 6
|
||||
|
||||
- name: Install cbindgen
|
||||
uses: taiki-e/install-action@0fd46367812ee04360509b4169d9f659d6892bb2 # v2.79.15
|
||||
timeout-minutes: 10 # fail fast on a stuck download instead of hanging the job
|
||||
with:
|
||||
tool: cbindgen
|
||||
|
||||
- name: Build the C ABI library (cdylib + staticlib)
|
||||
run: cargo build -p wickra-c --release
|
||||
|
||||
- name: Rust unit tests
|
||||
run: cargo test -p wickra-c
|
||||
|
||||
# The generated header is platform-independent, so checking drift on one OS
|
||||
# is enough — and avoids a spurious CRLF/LF diff on the Windows runner.
|
||||
- name: Check the committed header is in sync with cbindgen
|
||||
if: runner.os == 'Linux'
|
||||
shell: bash
|
||||
run: |
|
||||
cbindgen --config bindings/c/cbindgen.toml --crate wickra-c --output bindings/c/include/wickra.h
|
||||
if ! git diff --quiet -- bindings/c/include/wickra.h; then
|
||||
echo "::error::bindings/c/include/wickra.h is out of sync — run cbindgen and commit the result"
|
||||
git --no-pager diff -- bindings/c/include/wickra.h
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# The real cross-language test: a foreign C consumer links the generated
|
||||
# header + the compiled library and runs. If this passes on all three OSes,
|
||||
# every C-capable language can link the same way.
|
||||
- name: Build and run the C smoke example (CMake + ctest)
|
||||
shell: bash
|
||||
run: |
|
||||
cmake -S examples/c -B examples/c/build
|
||||
cmake --build examples/c/build --config Release
|
||||
ctest --test-dir examples/c/build -C Release --output-on-failure
|
||||
|
||||
csharp:
|
||||
name: C# on ${{ matrix.os }}
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [ubuntu-latest, macos-latest, windows-latest]
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Install Rust toolchain
|
||||
uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable branch, 2026-03-27
|
||||
|
||||
- name: Cache cargo
|
||||
uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2
|
||||
continue-on-error: true # cache is an optimisation; never block on a stuck/slow restore
|
||||
timeout-minutes: 6
|
||||
|
||||
# The binding links against the C ABI hub at runtime; build it first so the
|
||||
# DllImportResolver finds target/release/wickra.{dll,so,dylib}. .NET 8 SDK is
|
||||
# preinstalled on the GitHub runners, so no setup-dotnet step is needed.
|
||||
- name: Build the C ABI library
|
||||
run: cargo build -p wickra-c --release
|
||||
|
||||
- name: .NET info
|
||||
run: dotnet --info
|
||||
|
||||
- name: Test the C# binding
|
||||
run: dotnet test bindings/csharp/Wickra.Tests/Wickra.Tests.csproj -c Release
|
||||
|
||||
- name: Build the C# examples
|
||||
shell: bash
|
||||
run: |
|
||||
for d in streaming backtest multi_timeframe parallel_assets \
|
||||
strategy_rsi_mean_reversion strategy_macd_adx strategy_bollinger_squeeze \
|
||||
fetch_btcusdt live_binance; do
|
||||
dotnet build "examples/csharp/$d" -c Release
|
||||
done
|
||||
|
||||
# Run only the offline examples (fetch_btcusdt / live_binance need network).
|
||||
- name: Run the offline C# examples
|
||||
shell: bash
|
||||
run: |
|
||||
for d in streaming backtest multi_timeframe parallel_assets \
|
||||
strategy_rsi_mean_reversion strategy_macd_adx strategy_bollinger_squeeze; do
|
||||
dotnet run --project "examples/csharp/$d" -c Release --no-build
|
||||
done
|
||||
|
||||
go:
|
||||
name: Go on ${{ matrix.os }}
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [ubuntu-latest, macos-latest, windows-latest]
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
# Go is not reliably on PATH on every runner image, so install it
|
||||
# explicitly. cache: false — the cargo build dominates and the modules
|
||||
# have no external Go deps worth caching.
|
||||
- name: Set up Go
|
||||
id: setup-go
|
||||
continue-on-error: true
|
||||
uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5
|
||||
with:
|
||||
go-version: "stable"
|
||||
cache: false
|
||||
|
||||
- name: Retry Go setup (CDN flake)
|
||||
if: steps.setup-go.outcome == 'failure'
|
||||
shell: bash
|
||||
run: |
|
||||
echo "::warning::setup-go failed (likely CDN flake), waiting 30s before retry..."
|
||||
sleep 30
|
||||
|
||||
- name: Set up Go (retry)
|
||||
if: steps.setup-go.outcome == 'failure'
|
||||
uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5
|
||||
with:
|
||||
go-version: "stable"
|
||||
cache: false
|
||||
|
||||
- name: Install Rust toolchain
|
||||
uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable branch, 2026-03-27
|
||||
|
||||
- name: Cache cargo
|
||||
uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2
|
||||
continue-on-error: true # cache is an optimisation; never block on a stuck/slow restore
|
||||
timeout-minutes: 6
|
||||
|
||||
# The binding links against the C ABI hub via cgo; build it first and stage
|
||||
# the platform library under bindings/go/lib so cgo's LDFLAGS find it. Go is
|
||||
# preinstalled on the GitHub runners, so no setup-go step is needed.
|
||||
- name: Build the C ABI library
|
||||
run: cargo build -p wickra-c --release
|
||||
|
||||
- name: Stage the native library
|
||||
shell: bash
|
||||
run: |
|
||||
mkdir -p bindings/go/lib
|
||||
case "$RUNNER_OS" in
|
||||
Linux) cp target/release/libwickra.so bindings/go/lib/ ;;
|
||||
macOS) cp target/release/libwickra.dylib bindings/go/lib/ ;;
|
||||
Windows) cp target/release/wickra.dll bindings/go/lib/ ;;
|
||||
esac
|
||||
|
||||
- name: Go info
|
||||
run: go version
|
||||
|
||||
- name: Check gofmt
|
||||
shell: bash
|
||||
run: |
|
||||
unformatted="$(gofmt -l bindings/go examples/go)"
|
||||
if [ -n "$unformatted" ]; then
|
||||
echo "gofmt needed on:"; echo "$unformatted"; exit 1
|
||||
fi
|
||||
|
||||
- name: Vet and test the Go binding
|
||||
shell: bash
|
||||
# On Windows there is no rpath; the loader resolves wickra.dll via PATH.
|
||||
run: |
|
||||
export PATH="$PWD/bindings/go/lib:$PATH"
|
||||
cd bindings/go
|
||||
go vet ./...
|
||||
go test ./...
|
||||
|
||||
- name: Build the Go examples
|
||||
shell: bash
|
||||
run: |
|
||||
export PATH="$PWD/bindings/go/lib:$PATH"
|
||||
cd examples/go
|
||||
go build ./...
|
||||
|
||||
# Run only the offline examples (fetch_btcusdt / live_binance need network).
|
||||
- name: Run the offline Go examples
|
||||
shell: bash
|
||||
run: |
|
||||
export PATH="$PWD/bindings/go/lib:$PATH"
|
||||
cd examples/go
|
||||
for d in streaming backtest multi_timeframe parallel_assets \
|
||||
strategy_rsi_mean_reversion strategy_macd_adx strategy_bollinger_squeeze; do
|
||||
go run "./$d"
|
||||
done
|
||||
|
||||
# The cross-library benchmark has moved to a dedicated scheduled workflow
|
||||
# (.github/workflows/bench.yml) — see audit finding R10. It runs nightly
|
||||
# at 03:00 UTC and on-demand via `workflow_dispatch`, and is no longer on
|
||||
|
||||
@@ -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, .NET, and Go bindings. ${n} indicators, O(1) per-tick updates, no system dependencies. Drop-in TA-Lib replacement."
|
||||
# Enforce the homepage unconditionally — it is a constant, so this both
|
||||
# corrects the stale kingchenc URL and self-heals any future drift.
|
||||
# Same Administration-write permission as --description (no extra scope).
|
||||
@@ -366,14 +366,14 @@ jobs:
|
||||
exit 0
|
||||
fi
|
||||
cd webpage-count
|
||||
sed -i -E "s/[0-9]+ (streaming-first )?indicators/${n} \1indicators/g" index.md .vitepress/config.ts
|
||||
sed -i -E "s/[0-9]+ (streaming-first )?indicators/${n} \1indicators/g" index.md about.md .vitepress/config.ts
|
||||
if git diff --quiet; then
|
||||
echo "Webpage indicator count unchanged."
|
||||
exit 0
|
||||
fi
|
||||
git config user.name "wickra-bot"
|
||||
git config user.email "wickra-bot@users.noreply.github.com"
|
||||
git add index.md .vitepress/config.ts
|
||||
git add index.md about.md .vitepress/config.ts
|
||||
git commit -m "chore: sync indicator count to ${n}"
|
||||
if ! git push 2>/dev/null; then
|
||||
echo "::warning::push to wickra-lib/webpage failed — ABOUT_SYNC_TOKEN likely lacks write (findings P10.0a)."
|
||||
|
||||
+21
-8
@@ -27,16 +27,28 @@ or replace lives behind a separate crate boundary.
|
||||
│ no I/O, no deps │ │ optional features │
|
||||
└──────────────────────┘ └────────────────────┘
|
||||
▲
|
||||
│ (every binding wraps the same core)
|
||||
│
|
||||
┌────────────┴───────────┬─────────────────────┐
|
||||
│ │ │
|
||||
┌──▼──────┐ ┌───────▼──────┐ ┌───────▼────────┐
|
||||
│ Python │ │ Node │ │ WASM │
|
||||
│ (PyO3) │ │ (napi-rs) │ │ (wasm-bindgen) │
|
||||
└─────────┘ └──────────────┘ └────────────────┘
|
||||
│ every binding wraps the same core
|
||||
┌────────────┼────────────┬────────────────┐
|
||||
│ │ │ │
|
||||
┌──▼───────┐ ┌──▼───────┐ ┌──▼───────────┐ ┌──▼──────────────────┐
|
||||
│ Python │ │ Node │ │ WASM │ │ C ABI (cbindgen) │
|
||||
│ (PyO3) │ │ (napi-rs)│ │(wasm-bindgen)│ │ cdylib + header │
|
||||
└──────────┘ └──────────┘ └──────────────┘ └─────────┬───────────┘
|
||||
│ linked by
|
||||
┌──────────▼──────────┐
|
||||
│ C · C++ · C# · Go │
|
||||
│ · Java · R │
|
||||
└─────────────────────┘
|
||||
```
|
||||
|
||||
Python, Node and WASM are *native* Rust bindings (PyO3 / napi-rs /
|
||||
wasm-bindgen). The C ABI is the *hub* every other C-capable language links
|
||||
against: it builds to a `cdylib`/`staticlib` plus a generated `wickra.h`, and
|
||||
downstream languages link that one artifact rather than each re-wrapping the
|
||||
core. C and C++ link it directly; the **C# / .NET** binding (`bindings/csharp`,
|
||||
on NuGet) and the **Go** binding (`bindings/go`, cgo) are generated from
|
||||
`wickra.h`, with Java / R planned the same way.
|
||||
|
||||
| Crate | Path | What it owns | Public deps |
|
||||
|---|---|---|---|
|
||||
| `wickra-core` | `crates/wickra-core` | every indicator, the `Indicator` trait, `BatchExt`, `Candle`/`Tick` types, `Error` | `thiserror`, `rayon` (parallel batch) |
|
||||
@@ -45,6 +57,7 @@ or replace lives behind a separate crate boundary.
|
||||
| `wickra-python` | `bindings/python` | `_wickra` PyO3 module + Python package | `pyo3`, `numpy`, depends on `wickra-core` |
|
||||
| `wickra-node` | `bindings/node` | NAPI-RS native binding | `napi`, depends on `wickra-core` |
|
||||
| `wickra-wasm` | `bindings/wasm` | WebAssembly binding | `wasm-bindgen`, depends on `wickra-core` |
|
||||
| `wickra-c` | `bindings/c` | C ABI hub — `cdylib`/`staticlib` + generated `wickra.h` (cbindgen) | depends on `wickra-core` |
|
||||
| `wickra-examples` | `examples/rust` | runnable binary examples | depends on `wickra`, `wickra-data` |
|
||||
|
||||
The `fuzz/` directory is **excluded** from the workspace (it has its own
|
||||
|
||||
+53
-1
@@ -7,6 +7,53 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [0.7.7] - 2026-06-09
|
||||
### Added
|
||||
- **Go binding (`bindings/go`)** — a cgo binding over the C ABI hub exposing all
|
||||
514 indicators as idiomatic types with `New<Indicator>` constructors and
|
||||
`Update`/`Batch`/`Reset`/`Close` methods, generated from `wickra.h`. Handles are
|
||||
freed by `Close()` with a `runtime.SetFinalizer` backstop. Ships a full example
|
||||
suite mirroring the C and C# examples; distributed as a subdirectory module
|
||||
(`go get github.com/wickra-lib/wickra/bindings/go`).
|
||||
|
||||
## [0.7.6] - 2026-06-09
|
||||
### Added
|
||||
- **C# / .NET binding (`bindings/csharp`)** — the first language stecker on the
|
||||
C ABI hub. Exposes all 514 indicators as idiomatic `IDisposable` classes via
|
||||
`[LibraryImport]` source-generated P/Invoke, generated from `wickra.h`. Ships
|
||||
on NuGet as `Wickra` with prebuilt native libraries for six target triples
|
||||
(win/linux/osx × x64/arm64), plus a full example suite mirroring the C examples.
|
||||
|
||||
## [0.7.5] - 2026-06-09
|
||||
### Added
|
||||
- **C ABI (`bindings/c`)** — a `cdylib` + `staticlib` plus a generated
|
||||
`include/wickra.h` exposing all 514 indicators and 10 bar builders over an
|
||||
opaque-handle C ABI: the hub any C-capable language (C, C++, Go, C#, Java, R)
|
||||
links against, complementing the native Python/Node/WASM bindings. Ships a
|
||||
full example suite (streaming, backtest, multi-timeframe, OpenMP parallel
|
||||
fan-out, three educational strategies, and Binance fetch/live over `curl`)
|
||||
mirroring the other bindings, plus an optional `wickra.hpp` C++ RAII wrapper.
|
||||
|
||||
## [0.7.4] - 2026-06-08
|
||||
- **Three-Line Break** — Three-line-break bars (reversal needs N-line break) (`THREE_LINE_BREAK_BARS`).
|
||||
- **Run** — Run bars (consecutive same-direction tick runs) (`RUN_BARS`).
|
||||
- **Imbalance** — Imbalance bars (tick-rule signed imbalance threshold) (`IMBALANCE_BARS`).
|
||||
- **Dollar** — Dollar bars (fixed traded value per bar, Lopez de Prado) (`DOLLAR_BARS`).
|
||||
- **Volume** — Volume bars (fixed traded volume per bar) (`VOLUME_BARS`).
|
||||
- **Tick** — Tick bars (fixed candle count per bar) (`TICK_BARS`).
|
||||
- **Range** — Range bars (fixed price-range bricks) (`RANGE_BARS`).
|
||||
|
||||
## [0.7.3] - 2026-06-08
|
||||
- **M2Measure** — M2 measure (Modigliani; Sharpe expressed in benchmark return units) (`M2Measure`).
|
||||
- **UpsidePotentialRatio** — Upside Potential Ratio (upside mean over downside deviation) (`UpsidePotentialRatio`).
|
||||
- **GainToPainRatio** — Gain-to-Pain Ratio (sum of returns over sum of losses) (`GainToPainRatio`).
|
||||
- **CommonSenseRatio** — Common Sense Ratio (tail ratio times gain-to-pain) (`CommonSenseRatio`).
|
||||
- **KRatio** — K-Ratio (Kestner; equity-curve slope over its standard error) (`KRatio`).
|
||||
- **TailRatio** — Tail Ratio (95th over absolute 5th return percentile) (`TailRatio`).
|
||||
- **MartinRatio** — Martin Ratio (Ulcer Performance Index; return over RMS drawdown) (`MartinRatio`).
|
||||
- **BurkeRatio** — Burke Ratio (return over root-sum-squared drawdowns) (`BurkeRatio`).
|
||||
- **SterlingRatio** — Sterling Ratio (mean return over average drawdown) (`SterlingRatio`).
|
||||
|
||||
## [0.7.2] - 2026-06-08
|
||||
- **Composite Profile** — multi-session composite volume profile exposing POC, VAH and VAL (`CompositeProfile`).
|
||||
- **High/Low Volume Nodes** — highest- and lowest-volume price nodes in the profile (`HighLowVolumeNodes`).
|
||||
@@ -1396,7 +1443,12 @@ 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.2...HEAD
|
||||
[Unreleased]: https://github.com/wickra-lib/wickra/compare/v0.7.7...HEAD
|
||||
[0.7.7]: https://github.com/wickra-lib/wickra/compare/v0.7.6...v0.7.7
|
||||
[0.7.6]: https://github.com/wickra-lib/wickra/compare/v0.7.5...v0.7.6
|
||||
[0.7.5]: https://github.com/wickra-lib/wickra/compare/v0.7.4...v0.7.5
|
||||
[0.7.4]: https://github.com/wickra-lib/wickra/compare/v0.7.3...v0.7.4
|
||||
[0.7.3]: https://github.com/wickra-lib/wickra/compare/v0.7.2...v0.7.3
|
||||
[0.7.2]: https://github.com/wickra-lib/wickra/compare/v0.7.1...v0.7.2
|
||||
[0.7.1]: https://github.com/wickra-lib/wickra/compare/v0.7.0...v0.7.1
|
||||
[0.7.0]: https://github.com/wickra-lib/wickra/compare/v0.6.9...v0.7.0
|
||||
|
||||
+9
-1
@@ -21,6 +21,9 @@ licensed as above, without any additional terms or conditions.
|
||||
| `bindings/python` | PyO3 bindings (`wickra` on PyPI). |
|
||||
| `bindings/node` | napi-rs bindings (`wickra` on npm). |
|
||||
| `bindings/wasm` | wasm-bindgen bindings (`wickra-wasm` on npm). |
|
||||
| `bindings/c` | C ABI — `cdylib` + `staticlib` + generated `include/wickra.h`. The hub for C / C++ and any C-capable language. |
|
||||
| `bindings/csharp` | .NET binding over the C ABI (`Wickra` on NuGet) — `[LibraryImport]` P/Invoke generated from `wickra.h`. |
|
||||
| `bindings/go` | Go binding over the C ABI via cgo (module tag `bindings/go/vX.Y.Z`) — wrappers generated from `wickra.h`. |
|
||||
| `examples/` | Runnable examples. |
|
||||
| `docs/` | Pointer to the documentation site (docs.wickra.org); the docs live in the `wickra-lib/wickra-docs` repo. |
|
||||
|
||||
@@ -102,7 +105,12 @@ installed. Dependabot also keeps the `.github/requirements` pins current.
|
||||
- **Streaming parity.** An indicator's `batch` output must equal the sequence
|
||||
of `update` calls.
|
||||
- **Bindings.** A change to a public indicator API must be mirrored across the
|
||||
Python, Node, and WASM bindings, including their type stubs / `.d.ts`.
|
||||
Python, Node, and WASM bindings, including their type stubs / `.d.ts`. The C ABI
|
||||
(`bindings/c`) is generated from the core, so regenerate it from the core and
|
||||
commit `src/lib.rs` + `include/wickra.h`. The C# binding (`bindings/csharp`) is
|
||||
generated from `wickra.h`, so regenerate and commit its `Generated/*.g.cs` too.
|
||||
The Go binding (`bindings/go`) is likewise generated from `wickra.h`, so
|
||||
regenerate and commit `indicators_gen.go` (`gofmt`-clean).
|
||||
- **Docs.** Update the relevant page on the
|
||||
[documentation site](https://docs.wickra.org) and the
|
||||
`README.md` when behaviour or the public API changes. The docs live in
|
||||
|
||||
Generated
+15
-8
@@ -1944,7 +1944,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wickra"
|
||||
version = "0.7.2"
|
||||
version = "0.7.7"
|
||||
dependencies = [
|
||||
"approx",
|
||||
"criterion",
|
||||
@@ -1955,7 +1955,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wickra-bench"
|
||||
version = "0.7.2"
|
||||
version = "0.7.7"
|
||||
dependencies = [
|
||||
"criterion",
|
||||
"kand",
|
||||
@@ -1965,9 +1965,16 @@ dependencies = [
|
||||
"yata",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wickra-c"
|
||||
version = "0.7.7"
|
||||
dependencies = [
|
||||
"wickra-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wickra-core"
|
||||
version = "0.7.2"
|
||||
version = "0.7.7"
|
||||
dependencies = [
|
||||
"approx",
|
||||
"proptest",
|
||||
@@ -1977,7 +1984,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wickra-data"
|
||||
version = "0.7.2"
|
||||
version = "0.7.7"
|
||||
dependencies = [
|
||||
"approx",
|
||||
"csv",
|
||||
@@ -1994,7 +2001,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wickra-examples"
|
||||
version = "0.7.2"
|
||||
version = "0.7.7"
|
||||
dependencies = [
|
||||
"serde_json",
|
||||
"tokio",
|
||||
@@ -2004,7 +2011,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wickra-node"
|
||||
version = "0.7.2"
|
||||
version = "0.7.7"
|
||||
dependencies = [
|
||||
"napi",
|
||||
"napi-build",
|
||||
@@ -2014,7 +2021,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wickra-python"
|
||||
version = "0.7.2"
|
||||
version = "0.7.7"
|
||||
dependencies = [
|
||||
"numpy",
|
||||
"pyo3",
|
||||
@@ -2023,7 +2030,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wickra-wasm"
|
||||
version = "0.7.2"
|
||||
version = "0.7.7"
|
||||
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.2"
|
||||
version = "0.7.7"
|
||||
authors = ["kingchenc <support@wickra.org>"]
|
||||
edition = "2021"
|
||||
rust-version = "1.86"
|
||||
@@ -25,7 +26,7 @@ keywords = ["finance", "trading", "indicators", "technical-analysis", "ta"]
|
||||
categories = ["finance", "mathematics", "science"]
|
||||
|
||||
[workspace.dependencies]
|
||||
wickra-core = { path = "crates/wickra-core", version = "0.7.2" }
|
||||
wickra-core = { path = "crates/wickra-core", version = "0.7.7" }
|
||||
|
||||
thiserror = "2"
|
||||
rayon = "1.10"
|
||||
|
||||
@@ -1,25 +1,27 @@
|
||||
<p align="center">
|
||||
<a href="https://wickra.org"><img src="https://raw.githubusercontent.com/wickra-lib/.github/main/profile/wickra-banner.webp?v=498" alt="Wickra — streaming-first technical indicators" width="100%"></a>
|
||||
<a href="https://wickra.org"><img src="https://raw.githubusercontent.com/wickra-lib/.github/main/profile/wickra-banner.webp?v=514" alt="Wickra — streaming-first technical indicators" width="100%"></a>
|
||||
</p>
|
||||
|
||||
[](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, Go and any other C-capable language links against. Every indicator is a
|
||||
state machine that updates in O(1) per new data point, so live trading bots and
|
||||
historical backtests share the exact same implementation.
|
||||
|
||||
```python
|
||||
@@ -46,9 +48,12 @@ Full documentation lives at **[docs.wickra.org](https://docs.wickra.org)**:
|
||||
- **Quickstarts** — [Rust](https://docs.wickra.org/Quickstart-Rust),
|
||||
[Python](https://docs.wickra.org/Quickstart-Python),
|
||||
[Node](https://docs.wickra.org/Quickstart-Node),
|
||||
[WASM](https://docs.wickra.org/Quickstart-WASM).
|
||||
[WASM](https://docs.wickra.org/Quickstart-WASM),
|
||||
[C](https://docs.wickra.org/Quickstart-C),
|
||||
[C#](https://docs.wickra.org/Quickstart-CSharp),
|
||||
[Go](https://docs.wickra.org/Quickstart-Go).
|
||||
- **Indicators** — a per-indicator deep dive (formula, parameters, warmup) for
|
||||
every one of the 498 indicators; start at the
|
||||
every one of the 514 indicators; start at the
|
||||
[indicators overview](https://docs.wickra.org/Indicators-Overview).
|
||||
- **Reference** — [warmup periods](https://docs.wickra.org/Warmup-Periods),
|
||||
[streaming vs batch](https://docs.wickra.org/Streaming-vs-Batch),
|
||||
@@ -66,18 +71,19 @@ an afterthought — **live, tick-by-tick data** — without giving up the breadt
|
||||
a full batch library, and without making you reimplement your indicators four
|
||||
times to get there.
|
||||
|
||||
- **The biggest streaming-native catalogue, period.** 498 indicators across 24
|
||||
- **The biggest streaming-native catalogue, period.** 514 indicators across 24
|
||||
families — candlesticks, harmonic & chart patterns, market profile, market
|
||||
breadth, Renko/Kagi/Point&Figure bars, Ehlers DSP cycles, risk/performance
|
||||
metrics — every single one updating in **O(1) per tick**. TA-Lib ships ~150 and
|
||||
none of them stream.
|
||||
- **One Rust core, four first-class targets.** Native **Python · Node.js ·
|
||||
WebAssembly · Rust** — identical math, identical results, zero per-language
|
||||
reimplementation and zero GIL bottleneck.
|
||||
- **One Rust core, five first-class targets.** Native **Python · Node.js ·
|
||||
WebAssembly · Rust** plus a **C ABI** for C / C++, C# / .NET, Go and any other C-capable language —
|
||||
identical math, identical results, zero per-language reimplementation and zero
|
||||
GIL bottleneck.
|
||||
- **Correct by construction, not by hope.** Every `update` validates its input,
|
||||
runs a real warmup, and returns an `Option` so a single bad tick can't silently
|
||||
poison state. `batch == streaming` is **bit-exact, fuzzed and 100 %-line-covered
|
||||
for all 498 indicators**.
|
||||
for all 514 indicators**.
|
||||
- **Orders of magnitude faster where it counts.** In streaming Wickra is **11–56×**
|
||||
faster than the only other incremental peer and **thousands of times** faster
|
||||
than recompute-on-every-tick libraries. On batch it wins several rows outright
|
||||
@@ -95,7 +101,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** | **498** | **yes** |
|
||||
| **★ Wickra**| **clean** | **yes, O(1)** | **Rust · Python · Node · WASM** | **514** | **yes** |
|
||||
| | | | **C · C# · Go** | | |
|
||||
| kand | clean | yes | Python · WASM · Rust | ~60 | yes |
|
||||
| ta-rs | clean | yes | Rust only | ~30 | stale |
|
||||
| yata | clean | partial | Rust only | ~35 | yes |
|
||||
@@ -128,7 +135,7 @@ Full tables (Rust + Python, streaming + batch) and how to reproduce them live in
|
||||
|
||||
## Indicators
|
||||
|
||||
498 streaming-first indicators across twenty-four families. Every one passes the
|
||||
514 streaming-first indicators across twenty-four families. Every one passes the
|
||||
`batch == streaming` equivalence test, reference-value tests, and reset
|
||||
semantics tests. Each has a per-indicator deep dive (formula, parameters,
|
||||
warmup) at [docs.wickra.org](https://docs.wickra.org/Indicators-Overview).
|
||||
@@ -148,7 +155,7 @@ warmup) at [docs.wickra.org](https://docs.wickra.org/Indicators-Overview).
|
||||
| Pivots & S/R | Classic Pivots, Fibonacci Pivots, Camarilla, Woodie Pivots, DeMark Pivots, Williams Fractals, ZigZag, Central Pivot Range, Murrey Math Lines, Andrews Pitchfork, Volume-Weighted Support/Resistance, Pivot Reversal |
|
||||
| DeMark | TD Setup, TD Sequential, TD DeMarker, TD REI, TD Pressure, TD Combo, TD Countdown, TD Lines, TD Range Projection, TD Differential, TD Open, TD Risk Level, TD Camouflage, TD Clop, TD Clopwin, TD Propulsion, TD Trap, TD D-Wave, TD Moving Averages |
|
||||
| Ichimoku & Charts | Ichimoku Kinko Hyo (Tenkan, Kijun, Senkou A/B, Chikou), Heikin-Ashi, Heikin-Ashi Oscillator, Three Line Break, Smoothed Heikin-Ashi, Equivolume, CandleVolume |
|
||||
| Alt-Chart Bars | Renko (box-size bricks), Kagi (reversal-amount lines), Point & Figure (X/O columns) |
|
||||
| Alt-Chart Bars | Renko (box-size bricks), Kagi (reversal-amount lines), Point & Figure (X/O columns), Range, Tick, Volume, Dollar, Imbalance, Run, Three-Line Break |
|
||||
| Candlestick Patterns | Doji, Hammer, Inverted Hammer, Hanging Man, Shooting Star, Engulfing, Harami, Morning/Evening Star, Three White Soldiers/Black Crows, Piercing Line/Dark Cloud Cover, Marubozu, Tweezer, Spinning Top, Three Inside Up/Down, Three Outside Up/Down, Two Crows, Upside Gap Two Crows, Identical Three Crows, Three Line Strike, Three Stars in the South, Abandoned Baby, Advance Block, Belt-hold, Breakaway, Counterattack, Doji Star, Dragonfly Doji, Gravestone Doji, Long-Legged Doji, Rickshaw Man, Evening Doji Star, Morning Doji Star, Gap Side-by-Side White, High-Wave, Hikkake, Modified Hikkake, Homing Pigeon, On-Neck, In-Neck, Thrusting, Separating Lines, Kicking, Kicking by Length, Ladder Bottom, Mat Hold, Matching Low, Long Line, Short Line, Rising Three Methods, Falling Three Methods, Upside Gap Three Methods, Downside Gap Three Methods, Stalled Pattern, Stick Sandwich, Takuri, Closing Marubozu, Opening Marubozu, Tasuki Gap, Unique Three River, Concealing Baby Swallow, Tristar, Harami Cross, Tower Top/Bottom, Dumpling Top, New Price Lines, Frying Pan Bottom |
|
||||
| Chart Patterns | Double Top / Bottom, Triple Top / Bottom, Head and Shoulders, Triangle (asc/desc/sym), Wedge (rising/falling), Flag / Pennant, Rectangle / Range, Cup and Handle |
|
||||
| Harmonic Patterns | AB=CD, Gartley, Butterfly, Bat, Crab, Shark, Cypher, Three Drives |
|
||||
@@ -166,8 +173,9 @@ as one column each. `Doji` is direction-less by default (`+1.0` / `0.0`);
|
||||
construct it in signed mode (`Doji::new().signed()`, `Doji(signed=True)`,
|
||||
`new Doji(true)`) for a dragonfly / gravestone `±1` reading.
|
||||
|
||||
Adding a new indicator means implementing one trait in Rust; all four bindings
|
||||
inherit it automatically.
|
||||
Adding a new indicator means implementing one trait in Rust; every binding
|
||||
inherits it automatically (the C ABI — and the C# and Go bindings generated from
|
||||
it — regenerate from the core).
|
||||
|
||||
## Languages
|
||||
|
||||
@@ -177,12 +185,16 @@ inherit it automatically.
|
||||
| Node.js (napi-rs) | `npm install wickra` | `examples/node/backtest.js` |
|
||||
| Browser / WASM | `npm install wickra-wasm` | `examples/wasm/index.html` |
|
||||
| Rust | `cargo add wickra` | `examples/rust/src/bin/backtest.rs` |
|
||||
| C / C++ (C ABI) | header + library, see [`bindings/c`](bindings/c) | `examples/c/streaming.c` |
|
||||
| C# / .NET (C ABI) | `dotnet add package Wickra`, see [`bindings/csharp`](bindings/csharp) | `examples/csharp/streaming` |
|
||||
| Go (cgo, C ABI) | `go get github.com/wickra-lib/wickra/bindings/go`, see [`bindings/go`](bindings/go) | `examples/go/streaming` |
|
||||
|
||||
Each binding ships several runnable examples (streaming, backtest, live feed);
|
||||
[`examples/README.md`](examples/README.md) is the full cross-language index.
|
||||
|
||||
The wickra-core crate is `unsafe`-forbidden, so every binding inherits a
|
||||
memory-safe implementation.
|
||||
The wickra-core crate is `unsafe`-forbidden, so the native bindings are
|
||||
memory-safe end to end. The C ABI runs the same safe core; only its thin FFI
|
||||
boundary uses `unsafe`, and the caller owns handle lifetimes (`_new` / `_free`).
|
||||
|
||||
## Rust API
|
||||
|
||||
@@ -237,20 +249,26 @@ A Python live-trading example using the public `websockets` package lives at
|
||||
```
|
||||
wickra/
|
||||
├── crates/
|
||||
│ ├── wickra-core/ core engine + all 498 indicators
|
||||
│ ├── wickra-core/ core engine + all 514 indicators
|
||||
│ ├── wickra/ top-level facade crate (publishes on crates.io) + benches/
|
||||
│ ├── wickra-data/ CSV reader, tick aggregator, live exchange feeds
|
||||
│ └── wickra-bench/ internal cross-library benchmark harness (not published)
|
||||
├── bindings/
|
||||
│ ├── python/ PyO3 + maturin (publishes on PyPI)
|
||||
│ ├── node/ napi-rs (publishes on npm)
|
||||
│ └── wasm/ wasm-bindgen (browsers, bundlers, Node)
|
||||
│ ├── wasm/ wasm-bindgen (browsers, bundlers, Node)
|
||||
│ ├── c/ C ABI (cdylib + staticlib) + generated include/wickra.h
|
||||
│ ├── csharp/ .NET binding over the C ABI (publishes on NuGet)
|
||||
│ └── go/ Go binding over the C ABI via cgo (module tag)
|
||||
├── examples/ examples/README.md indexes every language
|
||||
│ ├── data/ real BTCUSDT OHLCV datasets, one per timeframe
|
||||
│ ├── rust/ Rust workspace member (`wickra-examples`)
|
||||
│ ├── python/ backtest, live trading, parallel assets, multi-tf
|
||||
│ ├── node/ streaming, backtest, live trading (load `wickra`)
|
||||
│ └── wasm/ browser demo for `wickra-wasm`
|
||||
│ ├── wasm/ browser demo for `wickra-wasm`
|
||||
│ ├── c/ C smoke + streaming, C++ RAII wrapper
|
||||
│ ├── csharp/ streaming, backtest, strategies (load `Wickra`)
|
||||
│ └── go/ streaming, backtest, strategies (cgo binding)
|
||||
└── .github/workflows/ CI and release pipelines
|
||||
```
|
||||
|
||||
@@ -278,6 +296,18 @@ wasm-pack build bindings/wasm --target web --release --features panic-hook
|
||||
|
||||
# Node binding (requires @napi-rs/cli)
|
||||
cd bindings/node && npm install && npm run build && npm test
|
||||
|
||||
# C ABI (cdylib + staticlib + generated header)
|
||||
cargo build -p wickra-c --release
|
||||
cmake -S examples/c -B examples/c/build -DWICKRA_LIB_DIR="$PWD/target/release"
|
||||
cmake --build examples/c/build && ctest --test-dir examples/c/build --output-on-failure
|
||||
|
||||
# C# / .NET binding (requires the .NET 8 SDK; links the C ABI above)
|
||||
dotnet test bindings/csharp/Wickra.Tests/Wickra.Tests.csproj
|
||||
|
||||
# Go binding (requires a C compiler for cgo; links the C ABI above)
|
||||
cp target/release/libwickra.so bindings/go/lib/ # .dylib on macOS, wickra.dll on Windows
|
||||
cd bindings/go && go test ./...
|
||||
```
|
||||
|
||||
## Testing
|
||||
@@ -297,6 +327,8 @@ Every layer is covered; run the suites with the commands in
|
||||
values across all indicators.
|
||||
- `bindings/wasm`: `wasm-bindgen-test` cases for constructors, equivalence,
|
||||
and reference values.
|
||||
- `bindings/go`: `go test` cases covering one indicator per FFI archetype
|
||||
(scalar/batch, multi-output, bars, profile, array input), reset, and lifecycle.
|
||||
|
||||
## Contributing
|
||||
|
||||
|
||||
+3
-2
@@ -21,8 +21,9 @@ minor releases; breaking changes are called out in the changelog.
|
||||
versioning stability for a 1.0 release.
|
||||
- **Performance.** Keep per-tick updates O(1) and maintain the benchmark suite;
|
||||
investigate further allocation and cache improvements.
|
||||
- **Bindings parity.** Keep the Python, Node.js and WebAssembly bindings in
|
||||
lockstep with the Rust core, including type stubs and platform coverage.
|
||||
- **Bindings parity.** Keep the Python, Node.js and WebAssembly bindings — plus
|
||||
the C ABI and the C# / .NET and Go bindings generated from it — in lockstep with the
|
||||
Rust core, including type stubs and platform coverage.
|
||||
- **Documentation.** Maintain a deep-dive page per indicator on
|
||||
<https://docs.wickra.org>, plus quickstarts and cookbook material.
|
||||
- **Project health.** Maintain test coverage, static and dynamic analysis,
|
||||
|
||||
+5
-1
@@ -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, C# and Go, a per-indicator reference, warmup periods, the
|
||||
data layer, and an FAQ.
|
||||
- **README:** <https://github.com/wickra-lib/wickra#readme> — installation and a
|
||||
quick overview.
|
||||
- **API docs (Rust):** <https://docs.rs/wickra>.
|
||||
|
||||
+5
-2
@@ -3,8 +3,10 @@
|
||||
This document describes Wickra's attack surface and the threats considered,
|
||||
together with their mitigations. It complements the security assurance case in
|
||||
[`SECURITY.md`](SECURITY.md). Wickra is a computational technical-analysis
|
||||
library (a Rust core with Python, Node.js and WebAssembly bindings), not a
|
||||
network service or trading system; the attack surface is correspondingly small.
|
||||
library (a Rust core with Python, Node.js and WebAssembly bindings plus a C ABI
|
||||
and the .NET and Go bindings built on it),
|
||||
not a network service or trading system; the attack surface is correspondingly
|
||||
small.
|
||||
|
||||
## Assets
|
||||
|
||||
@@ -31,6 +33,7 @@ network service or trading system; the attack surface is correspondingly small.
|
||||
| Threat | Mitigation |
|
||||
| --- | --- |
|
||||
| Memory-safety exploit (buffer overflow, UAF) via crafted input | Pure safe Rust; `unsafe` is forbidden/minimised, so the compiler precludes these classes. |
|
||||
| Misuse of the C ABI FFI boundary (invalid/dangling handle, undersized batch buffer) | The C ABI (`bindings/c`) is the sole `unsafe` surface. Its shim adds no logic, NULL-checks every handle (returning `NaN`/no-op), writes only into caller-sized buffers, and catches panics so none cross the boundary. A caller passing a non-NULL but dangling pointer is undefined behaviour by C's own contract — out of scope, the same as any C library. |
|
||||
| Denial of service via malformed/degenerate input (NaN, infinities, extreme magnitudes) | Indicators reject non-finite inputs and validate parameters at construction; update paths are exercised by coverage-guided fuzzing and unit tests for edge cases. |
|
||||
| Silently incorrect results | 100% line coverage on the core crate; reference-value tests against known-good sources; streaming/batch parity tests. |
|
||||
| Integer overflow / panics | `clippy::pedantic` with `-D warnings`; debug assertions and overflow checks enabled in test/fuzz builds. |
|
||||
|
||||
@@ -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#, Go and any
|
||||
other C-capable language. Every indicator is an O(1)
|
||||
streaming state machine, so live trading bots and historical backtests share
|
||||
the exact same implementation. This package is the **C ABI hub**: it compiles the
|
||||
core to a C-compatible shared/static library plus a generated header, so any
|
||||
C-capable language (C, C++, Go, C#, Java, R) links against one artifact instead
|
||||
of re-wrapping every indicator natively.
|
||||
|
||||
## Install
|
||||
|
||||
Grab the prebuilt header + library for your platform from the
|
||||
[GitHub releases](https://github.com/wickra-lib/wickra/releases) — each archive
|
||||
has `wickra.h`, the optional `wickra.hpp` C++ wrapper, and the shared/static
|
||||
library — or build from source:
|
||||
|
||||
```bash
|
||||
cargo build -p wickra-c --release
|
||||
# -> target/release/libwickra.{so,dylib} or wickra.dll (+ import lib) + a staticlib
|
||||
```
|
||||
|
||||
Then compile against the header and link the library
|
||||
(`cc app.c -I include -L lib -lwickra -lm -o app`).
|
||||
|
||||
## Quick start
|
||||
|
||||
```c
|
||||
#include "wickra.h"
|
||||
|
||||
struct Rsi *rsi = wickra_rsi_new(14); /* NULL on invalid params */
|
||||
for (size_t i = 0; i < n; ++i) {
|
||||
double v = wickra_rsi_update(rsi, prices[i]); /* NaN during warmup */
|
||||
if (v == v && v > 70.0) /* v == v is the NaN check */
|
||||
printf("overbought\n");
|
||||
}
|
||||
wickra_rsi_free(rsi); /* exactly once per _new */
|
||||
```
|
||||
|
||||
Every indicator is an opaque handle with the same five functions —
|
||||
`_new` / `_update` / `_batch` / `_reset` / `_free`. `update` is O(1); there is no
|
||||
RAII across the C boundary, so each `_new` needs exactly one `_free`, and every
|
||||
function is NULL-safe (a NULL handle yields `NaN` or a no-op, never a crash).
|
||||
Multi-output indicators (MACD, Bollinger, ADX, …) take a pointer to a `#[repr(C)]`
|
||||
struct and return a `bool`. The optional `wickra.hpp` wraps any handle in a
|
||||
move-only `wickra::Handle` for exception-safe C++ lifetimes.
|
||||
|
||||
## Documentation
|
||||
|
||||
The full indicator catalogue, guides, quickstarts, and API reference live in
|
||||
the main repository and documentation site:
|
||||
|
||||
- **Repository & full indicator list:** <https://github.com/wickra-lib/wickra>
|
||||
- **Docs** (C quickstart, cookbook, TA-Lib migration): <https://docs.wickra.org/Quickstart-C>
|
||||
- **Runnable examples:** [`examples/c/`](https://github.com/wickra-lib/wickra/tree/main/examples/c)
|
||||
|
||||
Wickra ships native bindings for Python, Node.js, WebAssembly and Rust, plus this
|
||||
C ABI hub that any C-capable language (C, C++, Go, C#, Java, R) links against —
|
||||
all exposing the same indicators from the shared Rust core.
|
||||
|
||||
## Disclaimer
|
||||
|
||||
Wickra is an indicator toolkit, not a trading system. The values it computes
|
||||
are deterministic transforms of the input data — they are not financial advice
|
||||
and do not predict the market. Any use in a live trading context is at your own
|
||||
risk. The library is provided **as is**, without warranty of any kind.
|
||||
|
||||
## License
|
||||
|
||||
Licensed under either of [Apache-2.0](https://github.com/wickra-lib/wickra/blob/main/LICENSE-APACHE)
|
||||
or [MIT](https://github.com/wickra-lib/wickra/blob/main/LICENSE-MIT) at your option.
|
||||
@@ -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#, Go and any
|
||||
other C-capable language. Every indicator is an O(1)
|
||||
streaming state machine, so live trading bots and historical backtests share
|
||||
the exact same implementation. This package is the .NET binding; it consumes the
|
||||
C ABI hub through `[LibraryImport]` P/Invoke and exposes all 514 streaming-first
|
||||
indicators as idiomatic `IDisposable` classes.
|
||||
|
||||
## Install
|
||||
|
||||
```bash
|
||||
dotnet add package Wickra
|
||||
```
|
||||
|
||||
The native library ships prebuilt per platform (Linux, macOS, Windows — x64 and
|
||||
arm64) under `runtimes/<rid>/native/`, selected automatically. There is nothing
|
||||
to compile. Targets .NET 8 and later.
|
||||
|
||||
## Quick start
|
||||
|
||||
```csharp
|
||||
using Wickra;
|
||||
|
||||
// Batch: run an indicator over a whole series (NaN at warmup positions).
|
||||
var prices = Enumerable.Range(0, 1000).Select(i => 100.0 + i * 0.1).ToArray();
|
||||
using var sma = new Sma(20);
|
||||
double[] values = sma.Batch(prices);
|
||||
|
||||
// Streaming: the same indicator, fed tick by tick in O(1).
|
||||
using var rsi = new Rsi(14);
|
||||
foreach (var price in liveFeed)
|
||||
{
|
||||
var value = rsi.Update(price); // NaN during warmup, no recomputation
|
||||
if (double.IsFinite(value) && value > 70)
|
||||
{
|
||||
Console.WriteLine("overbought");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`Batch(prices)` and feeding the same prices through `Update()` produce identical
|
||||
values — the equivalence is enforced by the test suite. Multi-output indicators
|
||||
(MACD, Bollinger, ADX, …) return a nullable `record struct`, `null` while warming up.
|
||||
|
||||
## Documentation
|
||||
|
||||
The full indicator catalogue, guides, quickstarts, and API reference live in
|
||||
the main repository and documentation site:
|
||||
|
||||
- **Repository & full indicator list:** <https://github.com/wickra-lib/wickra>
|
||||
- **Docs** (quickstarts, cookbook, TA-Lib migration): <https://docs.wickra.org>
|
||||
- **Runnable examples:** [`examples/csharp/`](https://github.com/wickra-lib/wickra/tree/main/examples/csharp)
|
||||
|
||||
Wickra ships native bindings for Python, Node.js, WebAssembly and Rust, plus a
|
||||
C ABI hub that any C-capable language (C, C++, Go, C#, Java, R) links against —
|
||||
all exposing the same indicators from the shared, `unsafe`-forbidden Rust core.
|
||||
|
||||
## Disclaimer
|
||||
|
||||
Wickra is an indicator toolkit, not a trading system. The values it computes
|
||||
are deterministic transforms of the input data — they are not financial advice
|
||||
and do not predict the market. Any use in a live trading context is at your own
|
||||
risk. The library is provided **as is**, without warranty of any kind.
|
||||
|
||||
## License
|
||||
|
||||
Licensed under either of [Apache-2.0](https://github.com/wickra-lib/wickra/blob/main/LICENSE-APACHE)
|
||||
or [MIT](https://github.com/wickra-lib/wickra/blob/main/LICENSE-MIT) at your option.
|
||||
@@ -0,0 +1,144 @@
|
||||
using Wickra;
|
||||
using Xunit;
|
||||
|
||||
namespace Wickra.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// One representative per FFI archetype, exercising every marshalling path the
|
||||
/// generator produces (scalar, candle, pairwise, multi-output, bars, profile,
|
||||
/// values-profile, array-input). Garbage marshalling surfaces as NaN, wild
|
||||
/// values, or crashes — so finite/sane assertions are the real check.
|
||||
/// </summary>
|
||||
public class ArchetypeTests
|
||||
{
|
||||
private static (double open, double high, double low, double close, double volume, long ts) Candle(int i)
|
||||
{
|
||||
var close = 100.0 + 10.0 * Math.Sin(i * 0.3);
|
||||
var open = 100.0 + 10.0 * Math.Sin((i - 1) * 0.3);
|
||||
var high = Math.Max(open, close) + 1.0;
|
||||
var low = Math.Min(open, close) - 1.0;
|
||||
return (open, high, low, close, 1_000.0, i * 60_000L);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Scalar_Ema_IsFiniteAfterWarmup()
|
||||
{
|
||||
using var ema = new Ema(3);
|
||||
double last = double.NaN;
|
||||
for (var i = 1; i <= 10; i++)
|
||||
{
|
||||
last = ema.Update(i);
|
||||
}
|
||||
|
||||
Assert.True(double.IsFinite(last));
|
||||
Assert.InRange(last, 1.0, 10.0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Candle_Atr_IsFinitePositive()
|
||||
{
|
||||
using var atr = new Atr(3);
|
||||
double last = double.NaN;
|
||||
for (var i = 0; i < 20; i++)
|
||||
{
|
||||
var (o, h, l, c, v, ts) = Candle(i);
|
||||
last = atr.Update(o, h, l, c, v, ts);
|
||||
}
|
||||
|
||||
Assert.True(double.IsFinite(last));
|
||||
Assert.True(last > 0.0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Pairwise_Beta_IsFinite()
|
||||
{
|
||||
using var beta = new Beta(5);
|
||||
double last = double.NaN;
|
||||
for (var i = 0; i < 30; i++)
|
||||
{
|
||||
var market = 100.0 + 10.0 * Math.Sin(i * 0.5);
|
||||
var asset = 50.0 + 6.0 * Math.Sin(i * 0.5 + 0.2);
|
||||
last = beta.Update(market, asset);
|
||||
}
|
||||
|
||||
Assert.True(double.IsFinite(last));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MultiOutput_Adx_ReturnsFiniteStruct()
|
||||
{
|
||||
using var adx = new Adx(5);
|
||||
AdxOutput? result = null;
|
||||
for (var i = 0; i < 60; i++)
|
||||
{
|
||||
var (o, h, l, c, v, ts) = Candle(i);
|
||||
result = adx.Update(o, h, l, c, v, ts);
|
||||
}
|
||||
|
||||
Assert.NotNull(result);
|
||||
Assert.True(double.IsFinite(result!.Value.Adx));
|
||||
Assert.True(double.IsFinite(result.Value.PlusDi));
|
||||
Assert.True(double.IsFinite(result.Value.MinusDi));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Bars_DollarBars_EmitsBars()
|
||||
{
|
||||
using var bars = new DollarBars(5_000.0);
|
||||
var total = 0;
|
||||
for (var i = 0; i < 200; i++)
|
||||
{
|
||||
var (o, h, l, c, v, ts) = Candle(i);
|
||||
total += bars.Update(o, h, l, c, v, ts).Length;
|
||||
}
|
||||
|
||||
Assert.True(total > 0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Profile_VolumeProfile_ReturnsValues()
|
||||
{
|
||||
using var profile = new VolumeProfile(20, 8);
|
||||
VolumeProfileOutputScalars? result = null;
|
||||
for (var i = 0; i < 60; i++)
|
||||
{
|
||||
var (o, h, l, c, v, ts) = Candle(i);
|
||||
result = profile.Update(o, h, l, c, v, ts);
|
||||
}
|
||||
|
||||
Assert.NotNull(result);
|
||||
Assert.NotNull(result!.Value.Values);
|
||||
Assert.True(result.Value.PriceLow <= result.Value.PriceHigh);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ProfileValues_DayOfWeekProfile_NoCrash()
|
||||
{
|
||||
using var profile = new DayOfWeekProfile(0);
|
||||
double[]? result = null;
|
||||
for (var i = 0; i < 60; i++)
|
||||
{
|
||||
var close = 100.0 + 5.0 * Math.Sin(i * 0.2);
|
||||
// one day apart so the day-of-week buckets fill
|
||||
result = profile.Update(close, close + 1, close - 1, close, 1_000.0, i * 86_400_000L);
|
||||
}
|
||||
|
||||
if (result is not null)
|
||||
{
|
||||
Assert.All(result, v => Assert.True(double.IsFinite(v)));
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ArrayInput_DepthSlope_IsFinite()
|
||||
{
|
||||
using var slope = new DepthSlope();
|
||||
ReadOnlySpan<double> bidPrice = stackalloc double[] { 99.0, 98.0, 97.0 };
|
||||
ReadOnlySpan<double> bidSize = stackalloc double[] { 10.0, 20.0, 30.0 };
|
||||
ReadOnlySpan<double> askPrice = stackalloc double[] { 101.0, 102.0, 103.0 };
|
||||
ReadOnlySpan<double> askSize = stackalloc double[] { 12.0, 22.0, 32.0 };
|
||||
|
||||
var result = slope.Update(bidPrice, bidSize, askPrice, askSize);
|
||||
Assert.True(double.IsFinite(result));
|
||||
}
|
||||
}
|
||||
@@ -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.7</Version>
|
||||
<Authors>kingchenc</Authors>
|
||||
<Description>High-performance streaming technical-analysis indicators (514 indicators) for .NET, backed by the native Rust core via the Wickra C ABI.</Description>
|
||||
<PackageLicenseExpression>MIT OR Apache-2.0</PackageLicenseExpression>
|
||||
<PackageProjectUrl>https://github.com/wickra-lib/wickra</PackageProjectUrl>
|
||||
<RepositoryUrl>https://github.com/wickra-lib/wickra</RepositoryUrl>
|
||||
<RepositoryType>git</RepositoryType>
|
||||
<PackageTags>technical-analysis;indicators;trading;finance;streaming;ffi;native</PackageTags>
|
||||
<PackageReadmeFile>README.md</PackageReadmeFile>
|
||||
<IncludeSymbols>true</IncludeSymbols>
|
||||
<SymbolPackageFormat>snupkg</SymbolPackageFormat>
|
||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||
<!-- A managed package carrying per-RID native assets; not built per-RID itself. -->
|
||||
<IncludeBuildOutput>true</IncludeBuildOutput>
|
||||
<!-- NU5128: managed package carrying only per-RID native assets.
|
||||
CS1591: generated members are self-descriptive; hand-written API is documented. -->
|
||||
<NoWarn>$(NoWarn);NU5128;CS1591</NoWarn>
|
||||
</PropertyGroup>
|
||||
|
||||
<!--
|
||||
Supported native runtime identifiers. The release pipeline builds the C ABI
|
||||
per target triple and stages the libraries under
|
||||
Wickra/runtimes/<rid>/native/ before `dotnet pack`:
|
||||
win-x64 win-arm64 linux-x64 linux-arm64 osx-x64 osx-arm64
|
||||
-->
|
||||
<PropertyGroup>
|
||||
<WickraRuntimeIdentifiers>win-x64;win-arm64;linux-x64;linux-arm64;osx-x64;osx-arm64</WickraRuntimeIdentifiers>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Include="..\README.md" Pack="true" PackagePath="\" />
|
||||
</ItemGroup>
|
||||
|
||||
<!--
|
||||
Native libraries are packed under runtimes/<rid>/native/ by the release pipeline,
|
||||
which unpacks the wickra-c-<triple>.tar.gz assets into the matching RID folders.
|
||||
For local development and tests the natives are resolved from the cargo target dir
|
||||
via WickraNative's DllImportResolver.
|
||||
-->
|
||||
<ItemGroup>
|
||||
<None Include="runtimes/**/native/*" Pack="true" PackagePath="runtimes" Condition="Exists('runtimes')" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -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";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
# Wickra — Go
|
||||
|
||||
[](https://github.com/wickra-lib/wickra/actions/workflows/ci.yml)
|
||||
[](https://codecov.io/gh/wickra-lib/wickra)
|
||||
[](https://pkg.go.dev/github.com/wickra-lib/wickra/bindings/go)
|
||||
[](https://github.com/wickra-lib/wickra#license)
|
||||
|
||||
**Streaming-first technical indicators for Go, over the Wickra C ABI hub via cgo.**
|
||||
|
||||
Wickra is a multi-language technical-analysis library with a Rust core and
|
||||
bindings for Python, Node.js and WebAssembly, plus a C ABI for C/C++, C#, Go and
|
||||
any other C-capable language. Every indicator is an O(1) streaming state machine,
|
||||
so live trading bots and historical backtests share the exact same
|
||||
implementation. This package is the Go binding; it consumes the C ABI hub through
|
||||
cgo and exposes all 514 streaming-first indicators as idiomatic types.
|
||||
|
||||
## Install
|
||||
|
||||
```bash
|
||||
go get github.com/wickra-lib/wickra/bindings/go
|
||||
```
|
||||
|
||||
The binding uses cgo, so a C compiler is required, and it links against the
|
||||
prebuilt Wickra C ABI library. Build that library from the workspace and stage
|
||||
it under this package's `lib/` directory:
|
||||
|
||||
```bash
|
||||
cargo build -p wickra-c --release
|
||||
cp target/release/libwickra.so bindings/go/lib/ # Linux
|
||||
cp target/release/libwickra.dylib bindings/go/lib/ # macOS
|
||||
cp target/release/wickra.dll bindings/go/lib/ # Windows (also on PATH at run time)
|
||||
```
|
||||
|
||||
On Linux and macOS the library path is baked in via rpath; on Windows the DLL
|
||||
must be discoverable at run time (next to the executable or on `PATH`).
|
||||
|
||||
## Quick start
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
wickra "github.com/wickra-lib/wickra/bindings/go"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// Batch: run an indicator over a whole series (NaN at warmup positions).
|
||||
prices := make([]float64, 1000)
|
||||
for i := range prices {
|
||||
prices[i] = 100.0 + float64(i)*0.1
|
||||
}
|
||||
sma, _ := wickra.NewSma(20)
|
||||
defer sma.Close()
|
||||
values := sma.Batch(prices)
|
||||
|
||||
// Streaming: the same indicator, fed tick by tick in O(1).
|
||||
rsi, _ := wickra.NewRsi(14)
|
||||
defer rsi.Close()
|
||||
for _, price := range prices {
|
||||
value := rsi.Update(price) // NaN during warmup, no recomputation
|
||||
if value > 70 {
|
||||
fmt.Println("overbought")
|
||||
}
|
||||
}
|
||||
_ = values
|
||||
}
|
||||
```
|
||||
|
||||
`Batch(prices)` and feeding the same prices through `Update()` produce identical
|
||||
values — the equivalence is enforced by the test suite. Multi-output indicators
|
||||
(MACD, Bollinger, ADX, …) return `(Output, bool)`, with `false` while warming up.
|
||||
Every indicator owns a native handle freed by `Close()`; a finalizer is wired as
|
||||
a backstop, but call `Close()` (e.g. with `defer`) to release memory promptly.
|
||||
|
||||
## Documentation
|
||||
|
||||
The full indicator catalogue, guides, quickstarts, and API reference live in the
|
||||
main repository and documentation site:
|
||||
|
||||
- **Repository & full indicator list:** <https://github.com/wickra-lib/wickra>
|
||||
- **Docs** (quickstarts, cookbook, TA-Lib migration): <https://docs.wickra.org>
|
||||
- **Runnable examples:** [`examples/go/`](https://github.com/wickra-lib/wickra/tree/main/examples/go)
|
||||
|
||||
Wickra ships native bindings for Python, Node.js, WebAssembly and Rust, plus a
|
||||
C ABI hub that any C-capable language (C, C++, C#, Go, Java, R) links against —
|
||||
all exposing the same indicators from the shared, `unsafe`-forbidden Rust core.
|
||||
|
||||
## Disclaimer
|
||||
|
||||
Wickra is an indicator toolkit, not a trading system. The values it computes are
|
||||
deterministic transforms of the input data — they are not financial advice and
|
||||
do not predict the market. Any use in a live trading context is at your own risk.
|
||||
The library is provided **as is**, without warranty of any kind.
|
||||
|
||||
## License
|
||||
|
||||
Licensed under either of [Apache-2.0](https://github.com/wickra-lib/wickra/blob/main/LICENSE-APACHE)
|
||||
or [MIT](https://github.com/wickra-lib/wickra/blob/main/LICENSE-MIT) at your option.
|
||||
@@ -0,0 +1,3 @@
|
||||
module github.com/wickra-lib/wickra/bindings/go
|
||||
|
||||
go 1.23
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,7 @@
|
||||
# Prebuilt Wickra C ABI libraries are provisioned locally / in CI, not committed.
|
||||
*.so
|
||||
*.dylib
|
||||
*.dll
|
||||
*.a
|
||||
*.lib
|
||||
*.exp
|
||||
@@ -0,0 +1,25 @@
|
||||
// Package wickra provides idiomatic Go bindings for the Wickra
|
||||
// technical-analysis library over its C ABI hub.
|
||||
//
|
||||
// Each indicator is an opaque-handle type with a New<Indicator> constructor and
|
||||
// Update/Batch/Reset/Close methods. Handles are freed by Close and, as a
|
||||
// backstop, by a finalizer; call Close explicitly to release native memory
|
||||
// promptly. The binding links against the prebuilt Wickra C ABI library
|
||||
// (libwickra.so/.dylib or wickra.dll) staged under ./lib — see the package
|
||||
// README for how to provision it.
|
||||
package wickra
|
||||
|
||||
/*
|
||||
#cgo CFLAGS: -I${SRCDIR}/../c/include
|
||||
#cgo linux LDFLAGS: -L${SRCDIR}/lib -lwickra -Wl,-rpath,${SRCDIR}/lib
|
||||
#cgo darwin LDFLAGS: -L${SRCDIR}/lib -lwickra -Wl,-rpath,${SRCDIR}/lib
|
||||
#cgo windows LDFLAGS: -L${SRCDIR}/lib -l:wickra.dll
|
||||
#include "wickra.h"
|
||||
*/
|
||||
import "C"
|
||||
|
||||
import "errors"
|
||||
|
||||
// ErrInvalidParams is returned by a New<Indicator> constructor when the native
|
||||
// constructor rejects the supplied parameters (for example a zero period).
|
||||
var ErrInvalidParams = errors.New("wickra: invalid indicator parameters")
|
||||
@@ -0,0 +1,151 @@
|
||||
package wickra
|
||||
|
||||
import (
|
||||
"math"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// One indicator per FFI archetype, exercising the full New/Update/Batch/Reset/
|
||||
// Close surface against the real native library.
|
||||
|
||||
func TestScalarKnownValue(t *testing.T) {
|
||||
s, err := NewSma(3)
|
||||
if err != nil {
|
||||
t.Fatalf("NewSma: %v", err)
|
||||
}
|
||||
defer s.Close()
|
||||
|
||||
var last float64
|
||||
for _, v := range []float64{1, 2, 3, 4, 5} {
|
||||
last = s.Update(v)
|
||||
}
|
||||
if math.Abs(last-4.0) > 1e-9 {
|
||||
t.Fatalf("sma(3) last = %v, want 4.0", last)
|
||||
}
|
||||
}
|
||||
|
||||
func TestScalarBatchMatchesStreaming(t *testing.T) {
|
||||
input := []float64{1, 2, 3, 4, 5, 6, 7, 8}
|
||||
|
||||
stream, _ := NewSma(3)
|
||||
defer stream.Close()
|
||||
want := make([]float64, len(input))
|
||||
for i, v := range input {
|
||||
want[i] = stream.Update(v)
|
||||
}
|
||||
|
||||
batchInd, _ := NewSma(3)
|
||||
defer batchInd.Close()
|
||||
got := batchInd.Batch(input)
|
||||
|
||||
if len(got) != len(want) {
|
||||
t.Fatalf("batch len = %d, want %d", len(got), len(want))
|
||||
}
|
||||
for i := range want {
|
||||
if math.IsNaN(want[i]) && math.IsNaN(got[i]) {
|
||||
continue
|
||||
}
|
||||
if math.Abs(got[i]-want[i]) > 1e-9 {
|
||||
t.Fatalf("batch[%d] = %v, streaming = %v", i, got[i], want[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestMultiOutput(t *testing.T) {
|
||||
m, err := NewMacdIndicator(3, 6, 3)
|
||||
if err != nil {
|
||||
t.Fatalf("NewMacdIndicator: %v", err)
|
||||
}
|
||||
defer m.Close()
|
||||
|
||||
var ok bool
|
||||
var out MacdOutput
|
||||
for i := 0; i < 30; i++ {
|
||||
out, ok = m.Update(100 + float64(i))
|
||||
}
|
||||
if !ok {
|
||||
t.Fatal("macd never produced a value after warmup")
|
||||
}
|
||||
if math.IsNaN(out.Macd) {
|
||||
t.Fatal("macd value is NaN after warmup")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBars(t *testing.T) {
|
||||
rb, err := NewRangeBars(2.0)
|
||||
if err != nil {
|
||||
t.Fatalf("NewRangeBars: %v", err)
|
||||
}
|
||||
defer rb.Close()
|
||||
|
||||
total := 0
|
||||
for _, p := range []float64{100, 101, 103, 104, 99, 96, 102, 108, 95, 110} {
|
||||
bars := rb.Update(p, p, p, p, 1, 0)
|
||||
total += len(bars)
|
||||
}
|
||||
if total == 0 {
|
||||
t.Fatal("range bars produced no bars over a 15-point move")
|
||||
}
|
||||
}
|
||||
|
||||
func TestProfile(t *testing.T) {
|
||||
vp, err := NewVolumeProfile(10, 24)
|
||||
if err != nil {
|
||||
t.Fatalf("NewVolumeProfile: %v", err)
|
||||
}
|
||||
defer vp.Close()
|
||||
|
||||
var ok bool
|
||||
var snap VolumeProfileOutputScalars
|
||||
for i := 0; i < 50; i++ {
|
||||
price := 100 + 5*math.Sin(float64(i)*0.3)
|
||||
snap, ok = vp.Update(price, price+1, price-1, price, 1000, int64(i))
|
||||
}
|
||||
if !ok {
|
||||
t.Fatal("volume profile never produced a snapshot")
|
||||
}
|
||||
if len(snap.Values) == 0 {
|
||||
t.Fatal("volume profile returned an empty values buffer")
|
||||
}
|
||||
}
|
||||
|
||||
func TestArrayInput(t *testing.T) {
|
||||
ob, err := NewOrderBookImbalanceFull()
|
||||
if err != nil {
|
||||
t.Fatalf("NewOrderBookImbalanceFull: %v", err)
|
||||
}
|
||||
defer ob.Close()
|
||||
|
||||
bidPrice := []float64{99.9, 99.8, 99.7}
|
||||
bidSize := []float64{5, 3, 2}
|
||||
askPrice := []float64{100.1, 100.2, 100.3}
|
||||
askSize := []float64{1, 1, 1}
|
||||
v := ob.Update(bidPrice, bidSize, askPrice, askSize)
|
||||
if math.IsNaN(v) {
|
||||
t.Fatal("order-book imbalance is NaN on a populated book")
|
||||
}
|
||||
}
|
||||
|
||||
func TestResetReturnsToWarmup(t *testing.T) {
|
||||
s, _ := NewSma(3)
|
||||
defer s.Close()
|
||||
for _, v := range []float64{1, 2, 3} {
|
||||
s.Update(v)
|
||||
}
|
||||
s.Reset()
|
||||
if got := s.Update(10); !math.IsNaN(got) {
|
||||
t.Fatalf("after reset first update = %v, want NaN (warmup)", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestInvalidParams(t *testing.T) {
|
||||
if _, err := NewSma(0); err == nil {
|
||||
t.Fatal("NewSma(0) should return ErrInvalidParams")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCloseIsIdempotent(t *testing.T) {
|
||||
s, _ := NewSma(3)
|
||||
s.Close()
|
||||
s.Close() // must not panic or double-free
|
||||
}
|
||||
@@ -9,7 +9,8 @@
|
||||
prebuilt native binary, no system dependencies.**
|
||||
|
||||
Wickra is a multi-language technical-analysis library with a Rust core and
|
||||
bindings for Python, Node.js, and WebAssembly. Every indicator is an O(1)
|
||||
bindings for Python, Node.js and WebAssembly, plus a C ABI for C/C++, C#, Go and any
|
||||
other C-capable language. Every indicator is an O(1)
|
||||
streaming state machine, so live trading bots and historical backtests share
|
||||
the exact same implementation. This package is the Node.js binding (napi-rs);
|
||||
it exposes 200+ streaming-first indicators across sixteen families.
|
||||
@@ -55,8 +56,9 @@ the main repository and documentation site:
|
||||
- **Docs** (quickstarts, cookbook, TA-Lib migration): <https://docs.wickra.org>
|
||||
- **Runnable examples:** [`examples/node/`](https://github.com/wickra-lib/wickra/tree/main/examples/node)
|
||||
|
||||
Wickra ships four bindings — Python, Node.js, WebAssembly, and Rust — that all
|
||||
expose the same indicators from the shared, `unsafe`-forbidden Rust core.
|
||||
Wickra ships native bindings for Python, Node.js, WebAssembly and Rust, plus a
|
||||
C ABI hub that any C-capable language (C, C++, Go, C#, Java, R) links against —
|
||||
all exposing the same indicators from the shared, `unsafe`-forbidden Rust core.
|
||||
|
||||
## Disclaimer
|
||||
|
||||
|
||||
@@ -14,7 +14,18 @@ const wickra = require('..');
|
||||
// but intentionally not isReady/warmupPeriod, so they are excluded from the
|
||||
// Indicator completeness contract below (their interface is covered by the
|
||||
// dedicated bar-builder tests).
|
||||
const BAR_BUILDERS = new Set(['RenkoBars', 'KagiBars', 'PointAndFigureBars']);
|
||||
const BAR_BUILDERS = new Set([
|
||||
'RenkoBars',
|
||||
'KagiBars',
|
||||
'PointAndFigureBars',
|
||||
'RangeBars',
|
||||
'TickBars',
|
||||
'VolumeBars',
|
||||
'DollarBars',
|
||||
'ImbalanceBars',
|
||||
'RunBars',
|
||||
'ThreeLineBreakBars',
|
||||
]);
|
||||
|
||||
// An "indicator class" is an exported constructor whose prototype carries the
|
||||
// streaming `update` method. This excludes `version` (a plain function), the bar
|
||||
|
||||
@@ -28,6 +28,15 @@ function num(v) {
|
||||
// --- Scalar indicators: update(value) vs batch(prices) ---
|
||||
|
||||
const scalarFactories = {
|
||||
M2Measure: () => new wickra.M2Measure(20, 0.0, 0.02),
|
||||
UpsidePotentialRatio: () => new wickra.UpsidePotentialRatio(20, 0.0),
|
||||
GainToPainRatio: () => new wickra.GainToPainRatio(12),
|
||||
CommonSenseRatio: () => new wickra.CommonSenseRatio(20),
|
||||
KRatio: () => new wickra.KRatio(30),
|
||||
TailRatio: () => new wickra.TailRatio(20),
|
||||
MartinRatio: () => new wickra.MartinRatio(14),
|
||||
BurkeRatio: () => new wickra.BurkeRatio(12),
|
||||
SterlingRatio: () => new wickra.SterlingRatio(12),
|
||||
AUTOCORRPGRAM: () => new wickra.AUTOCORRPGRAM(10, 48),
|
||||
EVENBETTERSINE: () => new wickra.EVENBETTERSINE(40, 10),
|
||||
BANDPASS: () => new wickra.BANDPASS(20, 0.3),
|
||||
@@ -1760,3 +1769,72 @@ test('PointAndFigureBars closes a column on a 3-box reversal', () => {
|
||||
assert.equal(col[0].direction, 1);
|
||||
assert.ok(Math.abs(col[0].high - 15) < 1e-9 && Math.abs(col[0].low - 10) < 1e-9);
|
||||
});
|
||||
|
||||
test('RangeBars prints aligned bars on an up move', () => {
|
||||
const rb = new wickra.RangeBars(1.0);
|
||||
assert.deepEqual(rb.update(10), []); // seed
|
||||
const up = rb.update(13);
|
||||
assert.equal(up.length, 3);
|
||||
assert.ok(Math.abs(up[0].open - 10) < 1e-9 && Math.abs(up[2].close - 13) < 1e-9);
|
||||
assert.ok(up.every((b) => b.direction === 1));
|
||||
});
|
||||
|
||||
test('TickBars groups a fixed number of candles', () => {
|
||||
const tb = new wickra.TickBars(2);
|
||||
assert.deepEqual(tb.update(10, 11, 9, 10.5, 100), []);
|
||||
const out = tb.update(10.5, 12, 10, 11, 150);
|
||||
assert.equal(out.length, 1);
|
||||
assert.ok(Math.abs(out[0].open - 10) < 1e-9);
|
||||
assert.ok(Math.abs(out[0].high - 12) < 1e-9);
|
||||
assert.ok(Math.abs(out[0].low - 9) < 1e-9);
|
||||
assert.ok(Math.abs(out[0].close - 11) < 1e-9);
|
||||
assert.ok(Math.abs(out[0].volume - 250) < 1e-9);
|
||||
});
|
||||
|
||||
test('VolumeBars closes when accumulated volume crosses the threshold', () => {
|
||||
const vb = new wickra.VolumeBars(100);
|
||||
assert.deepEqual(vb.update(10, 10, 10, 10, 60), []);
|
||||
const out = vb.update(10.5, 10.5, 10.5, 10.5, 60);
|
||||
assert.equal(out.length, 1);
|
||||
assert.ok(Math.abs(out[0].volume - 120) < 1e-9);
|
||||
});
|
||||
|
||||
test('DollarBars closes when traded value crosses the threshold', () => {
|
||||
const db = new wickra.DollarBars(1000);
|
||||
assert.deepEqual(db.update(10, 10, 10, 10, 60), []);
|
||||
const out = db.update(10, 10, 10, 10, 60);
|
||||
assert.equal(out.length, 1);
|
||||
assert.ok(Math.abs(out[0].dollar - 1200) < 1e-9);
|
||||
assert.ok(Math.abs(out[0].volume - 120) < 1e-9);
|
||||
});
|
||||
|
||||
test('ImbalanceBars closes a buy bar at the threshold', () => {
|
||||
const ib = new wickra.ImbalanceBars(3.0);
|
||||
ib.update(10, 10, 10, 10);
|
||||
ib.update(11, 11, 11, 11);
|
||||
ib.update(12, 12, 12, 12);
|
||||
const out = ib.update(13, 13, 13, 13);
|
||||
assert.equal(out.length, 1);
|
||||
assert.equal(out[0].direction, 1);
|
||||
assert.ok(Math.abs(out[0].imbalance - 3) < 1e-9);
|
||||
});
|
||||
|
||||
test('RunBars closes a buy run at the run length', () => {
|
||||
const rb = new wickra.RunBars(3);
|
||||
rb.update(10, 10, 10, 10);
|
||||
rb.update(11, 11, 11, 11);
|
||||
rb.update(12, 12, 12, 12);
|
||||
const out = rb.update(13, 13, 13, 13);
|
||||
assert.equal(out.length, 1);
|
||||
assert.equal(out[0].direction, 1);
|
||||
assert.equal(out[0].length, 3);
|
||||
});
|
||||
|
||||
test('ThreeLineBreakBars draws a rising line', () => {
|
||||
const tlb = new wickra.ThreeLineBreakBars(3);
|
||||
assert.deepEqual(tlb.update(10), []); // seed
|
||||
const out = tlb.update(11);
|
||||
assert.equal(out.length, 1);
|
||||
assert.equal(out[0].direction, 1);
|
||||
assert.ok(Math.abs(out[0].open - 10) < 1e-9 && Math.abs(out[0].close - 11) < 1e-9);
|
||||
});
|
||||
|
||||
Vendored
+185
@@ -468,6 +468,54 @@ export interface PnfColumnValue {
|
||||
high: number
|
||||
low: number
|
||||
}
|
||||
export interface RangeBarValue {
|
||||
open: number
|
||||
close: number
|
||||
direction: number
|
||||
}
|
||||
export interface TickBarValue {
|
||||
open: number
|
||||
high: number
|
||||
low: number
|
||||
close: number
|
||||
volume: number
|
||||
}
|
||||
export interface VolumeBarValue {
|
||||
open: number
|
||||
high: number
|
||||
low: number
|
||||
close: number
|
||||
volume: number
|
||||
}
|
||||
export interface DollarBarValue {
|
||||
open: number
|
||||
high: number
|
||||
low: number
|
||||
close: number
|
||||
volume: number
|
||||
dollar: number
|
||||
}
|
||||
export interface ImbalanceBarValue {
|
||||
open: number
|
||||
high: number
|
||||
low: number
|
||||
close: number
|
||||
imbalance: number
|
||||
direction: number
|
||||
}
|
||||
export interface RunBarValue {
|
||||
open: number
|
||||
high: number
|
||||
low: number
|
||||
close: number
|
||||
length: number
|
||||
direction: number
|
||||
}
|
||||
export interface LineBreakBarValue {
|
||||
open: number
|
||||
close: number
|
||||
direction: number
|
||||
}
|
||||
export interface SessionHighLowValue {
|
||||
high: number
|
||||
low: number
|
||||
@@ -1176,6 +1224,87 @@ export declare class UNIVERSALOSC {
|
||||
isReady(): boolean
|
||||
warmupPeriod(): number
|
||||
}
|
||||
export type SterlingRatioNode = SterlingRatio
|
||||
export declare class SterlingRatio {
|
||||
constructor(period: number)
|
||||
update(value: number): number | null
|
||||
batch(prices: Array<number>): Array<number>
|
||||
reset(): void
|
||||
isReady(): boolean
|
||||
warmupPeriod(): number
|
||||
}
|
||||
export type BurkeRatioNode = BurkeRatio
|
||||
export declare class BurkeRatio {
|
||||
constructor(period: number)
|
||||
update(value: number): number | null
|
||||
batch(prices: Array<number>): Array<number>
|
||||
reset(): void
|
||||
isReady(): boolean
|
||||
warmupPeriod(): number
|
||||
}
|
||||
export type MartinRatioNode = MartinRatio
|
||||
export declare class MartinRatio {
|
||||
constructor(period: number)
|
||||
update(value: number): number | null
|
||||
batch(prices: Array<number>): Array<number>
|
||||
reset(): void
|
||||
isReady(): boolean
|
||||
warmupPeriod(): number
|
||||
}
|
||||
export type TailRatioNode = TailRatio
|
||||
export declare class TailRatio {
|
||||
constructor(period: number)
|
||||
update(value: number): number | null
|
||||
batch(prices: Array<number>): Array<number>
|
||||
reset(): void
|
||||
isReady(): boolean
|
||||
warmupPeriod(): number
|
||||
}
|
||||
export type KRatioNode = KRatio
|
||||
export declare class KRatio {
|
||||
constructor(period: number)
|
||||
update(value: number): number | null
|
||||
batch(prices: Array<number>): Array<number>
|
||||
reset(): void
|
||||
isReady(): boolean
|
||||
warmupPeriod(): number
|
||||
}
|
||||
export type CommonSenseRatioNode = CommonSenseRatio
|
||||
export declare class CommonSenseRatio {
|
||||
constructor(period: number)
|
||||
update(value: number): number | null
|
||||
batch(prices: Array<number>): Array<number>
|
||||
reset(): void
|
||||
isReady(): boolean
|
||||
warmupPeriod(): number
|
||||
}
|
||||
export type GainToPainRatioNode = GainToPainRatio
|
||||
export declare class GainToPainRatio {
|
||||
constructor(period: number)
|
||||
update(value: number): number | null
|
||||
batch(prices: Array<number>): Array<number>
|
||||
reset(): void
|
||||
isReady(): boolean
|
||||
warmupPeriod(): number
|
||||
}
|
||||
export type UpsidePotentialRatioNode = UpsidePotentialRatio
|
||||
export declare class UpsidePotentialRatio {
|
||||
constructor(period: number, mar: number)
|
||||
update(value: number): number | null
|
||||
batch(prices: Array<number>): Array<number>
|
||||
reset(): void
|
||||
isReady(): boolean
|
||||
warmupPeriod(): number
|
||||
}
|
||||
export type M2MeasureNode = M2Measure
|
||||
export declare class M2Measure {
|
||||
constructor(period: number, riskFree: number, benchmarkStddev: number)
|
||||
update(value: number): number | null
|
||||
batch(prices: Array<number>): Array<number>
|
||||
reset(): void
|
||||
isReady(): boolean
|
||||
warmupPeriod(): number
|
||||
}
|
||||
export type BandpassFilterNode = BANDPASS
|
||||
export declare class BANDPASS {
|
||||
constructor(period: number, bandwidth: number)
|
||||
@@ -4947,6 +5076,62 @@ export declare class PointAndFigureBars {
|
||||
reversal(): number
|
||||
reset(): void
|
||||
}
|
||||
export type RangeBarsNode = RangeBars
|
||||
export declare class RangeBars {
|
||||
constructor(range: number)
|
||||
update(close: number): Array<RangeBarValue>
|
||||
batch(close: Array<number>): Array<RangeBarValue>
|
||||
range(): number
|
||||
reset(): void
|
||||
}
|
||||
export type TickBarsNode = TickBars
|
||||
export declare class TickBars {
|
||||
constructor(ticks: number)
|
||||
update(open: number, high: number, low: number, close: number, volume: number): Array<TickBarValue>
|
||||
batch(open: Array<number>, high: Array<number>, low: Array<number>, close: Array<number>, volume: Array<number>): Array<TickBarValue>
|
||||
ticks(): number
|
||||
reset(): void
|
||||
}
|
||||
export type VolumeBarsNode = VolumeBars
|
||||
export declare class VolumeBars {
|
||||
constructor(volumePerBar: number)
|
||||
update(open: number, high: number, low: number, close: number, volume: number): Array<VolumeBarValue>
|
||||
batch(open: Array<number>, high: Array<number>, low: Array<number>, close: Array<number>, volume: Array<number>): Array<VolumeBarValue>
|
||||
volumePerBar(): number
|
||||
reset(): void
|
||||
}
|
||||
export type DollarBarsNode = DollarBars
|
||||
export declare class DollarBars {
|
||||
constructor(dollarPerBar: number)
|
||||
update(open: number, high: number, low: number, close: number, volume: number): Array<DollarBarValue>
|
||||
batch(open: Array<number>, high: Array<number>, low: Array<number>, close: Array<number>, volume: Array<number>): Array<DollarBarValue>
|
||||
dollarPerBar(): number
|
||||
reset(): void
|
||||
}
|
||||
export type ImbalanceBarsNode = ImbalanceBars
|
||||
export declare class ImbalanceBars {
|
||||
constructor(threshold: number)
|
||||
update(open: number, high: number, low: number, close: number): Array<ImbalanceBarValue>
|
||||
batch(open: Array<number>, high: Array<number>, low: Array<number>, close: Array<number>): Array<ImbalanceBarValue>
|
||||
threshold(): number
|
||||
reset(): void
|
||||
}
|
||||
export type RunBarsNode = RunBars
|
||||
export declare class RunBars {
|
||||
constructor(runLength: number)
|
||||
update(open: number, high: number, low: number, close: number): Array<RunBarValue>
|
||||
batch(open: Array<number>, high: Array<number>, low: Array<number>, close: Array<number>): Array<RunBarValue>
|
||||
runLength(): number
|
||||
reset(): void
|
||||
}
|
||||
export type ThreeLineBreakBarsNode = ThreeLineBreakBars
|
||||
export declare class ThreeLineBreakBars {
|
||||
constructor(lines: number)
|
||||
update(close: number): Array<LineBreakBarValue>
|
||||
batch(close: Array<number>): Array<LineBreakBarValue>
|
||||
lines(): number
|
||||
reset(): void
|
||||
}
|
||||
export type AlphaNode = Alpha
|
||||
export declare class Alpha {
|
||||
constructor(period: number, riskFree: number)
|
||||
|
||||
+17
-1
File diff suppressed because one or more lines are too long
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "wickra-darwin-arm64",
|
||||
"version": "0.7.2",
|
||||
"version": "0.7.7",
|
||||
"description": "Native binding for wickra (macOS Apple Silicon). Installed automatically as an optional dependency of wickra on matching platforms.",
|
||||
"main": "wickra.darwin-arm64.node",
|
||||
"files": [
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "wickra-darwin-x64",
|
||||
"version": "0.7.2",
|
||||
"version": "0.7.7",
|
||||
"description": "Native binding for wickra (macOS Intel). Installed automatically as an optional dependency of wickra on matching platforms.",
|
||||
"main": "wickra.darwin-x64.node",
|
||||
"files": [
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "wickra-linux-arm64-gnu",
|
||||
"version": "0.7.2",
|
||||
"version": "0.7.7",
|
||||
"description": "Native binding for wickra (linux arm64 GNU). Installed automatically as an optional dependency of wickra on matching platforms.",
|
||||
"main": "wickra.linux-arm64-gnu.node",
|
||||
"files": [
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "wickra-linux-x64-gnu",
|
||||
"version": "0.7.2",
|
||||
"version": "0.7.7",
|
||||
"description": "Native binding for wickra (linux x64 GNU). Installed automatically as an optional dependency of wickra on matching platforms.",
|
||||
"main": "wickra.linux-x64-gnu.node",
|
||||
"files": [
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "wickra-win32-arm64-msvc",
|
||||
"version": "0.7.2",
|
||||
"version": "0.7.7",
|
||||
"description": "Native binding for wickra (Windows arm64 MSVC). Installed automatically as an optional dependency of wickra on matching platforms.",
|
||||
"main": "wickra.win32-arm64-msvc.node",
|
||||
"files": [
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "wickra-win32-x64-msvc",
|
||||
"version": "0.7.2",
|
||||
"version": "0.7.7",
|
||||
"description": "Native binding for wickra (Windows x64 MSVC). Installed automatically as an optional dependency of wickra on matching platforms.",
|
||||
"main": "wickra.win32-x64-msvc.node",
|
||||
"files": [
|
||||
|
||||
Generated
+20
-20
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "wickra",
|
||||
"version": "0.7.2",
|
||||
"version": "0.7.7",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "wickra",
|
||||
"version": "0.7.2",
|
||||
"version": "0.7.7",
|
||||
"license": "MIT OR Apache-2.0",
|
||||
"devDependencies": {
|
||||
"@napi-rs/cli": "^2.18.0"
|
||||
@@ -15,12 +15,12 @@
|
||||
"node": ">= 18"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"wickra-darwin-arm64": "0.7.2",
|
||||
"wickra-darwin-x64": "0.7.2",
|
||||
"wickra-linux-arm64-gnu": "0.7.2",
|
||||
"wickra-linux-x64-gnu": "0.7.2",
|
||||
"wickra-win32-arm64-msvc": "0.7.2",
|
||||
"wickra-win32-x64-msvc": "0.7.2"
|
||||
"wickra-darwin-arm64": "0.7.7",
|
||||
"wickra-darwin-x64": "0.7.7",
|
||||
"wickra-linux-arm64-gnu": "0.7.7",
|
||||
"wickra-linux-x64-gnu": "0.7.7",
|
||||
"wickra-win32-arm64-msvc": "0.7.7",
|
||||
"wickra-win32-x64-msvc": "0.7.7"
|
||||
}
|
||||
},
|
||||
"node_modules/@napi-rs/cli": {
|
||||
@@ -41,8 +41,8 @@
|
||||
}
|
||||
},
|
||||
"node_modules/wickra-darwin-arm64": {
|
||||
"version": "0.7.2",
|
||||
"resolved": "https://registry.npmjs.org/wickra-darwin-arm64/-/wickra-darwin-arm64-0.7.2.tgz",
|
||||
"version": "0.7.7",
|
||||
"resolved": "https://registry.npmjs.org/wickra-darwin-arm64/-/wickra-darwin-arm64-0.7.7.tgz",
|
||||
"integrity": "sha512-4eZiBR/yGUdr4nzhEUFy2i69XgNx64iI2ax/LPamsThgylC0KpHOZKK19QzJ2d9KbK4C8nMjME5FLuR+4GNEwQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
@@ -57,8 +57,8 @@
|
||||
}
|
||||
},
|
||||
"node_modules/wickra-darwin-x64": {
|
||||
"version": "0.7.2",
|
||||
"resolved": "https://registry.npmjs.org/wickra-darwin-x64/-/wickra-darwin-x64-0.7.2.tgz",
|
||||
"version": "0.7.7",
|
||||
"resolved": "https://registry.npmjs.org/wickra-darwin-x64/-/wickra-darwin-x64-0.7.7.tgz",
|
||||
"integrity": "sha512-6hf8zI3QPjTFp4zCpmgUwDvNtu6jHqNUHKD5e55POo0CgA52HkpyxSPtVm8TGTIZDI7kPjlbOdBM8CJ76mmXwA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
@@ -73,8 +73,8 @@
|
||||
}
|
||||
},
|
||||
"node_modules/wickra-linux-arm64-gnu": {
|
||||
"version": "0.7.2",
|
||||
"resolved": "https://registry.npmjs.org/wickra-linux-arm64-gnu/-/wickra-linux-arm64-gnu-0.7.2.tgz",
|
||||
"version": "0.7.7",
|
||||
"resolved": "https://registry.npmjs.org/wickra-linux-arm64-gnu/-/wickra-linux-arm64-gnu-0.7.7.tgz",
|
||||
"integrity": "sha512-kSe6y0xBMSiqdPLXNjwop5WZdHtvdBNKSEBCwZ4hFq33p4apW25/wrlzv9/oDuyD4kuPabJEhCCnFOplh58CUg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
@@ -89,8 +89,8 @@
|
||||
}
|
||||
},
|
||||
"node_modules/wickra-linux-x64-gnu": {
|
||||
"version": "0.7.2",
|
||||
"resolved": "https://registry.npmjs.org/wickra-linux-x64-gnu/-/wickra-linux-x64-gnu-0.7.2.tgz",
|
||||
"version": "0.7.7",
|
||||
"resolved": "https://registry.npmjs.org/wickra-linux-x64-gnu/-/wickra-linux-x64-gnu-0.7.7.tgz",
|
||||
"integrity": "sha512-tWBWS4qz7hxM4xnpFb59bhf6TaLwXq0Z3jEa/2l7r8PiHA94g8r8S53NRMiT+4yiL5hSWe/nUiC/YXdRrhEZ4g==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
@@ -105,8 +105,8 @@
|
||||
}
|
||||
},
|
||||
"node_modules/wickra-win32-arm64-msvc": {
|
||||
"version": "0.7.2",
|
||||
"resolved": "https://registry.npmjs.org/wickra-win32-arm64-msvc/-/wickra-win32-arm64-msvc-0.7.2.tgz",
|
||||
"version": "0.7.7",
|
||||
"resolved": "https://registry.npmjs.org/wickra-win32-arm64-msvc/-/wickra-win32-arm64-msvc-0.7.7.tgz",
|
||||
"integrity": "sha512-EXIckHxAtF75PUGDKRzXyqMe9ldP0JjSdu68WFN6iJfp+McYrGu6h40TEJlQ/oUEIoPqiZB/xhVyo/el5Lg7zw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
@@ -121,8 +121,8 @@
|
||||
}
|
||||
},
|
||||
"node_modules/wickra-win32-x64-msvc": {
|
||||
"version": "0.7.2",
|
||||
"resolved": "https://registry.npmjs.org/wickra-win32-x64-msvc/-/wickra-win32-x64-msvc-0.7.2.tgz",
|
||||
"version": "0.7.7",
|
||||
"resolved": "https://registry.npmjs.org/wickra-win32-x64-msvc/-/wickra-win32-x64-msvc-0.7.7.tgz",
|
||||
"integrity": "sha512-Yfsqq1Xwp6hdxMyLze411vNdo7BDwI6+lPSe7A9XdqyPecNDbtKwYLpsal2r8EHbNzqM+R8XnuRtUaEQS5VlUQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "wickra",
|
||||
"version": "0.7.2",
|
||||
"version": "0.7.7",
|
||||
"description": "Streaming-first technical indicators: incremental, fast, install-free. Node bindings powered by Rust.",
|
||||
"author": "kingchenc <support@wickra.org>",
|
||||
"main": "index.js",
|
||||
@@ -47,12 +47,12 @@
|
||||
"node": ">= 18"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"wickra-linux-x64-gnu": "0.7.2",
|
||||
"wickra-linux-arm64-gnu": "0.7.2",
|
||||
"wickra-darwin-x64": "0.7.2",
|
||||
"wickra-darwin-arm64": "0.7.2",
|
||||
"wickra-win32-x64-msvc": "0.7.2",
|
||||
"wickra-win32-arm64-msvc": "0.7.2"
|
||||
"wickra-linux-x64-gnu": "0.7.7",
|
||||
"wickra-linux-arm64-gnu": "0.7.7",
|
||||
"wickra-darwin-x64": "0.7.7",
|
||||
"wickra-darwin-arm64": "0.7.7",
|
||||
"wickra-win32-x64-msvc": "0.7.7",
|
||||
"wickra-win32-arm64-msvc": "0.7.7"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "napi build --platform --release",
|
||||
|
||||
@@ -245,9 +245,91 @@ node_scalar_indicator!(
|
||||
"UNIVERSALOSC",
|
||||
wc::UniversalOscillator
|
||||
);
|
||||
node_scalar_indicator!(SterlingRatioNode, "SterlingRatio", wc::SterlingRatio);
|
||||
node_scalar_indicator!(BurkeRatioNode, "BurkeRatio", wc::BurkeRatio);
|
||||
node_scalar_indicator!(MartinRatioNode, "MartinRatio", wc::MartinRatio);
|
||||
node_scalar_indicator!(TailRatioNode, "TailRatio", wc::TailRatio);
|
||||
node_scalar_indicator!(KRatioNode, "KRatio", wc::KRatio);
|
||||
node_scalar_indicator!(
|
||||
CommonSenseRatioNode,
|
||||
"CommonSenseRatio",
|
||||
wc::CommonSenseRatio
|
||||
);
|
||||
node_scalar_indicator!(GainToPainRatioNode, "GainToPainRatio", wc::GainToPainRatio);
|
||||
|
||||
// Multi-arg Ehlers scalars: hand-written (node_scalar_indicator! is single-period).
|
||||
|
||||
#[napi(js_name = "UpsidePotentialRatio")]
|
||||
pub struct UpsidePotentialRatioNode {
|
||||
inner: wc::UpsidePotentialRatio,
|
||||
}
|
||||
|
||||
#[napi]
|
||||
impl UpsidePotentialRatioNode {
|
||||
#[napi(constructor)]
|
||||
pub fn new(period: u32, mar: f64) -> napi::Result<Self> {
|
||||
Ok(Self {
|
||||
inner: wc::UpsidePotentialRatio::new(period as usize, mar).map_err(map_err)?,
|
||||
})
|
||||
}
|
||||
#[napi]
|
||||
pub fn update(&mut self, value: f64) -> Option<f64> {
|
||||
self.inner.update(value)
|
||||
}
|
||||
#[napi]
|
||||
pub fn batch(&mut self, prices: Vec<f64>) -> Vec<f64> {
|
||||
flatten(self.inner.batch(&prices))
|
||||
}
|
||||
#[napi]
|
||||
pub fn reset(&mut self) {
|
||||
self.inner.reset();
|
||||
}
|
||||
#[napi(js_name = "isReady")]
|
||||
pub fn is_ready(&self) -> bool {
|
||||
self.inner.is_ready()
|
||||
}
|
||||
#[napi(js_name = "warmupPeriod")]
|
||||
pub fn warmup_period(&self) -> u32 {
|
||||
self.inner.warmup_period() as u32
|
||||
}
|
||||
}
|
||||
|
||||
#[napi(js_name = "M2Measure")]
|
||||
pub struct M2MeasureNode {
|
||||
inner: wc::M2Measure,
|
||||
}
|
||||
|
||||
#[napi]
|
||||
impl M2MeasureNode {
|
||||
#[napi(constructor)]
|
||||
pub fn new(period: u32, risk_free: f64, benchmark_stddev: f64) -> napi::Result<Self> {
|
||||
Ok(Self {
|
||||
inner: wc::M2Measure::new(period as usize, risk_free, benchmark_stddev)
|
||||
.map_err(map_err)?,
|
||||
})
|
||||
}
|
||||
#[napi]
|
||||
pub fn update(&mut self, value: f64) -> Option<f64> {
|
||||
self.inner.update(value)
|
||||
}
|
||||
#[napi]
|
||||
pub fn batch(&mut self, prices: Vec<f64>) -> Vec<f64> {
|
||||
flatten(self.inner.batch(&prices))
|
||||
}
|
||||
#[napi]
|
||||
pub fn reset(&mut self) {
|
||||
self.inner.reset();
|
||||
}
|
||||
#[napi(js_name = "isReady")]
|
||||
pub fn is_ready(&self) -> bool {
|
||||
self.inner.is_ready()
|
||||
}
|
||||
#[napi(js_name = "warmupPeriod")]
|
||||
pub fn warmup_period(&self) -> u32 {
|
||||
self.inner.warmup_period() as u32
|
||||
}
|
||||
}
|
||||
|
||||
#[napi(js_name = "BANDPASS")]
|
||||
pub struct BandpassFilterNode {
|
||||
inner: wc::BandpassFilter,
|
||||
@@ -17736,6 +17818,559 @@ impl PointAndFigureBarsNode {
|
||||
}
|
||||
}
|
||||
|
||||
#[napi(object)]
|
||||
pub struct RangeBarValue {
|
||||
pub open: f64,
|
||||
pub close: f64,
|
||||
pub direction: i32,
|
||||
}
|
||||
|
||||
#[napi(js_name = "RangeBars")]
|
||||
pub struct RangeBarsNode {
|
||||
inner: wc::RangeBars,
|
||||
}
|
||||
#[napi]
|
||||
impl RangeBarsNode {
|
||||
#[napi(constructor)]
|
||||
pub fn new(range: f64) -> napi::Result<Self> {
|
||||
Ok(Self {
|
||||
inner: wc::RangeBars::new(range).map_err(map_err)?,
|
||||
})
|
||||
}
|
||||
#[napi]
|
||||
pub fn update(&mut self, close: f64) -> napi::Result<Vec<RangeBarValue>> {
|
||||
let candle = wc::Candle::new(close, close, close, close, 1.0, 0).map_err(map_err)?;
|
||||
Ok(self
|
||||
.inner
|
||||
.update(candle)
|
||||
.into_iter()
|
||||
.map(|b| RangeBarValue {
|
||||
open: b.open,
|
||||
close: b.close,
|
||||
direction: i32::from(b.direction),
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
#[napi]
|
||||
pub fn batch(&mut self, close: Vec<f64>) -> napi::Result<Vec<RangeBarValue>> {
|
||||
let mut out = Vec::new();
|
||||
for price in close {
|
||||
let candle = wc::Candle::new(price, price, price, price, 1.0, 0).map_err(map_err)?;
|
||||
for b in self.inner.update(candle) {
|
||||
out.push(RangeBarValue {
|
||||
open: b.open,
|
||||
close: b.close,
|
||||
direction: i32::from(b.direction),
|
||||
});
|
||||
}
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
#[napi]
|
||||
pub fn range(&self) -> f64 {
|
||||
self.inner.range()
|
||||
}
|
||||
#[napi]
|
||||
pub fn reset(&mut self) {
|
||||
self.inner.reset();
|
||||
}
|
||||
}
|
||||
|
||||
#[napi(object)]
|
||||
pub struct TickBarValue {
|
||||
pub open: f64,
|
||||
pub high: f64,
|
||||
pub low: f64,
|
||||
pub close: f64,
|
||||
pub volume: f64,
|
||||
}
|
||||
|
||||
#[napi(js_name = "TickBars")]
|
||||
pub struct TickBarsNode {
|
||||
inner: wc::TickBars,
|
||||
}
|
||||
#[napi]
|
||||
impl TickBarsNode {
|
||||
#[napi(constructor)]
|
||||
pub fn new(ticks: u32) -> napi::Result<Self> {
|
||||
Ok(Self {
|
||||
inner: wc::TickBars::new(ticks as usize).map_err(map_err)?,
|
||||
})
|
||||
}
|
||||
#[napi]
|
||||
pub fn update(
|
||||
&mut self,
|
||||
open: f64,
|
||||
high: f64,
|
||||
low: f64,
|
||||
close: f64,
|
||||
volume: f64,
|
||||
) -> napi::Result<Vec<TickBarValue>> {
|
||||
let candle = wc::Candle::new(open, high, low, close, volume, 0).map_err(map_err)?;
|
||||
Ok(self
|
||||
.inner
|
||||
.update(candle)
|
||||
.into_iter()
|
||||
.map(|b| TickBarValue {
|
||||
open: b.open,
|
||||
high: b.high,
|
||||
low: b.low,
|
||||
close: b.close,
|
||||
volume: b.volume,
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
#[napi]
|
||||
pub fn batch(
|
||||
&mut self,
|
||||
open: Vec<f64>,
|
||||
high: Vec<f64>,
|
||||
low: Vec<f64>,
|
||||
close: Vec<f64>,
|
||||
volume: Vec<f64>,
|
||||
) -> napi::Result<Vec<TickBarValue>> {
|
||||
if open.len() != high.len()
|
||||
|| high.len() != low.len()
|
||||
|| low.len() != close.len()
|
||||
|| close.len() != volume.len()
|
||||
{
|
||||
return Err(NapiError::from_reason(
|
||||
"open, high, low, close, volume must be equal length".to_string(),
|
||||
));
|
||||
}
|
||||
let mut out = Vec::new();
|
||||
for i in 0..open.len() {
|
||||
let candle = wc::Candle::new(open[i], high[i], low[i], close[i], volume[i], 0)
|
||||
.map_err(map_err)?;
|
||||
for b in self.inner.update(candle) {
|
||||
out.push(TickBarValue {
|
||||
open: b.open,
|
||||
high: b.high,
|
||||
low: b.low,
|
||||
close: b.close,
|
||||
volume: b.volume,
|
||||
});
|
||||
}
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
#[napi]
|
||||
pub fn ticks(&self) -> u32 {
|
||||
self.inner.ticks() as u32
|
||||
}
|
||||
#[napi]
|
||||
pub fn reset(&mut self) {
|
||||
self.inner.reset();
|
||||
}
|
||||
}
|
||||
|
||||
#[napi(object)]
|
||||
pub struct VolumeBarValue {
|
||||
pub open: f64,
|
||||
pub high: f64,
|
||||
pub low: f64,
|
||||
pub close: f64,
|
||||
pub volume: f64,
|
||||
}
|
||||
|
||||
#[napi(js_name = "VolumeBars")]
|
||||
pub struct VolumeBarsNode {
|
||||
inner: wc::VolumeBars,
|
||||
}
|
||||
#[napi]
|
||||
impl VolumeBarsNode {
|
||||
#[napi(constructor)]
|
||||
pub fn new(volume_per_bar: f64) -> napi::Result<Self> {
|
||||
Ok(Self {
|
||||
inner: wc::VolumeBars::new(volume_per_bar).map_err(map_err)?,
|
||||
})
|
||||
}
|
||||
#[napi]
|
||||
pub fn update(
|
||||
&mut self,
|
||||
open: f64,
|
||||
high: f64,
|
||||
low: f64,
|
||||
close: f64,
|
||||
volume: f64,
|
||||
) -> napi::Result<Vec<VolumeBarValue>> {
|
||||
let candle = wc::Candle::new(open, high, low, close, volume, 0).map_err(map_err)?;
|
||||
Ok(self
|
||||
.inner
|
||||
.update(candle)
|
||||
.into_iter()
|
||||
.map(|b| VolumeBarValue {
|
||||
open: b.open,
|
||||
high: b.high,
|
||||
low: b.low,
|
||||
close: b.close,
|
||||
volume: b.volume,
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
#[napi]
|
||||
pub fn batch(
|
||||
&mut self,
|
||||
open: Vec<f64>,
|
||||
high: Vec<f64>,
|
||||
low: Vec<f64>,
|
||||
close: Vec<f64>,
|
||||
volume: Vec<f64>,
|
||||
) -> napi::Result<Vec<VolumeBarValue>> {
|
||||
if open.len() != high.len()
|
||||
|| high.len() != low.len()
|
||||
|| low.len() != close.len()
|
||||
|| close.len() != volume.len()
|
||||
{
|
||||
return Err(NapiError::from_reason(
|
||||
"open, high, low, close, volume must be equal length".to_string(),
|
||||
));
|
||||
}
|
||||
let mut out = Vec::new();
|
||||
for i in 0..open.len() {
|
||||
let candle = wc::Candle::new(open[i], high[i], low[i], close[i], volume[i], 0)
|
||||
.map_err(map_err)?;
|
||||
for b in self.inner.update(candle) {
|
||||
out.push(VolumeBarValue {
|
||||
open: b.open,
|
||||
high: b.high,
|
||||
low: b.low,
|
||||
close: b.close,
|
||||
volume: b.volume,
|
||||
});
|
||||
}
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
#[napi(js_name = "volumePerBar")]
|
||||
pub fn volume_per_bar(&self) -> f64 {
|
||||
self.inner.volume_per_bar()
|
||||
}
|
||||
#[napi]
|
||||
pub fn reset(&mut self) {
|
||||
self.inner.reset();
|
||||
}
|
||||
}
|
||||
|
||||
#[napi(object)]
|
||||
pub struct DollarBarValue {
|
||||
pub open: f64,
|
||||
pub high: f64,
|
||||
pub low: f64,
|
||||
pub close: f64,
|
||||
pub volume: f64,
|
||||
pub dollar: f64,
|
||||
}
|
||||
|
||||
#[napi(js_name = "DollarBars")]
|
||||
pub struct DollarBarsNode {
|
||||
inner: wc::DollarBars,
|
||||
}
|
||||
#[napi]
|
||||
impl DollarBarsNode {
|
||||
#[napi(constructor)]
|
||||
pub fn new(dollar_per_bar: f64) -> napi::Result<Self> {
|
||||
Ok(Self {
|
||||
inner: wc::DollarBars::new(dollar_per_bar).map_err(map_err)?,
|
||||
})
|
||||
}
|
||||
#[napi]
|
||||
pub fn update(
|
||||
&mut self,
|
||||
open: f64,
|
||||
high: f64,
|
||||
low: f64,
|
||||
close: f64,
|
||||
volume: f64,
|
||||
) -> napi::Result<Vec<DollarBarValue>> {
|
||||
let candle = wc::Candle::new(open, high, low, close, volume, 0).map_err(map_err)?;
|
||||
Ok(self
|
||||
.inner
|
||||
.update(candle)
|
||||
.into_iter()
|
||||
.map(|b| DollarBarValue {
|
||||
open: b.open,
|
||||
high: b.high,
|
||||
low: b.low,
|
||||
close: b.close,
|
||||
volume: b.volume,
|
||||
dollar: b.dollar,
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
#[napi]
|
||||
pub fn batch(
|
||||
&mut self,
|
||||
open: Vec<f64>,
|
||||
high: Vec<f64>,
|
||||
low: Vec<f64>,
|
||||
close: Vec<f64>,
|
||||
volume: Vec<f64>,
|
||||
) -> napi::Result<Vec<DollarBarValue>> {
|
||||
if open.len() != high.len()
|
||||
|| high.len() != low.len()
|
||||
|| low.len() != close.len()
|
||||
|| close.len() != volume.len()
|
||||
{
|
||||
return Err(NapiError::from_reason(
|
||||
"open, high, low, close, volume must be equal length".to_string(),
|
||||
));
|
||||
}
|
||||
let mut out = Vec::new();
|
||||
for i in 0..open.len() {
|
||||
let candle = wc::Candle::new(open[i], high[i], low[i], close[i], volume[i], 0)
|
||||
.map_err(map_err)?;
|
||||
for b in self.inner.update(candle) {
|
||||
out.push(DollarBarValue {
|
||||
open: b.open,
|
||||
high: b.high,
|
||||
low: b.low,
|
||||
close: b.close,
|
||||
volume: b.volume,
|
||||
dollar: b.dollar,
|
||||
});
|
||||
}
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
#[napi(js_name = "dollarPerBar")]
|
||||
pub fn dollar_per_bar(&self) -> f64 {
|
||||
self.inner.dollar_per_bar()
|
||||
}
|
||||
#[napi]
|
||||
pub fn reset(&mut self) {
|
||||
self.inner.reset();
|
||||
}
|
||||
}
|
||||
|
||||
#[napi(object)]
|
||||
pub struct ImbalanceBarValue {
|
||||
pub open: f64,
|
||||
pub high: f64,
|
||||
pub low: f64,
|
||||
pub close: f64,
|
||||
pub imbalance: f64,
|
||||
pub direction: i32,
|
||||
}
|
||||
|
||||
#[napi(js_name = "ImbalanceBars")]
|
||||
pub struct ImbalanceBarsNode {
|
||||
inner: wc::ImbalanceBars,
|
||||
}
|
||||
#[napi]
|
||||
impl ImbalanceBarsNode {
|
||||
#[napi(constructor)]
|
||||
pub fn new(threshold: f64) -> napi::Result<Self> {
|
||||
Ok(Self {
|
||||
inner: wc::ImbalanceBars::new(threshold).map_err(map_err)?,
|
||||
})
|
||||
}
|
||||
#[napi]
|
||||
pub fn update(
|
||||
&mut self,
|
||||
open: f64,
|
||||
high: f64,
|
||||
low: f64,
|
||||
close: f64,
|
||||
) -> napi::Result<Vec<ImbalanceBarValue>> {
|
||||
let candle = wc::Candle::new(open, high, low, close, 1.0, 0).map_err(map_err)?;
|
||||
Ok(self
|
||||
.inner
|
||||
.update(candle)
|
||||
.into_iter()
|
||||
.map(|b| ImbalanceBarValue {
|
||||
open: b.open,
|
||||
high: b.high,
|
||||
low: b.low,
|
||||
close: b.close,
|
||||
imbalance: b.imbalance,
|
||||
direction: i32::from(b.direction),
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
#[napi]
|
||||
pub fn batch(
|
||||
&mut self,
|
||||
open: Vec<f64>,
|
||||
high: Vec<f64>,
|
||||
low: Vec<f64>,
|
||||
close: Vec<f64>,
|
||||
) -> napi::Result<Vec<ImbalanceBarValue>> {
|
||||
if open.len() != high.len() || high.len() != low.len() || low.len() != close.len() {
|
||||
return Err(NapiError::from_reason(
|
||||
"open, high, low, close must be equal length".to_string(),
|
||||
));
|
||||
}
|
||||
let mut out = Vec::new();
|
||||
for i in 0..open.len() {
|
||||
let candle =
|
||||
wc::Candle::new(open[i], high[i], low[i], close[i], 1.0, 0).map_err(map_err)?;
|
||||
for b in self.inner.update(candle) {
|
||||
out.push(ImbalanceBarValue {
|
||||
open: b.open,
|
||||
high: b.high,
|
||||
low: b.low,
|
||||
close: b.close,
|
||||
imbalance: b.imbalance,
|
||||
direction: i32::from(b.direction),
|
||||
});
|
||||
}
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
#[napi]
|
||||
pub fn threshold(&self) -> f64 {
|
||||
self.inner.threshold()
|
||||
}
|
||||
#[napi]
|
||||
pub fn reset(&mut self) {
|
||||
self.inner.reset();
|
||||
}
|
||||
}
|
||||
|
||||
#[napi(object)]
|
||||
pub struct RunBarValue {
|
||||
pub open: f64,
|
||||
pub high: f64,
|
||||
pub low: f64,
|
||||
pub close: f64,
|
||||
pub length: u32,
|
||||
pub direction: i32,
|
||||
}
|
||||
|
||||
#[napi(js_name = "RunBars")]
|
||||
pub struct RunBarsNode {
|
||||
inner: wc::RunBars,
|
||||
}
|
||||
#[napi]
|
||||
impl RunBarsNode {
|
||||
#[napi(constructor)]
|
||||
pub fn new(run_length: u32) -> napi::Result<Self> {
|
||||
Ok(Self {
|
||||
inner: wc::RunBars::new(run_length as usize).map_err(map_err)?,
|
||||
})
|
||||
}
|
||||
#[napi]
|
||||
pub fn update(
|
||||
&mut self,
|
||||
open: f64,
|
||||
high: f64,
|
||||
low: f64,
|
||||
close: f64,
|
||||
) -> napi::Result<Vec<RunBarValue>> {
|
||||
let candle = wc::Candle::new(open, high, low, close, 1.0, 0).map_err(map_err)?;
|
||||
Ok(self
|
||||
.inner
|
||||
.update(candle)
|
||||
.into_iter()
|
||||
.map(|b| RunBarValue {
|
||||
open: b.open,
|
||||
high: b.high,
|
||||
low: b.low,
|
||||
close: b.close,
|
||||
length: b.length as u32,
|
||||
direction: i32::from(b.direction),
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
#[napi]
|
||||
pub fn batch(
|
||||
&mut self,
|
||||
open: Vec<f64>,
|
||||
high: Vec<f64>,
|
||||
low: Vec<f64>,
|
||||
close: Vec<f64>,
|
||||
) -> napi::Result<Vec<RunBarValue>> {
|
||||
if open.len() != high.len() || high.len() != low.len() || low.len() != close.len() {
|
||||
return Err(NapiError::from_reason(
|
||||
"open, high, low, close must be equal length".to_string(),
|
||||
));
|
||||
}
|
||||
let mut out = Vec::new();
|
||||
for i in 0..open.len() {
|
||||
let candle =
|
||||
wc::Candle::new(open[i], high[i], low[i], close[i], 1.0, 0).map_err(map_err)?;
|
||||
for b in self.inner.update(candle) {
|
||||
out.push(RunBarValue {
|
||||
open: b.open,
|
||||
high: b.high,
|
||||
low: b.low,
|
||||
close: b.close,
|
||||
length: b.length as u32,
|
||||
direction: i32::from(b.direction),
|
||||
});
|
||||
}
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
#[napi(js_name = "runLength")]
|
||||
pub fn run_length(&self) -> u32 {
|
||||
self.inner.run_length() as u32
|
||||
}
|
||||
#[napi]
|
||||
pub fn reset(&mut self) {
|
||||
self.inner.reset();
|
||||
}
|
||||
}
|
||||
|
||||
#[napi(object)]
|
||||
pub struct LineBreakBarValue {
|
||||
pub open: f64,
|
||||
pub close: f64,
|
||||
pub direction: i32,
|
||||
}
|
||||
|
||||
#[napi(js_name = "ThreeLineBreakBars")]
|
||||
pub struct ThreeLineBreakBarsNode {
|
||||
inner: wc::ThreeLineBreakBars,
|
||||
}
|
||||
#[napi]
|
||||
impl ThreeLineBreakBarsNode {
|
||||
#[napi(constructor)]
|
||||
pub fn new(lines: u32) -> napi::Result<Self> {
|
||||
Ok(Self {
|
||||
inner: wc::ThreeLineBreakBars::new(lines as usize).map_err(map_err)?,
|
||||
})
|
||||
}
|
||||
#[napi]
|
||||
pub fn update(&mut self, close: f64) -> napi::Result<Vec<LineBreakBarValue>> {
|
||||
let candle = wc::Candle::new(close, close, close, close, 1.0, 0).map_err(map_err)?;
|
||||
Ok(self
|
||||
.inner
|
||||
.update(candle)
|
||||
.into_iter()
|
||||
.map(|b| LineBreakBarValue {
|
||||
open: b.open,
|
||||
close: b.close,
|
||||
direction: i32::from(b.direction),
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
#[napi]
|
||||
pub fn batch(&mut self, close: Vec<f64>) -> napi::Result<Vec<LineBreakBarValue>> {
|
||||
let mut out = Vec::new();
|
||||
for price in close {
|
||||
let candle = wc::Candle::new(price, price, price, price, 1.0, 0).map_err(map_err)?;
|
||||
for b in self.inner.update(candle) {
|
||||
out.push(LineBreakBarValue {
|
||||
open: b.open,
|
||||
close: b.close,
|
||||
direction: i32::from(b.direction),
|
||||
});
|
||||
}
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
#[napi]
|
||||
pub fn lines(&self) -> u32 {
|
||||
self.inner.lines() as u32
|
||||
}
|
||||
#[napi]
|
||||
pub fn reset(&mut self) {
|
||||
self.inner.reset();
|
||||
}
|
||||
}
|
||||
|
||||
#[napi(js_name = "Alpha")]
|
||||
pub struct AlphaNode {
|
||||
inner: wc::Alpha,
|
||||
|
||||
@@ -9,7 +9,8 @@
|
||||
system dependencies, no C build tooling.**
|
||||
|
||||
Wickra is a multi-language technical-analysis library with a Rust core and
|
||||
bindings for Python, Node.js, and WebAssembly. Every indicator is an O(1)
|
||||
bindings for Python, Node.js and WebAssembly, plus a C ABI for C/C++, C#, Go and any
|
||||
other C-capable language. Every indicator is an O(1)
|
||||
streaming state machine, so live trading bots and historical backtests share
|
||||
the exact same implementation. This package is the Python binding (PyO3); it
|
||||
exposes 200+ streaming-first indicators across sixteen families.
|
||||
@@ -54,8 +55,9 @@ the main repository and documentation site:
|
||||
- **Docs** (quickstarts, cookbook, TA-Lib migration): <https://docs.wickra.org>
|
||||
- **Runnable examples:** [`examples/python/`](https://github.com/wickra-lib/wickra/tree/main/examples/python)
|
||||
|
||||
Wickra ships four bindings — Python, Node.js, WebAssembly, and Rust — that all
|
||||
expose the same indicators from the shared, `unsafe`-forbidden Rust core.
|
||||
Wickra ships native bindings for Python, Node.js, WebAssembly and Rust, plus a
|
||||
C ABI hub that any C-capable language (C, C++, Go, C#, Java, R) links against —
|
||||
all exposing the same indicators from the shared, `unsafe`-forbidden Rust core.
|
||||
|
||||
## Disclaimer
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ build-backend = "maturin"
|
||||
|
||||
[project]
|
||||
name = "wickra"
|
||||
version = "0.7.2"
|
||||
version = "0.7.7"
|
||||
description = "Streaming-first technical indicators: incremental, fast, install-free."
|
||||
readme = "README.md"
|
||||
license = "MIT OR Apache-2.0"
|
||||
|
||||
@@ -25,6 +25,15 @@ from __future__ import annotations
|
||||
|
||||
from ._wickra import (
|
||||
__version__,
|
||||
M2Measure,
|
||||
UpsidePotentialRatio,
|
||||
GainToPainRatio,
|
||||
CommonSenseRatio,
|
||||
KRatio,
|
||||
TailRatio,
|
||||
MartinRatio,
|
||||
BurkeRatio,
|
||||
SterlingRatio,
|
||||
AUTOCORRPGRAM,
|
||||
EVENBETTERSINE,
|
||||
BANDPASS,
|
||||
@@ -361,6 +370,13 @@ from ._wickra import (
|
||||
InitialBalance,
|
||||
OpeningRange,
|
||||
# Alt-Chart Bars
|
||||
ThreeLineBreakBars,
|
||||
RunBars,
|
||||
ImbalanceBars,
|
||||
DollarBars,
|
||||
VolumeBars,
|
||||
TickBars,
|
||||
RangeBars,
|
||||
RenkoBars,
|
||||
KagiBars,
|
||||
PointAndFigureBars,
|
||||
@@ -552,6 +568,15 @@ from ._wickra import (
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"M2Measure",
|
||||
"UpsidePotentialRatio",
|
||||
"GainToPainRatio",
|
||||
"CommonSenseRatio",
|
||||
"KRatio",
|
||||
"TailRatio",
|
||||
"MartinRatio",
|
||||
"BurkeRatio",
|
||||
"SterlingRatio",
|
||||
"AUTOCORRPGRAM",
|
||||
"EVENBETTERSINE",
|
||||
"BANDPASS",
|
||||
@@ -889,6 +914,13 @@ __all__ = [
|
||||
"InitialBalance",
|
||||
"OpeningRange",
|
||||
# Alt-Chart Bars
|
||||
"ThreeLineBreakBars",
|
||||
"RunBars",
|
||||
"ImbalanceBars",
|
||||
"DollarBars",
|
||||
"VolumeBars",
|
||||
"TickBars",
|
||||
"RangeBars",
|
||||
"RenkoBars",
|
||||
"KagiBars",
|
||||
"PointAndFigureBars",
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -45,6 +45,15 @@ def ohlcv() -> tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray]:
|
||||
# --- Scalar (f64 -> f64) indicators ---------------------------------------
|
||||
|
||||
SCALAR = [
|
||||
(ta.M2Measure, (20, 0.0, 0.02)),
|
||||
(ta.UpsidePotentialRatio, (20, 0.0)),
|
||||
(ta.GainToPainRatio, (12,)),
|
||||
(ta.CommonSenseRatio, (20,)),
|
||||
(ta.KRatio, (30,)),
|
||||
(ta.TailRatio, (20,)),
|
||||
(ta.MartinRatio, (14,)),
|
||||
(ta.BurkeRatio, (12,)),
|
||||
(ta.SterlingRatio, (12,)),
|
||||
(ta.AUTOCORRPGRAM, (10, 48)),
|
||||
(ta.EVENBETTERSINE, (40, 10)),
|
||||
(ta.BANDPASS, (20, 0.3)),
|
||||
@@ -4228,3 +4237,83 @@ def test_bar_builders_reset():
|
||||
r.update(15.0)
|
||||
r.reset()
|
||||
assert r.update(50.0) == [] # re-seeds after reset
|
||||
|
||||
|
||||
def test_range_bars_reference():
|
||||
rb = ta.RangeBars(1.0)
|
||||
assert rb.update(10.0) == [] # seed
|
||||
assert rb.update(13.0) == [(10.0, 11.0, 1), (11.0, 12.0, 1), (12.0, 13.0, 1)]
|
||||
|
||||
|
||||
def test_range_bars_batch_shape():
|
||||
rb = ta.RangeBars(1.0)
|
||||
out = rb.batch(np.array([10.0, 11.0, 12.0, 13.0]))
|
||||
assert out.shape == (3, 3)
|
||||
np.testing.assert_allclose(out[:, 2], [1.0, 1.0, 1.0])
|
||||
|
||||
|
||||
def test_tick_bars_reference():
|
||||
tb = ta.TickBars(2)
|
||||
assert tb.update(10.0, 11.0, 9.0, 10.5, 100.0) == []
|
||||
out = tb.update(10.5, 12.0, 10.0, 11.0, 150.0)
|
||||
assert len(out) == 1
|
||||
assert out[0] == (10.0, 12.0, 9.0, 11.0, 250.0)
|
||||
|
||||
|
||||
def test_tick_bars_batch_shape():
|
||||
tb = ta.TickBars(2)
|
||||
col = np.array([10.0, 10.0, 10.0, 10.0])
|
||||
vol = np.array([1.0, 1.0, 1.0, 1.0])
|
||||
out = tb.batch(col, col, col, col, vol)
|
||||
assert out.shape == (2, 5)
|
||||
|
||||
|
||||
def test_volume_bars_reference():
|
||||
vb = ta.VolumeBars(100.0)
|
||||
assert vb.update(10.0, 10.0, 10.0, 10.0, 60.0) == []
|
||||
out = vb.update(10.5, 10.5, 10.5, 10.5, 60.0)
|
||||
assert len(out) == 1
|
||||
assert out[0][4] == 120.0 # accumulated volume
|
||||
|
||||
|
||||
def test_dollar_bars_reference():
|
||||
db = ta.DollarBars(1000.0)
|
||||
assert db.update(10.0, 10.0, 10.0, 10.0, 60.0) == [] # 600
|
||||
out = db.update(10.0, 10.0, 10.0, 10.0, 60.0) # 1200 >= 1000
|
||||
assert len(out) == 1
|
||||
assert out[0][4] == 120.0 # volume
|
||||
assert out[0][5] == 1200.0 # traded value
|
||||
|
||||
|
||||
def test_imbalance_bars_reference():
|
||||
ib = ta.ImbalanceBars(3.0)
|
||||
assert ib.update(10.0, 10.0, 10.0, 10.0) == [] # seed
|
||||
ib.update(11.0, 11.0, 11.0, 11.0) # +1
|
||||
ib.update(12.0, 12.0, 12.0, 12.0) # +2
|
||||
out = ib.update(13.0, 13.0, 13.0, 13.0) # +3 -> close
|
||||
assert len(out) == 1
|
||||
assert out[0][4] == 3.0 # imbalance
|
||||
assert out[0][5] == 1 # direction
|
||||
|
||||
|
||||
def test_run_bars_reference():
|
||||
rb = ta.RunBars(3)
|
||||
assert rb.update(10.0, 10.0, 10.0, 10.0) == [] # seed
|
||||
rb.update(11.0, 11.0, 11.0, 11.0) # run 1
|
||||
rb.update(12.0, 12.0, 12.0, 12.0) # run 2
|
||||
out = rb.update(13.0, 13.0, 13.0, 13.0) # run 3 -> close
|
||||
assert len(out) == 1
|
||||
assert out[0][4] == 3 # length
|
||||
assert out[0][5] == 1 # direction
|
||||
|
||||
|
||||
def test_three_line_break_bars_reference():
|
||||
tlb = ta.ThreeLineBreakBars(3)
|
||||
assert tlb.update(10.0) == [] # seed
|
||||
assert tlb.update(11.0) == [(10.0, 11.0, 1)]
|
||||
|
||||
|
||||
def test_three_line_break_bars_batch_shape():
|
||||
tlb = ta.ThreeLineBreakBars(3)
|
||||
out = tlb.batch(np.array([10.0, 11.0, 12.0, 13.0]))
|
||||
assert out.shape[1] == 3
|
||||
|
||||
@@ -9,7 +9,8 @@
|
||||
wickra-wasm` — pure WebAssembly, runs anywhere a modern JS engine does.**
|
||||
|
||||
Wickra is a multi-language technical-analysis library with a Rust core and
|
||||
bindings for Python, Node.js, and WebAssembly. Every indicator is an O(1)
|
||||
bindings for Python, Node.js and WebAssembly, plus a C ABI for C/C++, C#, Go and any
|
||||
other C-capable language. Every indicator is an O(1)
|
||||
streaming state machine, so live trading dashboards and historical backtests
|
||||
share the exact same implementation. This package is the WebAssembly binding
|
||||
(wasm-bindgen, built for the `web` target); it exposes 200+ streaming-first
|
||||
@@ -54,8 +55,9 @@ the main repository and documentation site:
|
||||
- **Docs** (quickstarts, cookbook, TA-Lib migration): <https://docs.wickra.org>
|
||||
- **Runnable browser examples:** [`examples/wasm/`](https://github.com/wickra-lib/wickra/tree/main/examples/wasm)
|
||||
|
||||
Wickra ships four bindings — Python, Node.js, WebAssembly, and Rust — that all
|
||||
expose the same indicators from the shared, `unsafe`-forbidden Rust core.
|
||||
Wickra ships native bindings for Python, Node.js, WebAssembly and Rust, plus a
|
||||
C ABI hub that any C-capable language (C, C++, Go, C#, Java, R) links against —
|
||||
all exposing the same indicators from the shared, `unsafe`-forbidden Rust core.
|
||||
|
||||
## Disclaimer
|
||||
|
||||
|
||||
@@ -12755,6 +12755,15 @@ wasm_scalar_indicator!(WasmUniversalOscillator, "UNIVERSALOSC", wc::UniversalOsc
|
||||
wasm_scalar_indicator!(WasmBandpassFilter, "BANDPASS", wc::BandpassFilter, period: usize, bandwidth: f64);
|
||||
wasm_scalar_indicator!(WasmEvenBetterSinewave, "EVENBETTERSINE", wc::EvenBetterSinewave, hp_period: usize, ssf_length: usize);
|
||||
wasm_scalar_indicator!(WasmAutocorrelationPeriodogram, "AUTOCORRPGRAM", wc::AutocorrelationPeriodogram, min_period: usize, max_period: usize);
|
||||
wasm_scalar_indicator!(WasmSterlingRatio, "SterlingRatio", wc::SterlingRatio, period: usize);
|
||||
wasm_scalar_indicator!(WasmBurkeRatio, "BurkeRatio", wc::BurkeRatio, period: usize);
|
||||
wasm_scalar_indicator!(WasmMartinRatio, "MartinRatio", wc::MartinRatio, period: usize);
|
||||
wasm_scalar_indicator!(WasmTailRatio, "TailRatio", wc::TailRatio, period: usize);
|
||||
wasm_scalar_indicator!(WasmKRatio, "KRatio", wc::KRatio, period: usize);
|
||||
wasm_scalar_indicator!(WasmCommonSenseRatio, "CommonSenseRatio", wc::CommonSenseRatio, period: usize);
|
||||
wasm_scalar_indicator!(WasmGainToPainRatio, "GainToPainRatio", wc::GainToPainRatio, period: usize);
|
||||
wasm_scalar_indicator!(WasmUpsidePotentialRatio, "UpsidePotentialRatio", wc::UpsidePotentialRatio, period: usize, mar: f64);
|
||||
wasm_scalar_indicator!(WasmM2Measure, "M2Measure", wc::M2Measure, period: usize, risk_free: f64, benchmark_stddev: f64);
|
||||
|
||||
// --- VolatilityCone: Candle in, struct out (current/min/median/max/percentile) ---
|
||||
|
||||
@@ -13154,6 +13163,310 @@ impl WasmPointAndFigureBars {
|
||||
}
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = RangeBars)]
|
||||
pub struct WasmRangeBars {
|
||||
inner: wc::RangeBars,
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_class = RangeBars)]
|
||||
impl WasmRangeBars {
|
||||
#[wasm_bindgen(constructor)]
|
||||
pub fn new(range: f64) -> Result<WasmRangeBars, JsError> {
|
||||
Ok(Self {
|
||||
inner: wc::RangeBars::new(range).map_err(map_err)?,
|
||||
})
|
||||
}
|
||||
/// Returns an array of `{ open, close, direction }` bars completed on this close.
|
||||
pub fn update(&mut self, close: f64) -> Result<Array, JsError> {
|
||||
let candle = wc::Candle::new(close, close, close, close, 1.0, 0).map_err(map_err)?;
|
||||
let arr = Array::new();
|
||||
for b in self.inner.update(candle) {
|
||||
let obj = Object::new();
|
||||
Reflect::set(&obj, &"open".into(), &b.open.into()).ok();
|
||||
Reflect::set(&obj, &"close".into(), &b.close.into()).ok();
|
||||
Reflect::set(&obj, &"direction".into(), &f64::from(b.direction).into()).ok();
|
||||
arr.push(&obj);
|
||||
}
|
||||
Ok(arr)
|
||||
}
|
||||
pub fn batch(&mut self, close: &[f64]) -> Result<Array, JsError> {
|
||||
let arr = Array::new();
|
||||
for &price in close {
|
||||
let candle = wc::Candle::new(price, price, price, price, 1.0, 0).map_err(map_err)?;
|
||||
for b in self.inner.update(candle) {
|
||||
let obj = Object::new();
|
||||
Reflect::set(&obj, &"open".into(), &b.open.into()).ok();
|
||||
Reflect::set(&obj, &"close".into(), &b.close.into()).ok();
|
||||
Reflect::set(&obj, &"direction".into(), &f64::from(b.direction).into()).ok();
|
||||
arr.push(&obj);
|
||||
}
|
||||
}
|
||||
Ok(arr)
|
||||
}
|
||||
pub fn range(&self) -> f64 {
|
||||
self.inner.range()
|
||||
}
|
||||
pub fn reset(&mut self) {
|
||||
self.inner.reset();
|
||||
}
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = TickBars)]
|
||||
pub struct WasmTickBars {
|
||||
inner: wc::TickBars,
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_class = TickBars)]
|
||||
impl WasmTickBars {
|
||||
#[wasm_bindgen(constructor)]
|
||||
pub fn new(ticks: usize) -> Result<WasmTickBars, JsError> {
|
||||
Ok(Self {
|
||||
inner: wc::TickBars::new(ticks).map_err(map_err)?,
|
||||
})
|
||||
}
|
||||
/// Returns an array of `{ open, high, low, close, volume }` bars completed on this candle.
|
||||
pub fn update(
|
||||
&mut self,
|
||||
open: f64,
|
||||
high: f64,
|
||||
low: f64,
|
||||
close: f64,
|
||||
volume: f64,
|
||||
) -> Result<Array, JsError> {
|
||||
let candle = wc::Candle::new(open, high, low, close, volume, 0).map_err(map_err)?;
|
||||
let arr = Array::new();
|
||||
for b in self.inner.update(candle) {
|
||||
let obj = Object::new();
|
||||
Reflect::set(&obj, &"open".into(), &b.open.into()).ok();
|
||||
Reflect::set(&obj, &"high".into(), &b.high.into()).ok();
|
||||
Reflect::set(&obj, &"low".into(), &b.low.into()).ok();
|
||||
Reflect::set(&obj, &"close".into(), &b.close.into()).ok();
|
||||
Reflect::set(&obj, &"volume".into(), &b.volume.into()).ok();
|
||||
arr.push(&obj);
|
||||
}
|
||||
Ok(arr)
|
||||
}
|
||||
pub fn ticks(&self) -> usize {
|
||||
self.inner.ticks()
|
||||
}
|
||||
pub fn reset(&mut self) {
|
||||
self.inner.reset();
|
||||
}
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = VolumeBars)]
|
||||
pub struct WasmVolumeBars {
|
||||
inner: wc::VolumeBars,
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_class = VolumeBars)]
|
||||
impl WasmVolumeBars {
|
||||
#[wasm_bindgen(constructor)]
|
||||
pub fn new(volume_per_bar: f64) -> Result<WasmVolumeBars, JsError> {
|
||||
Ok(Self {
|
||||
inner: wc::VolumeBars::new(volume_per_bar).map_err(map_err)?,
|
||||
})
|
||||
}
|
||||
/// Returns an array of `{ open, high, low, close, volume }` bars completed on this candle.
|
||||
pub fn update(
|
||||
&mut self,
|
||||
open: f64,
|
||||
high: f64,
|
||||
low: f64,
|
||||
close: f64,
|
||||
volume: f64,
|
||||
) -> Result<Array, JsError> {
|
||||
let candle = wc::Candle::new(open, high, low, close, volume, 0).map_err(map_err)?;
|
||||
let arr = Array::new();
|
||||
for b in self.inner.update(candle) {
|
||||
let obj = Object::new();
|
||||
Reflect::set(&obj, &"open".into(), &b.open.into()).ok();
|
||||
Reflect::set(&obj, &"high".into(), &b.high.into()).ok();
|
||||
Reflect::set(&obj, &"low".into(), &b.low.into()).ok();
|
||||
Reflect::set(&obj, &"close".into(), &b.close.into()).ok();
|
||||
Reflect::set(&obj, &"volume".into(), &b.volume.into()).ok();
|
||||
arr.push(&obj);
|
||||
}
|
||||
Ok(arr)
|
||||
}
|
||||
#[wasm_bindgen(js_name = volumePerBar)]
|
||||
pub fn volume_per_bar(&self) -> f64 {
|
||||
self.inner.volume_per_bar()
|
||||
}
|
||||
pub fn reset(&mut self) {
|
||||
self.inner.reset();
|
||||
}
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = DollarBars)]
|
||||
pub struct WasmDollarBars {
|
||||
inner: wc::DollarBars,
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_class = DollarBars)]
|
||||
impl WasmDollarBars {
|
||||
#[wasm_bindgen(constructor)]
|
||||
pub fn new(dollar_per_bar: f64) -> Result<WasmDollarBars, JsError> {
|
||||
Ok(Self {
|
||||
inner: wc::DollarBars::new(dollar_per_bar).map_err(map_err)?,
|
||||
})
|
||||
}
|
||||
/// Returns an array of `{ open, high, low, close, volume, dollar }` bars completed on this candle.
|
||||
pub fn update(
|
||||
&mut self,
|
||||
open: f64,
|
||||
high: f64,
|
||||
low: f64,
|
||||
close: f64,
|
||||
volume: f64,
|
||||
) -> Result<Array, JsError> {
|
||||
let candle = wc::Candle::new(open, high, low, close, volume, 0).map_err(map_err)?;
|
||||
let arr = Array::new();
|
||||
for b in self.inner.update(candle) {
|
||||
let obj = Object::new();
|
||||
Reflect::set(&obj, &"open".into(), &b.open.into()).ok();
|
||||
Reflect::set(&obj, &"high".into(), &b.high.into()).ok();
|
||||
Reflect::set(&obj, &"low".into(), &b.low.into()).ok();
|
||||
Reflect::set(&obj, &"close".into(), &b.close.into()).ok();
|
||||
Reflect::set(&obj, &"volume".into(), &b.volume.into()).ok();
|
||||
Reflect::set(&obj, &"dollar".into(), &b.dollar.into()).ok();
|
||||
arr.push(&obj);
|
||||
}
|
||||
Ok(arr)
|
||||
}
|
||||
#[wasm_bindgen(js_name = dollarPerBar)]
|
||||
pub fn dollar_per_bar(&self) -> f64 {
|
||||
self.inner.dollar_per_bar()
|
||||
}
|
||||
pub fn reset(&mut self) {
|
||||
self.inner.reset();
|
||||
}
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = ImbalanceBars)]
|
||||
pub struct WasmImbalanceBars {
|
||||
inner: wc::ImbalanceBars,
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_class = ImbalanceBars)]
|
||||
impl WasmImbalanceBars {
|
||||
#[wasm_bindgen(constructor)]
|
||||
pub fn new(threshold: f64) -> Result<WasmImbalanceBars, JsError> {
|
||||
Ok(Self {
|
||||
inner: wc::ImbalanceBars::new(threshold).map_err(map_err)?,
|
||||
})
|
||||
}
|
||||
/// Returns an array of `{ open, high, low, close, imbalance, direction }` bars completed on this candle.
|
||||
pub fn update(&mut self, open: f64, high: f64, low: f64, close: f64) -> Result<Array, JsError> {
|
||||
let candle = wc::Candle::new(open, high, low, close, 1.0, 0).map_err(map_err)?;
|
||||
let arr = Array::new();
|
||||
for b in self.inner.update(candle) {
|
||||
let obj = Object::new();
|
||||
Reflect::set(&obj, &"open".into(), &b.open.into()).ok();
|
||||
Reflect::set(&obj, &"high".into(), &b.high.into()).ok();
|
||||
Reflect::set(&obj, &"low".into(), &b.low.into()).ok();
|
||||
Reflect::set(&obj, &"close".into(), &b.close.into()).ok();
|
||||
Reflect::set(&obj, &"imbalance".into(), &b.imbalance.into()).ok();
|
||||
Reflect::set(&obj, &"direction".into(), &f64::from(b.direction).into()).ok();
|
||||
arr.push(&obj);
|
||||
}
|
||||
Ok(arr)
|
||||
}
|
||||
pub fn threshold(&self) -> f64 {
|
||||
self.inner.threshold()
|
||||
}
|
||||
pub fn reset(&mut self) {
|
||||
self.inner.reset();
|
||||
}
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = RunBars)]
|
||||
pub struct WasmRunBars {
|
||||
inner: wc::RunBars,
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_class = RunBars)]
|
||||
impl WasmRunBars {
|
||||
#[wasm_bindgen(constructor)]
|
||||
pub fn new(run_length: usize) -> Result<WasmRunBars, JsError> {
|
||||
Ok(Self {
|
||||
inner: wc::RunBars::new(run_length).map_err(map_err)?,
|
||||
})
|
||||
}
|
||||
/// Returns an array of `{ open, high, low, close, length, direction }` bars completed on this candle.
|
||||
pub fn update(&mut self, open: f64, high: f64, low: f64, close: f64) -> Result<Array, JsError> {
|
||||
let candle = wc::Candle::new(open, high, low, close, 1.0, 0).map_err(map_err)?;
|
||||
let arr = Array::new();
|
||||
for b in self.inner.update(candle) {
|
||||
let obj = Object::new();
|
||||
Reflect::set(&obj, &"open".into(), &b.open.into()).ok();
|
||||
Reflect::set(&obj, &"high".into(), &b.high.into()).ok();
|
||||
Reflect::set(&obj, &"low".into(), &b.low.into()).ok();
|
||||
Reflect::set(&obj, &"close".into(), &b.close.into()).ok();
|
||||
#[allow(clippy::cast_precision_loss)]
|
||||
Reflect::set(&obj, &"length".into(), &(b.length as f64).into()).ok();
|
||||
Reflect::set(&obj, &"direction".into(), &f64::from(b.direction).into()).ok();
|
||||
arr.push(&obj);
|
||||
}
|
||||
Ok(arr)
|
||||
}
|
||||
#[wasm_bindgen(js_name = runLength)]
|
||||
pub fn run_length(&self) -> usize {
|
||||
self.inner.run_length()
|
||||
}
|
||||
pub fn reset(&mut self) {
|
||||
self.inner.reset();
|
||||
}
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = ThreeLineBreakBars)]
|
||||
pub struct WasmThreeLineBreakBars {
|
||||
inner: wc::ThreeLineBreakBars,
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_class = ThreeLineBreakBars)]
|
||||
impl WasmThreeLineBreakBars {
|
||||
#[wasm_bindgen(constructor)]
|
||||
pub fn new(lines: usize) -> Result<WasmThreeLineBreakBars, JsError> {
|
||||
Ok(Self {
|
||||
inner: wc::ThreeLineBreakBars::new(lines).map_err(map_err)?,
|
||||
})
|
||||
}
|
||||
/// Returns an array of `{ open, close, direction }` bars completed on this close.
|
||||
pub fn update(&mut self, close: f64) -> Result<Array, JsError> {
|
||||
let candle = wc::Candle::new(close, close, close, close, 1.0, 0).map_err(map_err)?;
|
||||
let arr = Array::new();
|
||||
for b in self.inner.update(candle) {
|
||||
let obj = Object::new();
|
||||
Reflect::set(&obj, &"open".into(), &b.open.into()).ok();
|
||||
Reflect::set(&obj, &"close".into(), &b.close.into()).ok();
|
||||
Reflect::set(&obj, &"direction".into(), &f64::from(b.direction).into()).ok();
|
||||
arr.push(&obj);
|
||||
}
|
||||
Ok(arr)
|
||||
}
|
||||
pub fn batch(&mut self, close: &[f64]) -> Result<Array, JsError> {
|
||||
let arr = Array::new();
|
||||
for &price in close {
|
||||
let candle = wc::Candle::new(price, price, price, price, 1.0, 0).map_err(map_err)?;
|
||||
for b in self.inner.update(candle) {
|
||||
let obj = Object::new();
|
||||
Reflect::set(&obj, &"open".into(), &b.open.into()).ok();
|
||||
Reflect::set(&obj, &"close".into(), &b.close.into()).ok();
|
||||
Reflect::set(&obj, &"direction".into(), &f64::from(b.direction).into()).ok();
|
||||
arr.push(&obj);
|
||||
}
|
||||
}
|
||||
Ok(arr)
|
||||
}
|
||||
pub fn lines(&self) -> usize {
|
||||
self.inner.lines()
|
||||
}
|
||||
pub fn reset(&mut self) {
|
||||
self.inner.reset();
|
||||
}
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = Alpha)]
|
||||
pub struct WasmAlpha {
|
||||
inner: wc::Alpha,
|
||||
|
||||
@@ -0,0 +1,218 @@
|
||||
//! Burke Ratio — mean return over the square root of the summed squared drawdowns.
|
||||
|
||||
use std::collections::VecDeque;
|
||||
|
||||
use crate::error::{Error, Result};
|
||||
use crate::traits::Indicator;
|
||||
|
||||
/// Burke Ratio over a trailing window of `period` returns.
|
||||
///
|
||||
/// ```text
|
||||
/// equity_t = Π_{i<=t} (1 + return_i) (compounded curve)
|
||||
/// peak_t = max_{s<=t} equity_s
|
||||
/// dd_t = (peak_t − equity_t) / peak_t (fractional drawdown, >= 0)
|
||||
/// Burke = mean(returns) / sqrt( Σ dd_t² )
|
||||
/// ```
|
||||
///
|
||||
/// The Burke Ratio divides the average per-period return by the **Euclidean norm of
|
||||
/// the drawdowns** — the square root of the *sum* of squared drawdowns. Squaring
|
||||
/// penalises deep drawdowns far more than shallow ones, and summing (rather than
|
||||
/// averaging) means the denominator grows with both the depth and the *number* of
|
||||
/// drawdowns. This makes Burke the most outlier-sensitive of Wickra's three
|
||||
/// drawdown ratios: where the [`SterlingRatio`](crate::SterlingRatio) averages raw
|
||||
/// drawdowns and shrugs off a single crater, Burke makes that crater dominate.
|
||||
/// The [`MartinRatio`](crate::MartinRatio) sits between them with a root-*mean*
|
||||
/// square of percentage drawdowns. A window that never draws down has a zero
|
||||
/// denominator and the indicator reports `0.0`.
|
||||
///
|
||||
/// The first value lands after `period` returns; each `update` rebuilds the equity
|
||||
/// curve over the window (O(period)), which is O(1) in the length of the overall
|
||||
/// series.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// use wickra_core::{Indicator, BurkeRatio};
|
||||
///
|
||||
/// let mut indicator = BurkeRatio::new(12).unwrap();
|
||||
/// let mut last = None;
|
||||
/// for i in 0..24 {
|
||||
/// last = indicator.update((f64::from(i) * 0.5).sin() * 0.05);
|
||||
/// }
|
||||
/// assert!(last.is_some());
|
||||
/// ```
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct BurkeRatio {
|
||||
period: usize,
|
||||
window: VecDeque<f64>,
|
||||
}
|
||||
|
||||
impl BurkeRatio {
|
||||
/// Construct a Burke Ratio over `period` returns.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns [`Error::InvalidPeriod`] if `period < 2`.
|
||||
pub fn new(period: usize) -> Result<Self> {
|
||||
if period < 2 {
|
||||
return Err(Error::InvalidPeriod {
|
||||
message: "burke ratio needs period >= 2",
|
||||
});
|
||||
}
|
||||
Ok(Self {
|
||||
period,
|
||||
window: VecDeque::with_capacity(period),
|
||||
})
|
||||
}
|
||||
|
||||
/// Configured window of returns.
|
||||
pub const fn period(&self) -> usize {
|
||||
self.period
|
||||
}
|
||||
|
||||
fn compute(&self) -> f64 {
|
||||
#[allow(clippy::cast_precision_loss)]
|
||||
let length = self.window.len() as f64;
|
||||
let mut sum_return = 0.0;
|
||||
let mut sum_drawdown_sq = 0.0;
|
||||
let mut equity = 1.0;
|
||||
let mut peak: f64 = 1.0;
|
||||
for ret in &self.window {
|
||||
sum_return += *ret;
|
||||
equity *= 1.0 + *ret;
|
||||
peak = peak.max(equity);
|
||||
let drawdown = (peak - equity) / peak;
|
||||
sum_drawdown_sq += drawdown * drawdown;
|
||||
}
|
||||
let denom = sum_drawdown_sq.sqrt();
|
||||
if denom > 0.0 {
|
||||
(sum_return / length) / denom
|
||||
} else {
|
||||
0.0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Indicator for BurkeRatio {
|
||||
type Input = f64;
|
||||
type Output = f64;
|
||||
|
||||
fn update(&mut self, ret: f64) -> Option<f64> {
|
||||
if !ret.is_finite() {
|
||||
return None;
|
||||
}
|
||||
if self.window.len() == self.period {
|
||||
self.window.pop_front();
|
||||
}
|
||||
self.window.push_back(ret);
|
||||
if self.window.len() < self.period {
|
||||
return None;
|
||||
}
|
||||
Some(self.compute())
|
||||
}
|
||||
|
||||
fn reset(&mut self) {
|
||||
self.window.clear();
|
||||
}
|
||||
|
||||
fn warmup_period(&self) -> usize {
|
||||
self.period
|
||||
}
|
||||
|
||||
fn is_ready(&self) -> bool {
|
||||
self.window.len() == self.period
|
||||
}
|
||||
|
||||
fn name(&self) -> &'static str {
|
||||
"BurkeRatio"
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::traits::BatchExt;
|
||||
use approx::assert_relative_eq;
|
||||
|
||||
#[test]
|
||||
fn rejects_period_less_than_two() {
|
||||
assert!(matches!(
|
||||
BurkeRatio::new(1),
|
||||
Err(Error::InvalidPeriod { .. })
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn accessors_and_metadata() {
|
||||
let br = BurkeRatio::new(12).unwrap();
|
||||
assert_eq!(br.period(), 12);
|
||||
assert_eq!(br.warmup_period(), 12);
|
||||
assert_eq!(br.name(), "BurkeRatio");
|
||||
assert!(!br.is_ready());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reference_value() {
|
||||
// returns [0.1, -0.1, 0.1]: dd = [0, 0.1, 0.01].
|
||||
// Σ dd² = 0.01 + 0.0001 = 0.0101; denom = sqrt(0.0101).
|
||||
// Burke = (0.1/3) / sqrt(0.0101).
|
||||
let mut br = BurkeRatio::new(3).unwrap();
|
||||
let out = br.batch(&[0.1, -0.1, 0.1]);
|
||||
let expected = (0.1_f64 / 3.0) / (0.0101_f64).sqrt();
|
||||
assert_relative_eq!(out[2].unwrap(), expected, epsilon = 1e-9);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn no_drawdown_is_zero() {
|
||||
let mut br = BurkeRatio::new(3).unwrap();
|
||||
let last = br
|
||||
.batch(&[0.01, 0.02, 0.03])
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.last()
|
||||
.unwrap();
|
||||
assert_relative_eq!(last, 0.0, epsilon = 1e-12);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn losing_window_is_negative() {
|
||||
let mut br = BurkeRatio::new(3).unwrap();
|
||||
let last = br
|
||||
.batch(&[-0.05, -0.02, -0.03])
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.last()
|
||||
.unwrap();
|
||||
assert!(last < 0.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ignores_non_finite_input() {
|
||||
let mut br = BurkeRatio::new(3).unwrap();
|
||||
assert_eq!(br.update(0.1), None);
|
||||
assert_eq!(br.update(f64::NAN), None);
|
||||
assert_eq!(br.update(-0.1), None);
|
||||
assert!(br.update(0.1).is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reset_clears_state() {
|
||||
let mut br = BurkeRatio::new(3).unwrap();
|
||||
br.batch(&[0.1, -0.1, 0.1]);
|
||||
assert!(br.is_ready());
|
||||
br.reset();
|
||||
assert!(!br.is_ready());
|
||||
assert_eq!(br.update(0.1), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn batch_equals_streaming() {
|
||||
let rets: Vec<f64> = (0..60)
|
||||
.map(|i| (f64::from(i) * 0.25).sin() * 0.05)
|
||||
.collect();
|
||||
let batch = BurkeRatio::new(12).unwrap().batch(&rets);
|
||||
let mut streamer = BurkeRatio::new(12).unwrap();
|
||||
let streamed: Vec<_> = rets.iter().map(|r| streamer.update(*r)).collect();
|
||||
assert_eq!(batch, streamed);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,248 @@
|
||||
//! Common Sense Ratio (Schwager / Carver) — profit factor multiplied by the tail ratio.
|
||||
|
||||
use std::collections::VecDeque;
|
||||
|
||||
use crate::error::{Error, Result};
|
||||
use crate::traits::Indicator;
|
||||
|
||||
/// Common Sense Ratio over a trailing window of `period` returns.
|
||||
///
|
||||
/// ```text
|
||||
/// ProfitFactor = Σ gains / Σ |losses| over the window
|
||||
/// TailRatio = P95(returns) / |P5(returns)| over the window
|
||||
/// CSR = ProfitFactor · TailRatio
|
||||
/// ```
|
||||
///
|
||||
/// The Common Sense Ratio fuses two views of a return series into one number. The
|
||||
/// [profit factor](crate::ProfitFactor) captures the *body* of the distribution —
|
||||
/// how much you make per unit you lose on the average bar. The
|
||||
/// [`TailRatio`](crate::TailRatio) captures the *extremes* — whether the largest
|
||||
/// gains outweigh the largest losses. Multiplying them produces a ratio that is
|
||||
/// only comfortably above `1.0` when a strategy wins on both fronts: a respectable
|
||||
/// profit factor can still hide catastrophic left-tail risk, and a fat right tail
|
||||
/// means little if the body bleeds. Above `1.0` the strategy is sound on a
|
||||
/// common-sense basis; below `1.0` something — body or tail — is working against it.
|
||||
///
|
||||
/// Percentiles use linear interpolation over the sorted window. A window with no
|
||||
/// losses (zero profit-factor denominator) or no left tail (zero P5) reports `0.0`
|
||||
/// rather than dividing by zero.
|
||||
///
|
||||
/// The first value lands after `period` returns; each `update` re-sorts the window
|
||||
/// (O(period log period)), which is O(1) in the length of the overall series.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// use wickra_core::{Indicator, CommonSenseRatio};
|
||||
///
|
||||
/// let mut indicator = CommonSenseRatio::new(20).unwrap();
|
||||
/// let mut last = None;
|
||||
/// for i in 0..40 {
|
||||
/// last = indicator.update((f64::from(i) * 0.3).sin() * 0.02);
|
||||
/// }
|
||||
/// assert!(last.is_some());
|
||||
/// ```
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct CommonSenseRatio {
|
||||
period: usize,
|
||||
window: VecDeque<f64>,
|
||||
}
|
||||
|
||||
impl CommonSenseRatio {
|
||||
/// Construct a Common Sense Ratio over `period` returns.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns [`Error::InvalidPeriod`] if `period < 2` (percentiles need at least
|
||||
/// two observations).
|
||||
pub fn new(period: usize) -> Result<Self> {
|
||||
if period < 2 {
|
||||
return Err(Error::InvalidPeriod {
|
||||
message: "common sense ratio needs period >= 2",
|
||||
});
|
||||
}
|
||||
Ok(Self {
|
||||
period,
|
||||
window: VecDeque::with_capacity(period),
|
||||
})
|
||||
}
|
||||
|
||||
/// Configured window of returns.
|
||||
pub const fn period(&self) -> usize {
|
||||
self.period
|
||||
}
|
||||
|
||||
fn compute(&self) -> f64 {
|
||||
let mut gains = 0.0;
|
||||
let mut losses = 0.0;
|
||||
for ret in &self.window {
|
||||
gains += ret.max(0.0);
|
||||
losses += (-ret).max(0.0);
|
||||
}
|
||||
if losses <= 0.0 {
|
||||
return 0.0;
|
||||
}
|
||||
let mut sorted: Vec<f64> = self.window.iter().copied().collect();
|
||||
sorted.sort_unstable_by(f64::total_cmp);
|
||||
let lower_tail = percentile(&sorted, 5.0).abs();
|
||||
if lower_tail <= 0.0 {
|
||||
return 0.0;
|
||||
}
|
||||
let profit_factor = gains / losses;
|
||||
let tail_ratio = percentile(&sorted, 95.0) / lower_tail;
|
||||
profit_factor * tail_ratio
|
||||
}
|
||||
}
|
||||
|
||||
/// Linear-interpolation percentile of an ascending, non-empty slice.
|
||||
fn percentile(sorted: &[f64], pct: f64) -> f64 {
|
||||
let last_index = sorted.len() - 1;
|
||||
#[allow(clippy::cast_precision_loss)]
|
||||
let rank = pct / 100.0 * last_index as f64;
|
||||
let floor = rank.floor();
|
||||
// `rank` lies in `[0, last_index]`, so its floor is a valid in-bounds index.
|
||||
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
|
||||
let lower = floor as usize;
|
||||
if lower >= last_index {
|
||||
return sorted[last_index];
|
||||
}
|
||||
let frac = rank - floor;
|
||||
sorted[lower] + frac * (sorted[lower + 1] - sorted[lower])
|
||||
}
|
||||
|
||||
impl Indicator for CommonSenseRatio {
|
||||
type Input = f64;
|
||||
type Output = f64;
|
||||
|
||||
fn update(&mut self, ret: f64) -> Option<f64> {
|
||||
if !ret.is_finite() {
|
||||
return None;
|
||||
}
|
||||
if self.window.len() == self.period {
|
||||
self.window.pop_front();
|
||||
}
|
||||
self.window.push_back(ret);
|
||||
if self.window.len() < self.period {
|
||||
return None;
|
||||
}
|
||||
Some(self.compute())
|
||||
}
|
||||
|
||||
fn reset(&mut self) {
|
||||
self.window.clear();
|
||||
}
|
||||
|
||||
fn warmup_period(&self) -> usize {
|
||||
self.period
|
||||
}
|
||||
|
||||
fn is_ready(&self) -> bool {
|
||||
self.window.len() == self.period
|
||||
}
|
||||
|
||||
fn name(&self) -> &'static str {
|
||||
"CommonSenseRatio"
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::traits::BatchExt;
|
||||
use approx::assert_relative_eq;
|
||||
|
||||
#[test]
|
||||
fn rejects_period_less_than_two() {
|
||||
assert!(matches!(
|
||||
CommonSenseRatio::new(1),
|
||||
Err(Error::InvalidPeriod { .. })
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn accessors_and_metadata() {
|
||||
let csr = CommonSenseRatio::new(20).unwrap();
|
||||
assert_eq!(csr.period(), 20);
|
||||
assert_eq!(csr.warmup_period(), 20);
|
||||
assert_eq!(csr.name(), "CommonSenseRatio");
|
||||
assert!(!csr.is_ready());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reference_value() {
|
||||
// window [-0.04, -0.02, 0.0, 0.02, 0.04].
|
||||
// gains = 0.06, losses = 0.06 -> profit factor 1.0.
|
||||
// P95 = 0.036, |P5| = 0.036 -> tail ratio 1.0. CSR = 1.0.
|
||||
let mut csr = CommonSenseRatio::new(5).unwrap();
|
||||
let out = csr.batch(&[-0.04, -0.02, 0.0, 0.02, 0.04]);
|
||||
assert_relative_eq!(out[4].unwrap(), 1.0, epsilon = 1e-9);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn no_losses_is_zero() {
|
||||
let mut csr = CommonSenseRatio::new(3).unwrap();
|
||||
let last = csr
|
||||
.batch(&[0.01, 0.02, 0.03])
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.last()
|
||||
.unwrap();
|
||||
assert_relative_eq!(last, 0.0, epsilon = 1e-12);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn flat_window_is_zero() {
|
||||
// All zeros: no losses denominator -> zero (the gains/losses guard fires).
|
||||
let mut csr = CommonSenseRatio::new(4).unwrap();
|
||||
let last = csr.batch(&[0.0; 4]).into_iter().flatten().last().unwrap();
|
||||
assert_relative_eq!(last, 0.0, epsilon = 1e-12);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ignores_non_finite_input() {
|
||||
let mut csr = CommonSenseRatio::new(3).unwrap();
|
||||
assert_eq!(csr.update(0.01), None);
|
||||
assert_eq!(csr.update(f64::NAN), None);
|
||||
assert_eq!(csr.update(-0.02), None);
|
||||
assert!(csr.update(0.03).is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reset_clears_state() {
|
||||
let mut csr = CommonSenseRatio::new(3).unwrap();
|
||||
csr.batch(&[-0.01, 0.0, 0.02]);
|
||||
assert!(csr.is_ready());
|
||||
csr.reset();
|
||||
assert!(!csr.is_ready());
|
||||
assert_eq!(csr.update(0.01), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn batch_equals_streaming() {
|
||||
let rets: Vec<f64> = (0..60)
|
||||
.map(|i| (f64::from(i) * 0.25).sin() * 0.02)
|
||||
.collect();
|
||||
let batch = CommonSenseRatio::new(15).unwrap().batch(&rets);
|
||||
let mut streamer = CommonSenseRatio::new(15).unwrap();
|
||||
let streamed: Vec<_> = rets.iter().map(|r| streamer.update(*r)).collect();
|
||||
assert_eq!(batch, streamed);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn percentile_at_top_returns_last() {
|
||||
// The rank floor reaching the final index returns the largest element.
|
||||
assert_relative_eq!(percentile(&[1.0, 2.0, 3.0], 100.0), 3.0, epsilon = 1e-12);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn zero_lower_tail_is_zero() {
|
||||
// One loss but a 5th percentile of exactly zero: the tail term collapses
|
||||
// and the indicator reports 0.0 rather than dividing by zero. With period
|
||||
// 21 the 5% rank lands on sorted index 1, which is 0.0 here.
|
||||
let mut returns = vec![0.0; 21];
|
||||
returns[0] = -0.1;
|
||||
let mut csr = CommonSenseRatio::new(21).unwrap();
|
||||
let last = csr.batch(&returns).into_iter().flatten().last().unwrap();
|
||||
assert_relative_eq!(last, 0.0, epsilon = 1e-12);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,224 @@
|
||||
//! Dollar bar builder — close a bar each time accumulated traded value reaches a threshold.
|
||||
|
||||
use crate::error::{Error, Result};
|
||||
use crate::ohlcv::Candle;
|
||||
use crate::traits::BarBuilder;
|
||||
|
||||
/// One completed dollar bar (an OHLC aggregate spanning ~`dollar_per_bar` of traded value).
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub struct DollarBar {
|
||||
/// Open of the first candle in the bar.
|
||||
pub open: f64,
|
||||
/// Highest high across the bar.
|
||||
pub high: f64,
|
||||
/// Lowest low across the bar.
|
||||
pub low: f64,
|
||||
/// Close of the candle that closed the bar.
|
||||
pub close: f64,
|
||||
/// Summed volume across the bar.
|
||||
pub volume: f64,
|
||||
/// Accumulated traded value (`Σ close · volume`, `>= dollar_per_bar`).
|
||||
pub dollar: f64,
|
||||
}
|
||||
|
||||
/// Dollar bar builder — emits a bar each time accumulated traded value
|
||||
/// (`price × volume`) reaches `dollar_per_bar`.
|
||||
///
|
||||
/// Dollar bars are the most drift-robust of the information-driven bar types. Where
|
||||
/// [`VolumeBars`](crate::VolumeBars) close on a fixed *quantity* of shares/contracts,
|
||||
/// dollar bars close on a fixed *value*: each candle contributes `close × volume` to
|
||||
/// the running total. As a market's price level rises over years, a fixed share
|
||||
/// count buys ever more value and volume bars drift in meaning; dollar bars stay
|
||||
/// economically comparable across the whole history, which is why they are the
|
||||
/// preferred sampling for long backtests and machine-learning features.
|
||||
///
|
||||
/// The bar is candle-granular: at most one bar closes per candle, and the candle
|
||||
/// that crosses the threshold closes the bar with its overshoot included.
|
||||
/// [`BarBuilder::update`] returns either an empty vector or a single [`DollarBar`].
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// use wickra_core::{BarBuilder, Candle, DollarBars};
|
||||
///
|
||||
/// let c = |cl, v| Candle::new(cl, cl, cl, cl, v, 0).unwrap();
|
||||
/// let mut bars = DollarBars::new(1000.0).unwrap();
|
||||
/// assert!(bars.update(c(10.0, 60.0)).is_empty()); // 600
|
||||
/// let out = bars.update(c(10.0, 60.0)); // 1200 >= 1000 -> close
|
||||
/// assert_eq!(out.len(), 1);
|
||||
/// assert_eq!(out[0].dollar, 1200.0);
|
||||
/// ```
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct DollarBars {
|
||||
dollar_per_bar: f64,
|
||||
count: usize,
|
||||
open: f64,
|
||||
high: f64,
|
||||
low: f64,
|
||||
close: f64,
|
||||
volume: f64,
|
||||
dollar: f64,
|
||||
}
|
||||
|
||||
impl DollarBars {
|
||||
/// Construct a dollar-bar builder with the given traded-value threshold.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns [`Error::InvalidPeriod`] if `dollar_per_bar` is not finite and positive.
|
||||
pub fn new(dollar_per_bar: f64) -> Result<Self> {
|
||||
if !dollar_per_bar.is_finite() || dollar_per_bar <= 0.0 {
|
||||
return Err(Error::InvalidPeriod {
|
||||
message: "dollar_per_bar must be finite and positive",
|
||||
});
|
||||
}
|
||||
Ok(Self {
|
||||
dollar_per_bar,
|
||||
count: 0,
|
||||
open: 0.0,
|
||||
high: 0.0,
|
||||
low: 0.0,
|
||||
close: 0.0,
|
||||
volume: 0.0,
|
||||
dollar: 0.0,
|
||||
})
|
||||
}
|
||||
|
||||
/// Configured traded-value threshold per bar.
|
||||
pub const fn dollar_per_bar(&self) -> f64 {
|
||||
self.dollar_per_bar
|
||||
}
|
||||
|
||||
/// Traded value accumulated into the in-progress bar.
|
||||
pub const fn accumulated(&self) -> f64 {
|
||||
self.dollar
|
||||
}
|
||||
}
|
||||
|
||||
impl BarBuilder for DollarBars {
|
||||
type Bar = DollarBar;
|
||||
|
||||
fn update(&mut self, candle: Candle) -> Vec<DollarBar> {
|
||||
if self.count == 0 {
|
||||
self.open = candle.open;
|
||||
self.high = candle.high;
|
||||
self.low = candle.low;
|
||||
self.volume = 0.0;
|
||||
} else {
|
||||
self.high = self.high.max(candle.high);
|
||||
self.low = self.low.min(candle.low);
|
||||
}
|
||||
self.close = candle.close;
|
||||
self.volume += candle.volume;
|
||||
self.dollar += candle.close * candle.volume;
|
||||
self.count += 1;
|
||||
if self.dollar < self.dollar_per_bar {
|
||||
return Vec::new();
|
||||
}
|
||||
let bar = DollarBar {
|
||||
open: self.open,
|
||||
high: self.high,
|
||||
low: self.low,
|
||||
close: self.close,
|
||||
volume: self.volume,
|
||||
dollar: self.dollar,
|
||||
};
|
||||
self.count = 0;
|
||||
self.dollar = 0.0;
|
||||
vec![bar]
|
||||
}
|
||||
|
||||
fn reset(&mut self) {
|
||||
self.count = 0;
|
||||
self.volume = 0.0;
|
||||
self.dollar = 0.0;
|
||||
}
|
||||
|
||||
fn name(&self) -> &'static str {
|
||||
"DollarBars"
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use approx::assert_relative_eq;
|
||||
|
||||
fn candle(open: f64, high: f64, low: f64, close: f64, volume: f64) -> Candle {
|
||||
Candle::new(open, high, low, close, volume, 0).unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_invalid_threshold() {
|
||||
assert!(matches!(
|
||||
DollarBars::new(0.0),
|
||||
Err(Error::InvalidPeriod { .. })
|
||||
));
|
||||
assert!(matches!(
|
||||
DollarBars::new(-1000.0),
|
||||
Err(Error::InvalidPeriod { .. })
|
||||
));
|
||||
assert!(matches!(
|
||||
DollarBars::new(f64::NAN),
|
||||
Err(Error::InvalidPeriod { .. })
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn accessors_and_metadata() {
|
||||
let bars = DollarBars::new(50_000.0).unwrap();
|
||||
assert_relative_eq!(bars.dollar_per_bar(), 50_000.0, epsilon = 1e-6);
|
||||
assert_relative_eq!(bars.accumulated(), 0.0, epsilon = 1e-12);
|
||||
assert_eq!(bars.name(), "DollarBars");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn closes_when_value_reached() {
|
||||
let mut bars = DollarBars::new(1000.0).unwrap();
|
||||
assert!(bars.update(candle(10.0, 10.0, 10.0, 10.0, 60.0)).is_empty()); // 600
|
||||
let out = bars.update(candle(10.0, 10.0, 10.0, 10.0, 60.0)); // 1200
|
||||
assert_eq!(out.len(), 1);
|
||||
assert_relative_eq!(out[0].dollar, 1200.0, epsilon = 1e-9);
|
||||
assert_relative_eq!(out[0].volume, 120.0, epsilon = 1e-12);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn aggregates_ohlc() {
|
||||
let mut bars = DollarBars::new(1000.0).unwrap();
|
||||
bars.update(candle(10.0, 11.0, 9.0, 10.0, 50.0)); // 500
|
||||
let out = bars.update(candle(10.0, 12.0, 9.5, 11.0, 60.0)); // 500 + 660 = 1160
|
||||
assert_relative_eq!(out[0].open, 10.0, epsilon = 1e-12);
|
||||
assert_relative_eq!(out[0].high, 12.0, epsilon = 1e-12);
|
||||
assert_relative_eq!(out[0].low, 9.0, epsilon = 1e-12);
|
||||
assert_relative_eq!(out[0].close, 11.0, epsilon = 1e-12);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn below_threshold_emits_nothing() {
|
||||
let mut bars = DollarBars::new(1000.0).unwrap();
|
||||
bars.update(candle(10.0, 10.0, 10.0, 10.0, 30.0)); // 300
|
||||
assert_relative_eq!(bars.accumulated(), 300.0, epsilon = 1e-9);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reset_clears_state() {
|
||||
let mut bars = DollarBars::new(1000.0).unwrap();
|
||||
bars.update(candle(10.0, 10.0, 10.0, 10.0, 60.0));
|
||||
bars.reset();
|
||||
assert_relative_eq!(bars.accumulated(), 0.0, epsilon = 1e-12);
|
||||
assert!(bars.update(candle(20.0, 20.0, 20.0, 20.0, 10.0)).is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn batch_concatenates_completed_bars() {
|
||||
let mut bars = DollarBars::new(1000.0).unwrap();
|
||||
let candles = [
|
||||
candle(10.0, 10.0, 10.0, 10.0, 60.0),
|
||||
candle(10.0, 10.0, 10.0, 10.0, 60.0),
|
||||
candle(10.0, 10.0, 10.0, 10.0, 60.0),
|
||||
candle(10.0, 10.0, 10.0, 10.0, 60.0),
|
||||
];
|
||||
let out = bars.batch(&candles);
|
||||
assert_eq!(out.len(), 2);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,229 @@
|
||||
//! Gain-to-Pain Ratio (Schwager) — sum of returns over the sum of losses.
|
||||
|
||||
use std::collections::VecDeque;
|
||||
|
||||
use crate::error::{Error, Result};
|
||||
use crate::traits::Indicator;
|
||||
|
||||
/// Gain-to-Pain Ratio — Jack Schwager's measure of return per unit of downside:
|
||||
/// the sum of all returns divided by the sum of the absolute *negative* returns.
|
||||
///
|
||||
/// ```text
|
||||
/// GPR = Σ returns / Σ |negative returns| over the window
|
||||
/// ```
|
||||
///
|
||||
/// Where the [`GainLossRatio`](crate::GainLossRatio) compares *average* win to
|
||||
/// *average* loss and the [`ProfitFactor`](crate::ProfitFactor) compares gross
|
||||
/// profit to gross loss, the Gain-to-Pain Ratio puts the **net** result over the
|
||||
/// total pain endured to earn it. Schwager treats a GPR above `1.0` as good and
|
||||
/// above `2.0` as excellent for a monthly return series: the strategy made more
|
||||
/// than it lost on the way, and twice as much when GPR is `2`. A flat series, or
|
||||
/// one with no losses, has no measurable pain and reports `0` (undefined).
|
||||
///
|
||||
/// The output is unbounded and may be negative (a net-losing window). The first
|
||||
/// value lands after `period` returns; each `update` is O(1).
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// use wickra_core::{Indicator, GainToPainRatio};
|
||||
///
|
||||
/// let mut indicator = GainToPainRatio::new(12).unwrap();
|
||||
/// let mut last = None;
|
||||
/// for i in 0..24 {
|
||||
/// last = indicator.update((f64::from(i) * 0.5).sin() * 0.02);
|
||||
/// }
|
||||
/// assert!(last.is_some());
|
||||
/// ```
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct GainToPainRatio {
|
||||
period: usize,
|
||||
window: VecDeque<f64>,
|
||||
sum_all: f64,
|
||||
sum_pain: f64,
|
||||
}
|
||||
|
||||
impl GainToPainRatio {
|
||||
/// Construct a Gain-to-Pain Ratio over `period` returns.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns [`Error::PeriodZero`] if `period == 0`.
|
||||
pub fn new(period: usize) -> Result<Self> {
|
||||
if period == 0 {
|
||||
return Err(Error::PeriodZero);
|
||||
}
|
||||
Ok(Self {
|
||||
period,
|
||||
window: VecDeque::with_capacity(period),
|
||||
sum_all: 0.0,
|
||||
sum_pain: 0.0,
|
||||
})
|
||||
}
|
||||
|
||||
/// Configured window of returns.
|
||||
pub const fn period(&self) -> usize {
|
||||
self.period
|
||||
}
|
||||
}
|
||||
|
||||
impl Indicator for GainToPainRatio {
|
||||
type Input = f64;
|
||||
type Output = f64;
|
||||
|
||||
fn update(&mut self, ret: f64) -> Option<f64> {
|
||||
if !ret.is_finite() {
|
||||
return if self.window.len() == self.period {
|
||||
Some(self.compute())
|
||||
} else {
|
||||
None
|
||||
};
|
||||
}
|
||||
if self.window.len() == self.period {
|
||||
let old = self.window.pop_front().expect("non-empty");
|
||||
self.sum_all -= old;
|
||||
if old < 0.0 {
|
||||
self.sum_pain -= -old;
|
||||
}
|
||||
}
|
||||
self.window.push_back(ret);
|
||||
self.sum_all += ret;
|
||||
if ret < 0.0 {
|
||||
self.sum_pain += -ret;
|
||||
}
|
||||
if self.window.len() < self.period {
|
||||
return None;
|
||||
}
|
||||
Some(self.compute())
|
||||
}
|
||||
|
||||
fn reset(&mut self) {
|
||||
self.window.clear();
|
||||
self.sum_all = 0.0;
|
||||
self.sum_pain = 0.0;
|
||||
}
|
||||
|
||||
fn warmup_period(&self) -> usize {
|
||||
self.period
|
||||
}
|
||||
|
||||
fn is_ready(&self) -> bool {
|
||||
self.window.len() == self.period
|
||||
}
|
||||
|
||||
fn name(&self) -> &'static str {
|
||||
"GainToPainRatio"
|
||||
}
|
||||
}
|
||||
|
||||
impl GainToPainRatio {
|
||||
fn compute(&self) -> f64 {
|
||||
if self.sum_pain > 0.0 {
|
||||
self.sum_all / self.sum_pain
|
||||
} else {
|
||||
0.0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::traits::BatchExt;
|
||||
use approx::assert_relative_eq;
|
||||
|
||||
#[test]
|
||||
fn rejects_zero_period() {
|
||||
assert!(matches!(GainToPainRatio::new(0), Err(Error::PeriodZero)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn accessors_and_metadata() {
|
||||
let g = GainToPainRatio::new(12).unwrap();
|
||||
assert_eq!(g.period(), 12);
|
||||
assert_eq!(g.warmup_period(), 12);
|
||||
assert_eq!(g.name(), "GainToPainRatio");
|
||||
assert!(!g.is_ready());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn first_emission_at_warmup_period() {
|
||||
let mut g = GainToPainRatio::new(4).unwrap();
|
||||
let out = g.batch(&[0.01, -0.01, 0.02, -0.01, 0.03]);
|
||||
for v in out.iter().take(3) {
|
||||
assert!(v.is_none());
|
||||
}
|
||||
assert!(out[3].is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reference_value() {
|
||||
// returns: +0.04, -0.02 -> sum_all = 0.02, pain = 0.02 -> GPR = 1.0.
|
||||
let mut g = GainToPainRatio::new(2).unwrap();
|
||||
let out = g.batch(&[0.04, -0.02]);
|
||||
assert_relative_eq!(out[1].unwrap(), 1.0, epsilon = 1e-9);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn net_losing_window_is_negative() {
|
||||
let mut g = GainToPainRatio::new(3).unwrap();
|
||||
let last = g
|
||||
.batch(&[-0.03, 0.01, -0.02])
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.last()
|
||||
.unwrap();
|
||||
assert!(last < 0.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn no_pain_is_zero() {
|
||||
let mut g = GainToPainRatio::new(3).unwrap();
|
||||
let last = g
|
||||
.batch(&[0.01, 0.02, 0.03])
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.last()
|
||||
.unwrap();
|
||||
assert_relative_eq!(last, 0.0, epsilon = 1e-12);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ignores_non_finite() {
|
||||
let mut g = GainToPainRatio::new(2).unwrap();
|
||||
let ready = g
|
||||
.batch(&[0.04, -0.02])
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.last()
|
||||
.unwrap();
|
||||
assert_eq!(g.update(f64::NAN), Some(ready));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn non_finite_before_ready_is_none() {
|
||||
// A non-finite value arriving before the window fills yields None.
|
||||
let mut g = GainToPainRatio::new(3).unwrap();
|
||||
assert_eq!(g.update(0.02), None);
|
||||
assert_eq!(g.update(f64::NAN), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reset_clears_state() {
|
||||
let mut g = GainToPainRatio::new(2).unwrap();
|
||||
g.batch(&[0.04, -0.02]);
|
||||
assert!(g.is_ready());
|
||||
g.reset();
|
||||
assert!(!g.is_ready());
|
||||
assert_eq!(g.update(0.01), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn batch_equals_streaming() {
|
||||
let rets: Vec<f64> = (0..60).map(|i| (f64::from(i) * 0.3).sin() * 0.02).collect();
|
||||
let batch = GainToPainRatio::new(12).unwrap().batch(&rets);
|
||||
let mut b = GainToPainRatio::new(12).unwrap();
|
||||
let streamed: Vec<_> = rets.iter().map(|r| b.update(*r)).collect();
|
||||
assert_eq!(batch, streamed);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,263 @@
|
||||
//! Tick-imbalance bar builder (simplified López de Prado) — sample on cumulative signed order flow.
|
||||
|
||||
use crate::error::{Error, Result};
|
||||
use crate::ohlcv::Candle;
|
||||
use crate::traits::BarBuilder;
|
||||
|
||||
/// One completed imbalance bar.
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub struct ImbalanceBar {
|
||||
/// Open of the first candle in the bar.
|
||||
pub open: f64,
|
||||
/// Highest high across the bar.
|
||||
pub high: f64,
|
||||
/// Lowest low across the bar.
|
||||
pub low: f64,
|
||||
/// Close of the candle that closed the bar.
|
||||
pub close: f64,
|
||||
/// Signed cumulative tick imbalance at the close (`Σ sign`).
|
||||
pub imbalance: f64,
|
||||
/// `+1` if buy-side imbalance closed the bar, `-1` if sell-side.
|
||||
pub direction: i8,
|
||||
}
|
||||
|
||||
/// Tick-imbalance bar builder — a **simplified** form of López de Prado's
|
||||
/// imbalance bars.
|
||||
///
|
||||
/// Each candle is assigned a tick sign by the tick rule: `+1` if its close is above
|
||||
/// the previous close, `-1` if below, and the previous sign is carried on an
|
||||
/// unchanged close. The signed imbalance `θ = Σ sign` accumulates until its absolute
|
||||
/// value reaches a fixed `threshold`, at which point a bar closes. Imbalance bars
|
||||
/// therefore sample the market when order flow becomes *one-sided* — a burst of
|
||||
/// persistent buying or selling — rather than on time, count, or volume. This makes
|
||||
/// them sensitive to informed, directional trading.
|
||||
///
|
||||
/// **Simplification.** The full method estimates a *dynamic* threshold
|
||||
/// `E[T] · |2P − 1|` from an EWMA of the expected bar length `E[T]` and the buy-tick
|
||||
/// probability `P`, and can weight each sign by volume (volume-imbalance bars) or
|
||||
/// traded value (dollar-imbalance bars). This builder uses a **fixed** threshold on
|
||||
/// the unweighted tick imbalance. For the adaptive estimator and the volume/dollar
|
||||
/// variants, see López de Prado (2018), ch. 2.
|
||||
///
|
||||
/// At most one bar closes per candle, so [`BarBuilder::update`] returns either an
|
||||
/// empty vector or a single [`ImbalanceBar`].
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// use wickra_core::{BarBuilder, Candle, ImbalanceBars};
|
||||
///
|
||||
/// let flat = |price: f64| Candle::new(price, price, price, price, 1.0, 0).unwrap();
|
||||
/// let mut bars = ImbalanceBars::new(3.0).unwrap();
|
||||
/// bars.update(flat(10.0)); // seed, no sign
|
||||
/// bars.update(flat(11.0)); // +1
|
||||
/// bars.update(flat(12.0)); // +2
|
||||
/// let out = bars.update(flat(13.0)); // +3 -> close
|
||||
/// assert_eq!(out.len(), 1);
|
||||
/// assert_eq!(out[0].direction, 1);
|
||||
/// ```
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ImbalanceBars {
|
||||
threshold: f64,
|
||||
count: usize,
|
||||
open: f64,
|
||||
high: f64,
|
||||
low: f64,
|
||||
close: f64,
|
||||
prev_close: Option<f64>,
|
||||
last_sign: i8,
|
||||
theta: f64,
|
||||
}
|
||||
|
||||
impl ImbalanceBars {
|
||||
/// Construct an imbalance-bar builder with the given absolute imbalance threshold.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns [`Error::InvalidPeriod`] if `threshold` is not finite and positive.
|
||||
pub fn new(threshold: f64) -> Result<Self> {
|
||||
if !threshold.is_finite() || threshold <= 0.0 {
|
||||
return Err(Error::InvalidPeriod {
|
||||
message: "threshold must be finite and positive",
|
||||
});
|
||||
}
|
||||
Ok(Self {
|
||||
threshold,
|
||||
count: 0,
|
||||
open: 0.0,
|
||||
high: 0.0,
|
||||
low: 0.0,
|
||||
close: 0.0,
|
||||
prev_close: None,
|
||||
last_sign: 0,
|
||||
theta: 0.0,
|
||||
})
|
||||
}
|
||||
|
||||
/// Configured absolute imbalance threshold.
|
||||
pub const fn threshold(&self) -> f64 {
|
||||
self.threshold
|
||||
}
|
||||
|
||||
/// Signed imbalance accumulated into the in-progress bar.
|
||||
pub const fn imbalance(&self) -> f64 {
|
||||
self.theta
|
||||
}
|
||||
}
|
||||
|
||||
impl BarBuilder for ImbalanceBars {
|
||||
type Bar = ImbalanceBar;
|
||||
|
||||
fn update(&mut self, candle: Candle) -> Vec<ImbalanceBar> {
|
||||
if self.count == 0 {
|
||||
self.open = candle.open;
|
||||
self.high = candle.high;
|
||||
self.low = candle.low;
|
||||
} else {
|
||||
self.high = self.high.max(candle.high);
|
||||
self.low = self.low.min(candle.low);
|
||||
}
|
||||
self.close = candle.close;
|
||||
self.count += 1;
|
||||
if let Some(prev) = self.prev_close {
|
||||
let sign = if candle.close > prev {
|
||||
1
|
||||
} else if candle.close < prev {
|
||||
-1
|
||||
} else {
|
||||
self.last_sign
|
||||
};
|
||||
self.last_sign = sign;
|
||||
self.theta += f64::from(sign);
|
||||
}
|
||||
self.prev_close = Some(candle.close);
|
||||
if self.theta.abs() < self.threshold {
|
||||
return Vec::new();
|
||||
}
|
||||
let direction = if self.theta > 0.0 { 1 } else { -1 };
|
||||
let bar = ImbalanceBar {
|
||||
open: self.open,
|
||||
high: self.high,
|
||||
low: self.low,
|
||||
close: self.close,
|
||||
imbalance: self.theta,
|
||||
direction,
|
||||
};
|
||||
self.count = 0;
|
||||
self.theta = 0.0;
|
||||
vec![bar]
|
||||
}
|
||||
|
||||
fn reset(&mut self) {
|
||||
self.count = 0;
|
||||
self.prev_close = None;
|
||||
self.last_sign = 0;
|
||||
self.theta = 0.0;
|
||||
}
|
||||
|
||||
fn name(&self) -> &'static str {
|
||||
"ImbalanceBars"
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use approx::assert_relative_eq;
|
||||
|
||||
fn flat(price: f64) -> Candle {
|
||||
Candle::new(price, price, price, price, 1.0, 0).unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_invalid_threshold() {
|
||||
assert!(matches!(
|
||||
ImbalanceBars::new(0.0),
|
||||
Err(Error::InvalidPeriod { .. })
|
||||
));
|
||||
assert!(matches!(
|
||||
ImbalanceBars::new(-3.0),
|
||||
Err(Error::InvalidPeriod { .. })
|
||||
));
|
||||
assert!(matches!(
|
||||
ImbalanceBars::new(f64::NAN),
|
||||
Err(Error::InvalidPeriod { .. })
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn accessors_and_metadata() {
|
||||
let bars = ImbalanceBars::new(10.0).unwrap();
|
||||
assert_relative_eq!(bars.threshold(), 10.0, epsilon = 1e-12);
|
||||
assert_relative_eq!(bars.imbalance(), 0.0, epsilon = 1e-12);
|
||||
assert_eq!(bars.name(), "ImbalanceBars");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn buy_imbalance_closes_up_bar() {
|
||||
let mut bars = ImbalanceBars::new(3.0).unwrap();
|
||||
bars.update(flat(10.0)); // seed
|
||||
bars.update(flat(11.0)); // +1
|
||||
bars.update(flat(12.0)); // +2
|
||||
let out = bars.update(flat(13.0)); // +3
|
||||
assert_eq!(out.len(), 1);
|
||||
assert_eq!(out[0].direction, 1);
|
||||
assert_relative_eq!(out[0].imbalance, 3.0, epsilon = 1e-12);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sell_imbalance_closes_down_bar() {
|
||||
let mut bars = ImbalanceBars::new(3.0).unwrap();
|
||||
bars.update(flat(10.0));
|
||||
bars.update(flat(9.0)); // -1
|
||||
bars.update(flat(8.0)); // -2
|
||||
let out = bars.update(flat(7.0)); // -3
|
||||
assert_eq!(out.len(), 1);
|
||||
assert_eq!(out[0].direction, -1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn flat_tick_carries_previous_sign() {
|
||||
let mut bars = ImbalanceBars::new(3.0).unwrap();
|
||||
bars.update(flat(10.0));
|
||||
bars.update(flat(11.0)); // +1
|
||||
bars.update(flat(11.0)); // flat -> carries +1 -> +2
|
||||
assert_relative_eq!(bars.imbalance(), 2.0, epsilon = 1e-12);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn oscillation_does_not_reach_threshold() {
|
||||
let mut bars = ImbalanceBars::new(3.0).unwrap();
|
||||
bars.update(flat(10.0));
|
||||
bars.update(flat(11.0)); // +1
|
||||
bars.update(flat(10.0)); // -1 -> theta 0
|
||||
assert!(bars.update(flat(11.0)).is_empty()); // +1
|
||||
assert_relative_eq!(bars.imbalance(), 1.0, epsilon = 1e-12);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reset_clears_state() {
|
||||
let mut bars = ImbalanceBars::new(3.0).unwrap();
|
||||
bars.update(flat(10.0));
|
||||
bars.update(flat(11.0));
|
||||
bars.reset();
|
||||
assert_relative_eq!(bars.imbalance(), 0.0, epsilon = 1e-12);
|
||||
// After reset the next candle re-seeds (no previous close).
|
||||
assert!(bars.update(flat(50.0)).is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn batch_concatenates_completed_bars() {
|
||||
let mut bars = ImbalanceBars::new(2.0).unwrap();
|
||||
let candles = [
|
||||
flat(10.0),
|
||||
flat(11.0), // +1
|
||||
flat(12.0), // +2 -> close
|
||||
flat(13.0), // +1
|
||||
flat(14.0), // +2 -> close
|
||||
];
|
||||
let out = bars.batch(&candles);
|
||||
assert_eq!(out.len(), 2);
|
||||
assert!(out.iter().all(|b| b.direction == 1));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,239 @@
|
||||
//! K-Ratio (Kestner) — slope of the cumulative-return curve over the standard error of that slope.
|
||||
|
||||
use std::collections::VecDeque;
|
||||
|
||||
use crate::error::{Error, Result};
|
||||
use crate::traits::Indicator;
|
||||
|
||||
/// K-Ratio over a trailing window of `period` returns.
|
||||
///
|
||||
/// Lars Kestner's K-Ratio measures the *consistency* of an equity curve, not just
|
||||
/// its return. It builds the cumulative-return curve over the window, fits an
|
||||
/// ordinary-least-squares trend line through it against time, and divides the
|
||||
/// fitted slope by the standard error of that slope:
|
||||
///
|
||||
/// ```text
|
||||
/// equity_t = Σ_{i<=t} return_i (cumulative curve, t = 1..period)
|
||||
/// slope, intercept = OLS(equity_t ~ t)
|
||||
/// SE(slope) = sqrt( (Σ residual² / (period − 2)) / Σ(t − t̄)² )
|
||||
/// K-Ratio = slope / SE(slope)
|
||||
/// ```
|
||||
///
|
||||
/// A high K-Ratio means the equity curve climbs *steadily* — a steep slope with
|
||||
/// little scatter around the trend. A strategy that earns the same total return in
|
||||
/// a few lucky jumps scores lower because its residual scatter inflates the
|
||||
/// standard error. This is the original 1996 form; later Kestner revisions scale by
|
||||
/// the number of periods (`slope / (SE · period)` in 2003, `slope / (SE · √period)`
|
||||
/// in 2013) — apply that scaling downstream if you need to compare across window
|
||||
/// lengths.
|
||||
///
|
||||
/// A perfectly straight window (e.g. constant returns) has zero residual scatter,
|
||||
/// so the slope's standard error is zero and the K-Ratio is undefined; the
|
||||
/// indicator reports `0.0` in that degenerate case. The statistic therefore needs
|
||||
/// some dispersion in the returns to be meaningful.
|
||||
///
|
||||
/// The first value lands after `period` returns; each `update` re-fits the line
|
||||
/// over the window (O(period)), which is O(1) in the length of the overall series.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// use wickra_core::{Indicator, KRatio};
|
||||
///
|
||||
/// let mut indicator = KRatio::new(30).unwrap();
|
||||
/// let mut last = None;
|
||||
/// for i in 0..60 {
|
||||
/// last = indicator.update(0.001 + (f64::from(i) * 0.3).sin() * 0.01);
|
||||
/// }
|
||||
/// assert!(last.is_some());
|
||||
/// ```
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct KRatio {
|
||||
period: usize,
|
||||
window: VecDeque<f64>,
|
||||
}
|
||||
|
||||
impl KRatio {
|
||||
/// Construct a K-Ratio over `period` returns.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns [`Error::InvalidPeriod`] if `period < 3` (the slope's standard error
|
||||
/// divides by `period − 2`).
|
||||
pub fn new(period: usize) -> Result<Self> {
|
||||
if period < 3 {
|
||||
return Err(Error::InvalidPeriod {
|
||||
message: "k-ratio needs period >= 3",
|
||||
});
|
||||
}
|
||||
Ok(Self {
|
||||
period,
|
||||
window: VecDeque::with_capacity(period),
|
||||
})
|
||||
}
|
||||
|
||||
/// Configured window of returns.
|
||||
pub const fn period(&self) -> usize {
|
||||
self.period
|
||||
}
|
||||
|
||||
fn compute(&self) -> f64 {
|
||||
let count = self.window.len();
|
||||
#[allow(clippy::cast_precision_loss)]
|
||||
let length = count as f64;
|
||||
// Build the cumulative-equity curve and its mean.
|
||||
let mut equity = 0.0;
|
||||
let mut curve: Vec<f64> = Vec::with_capacity(count);
|
||||
let mut sum_equity = 0.0;
|
||||
for ret in &self.window {
|
||||
equity += *ret;
|
||||
curve.push(equity);
|
||||
sum_equity += equity;
|
||||
}
|
||||
// Times are 1..=count, so Σt = count(count+1)/2 in closed form.
|
||||
let mean_time = f64::midpoint(length, 1.0);
|
||||
let mean_equity = sum_equity / length;
|
||||
let mut sxx = 0.0;
|
||||
let mut sxy = 0.0;
|
||||
for (index, value) in curve.iter().enumerate() {
|
||||
#[allow(clippy::cast_precision_loss)]
|
||||
let time = (index + 1) as f64;
|
||||
let dt = time - mean_time;
|
||||
sxx += dt * dt;
|
||||
sxy += dt * (value - mean_equity);
|
||||
}
|
||||
// sxx > 0 for count >= 2 (distinct integer times), guaranteed by period >= 3.
|
||||
let slope = sxy / sxx;
|
||||
let intercept = mean_equity - slope * mean_time;
|
||||
let mut sse = 0.0;
|
||||
for (index, value) in curve.iter().enumerate() {
|
||||
#[allow(clippy::cast_precision_loss)]
|
||||
let time = (index + 1) as f64;
|
||||
let residual = value - (intercept + slope * time);
|
||||
sse += residual * residual;
|
||||
}
|
||||
if sse <= 0.0 {
|
||||
return 0.0;
|
||||
}
|
||||
let se_slope = (sse / (length - 2.0) / sxx).sqrt();
|
||||
slope / se_slope
|
||||
}
|
||||
}
|
||||
|
||||
impl Indicator for KRatio {
|
||||
type Input = f64;
|
||||
type Output = f64;
|
||||
|
||||
fn update(&mut self, ret: f64) -> Option<f64> {
|
||||
if !ret.is_finite() {
|
||||
return None;
|
||||
}
|
||||
if self.window.len() == self.period {
|
||||
self.window.pop_front();
|
||||
}
|
||||
self.window.push_back(ret);
|
||||
if self.window.len() < self.period {
|
||||
return None;
|
||||
}
|
||||
Some(self.compute())
|
||||
}
|
||||
|
||||
fn reset(&mut self) {
|
||||
self.window.clear();
|
||||
}
|
||||
|
||||
fn warmup_period(&self) -> usize {
|
||||
self.period
|
||||
}
|
||||
|
||||
fn is_ready(&self) -> bool {
|
||||
self.window.len() == self.period
|
||||
}
|
||||
|
||||
fn name(&self) -> &'static str {
|
||||
"KRatio"
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::traits::BatchExt;
|
||||
use approx::assert_relative_eq;
|
||||
|
||||
#[test]
|
||||
fn rejects_period_less_than_three() {
|
||||
assert!(matches!(KRatio::new(2), Err(Error::InvalidPeriod { .. })));
|
||||
assert!(matches!(KRatio::new(0), Err(Error::InvalidPeriod { .. })));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn accessors_and_metadata() {
|
||||
let kr = KRatio::new(30).unwrap();
|
||||
assert_eq!(kr.period(), 30);
|
||||
assert_eq!(kr.warmup_period(), 30);
|
||||
assert_eq!(kr.name(), "KRatio");
|
||||
assert!(!kr.is_ready());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reference_value() {
|
||||
// returns [0.01, 0.02, 0.03] -> equity curve [0.01, 0.03, 0.06].
|
||||
// slope = 0.025, SE(slope) = sqrt((1/60000)/1/2) = 1/sqrt(120000).
|
||||
// K-Ratio = 0.025 * sqrt(120000) = 5*sqrt(3) ≈ 8.660254.
|
||||
let mut kr = KRatio::new(3).unwrap();
|
||||
let out = kr.batch(&[0.01, 0.02, 0.03]);
|
||||
let expected = 0.025_f64 / (1.0_f64 / 120_000.0).sqrt();
|
||||
assert_relative_eq!(out[2].unwrap(), expected, epsilon = 1e-6);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn constant_returns_are_degenerate_zero() {
|
||||
// A perfectly linear equity curve has zero residual scatter -> undefined.
|
||||
let mut kr = KRatio::new(4).unwrap();
|
||||
let last = kr.batch(&[0.01; 4]).into_iter().flatten().last().unwrap();
|
||||
assert_relative_eq!(last, 0.0, epsilon = 1e-12);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rising_curve_is_positive() {
|
||||
let mut kr = KRatio::new(5).unwrap();
|
||||
let last = kr
|
||||
.batch(&[0.01, 0.012, 0.009, 0.011, 0.013])
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.last()
|
||||
.unwrap();
|
||||
assert!(last > 0.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ignores_non_finite_input() {
|
||||
let mut kr = KRatio::new(3).unwrap();
|
||||
assert_eq!(kr.update(0.01), None);
|
||||
assert_eq!(kr.update(f64::NAN), None);
|
||||
assert_eq!(kr.update(0.02), None);
|
||||
assert!(kr.update(0.03).is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reset_clears_state() {
|
||||
let mut kr = KRatio::new(3).unwrap();
|
||||
kr.batch(&[0.01, 0.02, 0.03]);
|
||||
assert!(kr.is_ready());
|
||||
kr.reset();
|
||||
assert!(!kr.is_ready());
|
||||
assert_eq!(kr.update(0.01), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn batch_equals_streaming() {
|
||||
let rets: Vec<f64> = (0..60)
|
||||
.map(|i| 0.001 + (f64::from(i) * 0.25).sin() * 0.01)
|
||||
.collect();
|
||||
let batch = KRatio::new(20).unwrap().batch(&rets);
|
||||
let mut streamer = KRatio::new(20).unwrap();
|
||||
let streamed: Vec<_> = rets.iter().map(|r| streamer.update(*r)).collect();
|
||||
assert_eq!(batch, streamed);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,232 @@
|
||||
//! M² / Modigliani–Modigliani measure — Sharpe expressed in benchmark return units.
|
||||
|
||||
use std::collections::VecDeque;
|
||||
|
||||
use crate::error::{Error, Result};
|
||||
use crate::traits::Indicator;
|
||||
|
||||
/// M² (Modigliani–Modigliani) measure over a trailing window of `period` returns.
|
||||
///
|
||||
/// ```text
|
||||
/// Sharpe = (mean(returns) − risk_free) / stddev(returns)
|
||||
/// M² = risk_free + Sharpe · benchmark_stddev
|
||||
/// ```
|
||||
///
|
||||
/// The [`SharpeRatio`](crate::SharpeRatio) is dimensionless, which makes it hard to
|
||||
/// communicate: "0.8" means little to a client. M² rescales the Sharpe ratio back
|
||||
/// into *return units* by levering (or de-levering) the portfolio to the
|
||||
/// benchmark's volatility. The result answers a concrete question: "if this
|
||||
/// strategy had run at the market's risk level, what return would it have
|
||||
/// produced?" Two portfolios can then be ranked on the same risk-adjusted scale,
|
||||
/// and M² preserves the Sharpe ordering while being quoted as a percentage.
|
||||
///
|
||||
/// `stddev` is the sample standard deviation (Bessel's `n − 1`).
|
||||
/// `risk_free` is the per-period risk-free rate and `benchmark_stddev` the
|
||||
/// per-period volatility of the benchmark, both supplied by the caller at the
|
||||
/// return frequency. A flat window has zero volatility and the Sharpe ratio is
|
||||
/// undefined; the indicator returns `0.0` in that case rather than producing `NaN`.
|
||||
///
|
||||
/// Each `update` is O(1) — running sums maintain `Σr` and `Σr²` as the window slides.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// use wickra_core::{Indicator, M2Measure};
|
||||
///
|
||||
/// let mut indicator = M2Measure::new(20, 0.0, 0.02).unwrap();
|
||||
/// let mut last = None;
|
||||
/// for i in 0..40 {
|
||||
/// last = indicator.update(0.001 + (f64::from(i) * 0.1).sin() * 0.01);
|
||||
/// }
|
||||
/// assert!(last.is_some());
|
||||
/// ```
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct M2Measure {
|
||||
period: usize,
|
||||
risk_free: f64,
|
||||
benchmark_stddev: f64,
|
||||
window: VecDeque<f64>,
|
||||
sum: f64,
|
||||
sum_sq: f64,
|
||||
}
|
||||
|
||||
impl M2Measure {
|
||||
/// Construct an M² measure over `period` returns with the given per-period
|
||||
/// risk-free rate and benchmark standard deviation.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns [`Error::InvalidPeriod`] if `period < 2`, or
|
||||
/// [`Error::InvalidParameter`] if `risk_free` is not finite or
|
||||
/// `benchmark_stddev` is negative or not finite.
|
||||
pub fn new(period: usize, risk_free: f64, benchmark_stddev: f64) -> Result<Self> {
|
||||
if period < 2 {
|
||||
return Err(Error::InvalidPeriod {
|
||||
message: "m2 measure needs period >= 2",
|
||||
});
|
||||
}
|
||||
if !risk_free.is_finite() || !benchmark_stddev.is_finite() || benchmark_stddev < 0.0 {
|
||||
return Err(Error::InvalidParameter {
|
||||
message: "risk_free must be finite and benchmark_stddev finite and non-negative",
|
||||
});
|
||||
}
|
||||
Ok(Self {
|
||||
period,
|
||||
risk_free,
|
||||
benchmark_stddev,
|
||||
window: VecDeque::with_capacity(period),
|
||||
sum: 0.0,
|
||||
sum_sq: 0.0,
|
||||
})
|
||||
}
|
||||
|
||||
/// Configured window of returns.
|
||||
pub const fn period(&self) -> usize {
|
||||
self.period
|
||||
}
|
||||
|
||||
/// Configured per-period risk-free rate.
|
||||
pub const fn risk_free(&self) -> f64 {
|
||||
self.risk_free
|
||||
}
|
||||
|
||||
/// Configured per-period benchmark standard deviation.
|
||||
pub const fn benchmark_stddev(&self) -> f64 {
|
||||
self.benchmark_stddev
|
||||
}
|
||||
}
|
||||
|
||||
impl Indicator for M2Measure {
|
||||
type Input = f64;
|
||||
type Output = f64;
|
||||
|
||||
fn update(&mut self, ret: f64) -> Option<f64> {
|
||||
if !ret.is_finite() {
|
||||
return None;
|
||||
}
|
||||
if self.window.len() == self.period {
|
||||
let old = self.window.pop_front().expect("non-empty");
|
||||
self.sum -= old;
|
||||
self.sum_sq -= old * old;
|
||||
}
|
||||
self.window.push_back(ret);
|
||||
self.sum += ret;
|
||||
self.sum_sq += ret * ret;
|
||||
if self.window.len() < self.period {
|
||||
return None;
|
||||
}
|
||||
let n = self.period as f64;
|
||||
let mean = self.sum / n;
|
||||
let var = (self.sum_sq - n * mean * mean).max(0.0) / (n - 1.0);
|
||||
let sd = var.sqrt();
|
||||
if sd == 0.0 {
|
||||
return Some(0.0);
|
||||
}
|
||||
let sharpe = (mean - self.risk_free) / sd;
|
||||
Some(self.risk_free + sharpe * self.benchmark_stddev)
|
||||
}
|
||||
|
||||
fn reset(&mut self) {
|
||||
self.window.clear();
|
||||
self.sum = 0.0;
|
||||
self.sum_sq = 0.0;
|
||||
}
|
||||
|
||||
fn warmup_period(&self) -> usize {
|
||||
self.period
|
||||
}
|
||||
|
||||
fn is_ready(&self) -> bool {
|
||||
self.window.len() == self.period
|
||||
}
|
||||
|
||||
fn name(&self) -> &'static str {
|
||||
"M2Measure"
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::traits::BatchExt;
|
||||
use approx::assert_relative_eq;
|
||||
|
||||
#[test]
|
||||
fn rejects_period_less_than_two() {
|
||||
assert!(matches!(
|
||||
M2Measure::new(1, 0.0, 0.02),
|
||||
Err(Error::InvalidPeriod { .. })
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_invalid_benchmark_stddev() {
|
||||
assert!(matches!(
|
||||
M2Measure::new(10, 0.0, -0.01),
|
||||
Err(Error::InvalidParameter { .. })
|
||||
));
|
||||
assert!(matches!(
|
||||
M2Measure::new(10, f64::NAN, 0.02),
|
||||
Err(Error::InvalidParameter { .. })
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn accessors_and_metadata() {
|
||||
let m2 = M2Measure::new(20, 0.001, 0.02).unwrap();
|
||||
assert_eq!(m2.period(), 20);
|
||||
assert_relative_eq!(m2.risk_free(), 0.001, epsilon = 1e-12);
|
||||
assert_relative_eq!(m2.benchmark_stddev(), 0.02, epsilon = 1e-12);
|
||||
assert_eq!(m2.warmup_period(), 20);
|
||||
assert_eq!(m2.name(), "M2Measure");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reference_value() {
|
||||
// returns [0.01, 0.02, 0.03, 0.04], rf = 0, benchmark_stddev = 0.02.
|
||||
// mean = 0.025, sd = sqrt(0.000166666...), Sharpe = 0.025 / sd.
|
||||
// M2 = 0 + Sharpe * 0.02.
|
||||
let mut m2 = M2Measure::new(4, 0.0, 0.02).unwrap();
|
||||
let out = m2.batch(&[0.01, 0.02, 0.03, 0.04]);
|
||||
let sharpe = 0.025_f64 / (0.000_166_666_666_666_666_67_f64).sqrt();
|
||||
assert_relative_eq!(out[3].unwrap(), sharpe * 0.02, epsilon = 1e-9);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn constant_returns_yield_zero() {
|
||||
let mut m2 = M2Measure::new(5, 0.0, 0.02).unwrap();
|
||||
for v in m2.batch(&[0.01; 10]).into_iter().flatten() {
|
||||
assert_relative_eq!(v, 0.0, epsilon = 1e-12);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ignores_non_finite_input() {
|
||||
let mut m2 = M2Measure::new(3, 0.0, 0.02).unwrap();
|
||||
assert_eq!(m2.update(0.01), None);
|
||||
assert_eq!(m2.update(f64::NAN), None);
|
||||
assert_eq!(m2.update(0.02), None);
|
||||
assert!(m2.update(0.03).is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reset_clears_state() {
|
||||
let mut m2 = M2Measure::new(3, 0.0, 0.02).unwrap();
|
||||
m2.batch(&[0.01, 0.02, 0.03]);
|
||||
assert!(m2.is_ready());
|
||||
m2.reset();
|
||||
assert!(!m2.is_ready());
|
||||
assert_eq!(m2.update(0.01), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn batch_equals_streaming() {
|
||||
let rets: Vec<f64> = (0..50)
|
||||
.map(|i| 0.001 + (f64::from(i) * 0.2).sin() * 0.01)
|
||||
.collect();
|
||||
let batch = M2Measure::new(10, 0.0, 0.02).unwrap().batch(&rets);
|
||||
let mut streamer = M2Measure::new(10, 0.0, 0.02).unwrap();
|
||||
let streamed: Vec<_> = rets.iter().map(|r| streamer.update(*r)).collect();
|
||||
assert_eq!(batch, streamed);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,220 @@
|
||||
//! Martin Ratio (Ulcer Performance Index) — mean return over the Ulcer Index.
|
||||
|
||||
use std::collections::VecDeque;
|
||||
|
||||
use crate::error::{Error, Result};
|
||||
use crate::traits::Indicator;
|
||||
|
||||
/// Martin Ratio — also called the Ulcer Performance Index (UPI) — over a trailing
|
||||
/// window of `period` returns.
|
||||
///
|
||||
/// ```text
|
||||
/// equity_t = Π_{i<=t} (1 + return_i) (compounded curve)
|
||||
/// peak_t = max_{s<=t} equity_s
|
||||
/// dd_t% = 100 · (peak_t − equity_t) / peak_t (percentage drawdown)
|
||||
/// UlcerIdx = sqrt( mean( dd_t%² ) )
|
||||
/// Martin = mean(returns) / UlcerIdx
|
||||
/// ```
|
||||
///
|
||||
/// The Martin Ratio divides the average per-period return by the **Ulcer Index** —
|
||||
/// the root-mean-square of the *percentage* drawdowns. The Ulcer Index, by
|
||||
/// construction, measures the depth *and* duration of the time spent under water:
|
||||
/// a long shallow slump and a short deep one can score the same. Compared to
|
||||
/// Wickra's other drawdown ratios, Martin uses the RMS (not the average as in the
|
||||
/// [`SterlingRatio`](crate::SterlingRatio), nor the un-normalised sum-norm as in the
|
||||
/// [`BurkeRatio`](crate::BurkeRatio)) and expresses drawdowns in **percent**, so its
|
||||
/// denominator is on a `0..100` scale and its output is numerically smaller than
|
||||
/// the fractional-drawdown ratios. A window that never draws down has an Ulcer Index
|
||||
/// of zero and the indicator reports `0.0`.
|
||||
///
|
||||
/// The first value lands after `period` returns; each `update` rebuilds the equity
|
||||
/// curve over the window (O(period)), which is O(1) in the length of the overall
|
||||
/// series.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// use wickra_core::{Indicator, MartinRatio};
|
||||
///
|
||||
/// let mut indicator = MartinRatio::new(14).unwrap();
|
||||
/// let mut last = None;
|
||||
/// for i in 0..28 {
|
||||
/// last = indicator.update((f64::from(i) * 0.5).sin() * 0.05);
|
||||
/// }
|
||||
/// assert!(last.is_some());
|
||||
/// ```
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct MartinRatio {
|
||||
period: usize,
|
||||
window: VecDeque<f64>,
|
||||
}
|
||||
|
||||
impl MartinRatio {
|
||||
/// Construct a Martin Ratio over `period` returns.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns [`Error::InvalidPeriod`] if `period < 2`.
|
||||
pub fn new(period: usize) -> Result<Self> {
|
||||
if period < 2 {
|
||||
return Err(Error::InvalidPeriod {
|
||||
message: "martin ratio needs period >= 2",
|
||||
});
|
||||
}
|
||||
Ok(Self {
|
||||
period,
|
||||
window: VecDeque::with_capacity(period),
|
||||
})
|
||||
}
|
||||
|
||||
/// Configured window of returns.
|
||||
pub const fn period(&self) -> usize {
|
||||
self.period
|
||||
}
|
||||
|
||||
fn compute(&self) -> f64 {
|
||||
#[allow(clippy::cast_precision_loss)]
|
||||
let length = self.window.len() as f64;
|
||||
let mut sum_return = 0.0;
|
||||
let mut sum_drawdown_pct_sq = 0.0;
|
||||
let mut equity = 1.0;
|
||||
let mut peak: f64 = 1.0;
|
||||
for ret in &self.window {
|
||||
sum_return += *ret;
|
||||
equity *= 1.0 + *ret;
|
||||
peak = peak.max(equity);
|
||||
let drawdown_pct = 100.0 * (peak - equity) / peak;
|
||||
sum_drawdown_pct_sq += drawdown_pct * drawdown_pct;
|
||||
}
|
||||
let ulcer_index = (sum_drawdown_pct_sq / length).sqrt();
|
||||
if ulcer_index > 0.0 {
|
||||
(sum_return / length) / ulcer_index
|
||||
} else {
|
||||
0.0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Indicator for MartinRatio {
|
||||
type Input = f64;
|
||||
type Output = f64;
|
||||
|
||||
fn update(&mut self, ret: f64) -> Option<f64> {
|
||||
if !ret.is_finite() {
|
||||
return None;
|
||||
}
|
||||
if self.window.len() == self.period {
|
||||
self.window.pop_front();
|
||||
}
|
||||
self.window.push_back(ret);
|
||||
if self.window.len() < self.period {
|
||||
return None;
|
||||
}
|
||||
Some(self.compute())
|
||||
}
|
||||
|
||||
fn reset(&mut self) {
|
||||
self.window.clear();
|
||||
}
|
||||
|
||||
fn warmup_period(&self) -> usize {
|
||||
self.period
|
||||
}
|
||||
|
||||
fn is_ready(&self) -> bool {
|
||||
self.window.len() == self.period
|
||||
}
|
||||
|
||||
fn name(&self) -> &'static str {
|
||||
"MartinRatio"
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::traits::BatchExt;
|
||||
use approx::assert_relative_eq;
|
||||
|
||||
#[test]
|
||||
fn rejects_period_less_than_two() {
|
||||
assert!(matches!(
|
||||
MartinRatio::new(1),
|
||||
Err(Error::InvalidPeriod { .. })
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn accessors_and_metadata() {
|
||||
let mr = MartinRatio::new(14).unwrap();
|
||||
assert_eq!(mr.period(), 14);
|
||||
assert_eq!(mr.warmup_period(), 14);
|
||||
assert_eq!(mr.name(), "MartinRatio");
|
||||
assert!(!mr.is_ready());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reference_value() {
|
||||
// returns [0.1, -0.1, 0.1]: drawdowns% = [0, 10, 1].
|
||||
// Ulcer Index = sqrt((0 + 100 + 1)/3) = sqrt(101/3).
|
||||
// Martin = (0.1/3) / sqrt(101/3).
|
||||
let mut mr = MartinRatio::new(3).unwrap();
|
||||
let out = mr.batch(&[0.1, -0.1, 0.1]);
|
||||
let expected = (0.1_f64 / 3.0) / (101.0_f64 / 3.0).sqrt();
|
||||
assert_relative_eq!(out[2].unwrap(), expected, epsilon = 1e-9);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn no_drawdown_is_zero() {
|
||||
let mut mr = MartinRatio::new(3).unwrap();
|
||||
let last = mr
|
||||
.batch(&[0.01, 0.02, 0.03])
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.last()
|
||||
.unwrap();
|
||||
assert_relative_eq!(last, 0.0, epsilon = 1e-12);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn losing_window_is_negative() {
|
||||
let mut mr = MartinRatio::new(3).unwrap();
|
||||
let last = mr
|
||||
.batch(&[-0.05, -0.02, -0.03])
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.last()
|
||||
.unwrap();
|
||||
assert!(last < 0.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ignores_non_finite_input() {
|
||||
let mut mr = MartinRatio::new(3).unwrap();
|
||||
assert_eq!(mr.update(0.1), None);
|
||||
assert_eq!(mr.update(f64::NAN), None);
|
||||
assert_eq!(mr.update(-0.1), None);
|
||||
assert!(mr.update(0.1).is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reset_clears_state() {
|
||||
let mut mr = MartinRatio::new(3).unwrap();
|
||||
mr.batch(&[0.1, -0.1, 0.1]);
|
||||
assert!(mr.is_ready());
|
||||
mr.reset();
|
||||
assert!(!mr.is_ready());
|
||||
assert_eq!(mr.update(0.1), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn batch_equals_streaming() {
|
||||
let rets: Vec<f64> = (0..60)
|
||||
.map(|i| (f64::from(i) * 0.25).sin() * 0.05)
|
||||
.collect();
|
||||
let batch = MartinRatio::new(14).unwrap().batch(&rets);
|
||||
let mut streamer = MartinRatio::new(14).unwrap();
|
||||
let streamed: Vec<_> = rets.iter().map(|r| streamer.update(*r)).collect();
|
||||
assert_eq!(batch, streamed);
|
||||
}
|
||||
}
|
||||
@@ -63,6 +63,7 @@ mod bomar_bands;
|
||||
mod breadth_thrust;
|
||||
mod breakaway;
|
||||
mod bullish_percent_index;
|
||||
mod burke_ratio;
|
||||
mod butterfly;
|
||||
mod calendar_spread;
|
||||
mod calmar_ratio;
|
||||
@@ -84,6 +85,7 @@ mod cmf;
|
||||
mod cmo;
|
||||
mod coefficient_of_variation;
|
||||
mod cointegration;
|
||||
mod common_sense_ratio;
|
||||
mod composite_profile;
|
||||
mod concealing_baby_swallow;
|
||||
mod conditional_value_at_risk;
|
||||
@@ -110,6 +112,7 @@ mod disparity_index;
|
||||
mod distance_ssd;
|
||||
mod doji;
|
||||
mod doji_star;
|
||||
mod dollar_bars;
|
||||
mod donchian;
|
||||
mod donchian_stop;
|
||||
mod double_bollinger;
|
||||
@@ -163,6 +166,7 @@ mod funding_rate;
|
||||
mod funding_rate_mean;
|
||||
mod funding_rate_zscore;
|
||||
mod gain_loss_ratio;
|
||||
mod gain_to_pain_ratio;
|
||||
mod gap_side_by_side_white;
|
||||
mod garch11;
|
||||
mod garman_klass;
|
||||
@@ -201,6 +205,7 @@ mod hurst_channel;
|
||||
mod hurst_exponent;
|
||||
mod ichimoku;
|
||||
mod identical_three_crows;
|
||||
mod imbalance_bars;
|
||||
mod in_neck;
|
||||
mod inertia;
|
||||
mod information_ratio;
|
||||
@@ -214,6 +219,7 @@ mod inverted_hammer;
|
||||
mod jarque_bera;
|
||||
mod jma;
|
||||
mod jump_indicator;
|
||||
mod k_ratio;
|
||||
mod kagi_bars;
|
||||
mod kalman_hedge_ratio;
|
||||
mod kama;
|
||||
@@ -241,6 +247,7 @@ mod log_return;
|
||||
mod long_legged_doji;
|
||||
mod long_line;
|
||||
mod long_short_ratio;
|
||||
mod m2_measure;
|
||||
mod ma_envelope;
|
||||
mod macd;
|
||||
mod macd_ext;
|
||||
@@ -248,6 +255,7 @@ mod macd_fix;
|
||||
mod macd_histogram;
|
||||
mod mama;
|
||||
mod market_facilitation_index;
|
||||
mod martin_ratio;
|
||||
mod marubozu;
|
||||
mod mass_index;
|
||||
mod mat_hold;
|
||||
@@ -325,6 +333,7 @@ mod qstick;
|
||||
mod quartile_bands;
|
||||
mod quoted_spread;
|
||||
mod r_squared;
|
||||
mod range_bars;
|
||||
mod realized_spread;
|
||||
mod realized_volatility;
|
||||
mod recovery_factor;
|
||||
@@ -352,6 +361,7 @@ mod rolling_quantile;
|
||||
mod roofing_filter;
|
||||
mod rsi;
|
||||
mod rsx;
|
||||
mod run_bars;
|
||||
mod rvi;
|
||||
mod rvi_volatility;
|
||||
mod rwi;
|
||||
@@ -389,6 +399,7 @@ mod starc_bands;
|
||||
mod stc;
|
||||
mod std_dev;
|
||||
mod step_trailing_stop;
|
||||
mod sterling_ratio;
|
||||
mod stick_sandwich;
|
||||
mod stoch_rsi;
|
||||
mod stochastic;
|
||||
@@ -396,6 +407,7 @@ mod stochastic_cci;
|
||||
mod super_smoother;
|
||||
mod super_trend;
|
||||
mod t3;
|
||||
mod tail_ratio;
|
||||
mod taker_buy_sell_ratio;
|
||||
mod takuri;
|
||||
mod tasuki_gap;
|
||||
@@ -423,11 +435,13 @@ mod term_structure_basis;
|
||||
mod three_drives;
|
||||
mod three_inside;
|
||||
mod three_line_break;
|
||||
mod three_line_break_bars;
|
||||
mod three_line_strike;
|
||||
mod three_outside;
|
||||
mod three_soldiers_or_crows;
|
||||
mod three_stars_in_south;
|
||||
mod thrusting;
|
||||
mod tick_bars;
|
||||
mod tick_index;
|
||||
mod tii;
|
||||
mod time_based_stop;
|
||||
@@ -466,6 +480,7 @@ mod universal_oscillator;
|
||||
mod up_down_volume_ratio;
|
||||
mod upside_gap_three_methods;
|
||||
mod upside_gap_two_crows;
|
||||
mod upside_potential_ratio;
|
||||
mod value_area;
|
||||
mod value_at_risk;
|
||||
mod variance;
|
||||
@@ -476,6 +491,7 @@ mod volatility_cone;
|
||||
mod volatility_of_volatility;
|
||||
mod volatility_ratio;
|
||||
mod volty_stop;
|
||||
mod volume_bars;
|
||||
mod volume_by_time_profile;
|
||||
mod volume_oscillator;
|
||||
mod volume_profile;
|
||||
@@ -561,6 +577,7 @@ pub use bomar_bands::{BomarBands, BomarBandsOutput};
|
||||
pub use breadth_thrust::BreadthThrust;
|
||||
pub use breakaway::Breakaway;
|
||||
pub use bullish_percent_index::BullishPercentIndex;
|
||||
pub use burke_ratio::BurkeRatio;
|
||||
pub use butterfly::Butterfly;
|
||||
pub use calendar_spread::CalendarSpread;
|
||||
pub use calmar_ratio::CalmarRatio;
|
||||
@@ -582,6 +599,7 @@ pub use cmf::ChaikinMoneyFlow;
|
||||
pub use cmo::Cmo;
|
||||
pub use coefficient_of_variation::CoefficientOfVariation;
|
||||
pub use cointegration::{Cointegration, CointegrationOutput};
|
||||
pub use common_sense_ratio::CommonSenseRatio;
|
||||
pub use composite_profile::{CompositeProfile, CompositeProfileOutput};
|
||||
pub use concealing_baby_swallow::ConcealingBabySwallow;
|
||||
pub use conditional_value_at_risk::ConditionalValueAtRisk;
|
||||
@@ -608,6 +626,7 @@ pub use disparity_index::DisparityIndex;
|
||||
pub use distance_ssd::DistanceSsd;
|
||||
pub use doji::Doji;
|
||||
pub use doji_star::DojiStar;
|
||||
pub use dollar_bars::{DollarBar, DollarBars};
|
||||
pub use donchian::{Donchian, DonchianOutput};
|
||||
pub use donchian_stop::{DonchianStop, DonchianStopOutput};
|
||||
pub use double_bollinger::{DoubleBollinger, DoubleBollingerOutput};
|
||||
@@ -661,6 +680,7 @@ pub use funding_rate::FundingRate;
|
||||
pub use funding_rate_mean::FundingRateMean;
|
||||
pub use funding_rate_zscore::FundingRateZScore;
|
||||
pub use gain_loss_ratio::GainLossRatio;
|
||||
pub use gain_to_pain_ratio::GainToPainRatio;
|
||||
pub use gap_side_by_side_white::GapSideBySideWhite;
|
||||
pub use garch11::Garch11;
|
||||
pub use garman_klass::GarmanKlassVolatility;
|
||||
@@ -699,6 +719,7 @@ pub use hurst_channel::{HurstChannel, HurstChannelOutput};
|
||||
pub use hurst_exponent::HurstExponent;
|
||||
pub use ichimoku::{Ichimoku, IchimokuOutput};
|
||||
pub use identical_three_crows::IdenticalThreeCrows;
|
||||
pub use imbalance_bars::{ImbalanceBar, ImbalanceBars};
|
||||
pub use in_neck::InNeck;
|
||||
pub use inertia::Inertia;
|
||||
pub use information_ratio::InformationRatio;
|
||||
@@ -712,6 +733,7 @@ pub use inverted_hammer::InvertedHammer;
|
||||
pub use jarque_bera::JarqueBera;
|
||||
pub use jma::Jma;
|
||||
pub use jump_indicator::JumpIndicator;
|
||||
pub use k_ratio::KRatio;
|
||||
pub use kagi_bars::{KagiBar, KagiBars};
|
||||
pub use kalman_hedge_ratio::{KalmanHedgeRatio, KalmanHedgeRatioOutput};
|
||||
pub use kama::Kama;
|
||||
@@ -739,6 +761,7 @@ pub use log_return::LogReturn;
|
||||
pub use long_legged_doji::LongLeggedDoji;
|
||||
pub use long_line::LongLine;
|
||||
pub use long_short_ratio::LongShortRatio;
|
||||
pub use m2_measure::M2Measure;
|
||||
pub use ma_envelope::{MaEnvelope, MaEnvelopeOutput};
|
||||
pub use macd::{MacdIndicator, MacdOutput};
|
||||
pub use macd_ext::{MaType, MacdExt};
|
||||
@@ -746,6 +769,7 @@ pub use macd_fix::MacdFix;
|
||||
pub use macd_histogram::MacdHistogram;
|
||||
pub use mama::{Mama, MamaOutput};
|
||||
pub use market_facilitation_index::MarketFacilitationIndex;
|
||||
pub use martin_ratio::MartinRatio;
|
||||
pub use marubozu::Marubozu;
|
||||
pub use mass_index::MassIndex;
|
||||
pub use mat_hold::MatHold;
|
||||
@@ -823,6 +847,7 @@ pub use qstick::Qstick;
|
||||
pub use quartile_bands::{QuartileBands, QuartileBandsOutput};
|
||||
pub use quoted_spread::QuotedSpread;
|
||||
pub use r_squared::RSquared;
|
||||
pub use range_bars::{RangeBar, RangeBars};
|
||||
pub use realized_spread::RealizedSpread;
|
||||
pub use realized_volatility::RealizedVolatility;
|
||||
pub use recovery_factor::RecoveryFactor;
|
||||
@@ -850,6 +875,7 @@ pub use rolling_quantile::RollingQuantile;
|
||||
pub use roofing_filter::RoofingFilter;
|
||||
pub use rsi::Rsi;
|
||||
pub use rsx::Rsx;
|
||||
pub use run_bars::{RunBar, RunBars};
|
||||
pub use rvi::Rvi;
|
||||
pub use rvi_volatility::RviVolatility;
|
||||
pub use rwi::{Rwi, RwiOutput};
|
||||
@@ -887,6 +913,7 @@ pub use starc_bands::{StarcBands, StarcBandsOutput};
|
||||
pub use stc::Stc;
|
||||
pub use std_dev::StdDev;
|
||||
pub use step_trailing_stop::StepTrailingStop;
|
||||
pub use sterling_ratio::SterlingRatio;
|
||||
pub use stick_sandwich::StickSandwich;
|
||||
pub use stoch_rsi::StochRsi;
|
||||
pub use stochastic::{Stochastic, StochasticOutput};
|
||||
@@ -894,6 +921,7 @@ pub use stochastic_cci::StochasticCci;
|
||||
pub use super_smoother::SuperSmoother;
|
||||
pub use super_trend::{SuperTrend, SuperTrendOutput};
|
||||
pub use t3::T3;
|
||||
pub use tail_ratio::TailRatio;
|
||||
pub use taker_buy_sell_ratio::TakerBuySellRatio;
|
||||
pub use takuri::Takuri;
|
||||
pub use tasuki_gap::TasukiGap;
|
||||
@@ -921,11 +949,13 @@ pub use term_structure_basis::TermStructureBasis;
|
||||
pub use three_drives::ThreeDrives;
|
||||
pub use three_inside::ThreeInside;
|
||||
pub use three_line_break::ThreeLineBreak;
|
||||
pub use three_line_break_bars::{LineBreakBar, ThreeLineBreakBars};
|
||||
pub use three_line_strike::ThreeLineStrike;
|
||||
pub use three_outside::ThreeOutside;
|
||||
pub use three_soldiers_or_crows::ThreeSoldiersOrCrows;
|
||||
pub use three_stars_in_south::ThreeStarsInSouth;
|
||||
pub use thrusting::Thrusting;
|
||||
pub use tick_bars::{TickBar, TickBars};
|
||||
pub use tick_index::TickIndex;
|
||||
pub use tii::Tii;
|
||||
pub use time_based_stop::TimeBasedStop;
|
||||
@@ -964,6 +994,7 @@ pub use universal_oscillator::UniversalOscillator;
|
||||
pub use up_down_volume_ratio::UpDownVolumeRatio;
|
||||
pub use upside_gap_three_methods::UpsideGapThreeMethods;
|
||||
pub use upside_gap_two_crows::UpsideGapTwoCrows;
|
||||
pub use upside_potential_ratio::UpsidePotentialRatio;
|
||||
pub use value_area::{ValueArea, ValueAreaOutput};
|
||||
pub use value_at_risk::ValueAtRisk;
|
||||
pub use variance::Variance;
|
||||
@@ -974,6 +1005,7 @@ pub use volatility_cone::{VolatilityCone, VolatilityConeOutput};
|
||||
pub use volatility_of_volatility::VolatilityOfVolatility;
|
||||
pub use volatility_ratio::VolatilityRatio;
|
||||
pub use volty_stop::VoltyStop;
|
||||
pub use volume_bars::{VolumeBar, VolumeBars};
|
||||
pub use volume_by_time_profile::{VolumeByTimeProfile, VolumeByTimeProfileOutput};
|
||||
pub use volume_oscillator::VolumeOscillator;
|
||||
pub use volume_profile::{VolumeProfile, VolumeProfileOutput};
|
||||
@@ -1542,11 +1574,31 @@ pub const FAMILIES: &[(&str, &[&str])] = &[
|
||||
"Alpha",
|
||||
"WinRate",
|
||||
"Expectancy",
|
||||
"SterlingRatio",
|
||||
"BurkeRatio",
|
||||
"MartinRatio",
|
||||
"TailRatio",
|
||||
"KRatio",
|
||||
"CommonSenseRatio",
|
||||
"GainToPainRatio",
|
||||
"UpsidePotentialRatio",
|
||||
"M2Measure",
|
||||
],
|
||||
),
|
||||
(
|
||||
"Alt-Chart Bars",
|
||||
&["RenkoBars", "KagiBars", "PointAndFigureBars"],
|
||||
&[
|
||||
"RenkoBars",
|
||||
"KagiBars",
|
||||
"PointAndFigureBars",
|
||||
"RangeBars",
|
||||
"TickBars",
|
||||
"VolumeBars",
|
||||
"DollarBars",
|
||||
"ImbalanceBars",
|
||||
"RunBars",
|
||||
"ThreeLineBreakBars",
|
||||
],
|
||||
),
|
||||
(
|
||||
"Market Breadth",
|
||||
@@ -1654,6 +1706,6 @@ mod family_tests {
|
||||
// the actual indicator count is the early-warning signal that an
|
||||
// indicator was added without being assigned a family.
|
||||
let total: usize = FAMILIES.iter().map(|(_, ns)| ns.len()).sum();
|
||||
assert_eq!(total, 498, "FAMILIES total drifted from indicator count");
|
||||
assert_eq!(total, 514, "FAMILIES total drifted from indicator count");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,227 @@
|
||||
//! Range bar builder — fixed price-range bars with no reversal penalty.
|
||||
|
||||
use crate::error::{Error, Result};
|
||||
use crate::ohlcv::Candle;
|
||||
use crate::traits::BarBuilder;
|
||||
|
||||
/// One completed range bar.
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub struct RangeBar {
|
||||
/// Price at the bar's origin edge.
|
||||
pub open: f64,
|
||||
/// Price at the bar's far edge (`open ± range`).
|
||||
pub close: f64,
|
||||
/// `+1` for an up bar, `-1` for a down bar.
|
||||
pub direction: i8,
|
||||
}
|
||||
|
||||
/// Range bar builder using a fixed price increment on close prices.
|
||||
///
|
||||
/// A range bar completes every time price travels a fixed `range` from the current
|
||||
/// anchor, in *either* direction. This is the key difference from
|
||||
/// [`RenkoBars`](crate::RenkoBars): Renko imposes a `2 * box_size` penalty to
|
||||
/// reverse direction, so it filters out small oscillations; range bars have **no
|
||||
/// reversal penalty** — a move of exactly `range` against the trend prints a bar
|
||||
/// immediately. Range bars therefore track every leg of price movement, while Renko
|
||||
/// smooths them.
|
||||
///
|
||||
/// Construction rules:
|
||||
///
|
||||
/// - The first candle seeds the anchor and prints no bar.
|
||||
/// - Each subsequent candle prints one bar for every `range` of close movement away
|
||||
/// from the anchor; a candle that gaps several ranges prints them all in one
|
||||
/// [`BarBuilder::update`] call.
|
||||
/// - Bars are aligned to the `range` grid relative to the seed price.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// use wickra_core::{BarBuilder, Candle, RangeBars};
|
||||
///
|
||||
/// let flat = |price: f64| Candle::new(price, price, price, price, 1.0, 0).unwrap();
|
||||
/// let mut bars = RangeBars::new(1.0).unwrap();
|
||||
/// assert!(bars.update(flat(10.0)).is_empty()); // seed
|
||||
/// let up = bars.update(flat(12.0)); // +2 ranges
|
||||
/// assert_eq!(up.len(), 2);
|
||||
/// let down = bars.update(flat(11.0)); // -1 range, no penalty
|
||||
/// assert_eq!(down.len(), 1);
|
||||
/// ```
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct RangeBars {
|
||||
range: f64,
|
||||
anchor: Option<f64>,
|
||||
}
|
||||
|
||||
impl RangeBars {
|
||||
/// Construct a range-bar builder with the given price increment.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns [`Error::InvalidPeriod`] if `range` is not finite and positive.
|
||||
pub fn new(range: f64) -> Result<Self> {
|
||||
if !range.is_finite() || range <= 0.0 {
|
||||
return Err(Error::InvalidPeriod {
|
||||
message: "range must be finite and positive",
|
||||
});
|
||||
}
|
||||
Ok(Self {
|
||||
range,
|
||||
anchor: None,
|
||||
})
|
||||
}
|
||||
|
||||
/// Configured price range.
|
||||
pub const fn range(&self) -> f64 {
|
||||
self.range
|
||||
}
|
||||
|
||||
/// Current anchor level (the close of the last completed bar, or the seed
|
||||
/// price before any bar has formed).
|
||||
pub const fn anchor(&self) -> Option<f64> {
|
||||
self.anchor
|
||||
}
|
||||
}
|
||||
|
||||
impl BarBuilder for RangeBars {
|
||||
type Bar = RangeBar;
|
||||
|
||||
fn update(&mut self, candle: Candle) -> Vec<RangeBar> {
|
||||
let close = candle.close;
|
||||
let Some(mut anchor) = self.anchor else {
|
||||
self.anchor = Some(close);
|
||||
return Vec::new();
|
||||
};
|
||||
let range = self.range;
|
||||
let mut bars = Vec::new();
|
||||
while close >= anchor + range {
|
||||
bars.push(RangeBar {
|
||||
open: anchor,
|
||||
close: anchor + range,
|
||||
direction: 1,
|
||||
});
|
||||
anchor += range;
|
||||
}
|
||||
while close <= anchor - range {
|
||||
bars.push(RangeBar {
|
||||
open: anchor,
|
||||
close: anchor - range,
|
||||
direction: -1,
|
||||
});
|
||||
anchor -= range;
|
||||
}
|
||||
self.anchor = Some(anchor);
|
||||
bars
|
||||
}
|
||||
|
||||
fn reset(&mut self) {
|
||||
self.anchor = None;
|
||||
}
|
||||
|
||||
fn name(&self) -> &'static str {
|
||||
"RangeBars"
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use approx::assert_relative_eq;
|
||||
|
||||
fn flat(price: f64) -> Candle {
|
||||
Candle::new(price, price, price, price, 1.0, 0).unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_invalid_range() {
|
||||
assert!(matches!(
|
||||
RangeBars::new(0.0),
|
||||
Err(Error::InvalidPeriod { .. })
|
||||
));
|
||||
assert!(matches!(
|
||||
RangeBars::new(-1.0),
|
||||
Err(Error::InvalidPeriod { .. })
|
||||
));
|
||||
assert!(matches!(
|
||||
RangeBars::new(f64::NAN),
|
||||
Err(Error::InvalidPeriod { .. })
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn accessors_and_metadata() {
|
||||
let bars = RangeBars::new(2.5).unwrap();
|
||||
assert_eq!(bars.name(), "RangeBars");
|
||||
assert_relative_eq!(bars.range(), 2.5, epsilon = 1e-12);
|
||||
assert_eq!(bars.anchor(), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn first_candle_seeds_without_bar() {
|
||||
let mut bars = RangeBars::new(1.0).unwrap();
|
||||
assert!(bars.update(flat(10.0)).is_empty());
|
||||
assert_eq!(bars.anchor(), Some(10.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn up_move_prints_aligned_bars() {
|
||||
let mut bars = RangeBars::new(1.0).unwrap();
|
||||
bars.update(flat(10.0));
|
||||
let up = bars.update(flat(13.0));
|
||||
assert_eq!(up.len(), 3);
|
||||
assert_relative_eq!(up[0].open, 10.0, epsilon = 1e-12);
|
||||
assert_relative_eq!(up[2].close, 13.0, epsilon = 1e-12);
|
||||
assert!(up.iter().all(|b| b.direction == 1));
|
||||
assert_eq!(bars.anchor(), Some(13.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn down_move_prints_aligned_bars() {
|
||||
let mut bars = RangeBars::new(1.0).unwrap();
|
||||
bars.update(flat(10.0));
|
||||
let down = bars.update(flat(7.0));
|
||||
assert_eq!(down.len(), 3);
|
||||
assert!(down.iter().all(|b| b.direction == -1));
|
||||
assert_relative_eq!(down[2].close, 7.0, epsilon = 1e-12);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reversal_needs_only_one_range() {
|
||||
// Unlike Renko, a single-range move against the trend prints immediately.
|
||||
let mut bars = RangeBars::new(1.0).unwrap();
|
||||
bars.update(flat(10.0));
|
||||
bars.update(flat(12.0)); // anchor 12, up
|
||||
let down = bars.update(flat(11.0)); // drop of exactly one range
|
||||
assert_eq!(down.len(), 1);
|
||||
assert_eq!(down[0].direction, -1);
|
||||
assert_relative_eq!(down[0].close, 11.0, epsilon = 1e-12);
|
||||
assert_eq!(bars.anchor(), Some(11.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn small_move_prints_nothing() {
|
||||
let mut bars = RangeBars::new(1.0).unwrap();
|
||||
bars.update(flat(10.0));
|
||||
assert!(bars.update(flat(10.5)).is_empty());
|
||||
assert_eq!(bars.anchor(), Some(10.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reset_clears_state() {
|
||||
let mut bars = RangeBars::new(1.0).unwrap();
|
||||
bars.update(flat(10.0));
|
||||
bars.update(flat(13.0));
|
||||
bars.reset();
|
||||
assert_eq!(bars.anchor(), None);
|
||||
assert!(bars.update(flat(50.0)).is_empty());
|
||||
assert_eq!(bars.anchor(), Some(50.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn batch_concatenates_completed_bars() {
|
||||
let mut bars = RangeBars::new(1.0).unwrap();
|
||||
let candles = [flat(10.0), flat(12.0), flat(13.0)];
|
||||
let out = bars.batch(&candles);
|
||||
assert_eq!(out.len(), 3);
|
||||
assert!(out.iter().all(|b| b.direction == 1));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,257 @@
|
||||
//! Run bar builder (simplified López de Prado) — sample on runs of same-signed ticks.
|
||||
|
||||
use crate::error::{Error, Result};
|
||||
use crate::ohlcv::Candle;
|
||||
use crate::traits::BarBuilder;
|
||||
|
||||
/// One completed run bar.
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub struct RunBar {
|
||||
/// Open of the first candle in the bar.
|
||||
pub open: f64,
|
||||
/// Highest high across the bar.
|
||||
pub high: f64,
|
||||
/// Lowest low across the bar.
|
||||
pub low: f64,
|
||||
/// Close of the candle that closed the bar.
|
||||
pub close: f64,
|
||||
/// Length of the run that closed the bar (`== run_length`).
|
||||
pub length: usize,
|
||||
/// `+1` if a buy run closed the bar, `-1` if a sell run.
|
||||
pub direction: i8,
|
||||
}
|
||||
|
||||
/// Run bar builder — a **simplified** form of López de Prado's run bars.
|
||||
///
|
||||
/// A *run* is an uninterrupted sequence of same-signed ticks: a streak of up-ticks
|
||||
/// (a buy run) or down-ticks (a sell run), with unchanged closes extending the
|
||||
/// current run. This builder counts the current run's length and closes a bar when
|
||||
/// it reaches `run_length`; a tick in the opposite direction restarts the run from
|
||||
/// one. Where [`ImbalanceBars`](crate::ImbalanceBars) sample on the *net* signed
|
||||
/// imbalance (which oscillating flow can cancel back to zero), run bars sample on
|
||||
/// *persistence*: they fire only when the market pushes the same way without
|
||||
/// interruption, making them a cleaner sequential-trend detector.
|
||||
///
|
||||
/// **Simplification.** The full method estimates a *dynamic* expected run length
|
||||
/// from an EWMA and can weight runs by volume or traded value. This builder uses a
|
||||
/// **fixed** run-length threshold on unweighted ticks. See López de Prado (2018),
|
||||
/// ch. 2, for the adaptive estimator and weighted variants.
|
||||
///
|
||||
/// At most one bar closes per candle, so [`BarBuilder::update`] returns either an
|
||||
/// empty vector or a single [`RunBar`].
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// use wickra_core::{BarBuilder, Candle, RunBars};
|
||||
///
|
||||
/// let flat = |price: f64| Candle::new(price, price, price, price, 1.0, 0).unwrap();
|
||||
/// let mut bars = RunBars::new(3).unwrap();
|
||||
/// bars.update(flat(10.0)); // seed
|
||||
/// bars.update(flat(11.0)); // run 1
|
||||
/// bars.update(flat(12.0)); // run 2
|
||||
/// let out = bars.update(flat(13.0)); // run 3 -> close
|
||||
/// assert_eq!(out.len(), 1);
|
||||
/// assert_eq!(out[0].direction, 1);
|
||||
/// ```
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct RunBars {
|
||||
run_length: usize,
|
||||
count: usize,
|
||||
open: f64,
|
||||
high: f64,
|
||||
low: f64,
|
||||
close: f64,
|
||||
prev_close: Option<f64>,
|
||||
run_sign: i8,
|
||||
run_len: usize,
|
||||
}
|
||||
|
||||
impl RunBars {
|
||||
/// Construct a run-bar builder that closes a bar on a run of `run_length` ticks.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns [`Error::PeriodZero`] if `run_length == 0`.
|
||||
pub fn new(run_length: usize) -> Result<Self> {
|
||||
if run_length == 0 {
|
||||
return Err(Error::PeriodZero);
|
||||
}
|
||||
Ok(Self {
|
||||
run_length,
|
||||
count: 0,
|
||||
open: 0.0,
|
||||
high: 0.0,
|
||||
low: 0.0,
|
||||
close: 0.0,
|
||||
prev_close: None,
|
||||
run_sign: 0,
|
||||
run_len: 0,
|
||||
})
|
||||
}
|
||||
|
||||
/// Configured run length that closes a bar.
|
||||
pub const fn run_length(&self) -> usize {
|
||||
self.run_length
|
||||
}
|
||||
|
||||
/// Length of the in-progress run.
|
||||
pub const fn run(&self) -> usize {
|
||||
self.run_len
|
||||
}
|
||||
}
|
||||
|
||||
impl BarBuilder for RunBars {
|
||||
type Bar = RunBar;
|
||||
|
||||
fn update(&mut self, candle: Candle) -> Vec<RunBar> {
|
||||
if self.count == 0 {
|
||||
self.open = candle.open;
|
||||
self.high = candle.high;
|
||||
self.low = candle.low;
|
||||
} else {
|
||||
self.high = self.high.max(candle.high);
|
||||
self.low = self.low.min(candle.low);
|
||||
}
|
||||
self.close = candle.close;
|
||||
self.count += 1;
|
||||
if let Some(prev) = self.prev_close {
|
||||
let directional = if candle.close > prev {
|
||||
1
|
||||
} else if candle.close < prev {
|
||||
-1
|
||||
} else {
|
||||
0
|
||||
};
|
||||
if directional == 0 {
|
||||
// A flat tick extends the current run (if one is under way).
|
||||
if self.run_sign != 0 {
|
||||
self.run_len += 1;
|
||||
}
|
||||
} else if directional == self.run_sign {
|
||||
self.run_len += 1;
|
||||
} else {
|
||||
self.run_sign = directional;
|
||||
self.run_len = 1;
|
||||
}
|
||||
}
|
||||
self.prev_close = Some(candle.close);
|
||||
if self.run_sign == 0 || self.run_len < self.run_length {
|
||||
return Vec::new();
|
||||
}
|
||||
let bar = RunBar {
|
||||
open: self.open,
|
||||
high: self.high,
|
||||
low: self.low,
|
||||
close: self.close,
|
||||
length: self.run_len,
|
||||
direction: self.run_sign,
|
||||
};
|
||||
self.count = 0;
|
||||
self.run_sign = 0;
|
||||
self.run_len = 0;
|
||||
vec![bar]
|
||||
}
|
||||
|
||||
fn reset(&mut self) {
|
||||
self.count = 0;
|
||||
self.prev_close = None;
|
||||
self.run_sign = 0;
|
||||
self.run_len = 0;
|
||||
}
|
||||
|
||||
fn name(&self) -> &'static str {
|
||||
"RunBars"
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn flat(price: f64) -> Candle {
|
||||
Candle::new(price, price, price, price, 1.0, 0).unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_zero_run_length() {
|
||||
assert!(matches!(RunBars::new(0), Err(Error::PeriodZero)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn accessors_and_metadata() {
|
||||
let bars = RunBars::new(5).unwrap();
|
||||
assert_eq!(bars.run_length(), 5);
|
||||
assert_eq!(bars.run(), 0);
|
||||
assert_eq!(bars.name(), "RunBars");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn buy_run_closes_up_bar() {
|
||||
let mut bars = RunBars::new(3).unwrap();
|
||||
bars.update(flat(10.0)); // seed
|
||||
bars.update(flat(11.0)); // run 1
|
||||
bars.update(flat(12.0)); // run 2
|
||||
let out = bars.update(flat(13.0)); // run 3
|
||||
assert_eq!(out.len(), 1);
|
||||
assert_eq!(out[0].direction, 1);
|
||||
assert_eq!(out[0].length, 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sell_run_closes_down_bar() {
|
||||
let mut bars = RunBars::new(3).unwrap();
|
||||
bars.update(flat(10.0));
|
||||
bars.update(flat(9.0)); // run 1
|
||||
bars.update(flat(8.0)); // run 2
|
||||
let out = bars.update(flat(7.0)); // run 3
|
||||
assert_eq!(out.len(), 1);
|
||||
assert_eq!(out[0].direction, -1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn opposite_tick_restarts_run() {
|
||||
let mut bars = RunBars::new(3).unwrap();
|
||||
bars.update(flat(10.0));
|
||||
bars.update(flat(11.0)); // up run 1
|
||||
bars.update(flat(12.0)); // up run 2
|
||||
bars.update(flat(11.0)); // down -> run restarts at 1
|
||||
assert_eq!(bars.run(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn flat_tick_extends_run() {
|
||||
let mut bars = RunBars::new(3).unwrap();
|
||||
bars.update(flat(10.0));
|
||||
bars.update(flat(11.0)); // run 1
|
||||
bars.update(flat(11.0)); // flat -> run 2
|
||||
let out = bars.update(flat(12.0)); // run 3
|
||||
assert_eq!(out.len(), 1);
|
||||
assert_eq!(out[0].direction, 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reset_clears_state() {
|
||||
let mut bars = RunBars::new(3).unwrap();
|
||||
bars.update(flat(10.0));
|
||||
bars.update(flat(11.0));
|
||||
bars.reset();
|
||||
assert_eq!(bars.run(), 0);
|
||||
assert!(bars.update(flat(50.0)).is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn batch_concatenates_completed_bars() {
|
||||
let mut bars = RunBars::new(2).unwrap();
|
||||
let candles = [
|
||||
flat(10.0),
|
||||
flat(11.0), // run 1
|
||||
flat(12.0), // run 2 -> close
|
||||
flat(13.0), // run 1
|
||||
flat(14.0), // run 2 -> close
|
||||
];
|
||||
let out = bars.batch(&candles);
|
||||
assert_eq!(out.len(), 2);
|
||||
assert!(out.iter().all(|b| b.direction == 1));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,216 @@
|
||||
//! Sterling Ratio — mean return over the average drawdown of the equity curve.
|
||||
|
||||
use std::collections::VecDeque;
|
||||
|
||||
use crate::error::{Error, Result};
|
||||
use crate::traits::Indicator;
|
||||
|
||||
/// Sterling Ratio over a trailing window of `period` returns.
|
||||
///
|
||||
/// ```text
|
||||
/// equity_t = Π_{i<=t} (1 + return_i) (compounded curve)
|
||||
/// peak_t = max_{s<=t} equity_s
|
||||
/// dd_t = (peak_t − equity_t) / peak_t (fractional drawdown, >= 0)
|
||||
/// Sterling = mean(returns) / mean(dd_t)
|
||||
/// ```
|
||||
///
|
||||
/// The Sterling Ratio rewards return per unit of *typical* pain: it divides the
|
||||
/// average per-period return by the **average drawdown** experienced along the
|
||||
/// compounded equity curve. Of the three drawdown-based ratios Wickra ships it is
|
||||
/// the gentlest on outliers — averaging the drawdowns means one deep crater does
|
||||
/// not dominate the way it does in the [`BurkeRatio`](crate::BurkeRatio) (which
|
||||
/// sums squared drawdowns) or the [`MartinRatio`](crate::MartinRatio) (which uses
|
||||
/// the root-mean-square percentage drawdown). A window that never draws down has
|
||||
/// zero average drawdown and the indicator reports `0.0`.
|
||||
///
|
||||
/// The first value lands after `period` returns; each `update` rebuilds the equity
|
||||
/// curve over the window (O(period)), which is O(1) in the length of the overall
|
||||
/// series.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// use wickra_core::{Indicator, SterlingRatio};
|
||||
///
|
||||
/// let mut indicator = SterlingRatio::new(12).unwrap();
|
||||
/// let mut last = None;
|
||||
/// for i in 0..24 {
|
||||
/// last = indicator.update((f64::from(i) * 0.5).sin() * 0.05);
|
||||
/// }
|
||||
/// assert!(last.is_some());
|
||||
/// ```
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct SterlingRatio {
|
||||
period: usize,
|
||||
window: VecDeque<f64>,
|
||||
}
|
||||
|
||||
impl SterlingRatio {
|
||||
/// Construct a Sterling Ratio over `period` returns.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns [`Error::InvalidPeriod`] if `period < 2`.
|
||||
pub fn new(period: usize) -> Result<Self> {
|
||||
if period < 2 {
|
||||
return Err(Error::InvalidPeriod {
|
||||
message: "sterling ratio needs period >= 2",
|
||||
});
|
||||
}
|
||||
Ok(Self {
|
||||
period,
|
||||
window: VecDeque::with_capacity(period),
|
||||
})
|
||||
}
|
||||
|
||||
/// Configured window of returns.
|
||||
pub const fn period(&self) -> usize {
|
||||
self.period
|
||||
}
|
||||
|
||||
fn compute(&self) -> f64 {
|
||||
#[allow(clippy::cast_precision_loss)]
|
||||
let length = self.window.len() as f64;
|
||||
let mut sum_return = 0.0;
|
||||
let mut sum_drawdown = 0.0;
|
||||
let mut equity = 1.0;
|
||||
let mut peak: f64 = 1.0;
|
||||
for ret in &self.window {
|
||||
sum_return += *ret;
|
||||
equity *= 1.0 + *ret;
|
||||
peak = peak.max(equity);
|
||||
sum_drawdown += (peak - equity) / peak;
|
||||
}
|
||||
let avg_drawdown = sum_drawdown / length;
|
||||
if avg_drawdown > 0.0 {
|
||||
(sum_return / length) / avg_drawdown
|
||||
} else {
|
||||
0.0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Indicator for SterlingRatio {
|
||||
type Input = f64;
|
||||
type Output = f64;
|
||||
|
||||
fn update(&mut self, ret: f64) -> Option<f64> {
|
||||
if !ret.is_finite() {
|
||||
return None;
|
||||
}
|
||||
if self.window.len() == self.period {
|
||||
self.window.pop_front();
|
||||
}
|
||||
self.window.push_back(ret);
|
||||
if self.window.len() < self.period {
|
||||
return None;
|
||||
}
|
||||
Some(self.compute())
|
||||
}
|
||||
|
||||
fn reset(&mut self) {
|
||||
self.window.clear();
|
||||
}
|
||||
|
||||
fn warmup_period(&self) -> usize {
|
||||
self.period
|
||||
}
|
||||
|
||||
fn is_ready(&self) -> bool {
|
||||
self.window.len() == self.period
|
||||
}
|
||||
|
||||
fn name(&self) -> &'static str {
|
||||
"SterlingRatio"
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::traits::BatchExt;
|
||||
use approx::assert_relative_eq;
|
||||
|
||||
#[test]
|
||||
fn rejects_period_less_than_two() {
|
||||
assert!(matches!(
|
||||
SterlingRatio::new(1),
|
||||
Err(Error::InvalidPeriod { .. })
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn accessors_and_metadata() {
|
||||
let sr = SterlingRatio::new(12).unwrap();
|
||||
assert_eq!(sr.period(), 12);
|
||||
assert_eq!(sr.warmup_period(), 12);
|
||||
assert_eq!(sr.name(), "SterlingRatio");
|
||||
assert!(!sr.is_ready());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reference_value() {
|
||||
// returns [0.1, -0.1, 0.1]:
|
||||
// equity 1.1, 0.99, 1.089; peak stays 1.1.
|
||||
// dd = [0, 0.1, 0.01]; avg_dd = 0.11/3; mean_return = 0.1/3.
|
||||
// Sterling = (0.1/3) / (0.11/3) = 0.1/0.11.
|
||||
let mut sr = SterlingRatio::new(3).unwrap();
|
||||
let out = sr.batch(&[0.1, -0.1, 0.1]);
|
||||
assert_relative_eq!(out[2].unwrap(), 0.1_f64 / 0.11, epsilon = 1e-9);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn no_drawdown_is_zero() {
|
||||
// Monotonically rising equity never draws down.
|
||||
let mut sr = SterlingRatio::new(3).unwrap();
|
||||
let last = sr
|
||||
.batch(&[0.01, 0.02, 0.03])
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.last()
|
||||
.unwrap();
|
||||
assert_relative_eq!(last, 0.0, epsilon = 1e-12);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn losing_window_is_negative() {
|
||||
let mut sr = SterlingRatio::new(3).unwrap();
|
||||
let last = sr
|
||||
.batch(&[-0.05, -0.02, -0.03])
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.last()
|
||||
.unwrap();
|
||||
assert!(last < 0.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ignores_non_finite_input() {
|
||||
let mut sr = SterlingRatio::new(3).unwrap();
|
||||
assert_eq!(sr.update(0.1), None);
|
||||
assert_eq!(sr.update(f64::NAN), None);
|
||||
assert_eq!(sr.update(-0.1), None);
|
||||
assert!(sr.update(0.1).is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reset_clears_state() {
|
||||
let mut sr = SterlingRatio::new(3).unwrap();
|
||||
sr.batch(&[0.1, -0.1, 0.1]);
|
||||
assert!(sr.is_ready());
|
||||
sr.reset();
|
||||
assert!(!sr.is_ready());
|
||||
assert_eq!(sr.update(0.1), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn batch_equals_streaming() {
|
||||
let rets: Vec<f64> = (0..60)
|
||||
.map(|i| (f64::from(i) * 0.25).sin() * 0.05)
|
||||
.collect();
|
||||
let batch = SterlingRatio::new(12).unwrap().batch(&rets);
|
||||
let mut streamer = SterlingRatio::new(12).unwrap();
|
||||
let streamed: Vec<_> = rets.iter().map(|r| streamer.update(*r)).collect();
|
||||
assert_eq!(batch, streamed);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,224 @@
|
||||
//! Tail Ratio — the right tail (95th percentile) over the absolute left tail (5th percentile).
|
||||
|
||||
use std::collections::VecDeque;
|
||||
|
||||
use crate::error::{Error, Result};
|
||||
use crate::traits::Indicator;
|
||||
|
||||
/// Tail Ratio over a trailing window of `period` returns.
|
||||
///
|
||||
/// ```text
|
||||
/// TailRatio = P95(returns) / |P5(returns)|
|
||||
/// ```
|
||||
///
|
||||
/// The Tail Ratio contrasts the magnitude of the best outcomes against the worst:
|
||||
/// the 95th percentile of the return distribution divided by the absolute value of
|
||||
/// the 5th percentile. A value above `1.0` means the right tail (upside surprises)
|
||||
/// is fatter than the left tail (downside surprises); below `1.0` means crashes are
|
||||
/// larger than rallies. It is a distribution-shape statistic, distinct from the
|
||||
/// average-based [`SharpeRatio`](crate::SharpeRatio): two series with the same mean
|
||||
/// and variance can have very different tail ratios.
|
||||
///
|
||||
/// Percentiles are computed by linear interpolation over the sorted window
|
||||
/// (the same rule `NumPy` uses by default). A window whose 5th percentile is exactly
|
||||
/// zero has no measurable left tail and the indicator reports `0.0` rather than
|
||||
/// dividing by zero.
|
||||
///
|
||||
/// The first value lands after `period` returns; each `update` re-sorts the window
|
||||
/// (O(period log period)), which is O(1) in the length of the overall series.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// use wickra_core::{Indicator, TailRatio};
|
||||
///
|
||||
/// let mut indicator = TailRatio::new(20).unwrap();
|
||||
/// let mut last = None;
|
||||
/// for i in 0..40 {
|
||||
/// last = indicator.update((f64::from(i) * 0.3).sin() * 0.02);
|
||||
/// }
|
||||
/// assert!(last.is_some());
|
||||
/// ```
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct TailRatio {
|
||||
period: usize,
|
||||
window: VecDeque<f64>,
|
||||
}
|
||||
|
||||
impl TailRatio {
|
||||
/// Construct a Tail Ratio over `period` returns.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns [`Error::InvalidPeriod`] if `period < 2` (percentiles need at least
|
||||
/// two observations to interpolate).
|
||||
pub fn new(period: usize) -> Result<Self> {
|
||||
if period < 2 {
|
||||
return Err(Error::InvalidPeriod {
|
||||
message: "tail ratio needs period >= 2",
|
||||
});
|
||||
}
|
||||
Ok(Self {
|
||||
period,
|
||||
window: VecDeque::with_capacity(period),
|
||||
})
|
||||
}
|
||||
|
||||
/// Configured window of returns.
|
||||
pub const fn period(&self) -> usize {
|
||||
self.period
|
||||
}
|
||||
|
||||
fn compute(&self) -> f64 {
|
||||
let mut sorted: Vec<f64> = self.window.iter().copied().collect();
|
||||
sorted.sort_unstable_by(f64::total_cmp);
|
||||
let upper = percentile(&sorted, 95.0);
|
||||
let lower = percentile(&sorted, 5.0).abs();
|
||||
if lower > 0.0 {
|
||||
upper / lower
|
||||
} else {
|
||||
0.0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Linear-interpolation percentile of an ascending, non-empty slice.
|
||||
fn percentile(sorted: &[f64], pct: f64) -> f64 {
|
||||
let last_index = sorted.len() - 1;
|
||||
#[allow(clippy::cast_precision_loss)]
|
||||
let rank = pct / 100.0 * last_index as f64;
|
||||
let floor = rank.floor();
|
||||
// `rank` lies in `[0, last_index]`, so its floor is a valid in-bounds index.
|
||||
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
|
||||
let lower = floor as usize;
|
||||
if lower >= last_index {
|
||||
return sorted[last_index];
|
||||
}
|
||||
let frac = rank - floor;
|
||||
sorted[lower] + frac * (sorted[lower + 1] - sorted[lower])
|
||||
}
|
||||
|
||||
impl Indicator for TailRatio {
|
||||
type Input = f64;
|
||||
type Output = f64;
|
||||
|
||||
fn update(&mut self, ret: f64) -> Option<f64> {
|
||||
if !ret.is_finite() {
|
||||
return None;
|
||||
}
|
||||
if self.window.len() == self.period {
|
||||
self.window.pop_front();
|
||||
}
|
||||
self.window.push_back(ret);
|
||||
if self.window.len() < self.period {
|
||||
return None;
|
||||
}
|
||||
Some(self.compute())
|
||||
}
|
||||
|
||||
fn reset(&mut self) {
|
||||
self.window.clear();
|
||||
}
|
||||
|
||||
fn warmup_period(&self) -> usize {
|
||||
self.period
|
||||
}
|
||||
|
||||
fn is_ready(&self) -> bool {
|
||||
self.window.len() == self.period
|
||||
}
|
||||
|
||||
fn name(&self) -> &'static str {
|
||||
"TailRatio"
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::traits::BatchExt;
|
||||
use approx::assert_relative_eq;
|
||||
|
||||
#[test]
|
||||
fn rejects_period_less_than_two() {
|
||||
assert!(matches!(
|
||||
TailRatio::new(1),
|
||||
Err(Error::InvalidPeriod { .. })
|
||||
));
|
||||
assert!(matches!(
|
||||
TailRatio::new(0),
|
||||
Err(Error::InvalidPeriod { .. })
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn accessors_and_metadata() {
|
||||
let tr = TailRatio::new(20).unwrap();
|
||||
assert_eq!(tr.period(), 20);
|
||||
assert_eq!(tr.warmup_period(), 20);
|
||||
assert_eq!(tr.name(), "TailRatio");
|
||||
assert!(!tr.is_ready());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reference_value() {
|
||||
// sorted window [-0.04, -0.02, 0.0, 0.02, 0.04], last_index = 4.
|
||||
// P95: rank 3.8 -> 0.02 + 0.8*(0.04-0.02) = 0.036.
|
||||
// P5: rank 0.2 -> -0.04 + 0.2*(0.02) = -0.036, abs 0.036.
|
||||
// ratio = 0.036 / 0.036 = 1.0.
|
||||
let mut tr = TailRatio::new(5).unwrap();
|
||||
let out = tr.batch(&[-0.04, -0.02, 0.0, 0.02, 0.04]);
|
||||
assert_relative_eq!(out[4].unwrap(), 1.0, epsilon = 1e-9);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fatter_right_tail_exceeds_one() {
|
||||
let mut tr = TailRatio::new(5).unwrap();
|
||||
let out = tr.batch(&[-0.01, 0.0, 0.01, 0.02, 0.10]);
|
||||
assert!(out[4].unwrap() > 1.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn flat_window_is_zero() {
|
||||
let mut tr = TailRatio::new(4).unwrap();
|
||||
let last = tr.batch(&[0.0; 4]).into_iter().flatten().last().unwrap();
|
||||
assert_relative_eq!(last, 0.0, epsilon = 1e-12);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ignores_non_finite_input() {
|
||||
let mut tr = TailRatio::new(3).unwrap();
|
||||
assert_eq!(tr.update(0.01), None);
|
||||
assert_eq!(tr.update(f64::NAN), None);
|
||||
assert_eq!(tr.update(0.02), None);
|
||||
assert!(tr.update(0.03).is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reset_clears_state() {
|
||||
let mut tr = TailRatio::new(3).unwrap();
|
||||
tr.batch(&[-0.01, 0.0, 0.02]);
|
||||
assert!(tr.is_ready());
|
||||
tr.reset();
|
||||
assert!(!tr.is_ready());
|
||||
assert_eq!(tr.update(0.01), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn batch_equals_streaming() {
|
||||
let rets: Vec<f64> = (0..60)
|
||||
.map(|i| (f64::from(i) * 0.25).sin() * 0.02)
|
||||
.collect();
|
||||
let batch = TailRatio::new(15).unwrap().batch(&rets);
|
||||
let mut streamer = TailRatio::new(15).unwrap();
|
||||
let streamed: Vec<_> = rets.iter().map(|r| streamer.update(*r)).collect();
|
||||
assert_eq!(batch, streamed);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn percentile_at_top_returns_last() {
|
||||
// When the rank floor reaches the final index (the 100th percentile), the
|
||||
// helper returns the largest element without interpolating past the end.
|
||||
assert_relative_eq!(percentile(&[1.0, 2.0, 3.0], 100.0), 3.0, epsilon = 1e-12);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,305 @@
|
||||
//! Three-Line-Break bar builder — line-break chart segments driven by close prices.
|
||||
|
||||
use std::collections::VecDeque;
|
||||
|
||||
use crate::error::{Error, Result};
|
||||
use crate::ohlcv::Candle;
|
||||
use crate::traits::BarBuilder;
|
||||
|
||||
/// One completed line-break line.
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub struct LineBreakBar {
|
||||
/// Price where the line began (the previous line's far edge).
|
||||
pub open: f64,
|
||||
/// Price where the line ended (the new close that drew it).
|
||||
pub close: f64,
|
||||
/// `+1` for a rising line, `-1` for a falling line.
|
||||
pub direction: i8,
|
||||
}
|
||||
|
||||
/// Three-Line-Break bar builder using the classic close-based reversal rule.
|
||||
///
|
||||
/// A line-break chart draws a new line in the trend direction whenever the close
|
||||
/// makes a new extreme, and only reverses when the close breaks the extreme of the
|
||||
/// previous `lines` lines (three by default — hence "three-line break"). This filters
|
||||
/// minor noise: a pullback that fails to exceed the last three lines is ignored
|
||||
/// entirely, so the chart isolates meaningful reversals.
|
||||
///
|
||||
/// This is the **bar-builder** counterpart of the
|
||||
/// [`ThreeLineBreak`](crate::ThreeLineBreak) indicator: the indicator reports the
|
||||
/// current line *state* as a streaming value, whereas this builder emits each
|
||||
/// completed line as a [`LineBreakBar`] so you can reconstruct the full line-break
|
||||
/// chart. At most one line forms per candle, so [`BarBuilder::update`] returns either
|
||||
/// an empty vector or a single bar.
|
||||
///
|
||||
/// Construction rules:
|
||||
///
|
||||
/// - The first candle seeds a reference close and prints nothing.
|
||||
/// - The first subsequent move (up or down) draws the first line.
|
||||
/// - In an up-trend a close above the last line's top extends it (a new up line); a
|
||||
/// close below the lowest low of the last `lines` lines reverses to a down line.
|
||||
/// The down-trend is symmetric.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// use wickra_core::{BarBuilder, Candle, ThreeLineBreakBars};
|
||||
///
|
||||
/// let flat = |price: f64| Candle::new(price, price, price, price, 1.0, 0).unwrap();
|
||||
/// let mut bars = ThreeLineBreakBars::new(3).unwrap();
|
||||
/// bars.update(flat(10.0)); // seed
|
||||
/// let first = bars.update(flat(11.0)); // first up line
|
||||
/// assert_eq!(first.len(), 1);
|
||||
/// assert_eq!(first[0].direction, 1);
|
||||
/// ```
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ThreeLineBreakBars {
|
||||
lines: usize,
|
||||
seed: Option<f64>,
|
||||
recent: VecDeque<LineBreakBar>,
|
||||
}
|
||||
|
||||
impl ThreeLineBreakBars {
|
||||
/// Construct a line-break builder that reverses on a break of the last `lines`
|
||||
/// lines (3 for the classic three-line break).
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns [`Error::PeriodZero`] if `lines == 0`.
|
||||
pub fn new(lines: usize) -> Result<Self> {
|
||||
if lines == 0 {
|
||||
return Err(Error::PeriodZero);
|
||||
}
|
||||
Ok(Self {
|
||||
lines,
|
||||
seed: None,
|
||||
recent: VecDeque::with_capacity(lines),
|
||||
})
|
||||
}
|
||||
|
||||
/// Configured number of lines a reversal must break.
|
||||
pub const fn lines(&self) -> usize {
|
||||
self.lines
|
||||
}
|
||||
|
||||
/// Number of recent lines currently tracked for the reversal test.
|
||||
pub fn tracked(&self) -> usize {
|
||||
self.recent.len()
|
||||
}
|
||||
|
||||
fn push_line(&mut self, bar: LineBreakBar) {
|
||||
if self.recent.len() == self.lines {
|
||||
self.recent.pop_front();
|
||||
}
|
||||
self.recent.push_back(bar);
|
||||
}
|
||||
|
||||
fn lowest_low(&self) -> f64 {
|
||||
self.recent
|
||||
.iter()
|
||||
.map(|bar| bar.open.min(bar.close))
|
||||
.fold(f64::INFINITY, f64::min)
|
||||
}
|
||||
|
||||
fn highest_high(&self) -> f64 {
|
||||
self.recent
|
||||
.iter()
|
||||
.map(|bar| bar.open.max(bar.close))
|
||||
.fold(f64::NEG_INFINITY, f64::max)
|
||||
}
|
||||
}
|
||||
|
||||
impl BarBuilder for ThreeLineBreakBars {
|
||||
type Bar = LineBreakBar;
|
||||
|
||||
fn update(&mut self, candle: Candle) -> Vec<LineBreakBar> {
|
||||
let close = candle.close;
|
||||
let Some(last) = self.recent.back().copied() else {
|
||||
// No line yet: seed, then draw the first line on the first move.
|
||||
let Some(seed) = self.seed else {
|
||||
self.seed = Some(close);
|
||||
return Vec::new();
|
||||
};
|
||||
let bar = if close > seed {
|
||||
LineBreakBar {
|
||||
open: seed,
|
||||
close,
|
||||
direction: 1,
|
||||
}
|
||||
} else if close < seed {
|
||||
LineBreakBar {
|
||||
open: seed,
|
||||
close,
|
||||
direction: -1,
|
||||
}
|
||||
} else {
|
||||
return Vec::new();
|
||||
};
|
||||
self.push_line(bar);
|
||||
return vec![bar];
|
||||
};
|
||||
let new_bar = if last.direction > 0 {
|
||||
if close > last.close {
|
||||
Some(LineBreakBar {
|
||||
open: last.close,
|
||||
close,
|
||||
direction: 1,
|
||||
})
|
||||
} else if close < self.lowest_low() {
|
||||
Some(LineBreakBar {
|
||||
open: last.close,
|
||||
close,
|
||||
direction: -1,
|
||||
})
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else if close < last.close {
|
||||
Some(LineBreakBar {
|
||||
open: last.close,
|
||||
close,
|
||||
direction: -1,
|
||||
})
|
||||
} else if close > self.highest_high() {
|
||||
Some(LineBreakBar {
|
||||
open: last.close,
|
||||
close,
|
||||
direction: 1,
|
||||
})
|
||||
} else {
|
||||
None
|
||||
};
|
||||
if let Some(bar) = new_bar {
|
||||
self.push_line(bar);
|
||||
vec![bar]
|
||||
} else {
|
||||
Vec::new()
|
||||
}
|
||||
}
|
||||
|
||||
fn reset(&mut self) {
|
||||
self.seed = None;
|
||||
self.recent.clear();
|
||||
}
|
||||
|
||||
fn name(&self) -> &'static str {
|
||||
"ThreeLineBreakBars"
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use approx::assert_relative_eq;
|
||||
|
||||
fn flat(price: f64) -> Candle {
|
||||
Candle::new(price, price, price, price, 1.0, 0).unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_zero_lines() {
|
||||
assert!(matches!(ThreeLineBreakBars::new(0), Err(Error::PeriodZero)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn accessors_and_metadata() {
|
||||
let bars = ThreeLineBreakBars::new(3).unwrap();
|
||||
assert_eq!(bars.lines(), 3);
|
||||
assert_eq!(bars.tracked(), 0);
|
||||
assert_eq!(bars.name(), "ThreeLineBreakBars");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn seed_then_first_line() {
|
||||
let mut bars = ThreeLineBreakBars::new(3).unwrap();
|
||||
assert!(bars.update(flat(10.0)).is_empty()); // seed
|
||||
let first = bars.update(flat(11.0));
|
||||
assert_eq!(first.len(), 1);
|
||||
assert_eq!(first[0].direction, 1);
|
||||
assert_relative_eq!(first[0].open, 10.0, epsilon = 1e-12);
|
||||
assert_relative_eq!(first[0].close, 11.0, epsilon = 1e-12);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn new_high_extends_up_line() {
|
||||
let mut bars = ThreeLineBreakBars::new(3).unwrap();
|
||||
bars.update(flat(10.0));
|
||||
bars.update(flat(11.0)); // line 1 up
|
||||
let cont = bars.update(flat(12.0)); // new high -> extend
|
||||
assert_eq!(cont.len(), 1);
|
||||
assert_eq!(cont[0].direction, 1);
|
||||
assert_relative_eq!(cont[0].open, 11.0, epsilon = 1e-12);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn small_pullback_prints_nothing() {
|
||||
let mut bars = ThreeLineBreakBars::new(3).unwrap();
|
||||
bars.update(flat(10.0));
|
||||
bars.update(flat(11.0)); // line 1
|
||||
bars.update(flat(12.0)); // line 2
|
||||
bars.update(flat(13.0)); // line 3, lows are 10/11/12
|
||||
assert!(bars.update(flat(10.5)).is_empty()); // not > 13, not < 10
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reversal_breaks_three_lines() {
|
||||
let mut bars = ThreeLineBreakBars::new(3).unwrap();
|
||||
bars.update(flat(10.0));
|
||||
bars.update(flat(11.0)); // line 1, low 10
|
||||
bars.update(flat(12.0)); // line 2, low 11
|
||||
bars.update(flat(13.0)); // line 3, low 12
|
||||
let rev = bars.update(flat(9.0)); // 9 < lowest low 10 -> reverse
|
||||
assert_eq!(rev.len(), 1);
|
||||
assert_eq!(rev[0].direction, -1);
|
||||
assert_relative_eq!(rev[0].open, 13.0, epsilon = 1e-12);
|
||||
assert_relative_eq!(rev[0].close, 9.0, epsilon = 1e-12);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reset_clears_state() {
|
||||
let mut bars = ThreeLineBreakBars::new(3).unwrap();
|
||||
bars.update(flat(10.0));
|
||||
bars.update(flat(11.0));
|
||||
bars.reset();
|
||||
assert_eq!(bars.tracked(), 0);
|
||||
assert!(bars.update(flat(50.0)).is_empty()); // re-seeds
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn flat_first_move_prints_nothing() {
|
||||
let mut bars = ThreeLineBreakBars::new(3).unwrap();
|
||||
assert!(bars.update(flat(10.0)).is_empty()); // seed
|
||||
assert!(bars.update(flat(10.0)).is_empty()); // equal to seed -> no line
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn first_line_down_then_down_trend_and_reversal() {
|
||||
let mut bars = ThreeLineBreakBars::new(3).unwrap();
|
||||
assert!(bars.update(flat(10.0)).is_empty()); // seed
|
||||
let first = bars.update(flat(9.0)); // first line down
|
||||
assert_eq!(first.len(), 1);
|
||||
assert_eq!(first[0].direction, -1);
|
||||
assert_relative_eq!(first[0].open, 10.0, epsilon = 1e-12);
|
||||
assert_relative_eq!(first[0].close, 9.0, epsilon = 1e-12);
|
||||
let cont = bars.update(flat(8.0)); // new low extends the down line
|
||||
assert_eq!(cont.len(), 1);
|
||||
assert_eq!(cont[0].direction, -1);
|
||||
assert_relative_eq!(cont[0].open, 9.0, epsilon = 1e-12);
|
||||
bars.update(flat(7.0)); // third down line; highs are 10/9/8
|
||||
assert!(bars.update(flat(7.5)).is_empty()); // not < 7, not > highest high 10
|
||||
let rev = bars.update(flat(11.0)); // > highest high 10 -> reverse up
|
||||
assert_eq!(rev.len(), 1);
|
||||
assert_eq!(rev[0].direction, 1);
|
||||
assert_relative_eq!(rev[0].open, 7.0, epsilon = 1e-12);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn batch_concatenates_completed_lines() {
|
||||
let mut bars = ThreeLineBreakBars::new(3).unwrap();
|
||||
let candles = [flat(10.0), flat(11.0), flat(12.0), flat(13.0)];
|
||||
let out = bars.batch(&candles);
|
||||
// seed at 10, then three rising lines.
|
||||
assert_eq!(out.len(), 3);
|
||||
assert!(out.iter().all(|b| b.direction == 1));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,209 @@
|
||||
//! Tick bar builder — aggregate a fixed number of candles into one OHLCV bar.
|
||||
|
||||
use crate::error::{Error, Result};
|
||||
use crate::ohlcv::Candle;
|
||||
use crate::traits::BarBuilder;
|
||||
|
||||
/// One completed tick bar (an OHLCV aggregate of `ticks` input candles).
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub struct TickBar {
|
||||
/// Open of the first candle in the group.
|
||||
pub open: f64,
|
||||
/// Highest high across the group.
|
||||
pub high: f64,
|
||||
/// Lowest low across the group.
|
||||
pub low: f64,
|
||||
/// Close of the last candle in the group.
|
||||
pub close: f64,
|
||||
/// Summed volume across the group.
|
||||
pub volume: f64,
|
||||
}
|
||||
|
||||
/// Tick bar builder — emits one OHLCV bar for every `ticks` input candles.
|
||||
///
|
||||
/// Classic time bars (1-minute, 1-hour) sample the market on a clock; tick bars
|
||||
/// sample it on *activity* by grouping a fixed number of trades — here modelled as a
|
||||
/// fixed number of input candles. In fast markets a tick bar closes quickly; in
|
||||
/// quiet markets it takes longer, so each bar carries roughly equal information
|
||||
/// content. This is the simplest of the information-driven bar types; the
|
||||
/// [`VolumeBars`](crate::VolumeBars) and [`DollarBars`](crate::DollarBars) builders
|
||||
/// extend the idea to equal traded volume and equal traded value respectively.
|
||||
///
|
||||
/// The open is the first candle's open, the high and low are the extremes across the
|
||||
/// group, the close is the last candle's close, and the volume is the group sum.
|
||||
/// Exactly one bar completes every `ticks` candles, so [`BarBuilder::update`]
|
||||
/// returns either an empty vector or a single [`TickBar`].
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// use wickra_core::{BarBuilder, Candle, TickBars};
|
||||
///
|
||||
/// let c = |o, h, l, cl, v| Candle::new(o, h, l, cl, v, 0).unwrap();
|
||||
/// let mut bars = TickBars::new(3).unwrap();
|
||||
/// assert!(bars.update(c(10.0, 11.0, 9.0, 10.5, 100.0)).is_empty());
|
||||
/// assert!(bars.update(c(10.5, 12.0, 10.0, 11.0, 150.0)).is_empty());
|
||||
/// let out = bars.update(c(11.0, 11.5, 10.8, 11.2, 120.0));
|
||||
/// assert_eq!(out.len(), 1);
|
||||
/// assert_eq!(out[0].volume, 370.0);
|
||||
/// ```
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct TickBars {
|
||||
ticks: usize,
|
||||
count: usize,
|
||||
open: f64,
|
||||
high: f64,
|
||||
low: f64,
|
||||
close: f64,
|
||||
volume: f64,
|
||||
}
|
||||
|
||||
impl TickBars {
|
||||
/// Construct a tick-bar builder that groups `ticks` candles per bar.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns [`Error::PeriodZero`] if `ticks == 0`.
|
||||
pub fn new(ticks: usize) -> Result<Self> {
|
||||
if ticks == 0 {
|
||||
return Err(Error::PeriodZero);
|
||||
}
|
||||
Ok(Self {
|
||||
ticks,
|
||||
count: 0,
|
||||
open: 0.0,
|
||||
high: 0.0,
|
||||
low: 0.0,
|
||||
close: 0.0,
|
||||
volume: 0.0,
|
||||
})
|
||||
}
|
||||
|
||||
/// Configured number of candles per bar.
|
||||
pub const fn ticks(&self) -> usize {
|
||||
self.ticks
|
||||
}
|
||||
|
||||
/// Number of candles accumulated into the in-progress bar.
|
||||
pub const fn count(&self) -> usize {
|
||||
self.count
|
||||
}
|
||||
}
|
||||
|
||||
impl BarBuilder for TickBars {
|
||||
type Bar = TickBar;
|
||||
|
||||
fn update(&mut self, candle: Candle) -> Vec<TickBar> {
|
||||
if self.count == 0 {
|
||||
self.open = candle.open;
|
||||
self.high = candle.high;
|
||||
self.low = candle.low;
|
||||
self.volume = 0.0;
|
||||
} else {
|
||||
self.high = self.high.max(candle.high);
|
||||
self.low = self.low.min(candle.low);
|
||||
}
|
||||
self.close = candle.close;
|
||||
self.volume += candle.volume;
|
||||
self.count += 1;
|
||||
if self.count < self.ticks {
|
||||
return Vec::new();
|
||||
}
|
||||
self.count = 0;
|
||||
vec![TickBar {
|
||||
open: self.open,
|
||||
high: self.high,
|
||||
low: self.low,
|
||||
close: self.close,
|
||||
volume: self.volume,
|
||||
}]
|
||||
}
|
||||
|
||||
fn reset(&mut self) {
|
||||
self.count = 0;
|
||||
self.volume = 0.0;
|
||||
}
|
||||
|
||||
fn name(&self) -> &'static str {
|
||||
"TickBars"
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use approx::assert_relative_eq;
|
||||
|
||||
fn candle(open: f64, high: f64, low: f64, close: f64, volume: f64) -> Candle {
|
||||
Candle::new(open, high, low, close, volume, 0).unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_zero_ticks() {
|
||||
assert!(matches!(TickBars::new(0), Err(Error::PeriodZero)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn accessors_and_metadata() {
|
||||
let bars = TickBars::new(5).unwrap();
|
||||
assert_eq!(bars.ticks(), 5);
|
||||
assert_eq!(bars.count(), 0);
|
||||
assert_eq!(bars.name(), "TickBars");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn emits_every_n_candles() {
|
||||
let mut bars = TickBars::new(2).unwrap();
|
||||
assert!(bars.update(candle(10.0, 10.0, 10.0, 10.0, 1.0)).is_empty());
|
||||
assert_eq!(bars.update(candle(10.0, 10.0, 10.0, 10.0, 1.0)).len(), 1);
|
||||
assert!(bars.update(candle(10.0, 10.0, 10.0, 10.0, 1.0)).is_empty());
|
||||
assert_eq!(bars.update(candle(10.0, 10.0, 10.0, 10.0, 1.0)).len(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn aggregates_ohlcv() {
|
||||
let mut bars = TickBars::new(3).unwrap();
|
||||
bars.update(candle(10.0, 11.0, 9.0, 10.5, 100.0));
|
||||
bars.update(candle(10.5, 12.0, 10.0, 11.0, 150.0));
|
||||
let out = bars.update(candle(11.0, 11.5, 10.8, 11.2, 120.0));
|
||||
assert_eq!(out.len(), 1);
|
||||
assert_relative_eq!(out[0].open, 10.0, epsilon = 1e-12);
|
||||
assert_relative_eq!(out[0].high, 12.0, epsilon = 1e-12);
|
||||
assert_relative_eq!(out[0].low, 9.0, epsilon = 1e-12);
|
||||
assert_relative_eq!(out[0].close, 11.2, epsilon = 1e-12);
|
||||
assert_relative_eq!(out[0].volume, 370.0, epsilon = 1e-12);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn partial_group_emits_nothing() {
|
||||
let mut bars = TickBars::new(4).unwrap();
|
||||
bars.update(candle(10.0, 10.0, 10.0, 10.0, 1.0));
|
||||
bars.update(candle(10.0, 10.0, 10.0, 10.0, 1.0));
|
||||
assert_eq!(bars.count(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reset_clears_state() {
|
||||
let mut bars = TickBars::new(3).unwrap();
|
||||
bars.update(candle(10.0, 10.0, 10.0, 10.0, 1.0));
|
||||
bars.update(candle(10.0, 10.0, 10.0, 10.0, 1.0));
|
||||
bars.reset();
|
||||
assert_eq!(bars.count(), 0);
|
||||
// After reset the next candle starts a fresh group.
|
||||
assert!(bars.update(candle(20.0, 20.0, 20.0, 20.0, 5.0)).is_empty());
|
||||
assert_eq!(bars.count(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn batch_concatenates_completed_bars() {
|
||||
let mut bars = TickBars::new(2).unwrap();
|
||||
let candles = [
|
||||
candle(10.0, 10.0, 10.0, 10.0, 1.0),
|
||||
candle(10.0, 10.0, 10.0, 10.0, 1.0),
|
||||
candle(10.0, 10.0, 10.0, 10.0, 1.0),
|
||||
candle(10.0, 10.0, 10.0, 10.0, 1.0),
|
||||
];
|
||||
let out = bars.batch(&candles);
|
||||
assert_eq!(out.len(), 2);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,226 @@
|
||||
//! Upside Potential Ratio (Sortino, van der Meer & Plantinga) — upside mean over downside deviation.
|
||||
|
||||
use std::collections::VecDeque;
|
||||
|
||||
use crate::error::{Error, Result};
|
||||
use crate::traits::Indicator;
|
||||
|
||||
/// Upside Potential Ratio over a trailing window of `period` returns, measured
|
||||
/// relative to a minimal acceptable return (`mar`).
|
||||
///
|
||||
/// ```text
|
||||
/// upside = mean( max(r − mar, 0) ) over the window
|
||||
/// downside = sqrt( mean( min(r − mar, 0)² ) ) over the window
|
||||
/// UPR = upside / downside
|
||||
/// ```
|
||||
///
|
||||
/// Where the [`SharpeRatio`](crate::SharpeRatio) divides excess return by *total*
|
||||
/// volatility (penalising upside and downside symmetrically), the Upside Potential
|
||||
/// Ratio rewards only the average outperformance above the threshold while
|
||||
/// penalising solely the downside deviation below it. It is the purest expression
|
||||
/// of the Sortino philosophy: investors do not dislike upside variance, only
|
||||
/// shortfall risk.
|
||||
///
|
||||
/// `mar` (minimal acceptable return) is the per-period hurdle the caller supplies
|
||||
/// (e.g. `0.0` for break-even, or a target rate matching the return frequency). A
|
||||
/// window that never breaches the threshold has zero downside deviation; the
|
||||
/// indicator then reports `0.0` rather than dividing by zero.
|
||||
///
|
||||
/// Each `update` is O(1) — running sums maintain the upside total and the
|
||||
/// downside sum-of-squares as the window slides.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// use wickra_core::{Indicator, UpsidePotentialRatio};
|
||||
///
|
||||
/// let mut indicator = UpsidePotentialRatio::new(20, 0.0).unwrap();
|
||||
/// let mut last = None;
|
||||
/// for i in 0..40 {
|
||||
/// last = indicator.update((f64::from(i) * 0.3).sin() * 0.02);
|
||||
/// }
|
||||
/// assert!(last.is_some());
|
||||
/// ```
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct UpsidePotentialRatio {
|
||||
period: usize,
|
||||
mar: f64,
|
||||
window: VecDeque<f64>,
|
||||
sum_upside: f64,
|
||||
sum_downside_sq: f64,
|
||||
}
|
||||
|
||||
impl UpsidePotentialRatio {
|
||||
/// Construct an Upside Potential Ratio over `period` returns with minimal
|
||||
/// acceptable return `mar`.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns [`Error::InvalidPeriod`] if `period < 2`, or
|
||||
/// [`Error::InvalidParameter`] if `mar` is not finite.
|
||||
pub fn new(period: usize, mar: f64) -> Result<Self> {
|
||||
if period < 2 {
|
||||
return Err(Error::InvalidPeriod {
|
||||
message: "upside potential ratio needs period >= 2",
|
||||
});
|
||||
}
|
||||
if !mar.is_finite() {
|
||||
return Err(Error::InvalidParameter {
|
||||
message: "mar must be finite",
|
||||
});
|
||||
}
|
||||
Ok(Self {
|
||||
period,
|
||||
mar,
|
||||
window: VecDeque::with_capacity(period),
|
||||
sum_upside: 0.0,
|
||||
sum_downside_sq: 0.0,
|
||||
})
|
||||
}
|
||||
|
||||
/// Configured window of returns.
|
||||
pub const fn period(&self) -> usize {
|
||||
self.period
|
||||
}
|
||||
|
||||
/// Configured minimal acceptable return.
|
||||
pub const fn mar(&self) -> f64 {
|
||||
self.mar
|
||||
}
|
||||
}
|
||||
|
||||
impl Indicator for UpsidePotentialRatio {
|
||||
type Input = f64;
|
||||
type Output = f64;
|
||||
|
||||
fn update(&mut self, ret: f64) -> Option<f64> {
|
||||
if !ret.is_finite() {
|
||||
return None;
|
||||
}
|
||||
if self.window.len() == self.period {
|
||||
let old = self.window.pop_front().expect("non-empty");
|
||||
let excess = old - self.mar;
|
||||
self.sum_upside -= excess.max(0.0);
|
||||
self.sum_downside_sq -= excess.min(0.0).powi(2);
|
||||
}
|
||||
let excess = ret - self.mar;
|
||||
self.sum_upside += excess.max(0.0);
|
||||
self.sum_downside_sq += excess.min(0.0).powi(2);
|
||||
self.window.push_back(ret);
|
||||
if self.window.len() < self.period {
|
||||
return None;
|
||||
}
|
||||
let n = self.period as f64;
|
||||
let upside_mean = self.sum_upside / n;
|
||||
let downside_dev = (self.sum_downside_sq / n).sqrt();
|
||||
if downside_dev > 0.0 {
|
||||
Some(upside_mean / downside_dev)
|
||||
} else {
|
||||
Some(0.0)
|
||||
}
|
||||
}
|
||||
|
||||
fn reset(&mut self) {
|
||||
self.window.clear();
|
||||
self.sum_upside = 0.0;
|
||||
self.sum_downside_sq = 0.0;
|
||||
}
|
||||
|
||||
fn warmup_period(&self) -> usize {
|
||||
self.period
|
||||
}
|
||||
|
||||
fn is_ready(&self) -> bool {
|
||||
self.window.len() == self.period
|
||||
}
|
||||
|
||||
fn name(&self) -> &'static str {
|
||||
"UpsidePotentialRatio"
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::traits::BatchExt;
|
||||
use approx::assert_relative_eq;
|
||||
|
||||
#[test]
|
||||
fn rejects_period_less_than_two() {
|
||||
assert!(matches!(
|
||||
UpsidePotentialRatio::new(1, 0.0),
|
||||
Err(Error::InvalidPeriod { .. })
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_non_finite_mar() {
|
||||
assert!(matches!(
|
||||
UpsidePotentialRatio::new(10, f64::NAN),
|
||||
Err(Error::InvalidParameter { .. })
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn accessors_and_metadata() {
|
||||
let upr = UpsidePotentialRatio::new(20, 0.001).unwrap();
|
||||
assert_eq!(upr.period(), 20);
|
||||
assert_relative_eq!(upr.mar(), 0.001, epsilon = 1e-12);
|
||||
assert_eq!(upr.warmup_period(), 20);
|
||||
assert_eq!(upr.name(), "UpsidePotentialRatio");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reference_value() {
|
||||
// returns [0.02, -0.01, 0.03, -0.02], mar = 0.
|
||||
// upside = (0.02 + 0 + 0.03 + 0)/4 = 0.0125.
|
||||
// downside = sqrt((0 + 0.0001 + 0 + 0.0004)/4) = sqrt(0.000125).
|
||||
// UPR = 0.0125 / sqrt(0.000125).
|
||||
let mut upr = UpsidePotentialRatio::new(4, 0.0).unwrap();
|
||||
let out = upr.batch(&[0.02, -0.01, 0.03, -0.02]);
|
||||
let expected = 0.0125_f64 / (0.000_125_f64).sqrt();
|
||||
assert_relative_eq!(out[3].unwrap(), expected, epsilon = 1e-9);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn no_downside_is_zero() {
|
||||
let mut upr = UpsidePotentialRatio::new(3, 0.0).unwrap();
|
||||
let last = upr
|
||||
.batch(&[0.01, 0.02, 0.03])
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.last()
|
||||
.unwrap();
|
||||
assert_relative_eq!(last, 0.0, epsilon = 1e-12);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ignores_non_finite_input() {
|
||||
let mut upr = UpsidePotentialRatio::new(3, 0.0).unwrap();
|
||||
assert_eq!(upr.update(0.01), None);
|
||||
assert_eq!(upr.update(f64::INFINITY), None);
|
||||
assert_eq!(upr.update(-0.02), None);
|
||||
assert!(upr.update(0.03).is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reset_clears_state() {
|
||||
let mut upr = UpsidePotentialRatio::new(2, 0.0).unwrap();
|
||||
upr.batch(&[0.02, -0.01]);
|
||||
assert!(upr.is_ready());
|
||||
upr.reset();
|
||||
assert!(!upr.is_ready());
|
||||
assert_eq!(upr.update(0.01), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn batch_equals_streaming() {
|
||||
let rets: Vec<f64> = (0..60)
|
||||
.map(|i| (f64::from(i) * 0.25).sin() * 0.02)
|
||||
.collect();
|
||||
let batch = UpsidePotentialRatio::new(12, 0.0).unwrap().batch(&rets);
|
||||
let mut streamer = UpsidePotentialRatio::new(12, 0.0).unwrap();
|
||||
let streamed: Vec<_> = rets.iter().map(|r| streamer.update(*r)).collect();
|
||||
assert_eq!(batch, streamed);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,217 @@
|
||||
//! Volume bar builder — close a bar each time accumulated volume reaches a threshold.
|
||||
|
||||
use crate::error::{Error, Result};
|
||||
use crate::ohlcv::Candle;
|
||||
use crate::traits::BarBuilder;
|
||||
|
||||
/// One completed volume bar (an OHLCV aggregate spanning ~`volume_per_bar` of volume).
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub struct VolumeBar {
|
||||
/// Open of the first candle in the bar.
|
||||
pub open: f64,
|
||||
/// Highest high across the bar.
|
||||
pub high: f64,
|
||||
/// Lowest low across the bar.
|
||||
pub low: f64,
|
||||
/// Close of the candle that closed the bar.
|
||||
pub close: f64,
|
||||
/// Accumulated volume in the bar (`>= volume_per_bar`; the crossing candle's
|
||||
/// overshoot is kept in the bar that closes).
|
||||
pub volume: f64,
|
||||
}
|
||||
|
||||
/// Volume bar builder — emits a bar each time accumulated volume reaches
|
||||
/// `volume_per_bar`.
|
||||
///
|
||||
/// Where [`TickBars`](crate::TickBars) sample on trade *count*, volume bars sample on
|
||||
/// traded *quantity*: a bar closes once the candles fed into it have accumulated at
|
||||
/// least `volume_per_bar` of volume. This gives each bar roughly equal participation,
|
||||
/// which de-emphasises quiet periods and resolves bursts of heavy trading into more
|
||||
/// bars. The companion [`DollarBars`](crate::DollarBars) builder uses traded *value*
|
||||
/// (`price × volume`) instead, which is more robust to price-level drift over long
|
||||
/// histories.
|
||||
///
|
||||
/// The bar is candle-granular: at most one bar closes per candle, and the candle
|
||||
/// that crosses the threshold closes the bar with its overshoot included (the next
|
||||
/// bar starts fresh). [`BarBuilder::update`] therefore returns either an empty vector
|
||||
/// or a single [`VolumeBar`].
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// use wickra_core::{BarBuilder, Candle, VolumeBars};
|
||||
///
|
||||
/// let c = |cl, v| Candle::new(cl, cl, cl, cl, v, 0).unwrap();
|
||||
/// let mut bars = VolumeBars::new(100.0).unwrap();
|
||||
/// assert!(bars.update(c(10.0, 60.0)).is_empty());
|
||||
/// let out = bars.update(c(10.5, 60.0)); // 120 >= 100 -> close
|
||||
/// assert_eq!(out.len(), 1);
|
||||
/// assert_eq!(out[0].volume, 120.0);
|
||||
/// ```
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct VolumeBars {
|
||||
volume_per_bar: f64,
|
||||
count: usize,
|
||||
open: f64,
|
||||
high: f64,
|
||||
low: f64,
|
||||
close: f64,
|
||||
accumulated: f64,
|
||||
}
|
||||
|
||||
impl VolumeBars {
|
||||
/// Construct a volume-bar builder with the given volume threshold.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns [`Error::InvalidPeriod`] if `volume_per_bar` is not finite and positive.
|
||||
pub fn new(volume_per_bar: f64) -> Result<Self> {
|
||||
if !volume_per_bar.is_finite() || volume_per_bar <= 0.0 {
|
||||
return Err(Error::InvalidPeriod {
|
||||
message: "volume_per_bar must be finite and positive",
|
||||
});
|
||||
}
|
||||
Ok(Self {
|
||||
volume_per_bar,
|
||||
count: 0,
|
||||
open: 0.0,
|
||||
high: 0.0,
|
||||
low: 0.0,
|
||||
close: 0.0,
|
||||
accumulated: 0.0,
|
||||
})
|
||||
}
|
||||
|
||||
/// Configured volume threshold per bar.
|
||||
pub const fn volume_per_bar(&self) -> f64 {
|
||||
self.volume_per_bar
|
||||
}
|
||||
|
||||
/// Volume accumulated into the in-progress bar.
|
||||
pub const fn accumulated(&self) -> f64 {
|
||||
self.accumulated
|
||||
}
|
||||
}
|
||||
|
||||
impl BarBuilder for VolumeBars {
|
||||
type Bar = VolumeBar;
|
||||
|
||||
fn update(&mut self, candle: Candle) -> Vec<VolumeBar> {
|
||||
if self.count == 0 {
|
||||
self.open = candle.open;
|
||||
self.high = candle.high;
|
||||
self.low = candle.low;
|
||||
} else {
|
||||
self.high = self.high.max(candle.high);
|
||||
self.low = self.low.min(candle.low);
|
||||
}
|
||||
self.close = candle.close;
|
||||
self.accumulated += candle.volume;
|
||||
self.count += 1;
|
||||
if self.accumulated < self.volume_per_bar {
|
||||
return Vec::new();
|
||||
}
|
||||
let bar = VolumeBar {
|
||||
open: self.open,
|
||||
high: self.high,
|
||||
low: self.low,
|
||||
close: self.close,
|
||||
volume: self.accumulated,
|
||||
};
|
||||
self.count = 0;
|
||||
self.accumulated = 0.0;
|
||||
vec![bar]
|
||||
}
|
||||
|
||||
fn reset(&mut self) {
|
||||
self.count = 0;
|
||||
self.accumulated = 0.0;
|
||||
}
|
||||
|
||||
fn name(&self) -> &'static str {
|
||||
"VolumeBars"
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use approx::assert_relative_eq;
|
||||
|
||||
fn candle(open: f64, high: f64, low: f64, close: f64, volume: f64) -> Candle {
|
||||
Candle::new(open, high, low, close, volume, 0).unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_invalid_threshold() {
|
||||
assert!(matches!(
|
||||
VolumeBars::new(0.0),
|
||||
Err(Error::InvalidPeriod { .. })
|
||||
));
|
||||
assert!(matches!(
|
||||
VolumeBars::new(-100.0),
|
||||
Err(Error::InvalidPeriod { .. })
|
||||
));
|
||||
assert!(matches!(
|
||||
VolumeBars::new(f64::INFINITY),
|
||||
Err(Error::InvalidPeriod { .. })
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn accessors_and_metadata() {
|
||||
let bars = VolumeBars::new(1000.0).unwrap();
|
||||
assert_relative_eq!(bars.volume_per_bar(), 1000.0, epsilon = 1e-12);
|
||||
assert_relative_eq!(bars.accumulated(), 0.0, epsilon = 1e-12);
|
||||
assert_eq!(bars.name(), "VolumeBars");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn closes_when_threshold_reached() {
|
||||
let mut bars = VolumeBars::new(100.0).unwrap();
|
||||
assert!(bars.update(candle(10.0, 10.0, 10.0, 10.0, 60.0)).is_empty());
|
||||
let out = bars.update(candle(10.5, 10.5, 10.5, 10.5, 60.0));
|
||||
assert_eq!(out.len(), 1);
|
||||
assert_relative_eq!(out[0].volume, 120.0, epsilon = 1e-12);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn aggregates_ohlc() {
|
||||
let mut bars = VolumeBars::new(100.0).unwrap();
|
||||
bars.update(candle(10.0, 11.0, 9.0, 10.5, 50.0));
|
||||
let out = bars.update(candle(10.5, 12.0, 10.0, 11.0, 60.0));
|
||||
assert_relative_eq!(out[0].open, 10.0, epsilon = 1e-12);
|
||||
assert_relative_eq!(out[0].high, 12.0, epsilon = 1e-12);
|
||||
assert_relative_eq!(out[0].low, 9.0, epsilon = 1e-12);
|
||||
assert_relative_eq!(out[0].close, 11.0, epsilon = 1e-12);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn below_threshold_emits_nothing() {
|
||||
let mut bars = VolumeBars::new(100.0).unwrap();
|
||||
bars.update(candle(10.0, 10.0, 10.0, 10.0, 30.0));
|
||||
assert_relative_eq!(bars.accumulated(), 30.0, epsilon = 1e-12);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reset_clears_state() {
|
||||
let mut bars = VolumeBars::new(100.0).unwrap();
|
||||
bars.update(candle(10.0, 10.0, 10.0, 10.0, 60.0));
|
||||
bars.reset();
|
||||
assert_relative_eq!(bars.accumulated(), 0.0, epsilon = 1e-12);
|
||||
assert!(bars.update(candle(20.0, 20.0, 20.0, 20.0, 60.0)).is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn batch_concatenates_completed_bars() {
|
||||
let mut bars = VolumeBars::new(100.0).unwrap();
|
||||
let candles = [
|
||||
candle(10.0, 10.0, 10.0, 10.0, 60.0),
|
||||
candle(10.0, 10.0, 10.0, 10.0, 60.0),
|
||||
candle(10.0, 10.0, 10.0, 10.0, 60.0),
|
||||
candle(10.0, 10.0, 10.0, 10.0, 60.0),
|
||||
];
|
||||
let out = bars.batch(&candles);
|
||||
assert_eq!(out.len(), 2);
|
||||
}
|
||||
}
|
||||
@@ -55,6 +55,13 @@ pub mod indicators;
|
||||
pub use cross_section::{CrossSection, Member};
|
||||
pub use derivatives::DerivativesTick;
|
||||
pub use error::{Error, Result};
|
||||
pub use indicators::DollarBar;
|
||||
pub use indicators::ImbalanceBar;
|
||||
pub use indicators::LineBreakBar;
|
||||
pub use indicators::RangeBar;
|
||||
pub use indicators::RunBar;
|
||||
pub use indicators::TickBar;
|
||||
pub use indicators::VolumeBar;
|
||||
pub use indicators::{
|
||||
AbandonedBaby, Abcd, AbsoluteBreadthIndex, AccelerationBands, AccelerationBandsOutput,
|
||||
AcceleratorOscillator, AdOscillator, AdVolumeLine, AdaptiveCci, AdaptiveCycle,
|
||||
@@ -66,30 +73,31 @@ pub use indicators::{
|
||||
AverageDrawdown, AvgPrice, AwesomeOscillator, AwesomeOscillatorHistogram, BalanceOfPower,
|
||||
BandpassFilter, Bat, BeltHold, Beta, BetaNeutralSpread, BetterVolume, BipowerVariation,
|
||||
BodySizePct, BollingerBands, BollingerBandwidth, BollingerOutput, BomarBands, BomarBandsOutput,
|
||||
BreadthThrust, Breakaway, BullishPercentIndex, Butterfly, CalendarSpread, CalmarRatio,
|
||||
Camarilla, CamarillaPivotsOutput, CandleVolume, CandleVolumeOutput, Cci, CenterOfGravity,
|
||||
CentralPivotRange, CentralPivotRangeOutput, Cfo, ChaikinMoneyFlow, ChaikinOscillator,
|
||||
ChaikinVolatility, ChandeKrollStop, ChandeKrollStopOutput, ChandelierExit,
|
||||
BreadthThrust, Breakaway, BullishPercentIndex, BurkeRatio, Butterfly, CalendarSpread,
|
||||
CalmarRatio, Camarilla, CamarillaPivotsOutput, CandleVolume, CandleVolumeOutput, Cci,
|
||||
CenterOfGravity, CentralPivotRange, CentralPivotRangeOutput, Cfo, ChaikinMoneyFlow,
|
||||
ChaikinOscillator, ChaikinVolatility, ChandeKrollStop, ChandeKrollStopOutput, ChandelierExit,
|
||||
ChandelierExitOutput, ChoppinessIndex, ClassicPivots, ClassicPivotsOutput, CloseVsOpen,
|
||||
ClosingMarubozu, Cmo, CoefficientOfVariation, Cointegration, CointegrationOutput,
|
||||
CompositeProfile, CompositeProfileOutput, ConcealingBabySwallow, ConditionalValueAtRisk,
|
||||
ConnorsRsi, Coppock, CorrelationTrendIndicator, Counterattack, Crab, CumulativeVolumeDelta,
|
||||
CumulativeVolumeIndex, CupAndHandle, CyberneticCycle, Cypher, DayOfWeekProfile,
|
||||
DayOfWeekProfileOutput, Decycler, DecyclerOscillator, Dema, DemandIndex, DemarkPivots,
|
||||
DemarkPivotsOutput, DepthSlope, DerivativeOscillator, DetrendedStdDev, DisparityIndex,
|
||||
DistanceSsd, Doji, DojiStar, Donchian, DonchianOutput, DonchianStop, DonchianStopOutput,
|
||||
DoubleBollinger, DoubleBollingerOutput, DoubleTopBottom, DownsideGapThreeMethods, Dpo,
|
||||
DragonflyDoji, DrawdownDuration, DumplingTop, Dx, DynamicMomentumIndex, EaseOfMovement,
|
||||
EffectiveSpread, EhlersStochastic, Ehma, ElderImpulse, ElderRay, ElderRayOutput, ElderSafeZone,
|
||||
ElderSafeZoneOutput, Ema, EmpiricalModeDecomposition, Engulfing, Equivolume, EquivolumeOutput,
|
||||
EstimatedLeverageRatio, EvenBetterSinewave, EveningDojiStar, Evwma, EwmaVolatility, Expectancy,
|
||||
FallingThreeMethods, Fama, FibArcs, FibArcsOutput, FibChannel, FibChannelOutput, FibConfluence,
|
||||
FibConfluenceOutput, FibExtension, FibExtensionOutput, FibFan, FibFanOutput, FibProjection,
|
||||
FibProjectionOutput, FibRetracement, FibRetracementOutput, FibTimeZones, FibTimeZonesOutput,
|
||||
FibonacciPivots, FibonacciPivotsOutput, FisherRsi, FisherTransform, FlagPennant, Footprint,
|
||||
FootprintOutput, ForceIndex, FractalChaosBands, FractalChaosBandsOutput, Frama, FryPanBottom,
|
||||
FundingBasis, FundingImpliedApr, FundingRate, FundingRateMean, FundingRateZScore,
|
||||
GainLossRatio, GapSideBySideWhite, Garch11, GarmanKlassVolatility, Gartley, GatorOscillator,
|
||||
CommonSenseRatio, CompositeProfile, CompositeProfileOutput, ConcealingBabySwallow,
|
||||
ConditionalValueAtRisk, ConnorsRsi, Coppock, CorrelationTrendIndicator, Counterattack, Crab,
|
||||
CumulativeVolumeDelta, CumulativeVolumeIndex, CupAndHandle, CyberneticCycle, Cypher,
|
||||
DayOfWeekProfile, DayOfWeekProfileOutput, Decycler, DecyclerOscillator, Dema, DemandIndex,
|
||||
DemarkPivots, DemarkPivotsOutput, DepthSlope, DerivativeOscillator, DetrendedStdDev,
|
||||
DisparityIndex, DistanceSsd, Doji, DojiStar, DollarBars, Donchian, DonchianOutput,
|
||||
DonchianStop, DonchianStopOutput, DoubleBollinger, DoubleBollingerOutput, DoubleTopBottom,
|
||||
DownsideGapThreeMethods, Dpo, DragonflyDoji, DrawdownDuration, DumplingTop, Dx,
|
||||
DynamicMomentumIndex, EaseOfMovement, EffectiveSpread, EhlersStochastic, Ehma, ElderImpulse,
|
||||
ElderRay, ElderRayOutput, ElderSafeZone, ElderSafeZoneOutput, Ema, EmpiricalModeDecomposition,
|
||||
Engulfing, Equivolume, EquivolumeOutput, EstimatedLeverageRatio, EvenBetterSinewave,
|
||||
EveningDojiStar, Evwma, EwmaVolatility, Expectancy, FallingThreeMethods, Fama, FibArcs,
|
||||
FibArcsOutput, FibChannel, FibChannelOutput, FibConfluence, FibConfluenceOutput, FibExtension,
|
||||
FibExtensionOutput, FibFan, FibFanOutput, FibProjection, FibProjectionOutput, FibRetracement,
|
||||
FibRetracementOutput, FibTimeZones, FibTimeZonesOutput, FibonacciPivots, FibonacciPivotsOutput,
|
||||
FisherRsi, FisherTransform, FlagPennant, Footprint, FootprintOutput, ForceIndex,
|
||||
FractalChaosBands, FractalChaosBandsOutput, Frama, FryPanBottom, FundingBasis,
|
||||
FundingImpliedApr, FundingRate, FundingRateMean, FundingRateZScore, GainLossRatio,
|
||||
GainToPainRatio, GapSideBySideWhite, Garch11, GarmanKlassVolatility, Gartley, GatorOscillator,
|
||||
GatorOscillatorOutput, GeneralizedDema, GeometricMa, GoldenPocket, GoldenPocketOutput,
|
||||
GrangerCausality, GravestoneDoji, Hammer, HangingMan, Harami, HaramiCross,
|
||||
HasbrouckInformationShare, HeadAndShoulders, HeikinAshi, HeikinAshiOscillator,
|
||||
@@ -97,25 +105,25 @@ pub use indicators::{
|
||||
HighLowVolumeNodesOutput, HighWave, HighpassFilter, Hikkake, HikkakeModified,
|
||||
HilbertDominantCycle, HistoricalVolatility, Hma, HoltWinters, HomingPigeon, HtDcPhase,
|
||||
HtPhasor, HtPhasorOutput, HtTrendMode, HurstChannel, HurstChannelOutput, HurstExponent,
|
||||
Ichimoku, IchimokuOutput, IdenticalThreeCrows, InNeck, Inertia, InformationRatio,
|
||||
InitialBalance, InitialBalanceOutput, InstantaneousTrendline, IntradayIntensity,
|
||||
IntradayMomentumIndex, IntradayVolatilityProfile, IntradayVolatilityProfileOutput,
|
||||
InverseFisherTransform, InvertedHammer, JarqueBera, Jma, JumpIndicator, KagiBars,
|
||||
KalmanHedgeRatio, KalmanHedgeRatioOutput, Kama, KaseDevStop, KaseDevStopOutput,
|
||||
KasePermissionStochastic, KasePermissionStochasticOutput, KellyCriterion, Keltner,
|
||||
KeltnerOutput, KendallTau, Kicking, KickingByLength, Kst, KstOutput, Kurtosis, Kvo,
|
||||
Ichimoku, IchimokuOutput, IdenticalThreeCrows, ImbalanceBars, InNeck, Inertia,
|
||||
InformationRatio, InitialBalance, InitialBalanceOutput, InstantaneousTrendline,
|
||||
IntradayIntensity, IntradayMomentumIndex, IntradayVolatilityProfile,
|
||||
IntradayVolatilityProfileOutput, InverseFisherTransform, InvertedHammer, JarqueBera, Jma,
|
||||
JumpIndicator, KRatio, KagiBars, KalmanHedgeRatio, KalmanHedgeRatioOutput, Kama, KaseDevStop,
|
||||
KaseDevStopOutput, KasePermissionStochastic, KasePermissionStochasticOutput, KellyCriterion,
|
||||
Keltner, KeltnerOutput, KendallTau, Kicking, KickingByLength, Kst, KstOutput, Kurtosis, Kvo,
|
||||
KylesLambda, LadderBottom, LaguerreRsi, LeadLagCrossCorrelation, LeadLagCrossCorrelationOutput,
|
||||
LinRegAngle, LinRegChannel, LinRegChannelOutput, LinRegIntercept, LinRegSlope,
|
||||
LinearRegression, LiquidationFeatures, LiquidationFeaturesOutput, LogReturn, LongLeggedDoji,
|
||||
LongLine, LongShortRatio, MaEnvelope, MaEnvelopeOutput, MacdExt, MacdFix, MacdHistogram,
|
||||
MacdIndicator, MacdOutput, Mama, MamaOutput, MarketFacilitationIndex, Marubozu, MassIndex,
|
||||
MatHold, MatchingLow, MaxDrawdown, McClellanOscillator, McClellanSummationIndex,
|
||||
McGinleyDynamic, MedianAbsoluteDeviation, MedianChannel, MedianChannelOutput, MedianMa,
|
||||
MedianPrice, Mfi, Microprice, MidPoint, MidPrice, MinusDi, MinusDm, ModifiedMaStop,
|
||||
ModifiedMaStopOutput, Mom, MorningDojiStar, MorningEveningStar, MurreyMathLines,
|
||||
MurreyMathLinesOutput, NakedPoc, Natr, NewHighsNewLows, NewPriceLines, Nrtr, NrtrOutput, Nvi,
|
||||
OIPriceDivergence, OIWeighted, Obv, OiToVolumeRatio, OmegaRatio, OnNeck, OpenInterestDelta,
|
||||
OpenInterestMomentum, OpeningMarubozu, OpeningRange, OpeningRangeOutput,
|
||||
LongLine, LongShortRatio, M2Measure, MaEnvelope, MaEnvelopeOutput, MacdExt, MacdFix,
|
||||
MacdHistogram, MacdIndicator, MacdOutput, Mama, MamaOutput, MarketFacilitationIndex,
|
||||
MartinRatio, Marubozu, MassIndex, MatHold, MatchingLow, MaxDrawdown, McClellanOscillator,
|
||||
McClellanSummationIndex, McGinleyDynamic, MedianAbsoluteDeviation, MedianChannel,
|
||||
MedianChannelOutput, MedianMa, MedianPrice, Mfi, Microprice, MidPoint, MidPrice, MinusDi,
|
||||
MinusDm, ModifiedMaStop, ModifiedMaStopOutput, Mom, MorningDojiStar, MorningEveningStar,
|
||||
MurreyMathLines, MurreyMathLinesOutput, NakedPoc, Natr, NewHighsNewLows, NewPriceLines, Nrtr,
|
||||
NrtrOutput, Nvi, OIPriceDivergence, OIWeighted, Obv, OiToVolumeRatio, OmegaRatio, OnNeck,
|
||||
OpenInterestDelta, OpenInterestMomentum, OpeningMarubozu, OpeningRange, OpeningRangeOutput,
|
||||
OrderBookImbalanceFull, OrderBookImbalanceTop1, OrderBookImbalanceTopN, OrderFlowImbalance,
|
||||
OuHalfLife, OvernightGap, OvernightIntradayReturn, OvernightIntradayReturnOutput, PainIndex,
|
||||
PairSpreadZScore, PairwiseBeta, ParkinsonVolatility, PearsonCorrelation, PercentAboveMa,
|
||||
@@ -123,37 +131,38 @@ pub use indicators::{
|
||||
PivotReversal, PlusDi, PlusDm, Pmo, PointAndFigureBars, PolarizedFractalEfficiency, Ppo,
|
||||
PpoHistogram, ProfileShape, ProfitFactor, ProjectionBands, ProjectionBandsOutput,
|
||||
ProjectionOscillator, Psar, Pvi, Qqe, QqeOutput, Qstick, QuartileBands, QuartileBandsOutput,
|
||||
QuotedSpread, RSquared, RealizedSpread, RealizedVolatility, RecoveryFactor, RectangleRange,
|
||||
Reflex, RegimeLabel, RelativeStrengthAB, RelativeStrengthOutput, RenkoBars, RenkoTrailingStop,
|
||||
RickshawMan, RisingThreeMethods, Rmi, Roc, Rocp, Rocr, Rocr100, RogersSatchellVolatility,
|
||||
RollMeasure, RollingCorrelation, RollingCovariance, RollingIqr, RollingMinMaxScaler,
|
||||
RollingPercentileRank, RollingQuantile, RollingVwap, RoofingFilter, Rsi, Rsx, Rvi,
|
||||
RviVolatility, Rwi, RwiOutput, SampleEntropy, SarExt, SeasonalZScore, SeparatingLines,
|
||||
SessionHighLow, SessionHighLowOutput, SessionRange, SessionRangeOutput, SessionVwap,
|
||||
ShannonEntropy, Shark, SharpeRatio, ShootingStar, ShortLine, SignedVolume, SineWave,
|
||||
SineWeightedMa, SinglePrints, Skewness, Sma, Smi, Smma, SmoothedHeikinAshi,
|
||||
QuotedSpread, RSquared, RangeBars, RealizedSpread, RealizedVolatility, RecoveryFactor,
|
||||
RectangleRange, Reflex, RegimeLabel, RelativeStrengthAB, RelativeStrengthOutput, RenkoBars,
|
||||
RenkoTrailingStop, RickshawMan, RisingThreeMethods, Rmi, Roc, Rocp, Rocr, Rocr100,
|
||||
RogersSatchellVolatility, RollMeasure, RollingCorrelation, RollingCovariance, RollingIqr,
|
||||
RollingMinMaxScaler, RollingPercentileRank, RollingQuantile, RollingVwap, RoofingFilter, Rsi,
|
||||
Rsx, RunBars, Rvi, RviVolatility, Rwi, RwiOutput, SampleEntropy, SarExt, SeasonalZScore,
|
||||
SeparatingLines, SessionHighLow, SessionHighLowOutput, SessionRange, SessionRangeOutput,
|
||||
SessionVwap, ShannonEntropy, Shark, SharpeRatio, ShootingStar, ShortLine, SignedVolume,
|
||||
SineWave, SineWeightedMa, SinglePrints, Skewness, Sma, Smi, Smma, SmoothedHeikinAshi,
|
||||
SmoothedHeikinAshiOutput, SortinoRatio, SpearmanCorrelation, SpinningTop, SpreadAr1Coefficient,
|
||||
SpreadBollingerBands, SpreadBollingerBandsOutput, SpreadHurst, StalledPattern, StandardError,
|
||||
StandardErrorBands, StandardErrorBandsOutput, StarcBands, StarcBandsOutput, Stc, StdDev,
|
||||
StepTrailingStop, StickSandwich, StochRsi, Stochastic, StochasticCci, StochasticOutput,
|
||||
SuperSmoother, SuperTrend, SuperTrendOutput, TakerBuySellRatio, Takuri, TasukiGap,
|
||||
TdCamouflage, TdClop, TdClopwin, TdCombo, TdCountdown, TdDWave, TdDeMarker, TdDifferential,
|
||||
TdLines, TdLinesOutput, TdMovingAverage, TdMovingAverageOutput, TdOpen, TdPressure,
|
||||
TdPropulsion, TdRangeProjection, TdRangeProjectionOutput, TdRei, TdRiskLevel,
|
||||
StepTrailingStop, SterlingRatio, StickSandwich, StochRsi, Stochastic, StochasticCci,
|
||||
StochasticOutput, SuperSmoother, SuperTrend, SuperTrendOutput, TailRatio, TakerBuySellRatio,
|
||||
Takuri, TasukiGap, TdCamouflage, TdClop, TdClopwin, TdCombo, TdCountdown, TdDWave, TdDeMarker,
|
||||
TdDifferential, TdLines, TdLinesOutput, TdMovingAverage, TdMovingAverageOutput, TdOpen,
|
||||
TdPressure, TdPropulsion, TdRangeProjection, TdRangeProjectionOutput, TdRei, TdRiskLevel,
|
||||
TdRiskLevelOutput, TdSequential, TdSequentialOutput, TdSetup, TdTrap, Tema, TermStructureBasis,
|
||||
ThreeDrives, ThreeInside, ThreeLineBreak, ThreeLineStrike, ThreeOutside, ThreeSoldiersOrCrows,
|
||||
ThreeStarsInSouth, Thrusting, TickIndex, Tii, TimeBasedStop, TimeOfDayReturnProfile,
|
||||
TimeOfDayReturnProfileOutput, TowerTopBottom, TpoProfile, TpoProfileOutput, TradeImbalance,
|
||||
TradeSignAutocorrelation, TradeVolumeIndex, TrendLabel, TrendStrengthIndex, Trendflex,
|
||||
TreynorRatio, Triangle, Trima, Trin, TripleTopBottom, Tristar, Trix, TrueRange, Tsf,
|
||||
TsfOscillator, Tsi, Tsv, TtmSqueeze, TtmSqueezeOutput, TtmTrend, TurnOfMonth, Tweezer,
|
||||
TwiggsMoneyFlow, TwoCrows, TypicalPrice, UlcerIndex, UltimateOscillator, UniqueThreeRiver,
|
||||
UniversalOscillator, UpDownVolumeRatio, UpsideGapThreeMethods, UpsideGapTwoCrows, ValueArea,
|
||||
ValueAreaOutput, ValueAtRisk, Variance, VarianceRatio, VerticalHorizontalFilter, Vidya,
|
||||
VolatilityCone, VolatilityConeOutput, VolatilityOfVolatility, VolatilityRatio, VoltyStop,
|
||||
VolumeByTimeProfile, VolumeByTimeProfileOutput, VolumeOscillator, VolumePriceTrend,
|
||||
VolumeProfile, VolumeProfileOutput, VolumeRsi, VolumeWeightedMacd, VolumeWeightedMacdOutput,
|
||||
VolumeWeightedSr, VolumeWeightedSrOutput, Vortex, VortexOutput, Vpin, Vwap, VwapStdDevBands,
|
||||
ThreeDrives, ThreeInside, ThreeLineBreak, ThreeLineBreakBars, ThreeLineStrike, ThreeOutside,
|
||||
ThreeSoldiersOrCrows, ThreeStarsInSouth, Thrusting, TickBars, TickIndex, Tii, TimeBasedStop,
|
||||
TimeOfDayReturnProfile, TimeOfDayReturnProfileOutput, TowerTopBottom, TpoProfile,
|
||||
TpoProfileOutput, TradeImbalance, TradeSignAutocorrelation, TradeVolumeIndex, TrendLabel,
|
||||
TrendStrengthIndex, Trendflex, TreynorRatio, Triangle, Trima, Trin, TripleTopBottom, Tristar,
|
||||
Trix, TrueRange, Tsf, TsfOscillator, Tsi, Tsv, TtmSqueeze, TtmSqueezeOutput, TtmTrend,
|
||||
TurnOfMonth, Tweezer, TwiggsMoneyFlow, TwoCrows, TypicalPrice, UlcerIndex, UltimateOscillator,
|
||||
UniqueThreeRiver, UniversalOscillator, UpDownVolumeRatio, UpsideGapThreeMethods,
|
||||
UpsideGapTwoCrows, UpsidePotentialRatio, ValueArea, ValueAreaOutput, ValueAtRisk, Variance,
|
||||
VarianceRatio, VerticalHorizontalFilter, Vidya, VolatilityCone, VolatilityConeOutput,
|
||||
VolatilityOfVolatility, VolatilityRatio, VoltyStop, VolumeBars, VolumeByTimeProfile,
|
||||
VolumeByTimeProfileOutput, VolumeOscillator, VolumePriceTrend, VolumeProfile,
|
||||
VolumeProfileOutput, VolumeRsi, VolumeWeightedMacd, VolumeWeightedMacdOutput, VolumeWeightedSr,
|
||||
VolumeWeightedSrOutput, Vortex, VortexOutput, Vpin, Vwap, VwapStdDevBands,
|
||||
VwapStdDevBandsOutput, Vwma, Vzo, Wad, WavePm, WaveTrend, WaveTrendOutput, Wedge,
|
||||
WeightedClose, WickRatio, WilliamsFractals, WilliamsFractalsOutput, WilliamsR, WinRate, Wma,
|
||||
WoodiePivots, WoodiePivotsOutput, YangZhangVolatility, YoyoExit, ZScore, ZeroLagMacd,
|
||||
|
||||
+6
-3
@@ -6,9 +6,12 @@ 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).
|
||||
- A per-indicator deep dive for every one of the **498 indicators** across
|
||||
[Node](https://docs.wickra.org/Quickstart-Node),
|
||||
[WASM](https://docs.wickra.org/Quickstart-WASM),
|
||||
[C](https://docs.wickra.org/Quickstart-C),
|
||||
[C#](https://docs.wickra.org/Quickstart-CSharp), and
|
||||
[Go](https://docs.wickra.org/Quickstart-Go).
|
||||
- A per-indicator deep dive for every one of the **514 indicators** across
|
||||
the sixteen families (Moving Averages, Momentum Oscillators, Trend &
|
||||
Directional, Price Oscillators, Volatility & Bands, Bands & Channels,
|
||||
Trailing Stops, Volume, Price Statistics, Ehlers / Cycle DSP, Pivots &
|
||||
|
||||
@@ -21,6 +21,76 @@ The Rust examples live in the `wickra-examples` workspace member crate.
|
||||
| `strategy_macd_adx.rs` | Hourly BTCUSDT trend-follower: MACD crossover entries gated by ADX(14) > 20. | `cargo run --release -p wickra-examples --bin strategy_macd_adx` |
|
||||
| `strategy_bollinger_squeeze.rs` | Daily BTCUSDT Bollinger-squeeze breakout with ATR(14) trailing stop. | `cargo run --release -p wickra-examples --bin strategy_bollinger_squeeze` |
|
||||
|
||||
## C / C++ — `examples/c/`
|
||||
|
||||
Build the library first (`cargo build -p wickra-c --release`), then build and run
|
||||
the examples via CMake:
|
||||
`cmake -S examples/c -B examples/c/build -DWICKRA_LIB_DIR="$PWD/target/release"` →
|
||||
`cmake --build examples/c/build` → `ctest --test-dir examples/c/build`.
|
||||
|
||||
| Example | What it does | CMake target |
|
||||
| --- | --- | --- |
|
||||
| `smoke.c` | Links the generated header + library and asserts SMA streaming / batch values across the boundary. | `smoke` |
|
||||
| `streaming.c` | Feed a synthetic price series through SMA / EMA / RSI / MACD tick by tick. | `streaming` |
|
||||
| `backtest.c` | Basket of indicators over an OHLCV CSV; defaults to the bundled BTCUSDT daily dataset. | `backtest` |
|
||||
| `multi_timeframe.c` | Resample the bundled 1-minute CSV to 5m / 15m / 1h / 4h / 1d and print indicators per timeframe. | `multi_timeframe` |
|
||||
| `parallel_assets.c` | Serial vs OpenMP fan-out over a synthetic panel (one handle per asset), with speedup. | `parallel_assets` |
|
||||
| `strategy_rsi_mean_reversion.c` | Hourly BTCUSDT mean-reversion using RSI(14) thresholds, with PnL / Sharpe / max-DD summary. | `strategy_rsi_mean_reversion` |
|
||||
| `strategy_macd_adx.c` | Hourly BTCUSDT trend-follower: MACD crossover entries gated by ADX(14) > 20. | `strategy_macd_adx` |
|
||||
| `strategy_bollinger_squeeze.c` | Daily BTCUSDT Bollinger-squeeze breakout with ATR(14) stop. | `strategy_bollinger_squeeze` |
|
||||
| `fetch_btcusdt.c` | Download real BTCUSDT klines from the Binance REST API into `examples/data/` (shells out to `curl`). | `fetch_btcusdt` |
|
||||
| `live_binance.c` | Poll the Binance REST klines endpoint via `curl` and stream closed candles through RSI(14). | `live_binance` |
|
||||
| `smoke.cpp` | C++ RAII via `wickra::Handle` from [`wickra.hpp`](../bindings/c/include/wickra.hpp): construct, move, auto-free. | `cpp_smoke` |
|
||||
|
||||
The data-driven examples (`backtest`, `multi_timeframe`, `parallel_assets`, the
|
||||
three `strategy_*`) build against the bundled datasets and run under `ctest`.
|
||||
`fetch_btcusdt` and `live_binance` reach the network, so they are built but not
|
||||
run in CI; run them by hand. `parallel_assets` links OpenMP when the toolchain
|
||||
provides it and falls back to a single-threaded run otherwise.
|
||||
|
||||
## C# / .NET — `examples/csharp/`
|
||||
|
||||
Build the C ABI library first (`cargo build -p wickra-c --release`), then run any
|
||||
example with the .NET 8 SDK; the binding resolves the native library automatically.
|
||||
|
||||
| Example | What it does | Run |
|
||||
| --- | --- | --- |
|
||||
| `streaming` | Feed a synthetic price series through SMA / EMA / RSI / MACD tick by tick. | `dotnet run --project examples/csharp/streaming` |
|
||||
| `backtest` | Basket of indicators over an OHLCV series (CSV arg or synthetic). | `dotnet run --project examples/csharp/backtest -- <ohlcv.csv>` |
|
||||
| `multi_timeframe` | Resample a 1-minute series to 5m / 15m and print an indicator per timeframe. | `dotnet run --project examples/csharp/multi_timeframe` |
|
||||
| `parallel_assets` | SMA(20) batch over a panel, serial vs `Parallel.For`, with speedup. | `dotnet run -c Release --project examples/csharp/parallel_assets` |
|
||||
| `strategy_rsi_mean_reversion` | RSI(14) mean-reversion with PnL / Sharpe / max-DD summary. | `dotnet run -c Release --project examples/csharp/strategy_rsi_mean_reversion` |
|
||||
| `strategy_macd_adx` | Trend-follower: MACD crossover entries gated by ADX(14) > 20. | `dotnet run -c Release --project examples/csharp/strategy_macd_adx` |
|
||||
| `strategy_bollinger_squeeze` | Bollinger-squeeze breakout with an ATR(14) trailing stop. | `dotnet run -c Release --project examples/csharp/strategy_bollinger_squeeze` |
|
||||
| `fetch_btcusdt` | Download real BTCUSDT klines from the Binance REST API into a CSV. | `dotnet run --project examples/csharp/fetch_btcusdt` |
|
||||
| `live_binance` | Stream live Binance klines through EMA(20) over a WebSocket. | `dotnet run --project examples/csharp/live_binance` |
|
||||
|
||||
The offline examples run on deterministic synthetic data (and under CI on all
|
||||
three OSes); `fetch_btcusdt` and `live_binance` reach the network and are built
|
||||
but not run in CI.
|
||||
|
||||
## Go — `examples/go/`
|
||||
|
||||
Build the C ABI library first (`cargo build -p wickra-c --release`) and stage it
|
||||
under `bindings/go/lib/` (see the [Go binding README](../bindings/go)), then run
|
||||
any example from the `examples/go` module.
|
||||
|
||||
| Example | What it does | Run |
|
||||
| --- | --- | --- |
|
||||
| `streaming` | Feed a synthetic price series through SMA / EMA / RSI / MACD tick by tick. | `go run ./streaming` |
|
||||
| `backtest` | Basket of indicators over an OHLCV series (CSV arg or synthetic). | `go run ./backtest <ohlcv.csv>` |
|
||||
| `multi_timeframe` | Resample a 1-minute series to 5m / 15m and print an indicator per timeframe. | `go run ./multi_timeframe` |
|
||||
| `parallel_assets` | SMA(20) batch over a panel, serial vs goroutine fan-out, with speedup. | `go run ./parallel_assets 200 5000` |
|
||||
| `strategy_rsi_mean_reversion` | RSI(14) mean-reversion with PnL / Sharpe / max-DD summary. | `go run ./strategy_rsi_mean_reversion` |
|
||||
| `strategy_macd_adx` | Trend-follower: MACD crossover entries gated by ADX(14) > 20. | `go run ./strategy_macd_adx` |
|
||||
| `strategy_bollinger_squeeze` | Bollinger-squeeze breakout with an ATR(14) trailing stop. | `go run ./strategy_bollinger_squeeze` |
|
||||
| `fetch_btcusdt` | Download real BTCUSDT klines from the Binance REST API into a CSV. | `go run ./fetch_btcusdt` |
|
||||
| `live_binance` | Stream live Binance klines through EMA(20) over a WebSocket. | `go run ./live_binance` |
|
||||
|
||||
The offline examples run on deterministic synthetic data (and under CI on all
|
||||
three OSes); `fetch_btcusdt` and `live_binance` reach the network and are built
|
||||
but not run in CI.
|
||||
|
||||
## Python — `examples/python/`
|
||||
|
||||
| Example | What it does | Run |
|
||||
|
||||
@@ -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/
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user