Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8115d3b33d | |||
| 4250ed99f4 | |||
| 995f119010 | |||
| 05d2e5dc61 | |||
| 404bcb040c | |||
| 00ce899cc3 | |||
| b6ead740e8 | |||
| 755f4aa0f6 |
@@ -33,6 +33,13 @@ jobs:
|
||||
with:
|
||||
results_file: results.sarif
|
||||
results_format: sarif
|
||||
# The default GITHUB_TOKEN cannot read classic branch-protection
|
||||
# rules, so the Branch-Protection check fails with an internal error
|
||||
# and scores -1. A read-only fine-grained PAT (Administration: read,
|
||||
# Contents: read, Metadata: read) supplied as SCORECARD_TOKEN lets the
|
||||
# check read the protection settings. See
|
||||
# https://github.com/ossf/scorecard-action/blob/main/docs/authentication/fine-grained-auth-token.md
|
||||
repo_token: ${{ secrets.SCORECARD_TOKEN }}
|
||||
# Publish to the public OpenSSF endpoint that backs the README badge.
|
||||
publish_results: true
|
||||
|
||||
|
||||
+22
-1
@@ -7,6 +7,26 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [0.5.2] - 2026-06-03
|
||||
|
||||
### Added
|
||||
- **Three Drives** — three symmetric drives with extension legs; bullish +1, bearish -1 (`THREE_DRIVES`).
|
||||
- **Cypher** — five-point harmonic whose D retraces XC by 0.786; bullish +1, bearish -1 (`CYPHER`).
|
||||
- **Shark** — five-point harmonic with an expansion leg and 0.886-1.13 D; bullish +1, bearish -1 (`SHARK`).
|
||||
- **Crab** — five-point harmonic with the deepest (1.618 XA) D completion; bullish +1, bearish -1 (`CRAB`).
|
||||
- **Bat** — five-point harmonic with a shallow B and 0.886 D completion; bullish +1, bearish -1 (`BAT`).
|
||||
- **Butterfly** — five-point harmonic with an extended (1.27-1.618 XA) D; bullish +1, bearish -1 (`BUTTERFLY`).
|
||||
- **Gartley** — five-point harmonic with a 0.786 D completion; bullish +1, bearish -1 (`GARTLEY`).
|
||||
- **AB=CD** — four-point AB=CD harmonic: BC retraces AB, CD mirrors AB; bullish +1, bearish -1 (`ABCD`).
|
||||
- **Cup and Handle** — rounded base with a shallow handle near the rim; bullish +1, inverse -1 (`CUP_AND_HANDLE`).
|
||||
- **Rectangle / Range** — flat support and resistance; mean-reversion signal off the just-touched boundary; support +1, resistance -1 (`RECTANGLE_RANGE`).
|
||||
- **Flag / Pennant** — shallow consolidation against a sharp pole; continuation in the pole direction; bull +1, bear -1 (`FLAG_PENNANT`).
|
||||
- **Wedge (rising/falling)** — both trendlines slope the same way but converge; rising wedge -1, falling wedge +1 (`WEDGE`).
|
||||
- **Triangle (asc/desc/sym)** — converging trendlines; ascending +1, descending -1, symmetrical follows the last swing (`TRIANGLE`).
|
||||
- **Head and Shoulders** — central head flanked by two matching shoulders over a flat neckline; top -1, inverse +1 (`HEAD_AND_SHOULDERS`).
|
||||
- **Triple Top / Bottom** — three matching peaks / troughs; a stronger reversal than the double; bearish -1, bullish +1 (`TRIPLE_TOP_BOTTOM`).
|
||||
- **Double Top / Bottom** — twin-peak / twin-trough reversal confirmed on the second matching swing extreme; bearish -1, bullish +1 (`DOUBLE_TOP_BOTTOM`).
|
||||
|
||||
## [0.5.1] - 2026-06-03
|
||||
|
||||
### Added — Seasonality & Session family (12 indicators)
|
||||
@@ -1185,7 +1205,8 @@ 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.5.1...HEAD
|
||||
[Unreleased]: https://github.com/wickra-lib/wickra/compare/v0.5.2...HEAD
|
||||
[0.5.2]: https://github.com/wickra-lib/wickra/compare/v0.5.1...v0.5.2
|
||||
[0.5.1]: https://github.com/wickra-lib/wickra/compare/v0.5.0...v0.5.1
|
||||
[0.5.0]: https://github.com/wickra-lib/wickra/compare/v0.4.7...v0.5.0
|
||||
[0.4.7]: https://github.com/wickra-lib/wickra/compare/v0.4.6...v0.4.7
|
||||
|
||||
@@ -122,3 +122,33 @@ installed. Dependabot also keeps the `.github/requirements` pins current.
|
||||
Use the issue templates under
|
||||
[`.github/ISSUE_TEMPLATE`](.github/ISSUE_TEMPLATE). For security-sensitive
|
||||
reports, follow [`SECURITY.md`](SECURITY.md) instead of opening a public issue.
|
||||
|
||||
## Developer Certificate of Origin (DCO)
|
||||
|
||||
All contributions to Wickra are made under the [Developer Certificate of
|
||||
Origin (DCO) 1.1](DCO). By signing off on your commits you certify that you
|
||||
wrote the patch, or otherwise have the right to submit it under the project's
|
||||
`MIT OR Apache-2.0` license.
|
||||
|
||||
Sign off every commit by adding a `Signed-off-by` trailer with your real name
|
||||
and email — Git adds it automatically with the `-s` flag:
|
||||
|
||||
```bash
|
||||
git commit -s -m "your message"
|
||||
```
|
||||
|
||||
This produces a trailer of the form:
|
||||
|
||||
```
|
||||
Signed-off-by: Your Name <you@example.com>
|
||||
```
|
||||
|
||||
The name and email must match the commit author. Commits without a valid
|
||||
sign-off line cannot be merged. To sign off a commit you already made, amend it
|
||||
with `git commit -s --amend`, or sign off a range with an interactive rebase.
|
||||
|
||||
## Governance
|
||||
|
||||
Wickra's decision-making and maintainership are described in
|
||||
[`GOVERNANCE.md`](GOVERNANCE.md); the current maintainers are listed in
|
||||
[`MAINTAINERS.md`](MAINTAINERS.md).
|
||||
|
||||
Generated
+6
-6
@@ -1867,7 +1867,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wickra"
|
||||
version = "0.5.1"
|
||||
version = "0.5.2"
|
||||
dependencies = [
|
||||
"approx",
|
||||
"criterion",
|
||||
@@ -1878,7 +1878,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wickra-core"
|
||||
version = "0.5.1"
|
||||
version = "0.5.2"
|
||||
dependencies = [
|
||||
"approx",
|
||||
"proptest",
|
||||
@@ -1888,7 +1888,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wickra-data"
|
||||
version = "0.5.1"
|
||||
version = "0.5.2"
|
||||
dependencies = [
|
||||
"approx",
|
||||
"csv",
|
||||
@@ -1915,7 +1915,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wickra-node"
|
||||
version = "0.5.1"
|
||||
version = "0.5.2"
|
||||
dependencies = [
|
||||
"napi",
|
||||
"napi-build",
|
||||
@@ -1925,7 +1925,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wickra-python"
|
||||
version = "0.5.1"
|
||||
version = "0.5.2"
|
||||
dependencies = [
|
||||
"numpy",
|
||||
"pyo3",
|
||||
@@ -1934,7 +1934,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wickra-wasm"
|
||||
version = "0.5.1"
|
||||
version = "0.5.2"
|
||||
dependencies = [
|
||||
"console_error_panic_hook",
|
||||
"js-sys",
|
||||
|
||||
+2
-2
@@ -12,7 +12,7 @@ members = [
|
||||
exclude = ["fuzz"]
|
||||
|
||||
[workspace.package]
|
||||
version = "0.5.1"
|
||||
version = "0.5.2"
|
||||
authors = ["kingchenc <support@wickra.org>"]
|
||||
edition = "2021"
|
||||
rust-version = "1.86"
|
||||
@@ -24,7 +24,7 @@ keywords = ["finance", "trading", "indicators", "technical-analysis", "ta"]
|
||||
categories = ["finance", "mathematics", "science"]
|
||||
|
||||
[workspace.dependencies]
|
||||
wickra-core = { path = "crates/wickra-core", version = "0.5.1" }
|
||||
wickra-core = { path = "crates/wickra-core", version = "0.5.2" }
|
||||
|
||||
thiserror = "2"
|
||||
rayon = "1.10"
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
Developer Certificate of Origin
|
||||
Version 1.1
|
||||
|
||||
Copyright (C) 2004, 2006 The Linux Foundation and its contributors.
|
||||
|
||||
Everyone is permitted to copy and distribute verbatim copies of this
|
||||
license document, but changing it is not allowed.
|
||||
|
||||
|
||||
Developer's Certificate of Origin 1.1
|
||||
|
||||
By making a contribution to this project, I certify that:
|
||||
|
||||
(a) The contribution was created in whole or in part by me and I
|
||||
have the right to submit it under the open source license
|
||||
indicated in the file; or
|
||||
|
||||
(b) The contribution is based upon previous work that, to the best
|
||||
of my knowledge, is covered under an appropriate open source
|
||||
license and I have the right under that license to submit that
|
||||
work with modifications, whether created in whole or in part
|
||||
by me, under the same open source license (unless I am
|
||||
permitted to submit under a different license), as indicated
|
||||
in the file; or
|
||||
|
||||
(c) The contribution was provided directly to me by some other
|
||||
person who certified (a), (b) or (c) and I have not modified
|
||||
it.
|
||||
|
||||
(d) I understand and agree that this project and the contribution
|
||||
are public and that a record of the contribution (including all
|
||||
personal information I submit with it, including my sign-off) is
|
||||
maintained indefinitely and may be redistributed consistent with
|
||||
this project or the open source license(s) involved.
|
||||
@@ -0,0 +1,50 @@
|
||||
# Governance
|
||||
|
||||
Wickra is an open-source project maintained under a **single-maintainer
|
||||
("BDFL") model**. This document describes how decisions are made and how the
|
||||
project is run, so contributors know what to expect.
|
||||
|
||||
## Roles
|
||||
|
||||
- **Maintainer.** The maintainer (see [`MAINTAINERS.md`](MAINTAINERS.md)) is
|
||||
responsible for the project's direction, reviews and merges changes, cuts
|
||||
releases, and has final say on all technical and project decisions.
|
||||
- **Contributors.** Anyone who proposes changes via pull requests, files
|
||||
issues, improves documentation, or otherwise participates. Contributors do
|
||||
not need any special status to take part.
|
||||
|
||||
## Decision-making
|
||||
|
||||
- Day-to-day technical decisions (APIs, indicator implementations, refactors)
|
||||
are made by the maintainer, informed by discussion on issues and pull
|
||||
requests.
|
||||
- Proposals are raised as GitHub issues or pull requests. Significant or
|
||||
breaking changes should be opened as an issue first to agree on the approach
|
||||
before implementation.
|
||||
- The maintainer aims to act transparently: rationale for non-trivial decisions
|
||||
is recorded in the relevant issue, pull request, or commit message.
|
||||
|
||||
## Contribution flow
|
||||
|
||||
All changes — including the maintainer's own — go through pull requests so that
|
||||
CI (tests, linting, static analysis) runs against them, and so the change
|
||||
history is reviewable. Contribution requirements are documented in
|
||||
[`CONTRIBUTING.md`](CONTRIBUTING.md), including the Developer Certificate of
|
||||
Origin sign-off that every commit must carry.
|
||||
|
||||
## Becoming a maintainer
|
||||
|
||||
The project currently has one maintainer. Maintainership may be extended to
|
||||
contributors who have demonstrated sustained, high-quality involvement, at the
|
||||
current maintainer's discretion. If the project grows to multiple maintainers,
|
||||
this document will be updated to describe shared decision-making.
|
||||
|
||||
## Code of conduct
|
||||
|
||||
All participants are expected to follow the
|
||||
[Code of Conduct](CODE_OF_CONDUCT.md).
|
||||
|
||||
## Changes to this document
|
||||
|
||||
This governance model may evolve as the project grows. Changes are made via
|
||||
pull request and take effect once merged.
|
||||
@@ -0,0 +1,17 @@
|
||||
# Maintainers
|
||||
|
||||
This file lists the current maintainers of Wickra. See
|
||||
[`GOVERNANCE.md`](GOVERNANCE.md) for what the role entails and how the project
|
||||
is run.
|
||||
|
||||
| Maintainer | GitHub | Areas |
|
||||
| --- | --- | --- |
|
||||
| kingchenc | [@kingchenc](https://github.com/kingchenc) | All (core, bindings, CI/release, docs) |
|
||||
|
||||
## Contacting the maintainers
|
||||
|
||||
- General questions and support: see [`SUPPORT.md`](SUPPORT.md).
|
||||
- Bug reports and feature requests: open an issue using the
|
||||
[issue templates](.github/ISSUE_TEMPLATE).
|
||||
- Security reports: follow [`SECURITY.md`](SECURITY.md) — do **not** open a
|
||||
public issue.
|
||||
@@ -1,5 +1,5 @@
|
||||
<p align="center">
|
||||
<a href="https://wickra.org"><img src="https://raw.githubusercontent.com/wickra-lib/.github/main/profile/wickra-banner.webp?v=351" 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=367" alt="Wickra — streaming-first technical indicators" width="100%"></a>
|
||||
</p>
|
||||
|
||||
[](https://github.com/wickra-lib/wickra/actions/workflows/ci.yml)
|
||||
@@ -11,6 +11,7 @@
|
||||
[](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)
|
||||
|
||||
@@ -47,7 +48,7 @@ Full documentation lives at **[docs.wickra.org](https://docs.wickra.org)**:
|
||||
[Node](https://docs.wickra.org/Quickstart-Node),
|
||||
[WASM](https://docs.wickra.org/Quickstart-WASM).
|
||||
- **Indicators** — a per-indicator deep dive (formula, parameters, warmup) for
|
||||
every one of the 351 indicators; start at the
|
||||
every one of the 367 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),
|
||||
@@ -135,7 +136,7 @@ python -m benchmarks.compare_libraries
|
||||
|
||||
## Indicators
|
||||
|
||||
351 streaming-first indicators across twenty-one families. Every one passes the
|
||||
367 streaming-first indicators across twenty-three 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).
|
||||
@@ -157,6 +158,8 @@ warmup) at [docs.wickra.org](https://docs.wickra.org/Indicators-Overview).
|
||||
| Ichimoku & Charts | Ichimoku Kinko Hyo (Tenkan, Kijun, Senkou A/B, Chikou), Heikin-Ashi |
|
||||
| Alt-Chart Bars | Renko (box-size bricks), Kagi (reversal-amount lines), Point & Figure (X/O columns) |
|
||||
| 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 |
|
||||
| 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 |
|
||||
| Microstructure | Order-Book Imbalance (Top-1 / Top-N / Full), Microprice, Quoted Spread, Depth Slope, Signed Volume, Cumulative Volume Delta, Trade Imbalance, Effective Spread, Realized Spread, Kyle's Lambda, Footprint |
|
||||
| Derivatives | Funding Rate, Funding Rate Mean, Funding Rate Z-Score, Funding Basis, Open-Interest Delta, OI / Price Divergence, OI-Weighted Price, Long/Short Ratio, Taker Buy/Sell Ratio, Liquidation Features, Term-Structure Basis, Calendar Spread |
|
||||
| Market Profile | Value Area (POC / VAH / VAL), Volume Profile (histogram), TPO Profile, Initial Balance, Opening Range |
|
||||
@@ -241,7 +244,7 @@ A Python live-trading example using the public `websockets` package lives at
|
||||
```
|
||||
wickra/
|
||||
├── crates/
|
||||
│ ├── wickra-core/ core engine + all 351 indicators
|
||||
│ ├── wickra-core/ core engine + all 367 indicators
|
||||
│ ├── wickra/ top-level facade crate (publishes on crates.io) + benches/
|
||||
│ └── wickra-data/ CSV reader, tick aggregator, live exchange feeds
|
||||
├── bindings/
|
||||
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
# Roadmap
|
||||
|
||||
This roadmap describes the project's direction at a high level. It is
|
||||
intentionally non-binding: priorities shift with feedback and available time,
|
||||
and the authoritative, up-to-date view of planned work is the
|
||||
[issue tracker](https://github.com/wickra-lib/wickra/issues). Shipped changes
|
||||
are recorded in [`CHANGELOG.md`](CHANGELOG.md).
|
||||
|
||||
## Status
|
||||
|
||||
Wickra is **pre-1.0**. The public API is largely stable but may still change in
|
||||
minor releases; breaking changes are called out in the changelog.
|
||||
|
||||
## Themes
|
||||
|
||||
- **Indicator coverage.** Continue broadening the indicator catalogue across
|
||||
families (trend, momentum, volatility, volume, statistics, market profile,
|
||||
and more), each with the same streaming/batch parity and test guarantees.
|
||||
- **API stabilization toward 1.0.** Settle the public `Indicator` and
|
||||
`BarBuilder` traits and the binding surfaces, then commit to semantic
|
||||
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.
|
||||
- **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,
|
||||
signed releases, and supply-chain monitoring.
|
||||
|
||||
## How to influence the roadmap
|
||||
|
||||
Open or comment on an issue, or start with the
|
||||
[feature-request template](.github/ISSUE_TEMPLATE/feature_request.md).
|
||||
Well-scoped proposals and pull requests are the most effective way to move an
|
||||
item forward.
|
||||
+96
@@ -41,3 +41,99 @@ PyPI/npm packages, and the build/release workflows in `.github/workflows/`.
|
||||
|
||||
Out of scope: vulnerabilities in third-party dependencies (report those
|
||||
upstream; we track them via Dependabot and `cargo-deny`).
|
||||
|
||||
## Security assurance case
|
||||
|
||||
This is a short, evidence-backed argument for why Wickra can be used safely.
|
||||
|
||||
**Security requirements.** Wickra is a computational library: it ingests
|
||||
numeric market data and produces indicator values. It stores no user
|
||||
credentials, authenticates no external users, and implements no cryptography of
|
||||
its own. The requirements are therefore: (1) memory safety and freedom from
|
||||
undefined behaviour, (2) robust handling of untrusted/degenerate numeric input
|
||||
without panics or unbounded resource use, (3) integrity of the published
|
||||
artifacts, and (4) a healthy dependency supply chain.
|
||||
|
||||
**How the requirements are met.**
|
||||
|
||||
- *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.
|
||||
- *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
|
||||
exercised by coverage-guided fuzzing (`cargo-fuzz` / libFuzzer) in CI.
|
||||
- *Static and dynamic analysis* — every push and pull request runs Clippy
|
||||
(`clippy::pedantic`, warnings-as-errors), CodeQL, fuzzing, and the full test
|
||||
suite, with 100% line coverage on the core crate tracked by Codecov.
|
||||
- *Artifact integrity* — releases are built in CI, commits and tags are signed,
|
||||
the `main` branch requires signed commits, and release artifacts carry build
|
||||
provenance attestations.
|
||||
- *Supply chain* — dependencies are pinned and monitored with Dependabot and
|
||||
audited with `cargo-deny` (license + advisory checks) on every change.
|
||||
|
||||
**Residual risk.** The optional `live-binance` feature opens a TLS WebSocket to
|
||||
an exchange using the platform TLS library; transport security therefore
|
||||
depends on that library, not on Wickra. Wickra is not a trading system and is
|
||||
provided "as is" — see the disclaimers in `README.md` and the licenses.
|
||||
|
||||
## Secrets management
|
||||
|
||||
The project stores **no** secrets or credentials in the version control system.
|
||||
Secrets required by automation (publishing tokens, the about-sync PAT) are kept
|
||||
exclusively as **GitHub Actions encrypted secrets** and referenced via the
|
||||
`secrets.*` context; they are never written to the repository, logs, or build
|
||||
artifacts. GitHub **secret scanning with push protection** is enabled to block
|
||||
accidental commits of credentials. Secrets follow least privilege (the narrowest
|
||||
scope that works) and are rotated when a holder changes or on suspected
|
||||
exposure.
|
||||
|
||||
## Verifying releases
|
||||
|
||||
Released artifacts can be verified for integrity and authenticity:
|
||||
|
||||
- **Build provenance.** Release assets carry GitHub build provenance
|
||||
attestations. Verify a downloaded asset with the GitHub CLI:
|
||||
`gh attestation verify <file> --repo wickra-lib/wickra`.
|
||||
- **Signed tags.** Each release corresponds to a signed git tag (`vX.Y.Z`);
|
||||
the tag signature identifies the maintainer who authorised the release.
|
||||
- **Registry integrity.** Packages are distributed over HTTPS from crates.io,
|
||||
PyPI and npm, which serve package checksums that package managers verify on
|
||||
install.
|
||||
|
||||
The release is published only by the maintainer through the tag-triggered
|
||||
release workflow, so a verified tag signature establishes the expected
|
||||
publisher identity.
|
||||
|
||||
## Support timeline and end of support
|
||||
|
||||
Wickra is **pre-1.0**: only the **latest released `0.y.z`** version receives
|
||||
security fixes. When a newer release is published, the previous version
|
||||
**immediately reaches end of support** and will not receive further fixes;
|
||||
users should upgrade to the latest release. The supported-versions table above
|
||||
is authoritative. After the `1.0.0` release this policy will be revised to
|
||||
support a defined window of releases.
|
||||
|
||||
## Remediation policy (dependencies and code scanning)
|
||||
|
||||
- **Severity threshold.** Vulnerabilities of **medium severity or higher** in
|
||||
the project's own code or its dependencies are remediated promptly and before
|
||||
the next release; lower-severity findings are addressed on a best-effort
|
||||
basis.
|
||||
- **Automated enforcement (SCA).** Every change is evaluated by `cargo-deny`
|
||||
(RUSTSEC advisories + license policy) and Dependabot; a known-vulnerable
|
||||
dependency fails CI and **blocks the change** until resolved or explicitly
|
||||
waived with justification.
|
||||
- **Automated enforcement (SAST).** Every change is evaluated by CodeQL and
|
||||
Clippy (`-D warnings`); findings **block the change** in CI until fixed.
|
||||
- **Pre-release gate.** A release is not cut while an unresolved medium-or-higher
|
||||
SCA/SAST finding is outstanding.
|
||||
|
||||
## Vulnerability exploitability (VEX)
|
||||
|
||||
Advisories reported by `cargo-deny`/Dependabot for third-party dependencies that
|
||||
do **not** affect Wickra (e.g. the vulnerable code path is not reachable, or the
|
||||
affected feature is not enabled) are triaged and recorded — with the
|
||||
not-affected justification — in the `cargo-deny` configuration (`deny.toml`) and
|
||||
the relevant pull request, rather than forcing an unnecessary dependency bump.
|
||||
This serves as the project's exploitability (VEX) record.
|
||||
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
# Support
|
||||
|
||||
Thanks for using Wickra! Here is where to get help, depending on what you need.
|
||||
|
||||
## Documentation first
|
||||
|
||||
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.
|
||||
- **README:** <https://github.com/wickra-lib/wickra#readme> — installation and a
|
||||
quick overview.
|
||||
- **API docs (Rust):** <https://docs.rs/wickra>.
|
||||
|
||||
## Questions and help
|
||||
|
||||
- Ask a question with the
|
||||
[question issue template](.github/ISSUE_TEMPLATE/question.md).
|
||||
- Browse [existing issues](https://github.com/wickra-lib/wickra/issues) — your
|
||||
question may already be answered.
|
||||
|
||||
## Bugs and feature requests
|
||||
|
||||
- **Bugs:** use the bug-report issue template.
|
||||
- **Feature requests / new indicators:** use the feature-request template.
|
||||
|
||||
## Security issues
|
||||
|
||||
Please do **not** report security vulnerabilities through public issues. Follow
|
||||
the process in [`SECURITY.md`](SECURITY.md) (private GitHub advisory or email).
|
||||
|
||||
## Support expectations
|
||||
|
||||
Wickra is maintained by a single maintainer on a best-effort basis. Issues are
|
||||
triaged and acknowledged as time allows; there is no commercial support or SLA.
|
||||
Clear, reproducible reports get help fastest.
|
||||
@@ -0,0 +1,54 @@
|
||||
# Threat model
|
||||
|
||||
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.
|
||||
|
||||
## Assets
|
||||
|
||||
- **Integrity of computed indicator values** — consumers may use them in
|
||||
automated decisions, so silently wrong output is the primary concern.
|
||||
- **Availability of the calling process** — a library must not crash or hang
|
||||
its host on malformed input.
|
||||
- **Integrity of published artifacts** — the crates, wheels and npm packages
|
||||
users install.
|
||||
- **The build and release pipeline** and its secrets (publishing tokens).
|
||||
|
||||
## Actors / trust boundaries
|
||||
|
||||
- **Library consumer** (trusted) — calls the API with numeric data. Data may
|
||||
originate from untrusted sources (e.g. a market feed), so *input values* are
|
||||
treated as untrusted even though the caller is trusted.
|
||||
- **Optional live feed** — with the `live-binance` feature, data crosses a
|
||||
network boundary from an exchange over TLS.
|
||||
- **Contributors** (semi-trusted) — propose changes via pull requests.
|
||||
- **Supply chain** — upstream dependencies and the CI/CD platform.
|
||||
|
||||
## Threats and mitigations
|
||||
|
||||
| Threat | Mitigation |
|
||||
| --- | --- |
|
||||
| Memory-safety exploit (buffer overflow, UAF) via crafted input | Pure safe Rust; `unsafe` is forbidden/minimised, so the compiler precludes these classes. |
|
||||
| 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. |
|
||||
| Adversary-in-the-middle on the optional live feed | Connection uses TLS via the platform library; transport security is delegated to that reviewed implementation. |
|
||||
| Compromised dependency (supply chain) | Dependencies pinned (`Cargo.lock`, hash-locked CI requirements), monitored by Dependabot, audited by `cargo-deny` (advisories + licenses) on every change. |
|
||||
| Malicious or accidental change to `main` | Branch protection requires signed commits and blocks force-push and deletion; all changes flow through pull requests with required CI; static analysis (CodeQL, Clippy) and fuzzing run on every change. |
|
||||
| Compromised CI / leaked secrets | Workflows use least-privilege `permissions:`; secrets live only as encrypted GitHub Actions secrets; secret scanning with push protection is enabled; workflows are linted by `zizmor`. |
|
||||
| Tampered release artifact | Releases are built in CI, tags are signed, and assets carry build provenance attestations (verifiable with `gh attestation verify`). |
|
||||
|
||||
## Out of scope
|
||||
|
||||
- Wickra implements no authentication, authorization or cryptography of its own,
|
||||
stores no user data, and exposes no network listener; those threat classes do
|
||||
not apply.
|
||||
- Vulnerabilities in third-party dependencies that do not affect Wickra are
|
||||
tracked as exploitability (VEX) records (see [`SECURITY.md`](SECURITY.md)).
|
||||
|
||||
## Maintenance
|
||||
|
||||
This threat model is reviewed when the architecture changes materially (for
|
||||
example, a new input family, a new network feature, or a new release channel).
|
||||
@@ -297,6 +297,22 @@ const candleScalar = {
|
||||
TasukiGap: { make: () => new wickra.TasukiGap(), step: (ind, i) => ind.update(open[i], high[i], low[i], close[i]), batch: (ind) => ind.batch(open, high, low, close) },
|
||||
UniqueThreeRiver: { make: () => new wickra.UniqueThreeRiver(), step: (ind, i) => ind.update(open[i], high[i], low[i], close[i]), batch: (ind) => ind.batch(open, high, low, close) },
|
||||
ConcealingBabySwallow: { make: () => new wickra.ConcealingBabySwallow(), step: (ind, i) => ind.update(open[i], high[i], low[i], close[i]), batch: (ind) => ind.batch(open, high, low, close) },
|
||||
DoubleTopBottom: { make: () => new wickra.DoubleTopBottom(), step: (ind, i) => ind.update(open[i], high[i], low[i], close[i]), batch: (ind) => ind.batch(open, high, low, close) },
|
||||
TripleTopBottom: { make: () => new wickra.TripleTopBottom(), step: (ind, i) => ind.update(open[i], high[i], low[i], close[i]), batch: (ind) => ind.batch(open, high, low, close) },
|
||||
HeadAndShoulders: { make: () => new wickra.HeadAndShoulders(), step: (ind, i) => ind.update(open[i], high[i], low[i], close[i]), batch: (ind) => ind.batch(open, high, low, close) },
|
||||
Triangle: { make: () => new wickra.Triangle(), step: (ind, i) => ind.update(open[i], high[i], low[i], close[i]), batch: (ind) => ind.batch(open, high, low, close) },
|
||||
Wedge: { make: () => new wickra.Wedge(), step: (ind, i) => ind.update(open[i], high[i], low[i], close[i]), batch: (ind) => ind.batch(open, high, low, close) },
|
||||
FlagPennant: { make: () => new wickra.FlagPennant(), step: (ind, i) => ind.update(open[i], high[i], low[i], close[i]), batch: (ind) => ind.batch(open, high, low, close) },
|
||||
RectangleRange: { make: () => new wickra.RectangleRange(), step: (ind, i) => ind.update(open[i], high[i], low[i], close[i]), batch: (ind) => ind.batch(open, high, low, close) },
|
||||
CupAndHandle: { make: () => new wickra.CupAndHandle(), step: (ind, i) => ind.update(open[i], high[i], low[i], close[i]), batch: (ind) => ind.batch(open, high, low, close) },
|
||||
Abcd: { make: () => new wickra.Abcd(), step: (ind, i) => ind.update(open[i], high[i], low[i], close[i]), batch: (ind) => ind.batch(open, high, low, close) },
|
||||
Gartley: { make: () => new wickra.Gartley(), step: (ind, i) => ind.update(open[i], high[i], low[i], close[i]), batch: (ind) => ind.batch(open, high, low, close) },
|
||||
Butterfly: { make: () => new wickra.Butterfly(), step: (ind, i) => ind.update(open[i], high[i], low[i], close[i]), batch: (ind) => ind.batch(open, high, low, close) },
|
||||
Bat: { make: () => new wickra.Bat(), step: (ind, i) => ind.update(open[i], high[i], low[i], close[i]), batch: (ind) => ind.batch(open, high, low, close) },
|
||||
Crab: { make: () => new wickra.Crab(), step: (ind, i) => ind.update(open[i], high[i], low[i], close[i]), batch: (ind) => ind.batch(open, high, low, close) },
|
||||
Shark: { make: () => new wickra.Shark(), step: (ind, i) => ind.update(open[i], high[i], low[i], close[i]), batch: (ind) => ind.batch(open, high, low, close) },
|
||||
Cypher: { make: () => new wickra.Cypher(), step: (ind, i) => ind.update(open[i], high[i], low[i], close[i]), batch: (ind) => ind.batch(open, high, low, close) },
|
||||
ThreeDrives: { make: () => new wickra.ThreeDrives(), step: (ind, i) => ind.update(open[i], high[i], low[i], close[i]), batch: (ind) => ind.batch(open, high, low, close) },
|
||||
};
|
||||
|
||||
for (const [name, d] of Object.entries(candleScalar)) {
|
||||
|
||||
Vendored
+144
@@ -3032,6 +3032,150 @@ export declare class ConcealingBabySwallow {
|
||||
isReady(): boolean
|
||||
warmupPeriod(): number
|
||||
}
|
||||
export type DoubleTopBottomNode = DoubleTopBottom
|
||||
export declare class DoubleTopBottom {
|
||||
constructor()
|
||||
update(open: number, high: number, low: number, close: number): number | null
|
||||
batch(open: Array<number>, high: Array<number>, low: Array<number>, close: Array<number>): Array<number>
|
||||
reset(): void
|
||||
isReady(): boolean
|
||||
warmupPeriod(): number
|
||||
}
|
||||
export type TripleTopBottomNode = TripleTopBottom
|
||||
export declare class TripleTopBottom {
|
||||
constructor()
|
||||
update(open: number, high: number, low: number, close: number): number | null
|
||||
batch(open: Array<number>, high: Array<number>, low: Array<number>, close: Array<number>): Array<number>
|
||||
reset(): void
|
||||
isReady(): boolean
|
||||
warmupPeriod(): number
|
||||
}
|
||||
export type HeadAndShouldersNode = HeadAndShoulders
|
||||
export declare class HeadAndShoulders {
|
||||
constructor()
|
||||
update(open: number, high: number, low: number, close: number): number | null
|
||||
batch(open: Array<number>, high: Array<number>, low: Array<number>, close: Array<number>): Array<number>
|
||||
reset(): void
|
||||
isReady(): boolean
|
||||
warmupPeriod(): number
|
||||
}
|
||||
export type TriangleNode = Triangle
|
||||
export declare class Triangle {
|
||||
constructor()
|
||||
update(open: number, high: number, low: number, close: number): number | null
|
||||
batch(open: Array<number>, high: Array<number>, low: Array<number>, close: Array<number>): Array<number>
|
||||
reset(): void
|
||||
isReady(): boolean
|
||||
warmupPeriod(): number
|
||||
}
|
||||
export type WedgeNode = Wedge
|
||||
export declare class Wedge {
|
||||
constructor()
|
||||
update(open: number, high: number, low: number, close: number): number | null
|
||||
batch(open: Array<number>, high: Array<number>, low: Array<number>, close: Array<number>): Array<number>
|
||||
reset(): void
|
||||
isReady(): boolean
|
||||
warmupPeriod(): number
|
||||
}
|
||||
export type FlagPennantNode = FlagPennant
|
||||
export declare class FlagPennant {
|
||||
constructor()
|
||||
update(open: number, high: number, low: number, close: number): number | null
|
||||
batch(open: Array<number>, high: Array<number>, low: Array<number>, close: Array<number>): Array<number>
|
||||
reset(): void
|
||||
isReady(): boolean
|
||||
warmupPeriod(): number
|
||||
}
|
||||
export type RectangleRangeNode = RectangleRange
|
||||
export declare class RectangleRange {
|
||||
constructor()
|
||||
update(open: number, high: number, low: number, close: number): number | null
|
||||
batch(open: Array<number>, high: Array<number>, low: Array<number>, close: Array<number>): Array<number>
|
||||
reset(): void
|
||||
isReady(): boolean
|
||||
warmupPeriod(): number
|
||||
}
|
||||
export type CupAndHandleNode = CupAndHandle
|
||||
export declare class CupAndHandle {
|
||||
constructor()
|
||||
update(open: number, high: number, low: number, close: number): number | null
|
||||
batch(open: Array<number>, high: Array<number>, low: Array<number>, close: Array<number>): Array<number>
|
||||
reset(): void
|
||||
isReady(): boolean
|
||||
warmupPeriod(): number
|
||||
}
|
||||
export type AbcdNode = Abcd
|
||||
export declare class Abcd {
|
||||
constructor()
|
||||
update(open: number, high: number, low: number, close: number): number | null
|
||||
batch(open: Array<number>, high: Array<number>, low: Array<number>, close: Array<number>): Array<number>
|
||||
reset(): void
|
||||
isReady(): boolean
|
||||
warmupPeriod(): number
|
||||
}
|
||||
export type GartleyNode = Gartley
|
||||
export declare class Gartley {
|
||||
constructor()
|
||||
update(open: number, high: number, low: number, close: number): number | null
|
||||
batch(open: Array<number>, high: Array<number>, low: Array<number>, close: Array<number>): Array<number>
|
||||
reset(): void
|
||||
isReady(): boolean
|
||||
warmupPeriod(): number
|
||||
}
|
||||
export type ButterflyNode = Butterfly
|
||||
export declare class Butterfly {
|
||||
constructor()
|
||||
update(open: number, high: number, low: number, close: number): number | null
|
||||
batch(open: Array<number>, high: Array<number>, low: Array<number>, close: Array<number>): Array<number>
|
||||
reset(): void
|
||||
isReady(): boolean
|
||||
warmupPeriod(): number
|
||||
}
|
||||
export type BatNode = Bat
|
||||
export declare class Bat {
|
||||
constructor()
|
||||
update(open: number, high: number, low: number, close: number): number | null
|
||||
batch(open: Array<number>, high: Array<number>, low: Array<number>, close: Array<number>): Array<number>
|
||||
reset(): void
|
||||
isReady(): boolean
|
||||
warmupPeriod(): number
|
||||
}
|
||||
export type CrabNode = Crab
|
||||
export declare class Crab {
|
||||
constructor()
|
||||
update(open: number, high: number, low: number, close: number): number | null
|
||||
batch(open: Array<number>, high: Array<number>, low: Array<number>, close: Array<number>): Array<number>
|
||||
reset(): void
|
||||
isReady(): boolean
|
||||
warmupPeriod(): number
|
||||
}
|
||||
export type SharkNode = Shark
|
||||
export declare class Shark {
|
||||
constructor()
|
||||
update(open: number, high: number, low: number, close: number): number | null
|
||||
batch(open: Array<number>, high: Array<number>, low: Array<number>, close: Array<number>): Array<number>
|
||||
reset(): void
|
||||
isReady(): boolean
|
||||
warmupPeriod(): number
|
||||
}
|
||||
export type CypherNode = Cypher
|
||||
export declare class Cypher {
|
||||
constructor()
|
||||
update(open: number, high: number, low: number, close: number): number | null
|
||||
batch(open: Array<number>, high: Array<number>, low: Array<number>, close: Array<number>): Array<number>
|
||||
reset(): void
|
||||
isReady(): boolean
|
||||
warmupPeriod(): number
|
||||
}
|
||||
export type ThreeDrivesNode = ThreeDrives
|
||||
export declare class ThreeDrives {
|
||||
constructor()
|
||||
update(open: number, high: number, low: number, close: number): number | null
|
||||
batch(open: Array<number>, high: Array<number>, low: Array<number>, close: Array<number>): Array<number>
|
||||
reset(): void
|
||||
isReady(): boolean
|
||||
warmupPeriod(): number
|
||||
}
|
||||
export type OrderBookImbalanceTop1Node = OrderBookImbalanceTop1
|
||||
export declare class OrderBookImbalanceTop1 {
|
||||
constructor()
|
||||
|
||||
+17
-1
@@ -310,7 +310,7 @@ if (!nativeBinding) {
|
||||
throw new Error(`Failed to load native binding`)
|
||||
}
|
||||
|
||||
const { version, SMA, EMA, WMA, RSI, DEMA, TEMA, HMA, ROC, TRIX, SMMA, TRIMA, ZLEMA, MOM, CMO, DPO, StdDev, UlcerIndex, VerticalHorizontalFilter, ZScore, McGinleyDynamic, FRAMA, SuperSmoother, FisherTransform, Decycler, CenterOfGravity, CyberneticCycle, InstantaneousTrendline, EhlersStochastic, RVIVolatility, Variance, CoefficientOfVariation, Skewness, Kurtosis, StandardError, DetrendedStdDev, RSquared, MedianAbsoluteDeviation, MIDPOINT, ROCP, ROCR, ROCR100, LINEARREG_INTERCEPT, TSF, Autocorrelation, HurstExponent, PearsonCorrelation, Beta, PairwiseBeta, SpearmanCorrelation, RollingCorrelation, RollingCovariance, OuHalfLife, SpreadHurst, DistanceSsd, BetaNeutralSpread, PairSpreadZScore, LeadLagCrossCorrelation, Cointegration, RelativeStrengthAB, VarianceRatio, GrangerCausality, KalmanHedgeRatio, SpreadBollingerBands, MACD, MACDFIX, MACDEXT, BollingerBands, ATR, PLUS_DM, MINUS_DM, PLUS_DI, MINUS_DI, DX, MIDPRICE, AVGPRICE, SAREXT, HT_PHASOR, Stochastic, OBV, ADX, ADXR, CCI, WilliamsR, MFI, PSAR, Keltner, Donchian, VWAP, RollingVWAP, AwesomeOscillator, Aroon, Inertia, ConnorsRSI, LaguerreRSI, SMI, KST, PGO, RVI, AwesomeOscillatorHistogram, STC, ElderImpulse, ZeroLagMACD, CFO, APO, KAMA, EVWMA, Alligator, JMA, VIDYA, ALMA, T3, TSI, PMO, TII, ADL, VolumePriceTrend, ChaikinMoneyFlow, ChaikinOscillator, ForceIndex, NVI, PVI, VolumeOscillator, KVO, WilliamsAD, AnchoredRSI, AnchoredVWAP, DemandIndex, TSV, VZO, MarketFacilitationIndex, EaseOfMovement, SuperTrend, ChandelierExit, ChandeKrollStop, AtrTrailingStop, HiLoActivator, VoltyStop, YoyoExit, DonchianStop, PercentageTrailingStop, StepTrailingStop, RenkoTrailingStop, TypicalPrice, MedianPrice, WeightedClose, LinearRegression, LinRegSlope, AcceleratorOscillator, BalanceOfPower, ChoppinessIndex, TrueRange, ChaikinVolatility, YangZhangVolatility, RogersSatchellVolatility, GarmanKlassVolatility, ParkinsonVolatility, LinRegAngle, BollingerBandwidth, PercentB, NATR, HistoricalVolatility, AroonOscillator, WaveTrend, RWI, Vortex, MassIndex, StochRSI, UltimateOscillator, PPO, Coppock, VWMA, MaEnvelope, AccelerationBands, StarcBands, AtrBands, HurstChannel, LinRegChannel, StandardErrorBands, DoubleBollinger, TtmSqueeze, FractalChaosBands, VwapStdDevBands, ClassicPivots, FibonacciPivots, Camarilla, WoodiePivots, DemarkPivots, WilliamsFractals, ZigZag, TDSetup, TDSequential, TDDeMarker, TDREI, TDPressure, TDCombo, TDCountdown, TDLines, TDRangeProjection, TDDifferential, TDOpen, TDRiskLevel, InverseFisherTransform, DecyclerOscillator, RoofingFilter, EmpiricalModeDecomposition, HT_DCPHASE, HT_TRENDMODE, HilbertDominantCycle, AdaptiveCycle, SineWave, MAMA, FAMA, Ichimoku, HeikinAshi, ValueArea, VolumeProfile, TpoProfile, InitialBalance, OpeningRange, Doji, Hammer, InvertedHammer, HangingMan, ShootingStar, Engulfing, Harami, MorningEveningStar, ThreeSoldiersOrCrows, PiercingDarkCloud, Marubozu, Tweezer, SpinningTop, ThreeInside, ThreeOutside, TwoCrows, UpsideGapTwoCrows, IdenticalThreeCrows, ThreeLineStrike, ThreeStarsInSouth, AbandonedBaby, AdvanceBlock, BeltHold, Breakaway, Counterattack, DojiStar, DragonflyDoji, GravestoneDoji, LongLeggedDoji, RickshawMan, EveningDojiStar, MorningDojiStar, GapSideBySideWhite, HighWave, Hikkake, HikkakeModified, HomingPigeon, OnNeck, InNeck, Thrusting, SeparatingLines, Kicking, KickingByLength, LadderBottom, MatHold, MatchingLow, LongLine, ShortLine, RisingThreeMethods, FallingThreeMethods, UpsideGapThreeMethods, DownsideGapThreeMethods, StalledPattern, StickSandwich, Takuri, ClosingMarubozu, OpeningMarubozu, TasukiGap, UniqueThreeRiver, ConcealingBabySwallow, OrderBookImbalanceTop1, OrderBookImbalanceFull, Microprice, QuotedSpread, DepthSlope, OrderBookImbalanceTopN, SignedVolume, CumulativeVolumeDelta, TradeImbalance, EffectiveSpread, RealizedSpread, KylesLambda, Footprint, FundingRate, FundingRateMean, FundingRateZScore, FundingBasis, OpenInterestDelta, OIPriceDivergence, OIWeighted, LongShortRatio, TakerBuySellRatio, LiquidationFeatures, TermStructureBasis, CalendarSpread, AdvanceDecline, AdvanceDeclineRatio, AdVolumeLine, McClellanOscillator, McClellanSummationIndex, Trin, BreadthThrust, NewHighsNewLows, HighLowIndex, PercentAboveMa, UpDownVolumeRatio, BullishPercentIndex, CumulativeVolumeIndex, AbsoluteBreadthIndex, TickIndex, SharpeRatio, SortinoRatio, CalmarRatio, OmegaRatio, MaxDrawdown, AverageDrawdown, DrawdownDuration, PainIndex, ValueAtRisk, ConditionalValueAtRisk, ProfitFactor, GainLossRatio, RecoveryFactor, KellyCriterion, TreynorRatio, InformationRatio, RenkoBars, KagiBars, PointAndFigureBars, Alpha, SessionVwap, OvernightGap, SeasonalZScore, TimeOfDayReturnProfile, IntradayVolatilityProfile, VolumeByTimeProfile, DayOfWeekProfile, AverageDailyRange, TurnOfMonth, SessionHighLow, SessionRange, OvernightIntradayReturn } = nativeBinding
|
||||
const { version, SMA, EMA, WMA, RSI, DEMA, TEMA, HMA, ROC, TRIX, SMMA, TRIMA, ZLEMA, MOM, CMO, DPO, StdDev, UlcerIndex, VerticalHorizontalFilter, ZScore, McGinleyDynamic, FRAMA, SuperSmoother, FisherTransform, Decycler, CenterOfGravity, CyberneticCycle, InstantaneousTrendline, EhlersStochastic, RVIVolatility, Variance, CoefficientOfVariation, Skewness, Kurtosis, StandardError, DetrendedStdDev, RSquared, MedianAbsoluteDeviation, MIDPOINT, ROCP, ROCR, ROCR100, LINEARREG_INTERCEPT, TSF, Autocorrelation, HurstExponent, PearsonCorrelation, Beta, PairwiseBeta, SpearmanCorrelation, RollingCorrelation, RollingCovariance, OuHalfLife, SpreadHurst, DistanceSsd, BetaNeutralSpread, PairSpreadZScore, LeadLagCrossCorrelation, Cointegration, RelativeStrengthAB, VarianceRatio, GrangerCausality, KalmanHedgeRatio, SpreadBollingerBands, MACD, MACDFIX, MACDEXT, BollingerBands, ATR, PLUS_DM, MINUS_DM, PLUS_DI, MINUS_DI, DX, MIDPRICE, AVGPRICE, SAREXT, HT_PHASOR, Stochastic, OBV, ADX, ADXR, CCI, WilliamsR, MFI, PSAR, Keltner, Donchian, VWAP, RollingVWAP, AwesomeOscillator, Aroon, Inertia, ConnorsRSI, LaguerreRSI, SMI, KST, PGO, RVI, AwesomeOscillatorHistogram, STC, ElderImpulse, ZeroLagMACD, CFO, APO, KAMA, EVWMA, Alligator, JMA, VIDYA, ALMA, T3, TSI, PMO, TII, ADL, VolumePriceTrend, ChaikinMoneyFlow, ChaikinOscillator, ForceIndex, NVI, PVI, VolumeOscillator, KVO, WilliamsAD, AnchoredRSI, AnchoredVWAP, DemandIndex, TSV, VZO, MarketFacilitationIndex, EaseOfMovement, SuperTrend, ChandelierExit, ChandeKrollStop, AtrTrailingStop, HiLoActivator, VoltyStop, YoyoExit, DonchianStop, PercentageTrailingStop, StepTrailingStop, RenkoTrailingStop, TypicalPrice, MedianPrice, WeightedClose, LinearRegression, LinRegSlope, AcceleratorOscillator, BalanceOfPower, ChoppinessIndex, TrueRange, ChaikinVolatility, YangZhangVolatility, RogersSatchellVolatility, GarmanKlassVolatility, ParkinsonVolatility, LinRegAngle, BollingerBandwidth, PercentB, NATR, HistoricalVolatility, AroonOscillator, WaveTrend, RWI, Vortex, MassIndex, StochRSI, UltimateOscillator, PPO, Coppock, VWMA, MaEnvelope, AccelerationBands, StarcBands, AtrBands, HurstChannel, LinRegChannel, StandardErrorBands, DoubleBollinger, TtmSqueeze, FractalChaosBands, VwapStdDevBands, ClassicPivots, FibonacciPivots, Camarilla, WoodiePivots, DemarkPivots, WilliamsFractals, ZigZag, TDSetup, TDSequential, TDDeMarker, TDREI, TDPressure, TDCombo, TDCountdown, TDLines, TDRangeProjection, TDDifferential, TDOpen, TDRiskLevel, InverseFisherTransform, DecyclerOscillator, RoofingFilter, EmpiricalModeDecomposition, HT_DCPHASE, HT_TRENDMODE, HilbertDominantCycle, AdaptiveCycle, SineWave, MAMA, FAMA, Ichimoku, HeikinAshi, ValueArea, VolumeProfile, TpoProfile, InitialBalance, OpeningRange, Doji, Hammer, InvertedHammer, HangingMan, ShootingStar, Engulfing, Harami, MorningEveningStar, ThreeSoldiersOrCrows, PiercingDarkCloud, Marubozu, Tweezer, SpinningTop, ThreeInside, ThreeOutside, TwoCrows, UpsideGapTwoCrows, IdenticalThreeCrows, ThreeLineStrike, ThreeStarsInSouth, AbandonedBaby, AdvanceBlock, BeltHold, Breakaway, Counterattack, DojiStar, DragonflyDoji, GravestoneDoji, LongLeggedDoji, RickshawMan, EveningDojiStar, MorningDojiStar, GapSideBySideWhite, HighWave, Hikkake, HikkakeModified, HomingPigeon, OnNeck, InNeck, Thrusting, SeparatingLines, Kicking, KickingByLength, LadderBottom, MatHold, MatchingLow, LongLine, ShortLine, RisingThreeMethods, FallingThreeMethods, UpsideGapThreeMethods, DownsideGapThreeMethods, StalledPattern, StickSandwich, Takuri, ClosingMarubozu, OpeningMarubozu, TasukiGap, UniqueThreeRiver, ConcealingBabySwallow, DoubleTopBottom, TripleTopBottom, HeadAndShoulders, Triangle, Wedge, FlagPennant, RectangleRange, CupAndHandle, Abcd, Gartley, Butterfly, Bat, Crab, Shark, Cypher, ThreeDrives, OrderBookImbalanceTop1, OrderBookImbalanceFull, Microprice, QuotedSpread, DepthSlope, OrderBookImbalanceTopN, SignedVolume, CumulativeVolumeDelta, TradeImbalance, EffectiveSpread, RealizedSpread, KylesLambda, Footprint, FundingRate, FundingRateMean, FundingRateZScore, FundingBasis, OpenInterestDelta, OIPriceDivergence, OIWeighted, LongShortRatio, TakerBuySellRatio, LiquidationFeatures, TermStructureBasis, CalendarSpread, AdvanceDecline, AdvanceDeclineRatio, AdVolumeLine, McClellanOscillator, McClellanSummationIndex, Trin, BreadthThrust, NewHighsNewLows, HighLowIndex, PercentAboveMa, UpDownVolumeRatio, BullishPercentIndex, CumulativeVolumeIndex, AbsoluteBreadthIndex, TickIndex, SharpeRatio, SortinoRatio, CalmarRatio, OmegaRatio, MaxDrawdown, AverageDrawdown, DrawdownDuration, PainIndex, ValueAtRisk, ConditionalValueAtRisk, ProfitFactor, GainLossRatio, RecoveryFactor, KellyCriterion, TreynorRatio, InformationRatio, RenkoBars, KagiBars, PointAndFigureBars, Alpha, SessionVwap, OvernightGap, SeasonalZScore, TimeOfDayReturnProfile, IntradayVolatilityProfile, VolumeByTimeProfile, DayOfWeekProfile, AverageDailyRange, TurnOfMonth, SessionHighLow, SessionRange, OvernightIntradayReturn } = nativeBinding
|
||||
|
||||
module.exports.version = version
|
||||
module.exports.SMA = SMA
|
||||
@@ -592,6 +592,22 @@ module.exports.OpeningMarubozu = OpeningMarubozu
|
||||
module.exports.TasukiGap = TasukiGap
|
||||
module.exports.UniqueThreeRiver = UniqueThreeRiver
|
||||
module.exports.ConcealingBabySwallow = ConcealingBabySwallow
|
||||
module.exports.DoubleTopBottom = DoubleTopBottom
|
||||
module.exports.TripleTopBottom = TripleTopBottom
|
||||
module.exports.HeadAndShoulders = HeadAndShoulders
|
||||
module.exports.Triangle = Triangle
|
||||
module.exports.Wedge = Wedge
|
||||
module.exports.FlagPennant = FlagPennant
|
||||
module.exports.RectangleRange = RectangleRange
|
||||
module.exports.CupAndHandle = CupAndHandle
|
||||
module.exports.Abcd = Abcd
|
||||
module.exports.Gartley = Gartley
|
||||
module.exports.Butterfly = Butterfly
|
||||
module.exports.Bat = Bat
|
||||
module.exports.Crab = Crab
|
||||
module.exports.Shark = Shark
|
||||
module.exports.Cypher = Cypher
|
||||
module.exports.ThreeDrives = ThreeDrives
|
||||
module.exports.OrderBookImbalanceTop1 = OrderBookImbalanceTop1
|
||||
module.exports.OrderBookImbalanceFull = OrderBookImbalanceFull
|
||||
module.exports.Microprice = Microprice
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "wickra-darwin-arm64",
|
||||
"version": "0.5.1",
|
||||
"version": "0.5.2",
|
||||
"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.5.1",
|
||||
"version": "0.5.2",
|
||||
"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.5.1",
|
||||
"version": "0.5.2",
|
||||
"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.5.1",
|
||||
"version": "0.5.2",
|
||||
"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.5.1",
|
||||
"version": "0.5.2",
|
||||
"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.5.1",
|
||||
"version": "0.5.2",
|
||||
"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.5.1",
|
||||
"version": "0.5.2",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "wickra",
|
||||
"version": "0.5.1",
|
||||
"version": "0.5.2",
|
||||
"license": "MIT OR Apache-2.0",
|
||||
"devDependencies": {
|
||||
"@napi-rs/cli": "^2.18.0"
|
||||
@@ -15,12 +15,12 @@
|
||||
"node": ">= 18"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"wickra-darwin-arm64": "0.5.1",
|
||||
"wickra-darwin-x64": "0.5.1",
|
||||
"wickra-linux-arm64-gnu": "0.5.1",
|
||||
"wickra-linux-x64-gnu": "0.5.1",
|
||||
"wickra-win32-arm64-msvc": "0.5.1",
|
||||
"wickra-win32-x64-msvc": "0.5.1"
|
||||
"wickra-darwin-arm64": "0.5.2",
|
||||
"wickra-darwin-x64": "0.5.2",
|
||||
"wickra-linux-arm64-gnu": "0.5.2",
|
||||
"wickra-linux-x64-gnu": "0.5.2",
|
||||
"wickra-win32-arm64-msvc": "0.5.2",
|
||||
"wickra-win32-x64-msvc": "0.5.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@napi-rs/cli": {
|
||||
@@ -41,8 +41,8 @@
|
||||
}
|
||||
},
|
||||
"node_modules/wickra-darwin-arm64": {
|
||||
"version": "0.5.1",
|
||||
"resolved": "https://registry.npmjs.org/wickra-darwin-arm64/-/wickra-darwin-arm64-0.5.1.tgz",
|
||||
"version": "0.5.2",
|
||||
"resolved": "https://registry.npmjs.org/wickra-darwin-arm64/-/wickra-darwin-arm64-0.5.2.tgz",
|
||||
"integrity": "sha512-4eZiBR/yGUdr4nzhEUFy2i69XgNx64iI2ax/LPamsThgylC0KpHOZKK19QzJ2d9KbK4C8nMjME5FLuR+4GNEwQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
@@ -57,8 +57,8 @@
|
||||
}
|
||||
},
|
||||
"node_modules/wickra-darwin-x64": {
|
||||
"version": "0.5.1",
|
||||
"resolved": "https://registry.npmjs.org/wickra-darwin-x64/-/wickra-darwin-x64-0.5.1.tgz",
|
||||
"version": "0.5.2",
|
||||
"resolved": "https://registry.npmjs.org/wickra-darwin-x64/-/wickra-darwin-x64-0.5.2.tgz",
|
||||
"integrity": "sha512-6hf8zI3QPjTFp4zCpmgUwDvNtu6jHqNUHKD5e55POo0CgA52HkpyxSPtVm8TGTIZDI7kPjlbOdBM8CJ76mmXwA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
@@ -73,8 +73,8 @@
|
||||
}
|
||||
},
|
||||
"node_modules/wickra-linux-arm64-gnu": {
|
||||
"version": "0.5.1",
|
||||
"resolved": "https://registry.npmjs.org/wickra-linux-arm64-gnu/-/wickra-linux-arm64-gnu-0.5.1.tgz",
|
||||
"version": "0.5.2",
|
||||
"resolved": "https://registry.npmjs.org/wickra-linux-arm64-gnu/-/wickra-linux-arm64-gnu-0.5.2.tgz",
|
||||
"integrity": "sha512-kSe6y0xBMSiqdPLXNjwop5WZdHtvdBNKSEBCwZ4hFq33p4apW25/wrlzv9/oDuyD4kuPabJEhCCnFOplh58CUg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
@@ -89,8 +89,8 @@
|
||||
}
|
||||
},
|
||||
"node_modules/wickra-linux-x64-gnu": {
|
||||
"version": "0.5.1",
|
||||
"resolved": "https://registry.npmjs.org/wickra-linux-x64-gnu/-/wickra-linux-x64-gnu-0.5.1.tgz",
|
||||
"version": "0.5.2",
|
||||
"resolved": "https://registry.npmjs.org/wickra-linux-x64-gnu/-/wickra-linux-x64-gnu-0.5.2.tgz",
|
||||
"integrity": "sha512-tWBWS4qz7hxM4xnpFb59bhf6TaLwXq0Z3jEa/2l7r8PiHA94g8r8S53NRMiT+4yiL5hSWe/nUiC/YXdRrhEZ4g==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
@@ -105,8 +105,8 @@
|
||||
}
|
||||
},
|
||||
"node_modules/wickra-win32-arm64-msvc": {
|
||||
"version": "0.5.1",
|
||||
"resolved": "https://registry.npmjs.org/wickra-win32-arm64-msvc/-/wickra-win32-arm64-msvc-0.5.1.tgz",
|
||||
"version": "0.5.2",
|
||||
"resolved": "https://registry.npmjs.org/wickra-win32-arm64-msvc/-/wickra-win32-arm64-msvc-0.5.2.tgz",
|
||||
"integrity": "sha512-EXIckHxAtF75PUGDKRzXyqMe9ldP0JjSdu68WFN6iJfp+McYrGu6h40TEJlQ/oUEIoPqiZB/xhVyo/el5Lg7zw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
@@ -121,8 +121,8 @@
|
||||
}
|
||||
},
|
||||
"node_modules/wickra-win32-x64-msvc": {
|
||||
"version": "0.5.1",
|
||||
"resolved": "https://registry.npmjs.org/wickra-win32-x64-msvc/-/wickra-win32-x64-msvc-0.5.1.tgz",
|
||||
"version": "0.5.2",
|
||||
"resolved": "https://registry.npmjs.org/wickra-win32-x64-msvc/-/wickra-win32-x64-msvc-0.5.2.tgz",
|
||||
"integrity": "sha512-Yfsqq1Xwp6hdxMyLze411vNdo7BDwI6+lPSe7A9XdqyPecNDbtKwYLpsal2r8EHbNzqM+R8XnuRtUaEQS5VlUQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "wickra",
|
||||
"version": "0.5.1",
|
||||
"version": "0.5.2",
|
||||
"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.5.1",
|
||||
"wickra-linux-arm64-gnu": "0.5.1",
|
||||
"wickra-darwin-x64": "0.5.1",
|
||||
"wickra-darwin-arm64": "0.5.1",
|
||||
"wickra-win32-x64-msvc": "0.5.1",
|
||||
"wickra-win32-arm64-msvc": "0.5.1"
|
||||
"wickra-linux-x64-gnu": "0.5.2",
|
||||
"wickra-linux-arm64-gnu": "0.5.2",
|
||||
"wickra-darwin-x64": "0.5.2",
|
||||
"wickra-darwin-arm64": "0.5.2",
|
||||
"wickra-win32-x64-msvc": "0.5.2",
|
||||
"wickra-win32-arm64-msvc": "0.5.2"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "napi build --platform --release",
|
||||
|
||||
@@ -10014,6 +10014,26 @@ node_candle_pattern!(
|
||||
wc::ConcealingBabySwallow,
|
||||
"ConcealingBabySwallow"
|
||||
);
|
||||
node_candle_pattern!(DoubleTopBottomNode, wc::DoubleTopBottom, "DoubleTopBottom");
|
||||
node_candle_pattern!(TripleTopBottomNode, wc::TripleTopBottom, "TripleTopBottom");
|
||||
node_candle_pattern!(
|
||||
HeadAndShouldersNode,
|
||||
wc::HeadAndShoulders,
|
||||
"HeadAndShoulders"
|
||||
);
|
||||
node_candle_pattern!(TriangleNode, wc::Triangle, "Triangle");
|
||||
node_candle_pattern!(WedgeNode, wc::Wedge, "Wedge");
|
||||
node_candle_pattern!(FlagPennantNode, wc::FlagPennant, "FlagPennant");
|
||||
node_candle_pattern!(RectangleRangeNode, wc::RectangleRange, "RectangleRange");
|
||||
node_candle_pattern!(CupAndHandleNode, wc::CupAndHandle, "CupAndHandle");
|
||||
node_candle_pattern!(AbcdNode, wc::Abcd, "Abcd");
|
||||
node_candle_pattern!(GartleyNode, wc::Gartley, "Gartley");
|
||||
node_candle_pattern!(ButterflyNode, wc::Butterfly, "Butterfly");
|
||||
node_candle_pattern!(BatNode, wc::Bat, "Bat");
|
||||
node_candle_pattern!(CrabNode, wc::Crab, "Crab");
|
||||
node_candle_pattern!(SharkNode, wc::Shark, "Shark");
|
||||
node_candle_pattern!(CypherNode, wc::Cypher, "Cypher");
|
||||
node_candle_pattern!(ThreeDrivesNode, wc::ThreeDrives, "ThreeDrives");
|
||||
|
||||
// ============================== Microstructure: Order Book ==============================
|
||||
//
|
||||
|
||||
@@ -4,7 +4,7 @@ build-backend = "maturin"
|
||||
|
||||
[project]
|
||||
name = "wickra"
|
||||
version = "0.5.1"
|
||||
version = "0.5.2"
|
||||
description = "Streaming-first technical indicators: incremental, fast, install-free."
|
||||
readme = "README.md"
|
||||
license = "MIT OR Apache-2.0"
|
||||
|
||||
@@ -321,6 +321,24 @@ from ._wickra import (
|
||||
TasukiGap,
|
||||
UniqueThreeRiver,
|
||||
ConcealingBabySwallow,
|
||||
# Chart patterns
|
||||
CupAndHandle,
|
||||
RectangleRange,
|
||||
FlagPennant,
|
||||
Wedge,
|
||||
Triangle,
|
||||
HeadAndShoulders,
|
||||
TripleTopBottom,
|
||||
DoubleTopBottom,
|
||||
# Harmonic patterns
|
||||
ThreeDrives,
|
||||
Cypher,
|
||||
Shark,
|
||||
Crab,
|
||||
Bat,
|
||||
Butterfly,
|
||||
Gartley,
|
||||
Abcd,
|
||||
# Microstructure: order book
|
||||
OrderBookImbalanceTop1,
|
||||
OrderBookImbalanceTopN,
|
||||
@@ -698,6 +716,24 @@ __all__ = [
|
||||
"TasukiGap",
|
||||
"UniqueThreeRiver",
|
||||
"ConcealingBabySwallow",
|
||||
# Chart patterns
|
||||
"CupAndHandle",
|
||||
"RectangleRange",
|
||||
"FlagPennant",
|
||||
"Wedge",
|
||||
"Triangle",
|
||||
"HeadAndShoulders",
|
||||
"TripleTopBottom",
|
||||
"DoubleTopBottom",
|
||||
# Harmonic patterns
|
||||
"ThreeDrives",
|
||||
"Cypher",
|
||||
"Shark",
|
||||
"Crab",
|
||||
"Bat",
|
||||
"Butterfly",
|
||||
"Gartley",
|
||||
"Abcd",
|
||||
# Microstructure: order book
|
||||
"OrderBookImbalanceTop1",
|
||||
"OrderBookImbalanceTopN",
|
||||
|
||||
@@ -13675,6 +13675,22 @@ candle_pattern_no_param!(
|
||||
wc::ConcealingBabySwallow,
|
||||
"ConcealingBabySwallow"
|
||||
);
|
||||
candle_pattern_no_param!(PyDoubleTopBottom, wc::DoubleTopBottom, "DoubleTopBottom");
|
||||
candle_pattern_no_param!(PyTripleTopBottom, wc::TripleTopBottom, "TripleTopBottom");
|
||||
candle_pattern_no_param!(PyHeadAndShoulders, wc::HeadAndShoulders, "HeadAndShoulders");
|
||||
candle_pattern_no_param!(PyTriangle, wc::Triangle, "Triangle");
|
||||
candle_pattern_no_param!(PyWedge, wc::Wedge, "Wedge");
|
||||
candle_pattern_no_param!(PyFlagPennant, wc::FlagPennant, "FlagPennant");
|
||||
candle_pattern_no_param!(PyRectangleRange, wc::RectangleRange, "RectangleRange");
|
||||
candle_pattern_no_param!(PyCupAndHandle, wc::CupAndHandle, "CupAndHandle");
|
||||
candle_pattern_no_param!(PyAbcd, wc::Abcd, "Abcd");
|
||||
candle_pattern_no_param!(PyGartley, wc::Gartley, "Gartley");
|
||||
candle_pattern_no_param!(PyButterfly, wc::Butterfly, "Butterfly");
|
||||
candle_pattern_no_param!(PyBat, wc::Bat, "Bat");
|
||||
candle_pattern_no_param!(PyCrab, wc::Crab, "Crab");
|
||||
candle_pattern_no_param!(PyShark, wc::Shark, "Shark");
|
||||
candle_pattern_no_param!(PyCypher, wc::Cypher, "Cypher");
|
||||
candle_pattern_no_param!(PyThreeDrives, wc::ThreeDrives, "ThreeDrives");
|
||||
// ============================== Microstructure: Order Book ==============================
|
||||
//
|
||||
// Order-book indicators consume a depth snapshot rather than OHLCV. Streaming
|
||||
@@ -18196,5 +18212,21 @@ fn _wickra(_py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> {
|
||||
m.add_class::<PyDayOfWeekProfile>()?;
|
||||
m.add_class::<PyIntradayVolatilityProfile>()?;
|
||||
m.add_class::<PyVolumeByTimeProfile>()?;
|
||||
m.add_class::<PyDoubleTopBottom>()?;
|
||||
m.add_class::<PyTripleTopBottom>()?;
|
||||
m.add_class::<PyHeadAndShoulders>()?;
|
||||
m.add_class::<PyTriangle>()?;
|
||||
m.add_class::<PyWedge>()?;
|
||||
m.add_class::<PyFlagPennant>()?;
|
||||
m.add_class::<PyRectangleRange>()?;
|
||||
m.add_class::<PyCupAndHandle>()?;
|
||||
m.add_class::<PyAbcd>()?;
|
||||
m.add_class::<PyGartley>()?;
|
||||
m.add_class::<PyButterfly>()?;
|
||||
m.add_class::<PyBat>()?;
|
||||
m.add_class::<PyCrab>()?;
|
||||
m.add_class::<PyShark>()?;
|
||||
m.add_class::<PyCypher>()?;
|
||||
m.add_class::<PyThreeDrives>()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -330,6 +330,70 @@ def test_relative_strength_streaming_matches_batch():
|
||||
# 6-tuple candle; the batch helper takes only the columns it needs.
|
||||
|
||||
CANDLE_SCALAR = {
|
||||
"ThreeDrives": (
|
||||
lambda: ta.ThreeDrives(),
|
||||
lambda ind, h, l, c, v: ind.batch(c, h, l, c),
|
||||
),
|
||||
"Cypher": (
|
||||
lambda: ta.Cypher(),
|
||||
lambda ind, h, l, c, v: ind.batch(c, h, l, c),
|
||||
),
|
||||
"Shark": (
|
||||
lambda: ta.Shark(),
|
||||
lambda ind, h, l, c, v: ind.batch(c, h, l, c),
|
||||
),
|
||||
"Crab": (
|
||||
lambda: ta.Crab(),
|
||||
lambda ind, h, l, c, v: ind.batch(c, h, l, c),
|
||||
),
|
||||
"Bat": (
|
||||
lambda: ta.Bat(),
|
||||
lambda ind, h, l, c, v: ind.batch(c, h, l, c),
|
||||
),
|
||||
"Butterfly": (
|
||||
lambda: ta.Butterfly(),
|
||||
lambda ind, h, l, c, v: ind.batch(c, h, l, c),
|
||||
),
|
||||
"Gartley": (
|
||||
lambda: ta.Gartley(),
|
||||
lambda ind, h, l, c, v: ind.batch(c, h, l, c),
|
||||
),
|
||||
"Abcd": (
|
||||
lambda: ta.Abcd(),
|
||||
lambda ind, h, l, c, v: ind.batch(c, h, l, c),
|
||||
),
|
||||
"CupAndHandle": (
|
||||
lambda: ta.CupAndHandle(),
|
||||
lambda ind, h, l, c, v: ind.batch(c, h, l, c),
|
||||
),
|
||||
"RectangleRange": (
|
||||
lambda: ta.RectangleRange(),
|
||||
lambda ind, h, l, c, v: ind.batch(c, h, l, c),
|
||||
),
|
||||
"FlagPennant": (
|
||||
lambda: ta.FlagPennant(),
|
||||
lambda ind, h, l, c, v: ind.batch(c, h, l, c),
|
||||
),
|
||||
"Wedge": (
|
||||
lambda: ta.Wedge(),
|
||||
lambda ind, h, l, c, v: ind.batch(c, h, l, c),
|
||||
),
|
||||
"Triangle": (
|
||||
lambda: ta.Triangle(),
|
||||
lambda ind, h, l, c, v: ind.batch(c, h, l, c),
|
||||
),
|
||||
"HeadAndShoulders": (
|
||||
lambda: ta.HeadAndShoulders(),
|
||||
lambda ind, h, l, c, v: ind.batch(c, h, l, c),
|
||||
),
|
||||
"TripleTopBottom": (
|
||||
lambda: ta.TripleTopBottom(),
|
||||
lambda ind, h, l, c, v: ind.batch(c, h, l, c),
|
||||
),
|
||||
"DoubleTopBottom": (
|
||||
lambda: ta.DoubleTopBottom(),
|
||||
lambda ind, h, l, c, v: ind.batch(c, h, l, c),
|
||||
),
|
||||
"MIDPRICE": (lambda: ta.MIDPRICE(14), lambda ind, h, l, c, v: ind.batch(h, l, c)),
|
||||
"AVGPRICE": (lambda: ta.AVGPRICE(), lambda ind, h, l, c, v: ind.batch(c, h, l, c)),
|
||||
"DX": (lambda: ta.DX(14), lambda ind, h, l, c, v: ind.batch(h, l, c)),
|
||||
@@ -2354,6 +2418,166 @@ def test_granger_causality_reference():
|
||||
assert t.update(1.0, 1.0) is None
|
||||
assert t.update(2.0, 1.5) is None
|
||||
|
||||
|
||||
def test_double_top_bottom_reference():
|
||||
t = ta.DoubleTopBottom()
|
||||
assert t.update((119.88, 120.0, 119.88, 119.88, 1.0, 0)) == pytest.approx(0.0)
|
||||
assert t.update((100.0, 118.8, 100.0, 100.0, 1.0, 1)) == pytest.approx(0.0)
|
||||
assert t.update((101.0, 120.0, 101.0, 101.0, 1.0, 2)) == pytest.approx(0.0)
|
||||
assert t.update((108.0, 118.8, 108.0, 108.0, 1.0, 3)) == pytest.approx(-1.0)
|
||||
|
||||
|
||||
def test_triple_top_bottom_reference():
|
||||
t = ta.TripleTopBottom()
|
||||
assert t.update((119.88, 120.0, 119.88, 119.88, 1.0, 0)) == pytest.approx(0.0)
|
||||
assert t.update((100.0, 118.8, 100.0, 100.0, 1.0, 1)) == pytest.approx(0.0)
|
||||
assert t.update((101.0, 121.0, 101.0, 101.0, 1.0, 2)) == pytest.approx(0.0)
|
||||
assert t.update((99.0, 119.79, 99.0, 99.0, 1.0, 3)) == pytest.approx(0.0)
|
||||
assert t.update((99.99, 119.0, 99.99, 99.99, 1.0, 4)) == pytest.approx(0.0)
|
||||
assert t.update((107.1, 117.81, 107.1, 107.1, 1.0, 5)) == pytest.approx(-1.0)
|
||||
|
||||
|
||||
def test_head_and_shoulders_reference():
|
||||
t = ta.HeadAndShoulders()
|
||||
assert t.update((99.9, 100.0, 99.9, 99.9, 1.0, 0)) == pytest.approx(0.0)
|
||||
assert t.update((90.0, 99.0, 90.0, 90.0, 1.0, 1)) == pytest.approx(0.0)
|
||||
assert t.update((90.9, 120.0, 90.9, 90.9, 1.0, 2)) == pytest.approx(0.0)
|
||||
assert t.update((92.0, 118.8, 92.0, 92.0, 1.0, 3)) == pytest.approx(0.0)
|
||||
assert t.update((92.92, 101.0, 92.92, 92.92, 1.0, 4)) == pytest.approx(0.0)
|
||||
assert t.update((90.9, 99.99, 90.9, 90.9, 1.0, 5)) == pytest.approx(-1.0)
|
||||
|
||||
|
||||
def test_triangle_reference():
|
||||
t = ta.Triangle()
|
||||
assert t.update((129.87, 130.0, 129.87, 129.87, 1.0, 0)) == pytest.approx(0.0)
|
||||
assert t.update((100.0, 128.7, 100.0, 100.0, 1.0, 1)) == pytest.approx(0.0)
|
||||
assert t.update((101.0, 120.0, 101.0, 101.0, 1.0, 2)) == pytest.approx(0.0)
|
||||
assert t.update((110.0, 118.8, 110.0, 110.0, 1.0, 3)) == pytest.approx(0.0)
|
||||
assert t.update((111.1, 120.0, 111.1, 111.1, 1.0, 4)) == pytest.approx(1.0)
|
||||
assert t.update((108.0, 118.8, 108.0, 108.0, 1.0, 5)) == pytest.approx(1.0)
|
||||
|
||||
|
||||
def test_wedge_reference():
|
||||
t = ta.Wedge()
|
||||
assert t.update((109.89, 110.0, 109.89, 109.89, 1.0, 0)) == pytest.approx(0.0)
|
||||
assert t.update((90.0, 108.9, 90.0, 90.0, 1.0, 1)) == pytest.approx(0.0)
|
||||
assert t.update((90.9, 100.0, 90.9, 90.9, 1.0, 2)) == pytest.approx(0.0)
|
||||
assert t.update((94.0, 99.0, 94.0, 94.0, 1.0, 3)) == pytest.approx(0.0)
|
||||
assert t.update((94.94, 103.0, 94.94, 94.94, 1.0, 4)) == pytest.approx(0.0)
|
||||
assert t.update((92.7, 101.97, 92.7, 92.7, 1.0, 5)) == pytest.approx(-1.0)
|
||||
|
||||
|
||||
def test_flag_pennant_reference():
|
||||
t = ta.FlagPennant()
|
||||
assert t.update((149.85, 150.0, 149.85, 149.85, 1.0, 0)) == pytest.approx(0.0)
|
||||
assert t.update((100.0, 148.5, 100.0, 100.0, 1.0, 1)) == pytest.approx(0.0)
|
||||
assert t.update((101.0, 140.0, 101.0, 101.0, 1.0, 2)) == pytest.approx(0.0)
|
||||
assert t.update((130.0, 138.6, 130.0, 130.0, 1.0, 3)) == pytest.approx(0.0)
|
||||
assert t.update((131.3, 143.0, 131.3, 131.3, 1.0, 4)) == pytest.approx(1.0)
|
||||
|
||||
|
||||
def test_rectangle_range_reference():
|
||||
t = ta.RectangleRange()
|
||||
assert t.update((119.88, 120.0, 119.88, 119.88, 1.0, 0)) == pytest.approx(0.0)
|
||||
assert t.update((100.0, 118.8, 100.0, 100.0, 1.0, 1)) == pytest.approx(0.0)
|
||||
assert t.update((101.0, 121.0, 101.0, 101.0, 1.0, 2)) == pytest.approx(0.0)
|
||||
assert t.update((99.0, 119.79, 99.0, 99.0, 1.0, 3)) == pytest.approx(0.0)
|
||||
assert t.update((99.99, 108.9, 99.99, 99.99, 1.0, 4)) == pytest.approx(1.0)
|
||||
|
||||
|
||||
def test_cup_and_handle_reference():
|
||||
t = ta.CupAndHandle()
|
||||
assert t.update((119.88, 120.0, 119.88, 119.88, 1.0, 0)) == pytest.approx(0.0)
|
||||
assert t.update((90.0, 118.8, 90.0, 90.0, 1.0, 1)) == pytest.approx(0.0)
|
||||
assert t.update((90.9, 121.0, 90.9, 90.9, 1.0, 2)) == pytest.approx(0.0)
|
||||
assert t.update((110.0, 119.79, 110.0, 110.0, 1.0, 3)) == pytest.approx(0.0)
|
||||
assert t.update((111.1, 121.0, 111.1, 111.1, 1.0, 4)) == pytest.approx(1.0)
|
||||
|
||||
|
||||
def test_abcd_reference():
|
||||
t = ta.Abcd()
|
||||
assert t.update((139.86, 140.0, 139.86, 139.86, 1.0, 0)) == pytest.approx(0.0)
|
||||
assert t.update((100.0, 138.6, 100.0, 100.0, 1.0, 1)) == pytest.approx(0.0)
|
||||
assert t.update((101.0, 124.7, 101.0, 101.0, 1.0, 2)) == pytest.approx(0.0)
|
||||
assert t.update((84.7, 123.453, 84.7, 84.7, 1.0, 3)) == pytest.approx(0.0)
|
||||
assert t.update((85.547, 93.17, 85.547, 85.547, 1.0, 4)) == pytest.approx(1.0)
|
||||
|
||||
|
||||
def test_gartley_reference():
|
||||
t = ta.Gartley()
|
||||
assert t.update((149.85, 150.0, 149.85, 149.85, 1.0, 0)) == pytest.approx(0.0)
|
||||
assert t.update((100.0, 148.5, 100.0, 100.0, 1.0, 1)) == pytest.approx(0.0)
|
||||
assert t.update((101.0, 140.0, 101.0, 101.0, 1.0, 2)) == pytest.approx(0.0)
|
||||
assert t.update((115.3, 138.6, 115.3, 115.3, 1.0, 3)) == pytest.approx(0.0)
|
||||
assert t.update((116.453, 127.65, 116.453, 116.453, 1.0, 4)) == pytest.approx(0.0)
|
||||
assert t.update((108.56, 126.3735, 108.56, 108.56, 1.0, 5)) == pytest.approx(0.0)
|
||||
assert t.update((109.6456, 119.416, 109.6456, 109.6456, 1.0, 6)) == pytest.approx(1.0)
|
||||
|
||||
|
||||
def test_butterfly_reference():
|
||||
t = ta.Butterfly()
|
||||
assert t.update((149.85, 150.0, 149.85, 149.85, 1.0, 0)) == pytest.approx(0.0)
|
||||
assert t.update((100.0, 148.5, 100.0, 100.0, 1.0, 1)) == pytest.approx(0.0)
|
||||
assert t.update((101.0, 140.0, 101.0, 101.0, 1.0, 2)) == pytest.approx(0.0)
|
||||
assert t.update((108.6, 138.6, 108.6, 108.6, 1.0, 3)) == pytest.approx(0.0)
|
||||
assert t.update((109.686, 128.0, 109.686, 109.686, 1.0, 4)) == pytest.approx(0.0)
|
||||
assert t.update((79.8, 126.72, 79.8, 79.8, 1.0, 5)) == pytest.approx(0.0)
|
||||
assert t.update((80.598, 87.78, 80.598, 80.598, 1.0, 6)) == pytest.approx(1.0)
|
||||
|
||||
|
||||
def test_bat_reference():
|
||||
t = ta.Bat()
|
||||
assert t.update((149.85, 150.0, 149.85, 149.85, 1.0, 0)) == pytest.approx(0.0)
|
||||
assert t.update((100.0, 148.5, 100.0, 100.0, 1.0, 1)) == pytest.approx(0.0)
|
||||
assert t.update((101.0, 140.0, 101.0, 101.0, 1.0, 2)) == pytest.approx(0.0)
|
||||
assert t.update((122.0, 138.6, 122.0, 122.0, 1.0, 3)) == pytest.approx(0.0)
|
||||
assert t.update((123.22, 137.0, 123.22, 123.22, 1.0, 4)) == pytest.approx(0.0)
|
||||
assert t.update((104.56, 135.63, 104.56, 104.56, 1.0, 5)) == pytest.approx(0.0)
|
||||
assert t.update((105.6056, 115.016, 105.6056, 105.6056, 1.0, 6)) == pytest.approx(1.0)
|
||||
|
||||
|
||||
def test_crab_reference():
|
||||
t = ta.Crab()
|
||||
assert t.update((149.85, 150.0, 149.85, 149.85, 1.0, 0)) == pytest.approx(0.0)
|
||||
assert t.update((100.0, 148.5, 100.0, 100.0, 1.0, 1)) == pytest.approx(0.0)
|
||||
assert t.update((101.0, 140.0, 101.0, 101.0, 1.0, 2)) == pytest.approx(0.0)
|
||||
assert t.update((120.0, 138.6, 120.0, 120.0, 1.0, 3)) == pytest.approx(0.0)
|
||||
assert t.update((121.2, 137.5, 121.2, 121.2, 1.0, 4)) == pytest.approx(0.0)
|
||||
assert t.update((75.3, 136.125, 75.3, 75.3, 1.0, 5)) == pytest.approx(0.0)
|
||||
assert t.update((76.053, 82.83, 76.053, 76.053, 1.0, 6)) == pytest.approx(1.0)
|
||||
|
||||
|
||||
def test_shark_reference():
|
||||
t = ta.Shark()
|
||||
assert t.update((149.85, 150.0, 149.85, 149.85, 1.0, 0)) == pytest.approx(0.0)
|
||||
assert t.update((100.0, 148.5, 100.0, 100.0, 1.0, 1)) == pytest.approx(0.0)
|
||||
assert t.update((101.0, 140.0, 101.0, 101.0, 1.0, 2)) == pytest.approx(0.0)
|
||||
assert t.update((88.0, 138.6, 88.0, 88.0, 1.0, 3)) == pytest.approx(0.0)
|
||||
assert t.update((88.88, 186.8, 88.88, 88.88, 1.0, 4)) == pytest.approx(0.0)
|
||||
assert t.update((100.0, 184.932, 100.0, 100.0, 1.0, 5)) == pytest.approx(0.0)
|
||||
assert t.update((101.0, 110.0, 101.0, 101.0, 1.0, 6)) == pytest.approx(1.0)
|
||||
|
||||
|
||||
def test_cypher_reference():
|
||||
t = ta.Cypher()
|
||||
assert t.update((149.85, 150.0, 149.85, 149.85, 1.0, 0)) == pytest.approx(0.0)
|
||||
assert t.update((100.0, 148.5, 100.0, 100.0, 1.0, 1)) == pytest.approx(0.0)
|
||||
assert t.update((101.0, 140.0, 101.0, 101.0, 1.0, 2)) == pytest.approx(0.0)
|
||||
assert t.update((120.0, 138.6, 120.0, 120.0, 1.0, 3)) == pytest.approx(0.0)
|
||||
assert t.update((121.2, 168.0, 121.2, 121.2, 1.0, 4)) == pytest.approx(0.0)
|
||||
assert t.update((114.55, 166.32, 114.55, 114.55, 1.0, 5)) == pytest.approx(0.0)
|
||||
assert t.update((115.6955, 126.005, 115.6955, 115.6955, 1.0, 6)) == pytest.approx(1.0)
|
||||
|
||||
|
||||
def test_three_drives_reference():
|
||||
t = ta.ThreeDrives()
|
||||
assert t.update((119.88, 120.0, 119.88, 119.88, 1.0, 0)) == pytest.approx(0.0)
|
||||
assert t.update((100.0, 118.8, 100.0, 100.0, 1.0, 1)) == pytest.approx(0.0)
|
||||
assert t.update((101.0, 128.0, 101.0, 101.0, 1.0, 2)) == pytest.approx(0.0)
|
||||
assert t.update((108.0, 126.72, 108.0, 108.0, 1.0, 3)) == pytest.approx(0.0)
|
||||
assert t.update((109.08, 136.0, 109.08, 109.08, 1.0, 4)) == pytest.approx(0.0)
|
||||
assert t.update((122.4, 134.64, 122.4, 122.4, 1.0, 5)) == pytest.approx(-1.0)
|
||||
|
||||
# --- Lifecycle ------------------------------------------------------------
|
||||
|
||||
|
||||
|
||||
@@ -7354,6 +7354,22 @@ wasm_candle_pattern!(
|
||||
wc::ConcealingBabySwallow,
|
||||
ConcealingBabySwallow
|
||||
);
|
||||
wasm_candle_pattern!(WasmDoubleTopBottom, wc::DoubleTopBottom, DoubleTopBottom);
|
||||
wasm_candle_pattern!(WasmTripleTopBottom, wc::TripleTopBottom, TripleTopBottom);
|
||||
wasm_candle_pattern!(WasmHeadAndShoulders, wc::HeadAndShoulders, HeadAndShoulders);
|
||||
wasm_candle_pattern!(WasmTriangle, wc::Triangle, Triangle);
|
||||
wasm_candle_pattern!(WasmWedge, wc::Wedge, Wedge);
|
||||
wasm_candle_pattern!(WasmFlagPennant, wc::FlagPennant, FlagPennant);
|
||||
wasm_candle_pattern!(WasmRectangleRange, wc::RectangleRange, RectangleRange);
|
||||
wasm_candle_pattern!(WasmCupAndHandle, wc::CupAndHandle, CupAndHandle);
|
||||
wasm_candle_pattern!(WasmAbcd, wc::Abcd, Abcd);
|
||||
wasm_candle_pattern!(WasmGartley, wc::Gartley, Gartley);
|
||||
wasm_candle_pattern!(WasmButterfly, wc::Butterfly, Butterfly);
|
||||
wasm_candle_pattern!(WasmBat, wc::Bat, Bat);
|
||||
wasm_candle_pattern!(WasmCrab, wc::Crab, Crab);
|
||||
wasm_candle_pattern!(WasmShark, wc::Shark, Shark);
|
||||
wasm_candle_pattern!(WasmCypher, wc::Cypher, Cypher);
|
||||
wasm_candle_pattern!(WasmThreeDrives, wc::ThreeDrives, ThreeDrives);
|
||||
|
||||
// ============================== Microstructure: Order Book ==============================
|
||||
//
|
||||
|
||||
@@ -0,0 +1,154 @@
|
||||
//! AB=CD harmonic pattern.
|
||||
|
||||
use crate::indicators::pattern_swing::{approx_equal, ratios_in, SwingTracker, SWING_THRESHOLD};
|
||||
use crate::ohlcv::Candle;
|
||||
use crate::traits::Indicator;
|
||||
|
||||
/// AB=CD — the simplest four-point harmonic pattern: an A→B leg, a B→C
|
||||
/// retracement, and a C→D leg that mirrors A→B in length:
|
||||
///
|
||||
/// ```text
|
||||
/// BC / AB ∈ [0.382, 0.886] (C retraces AB)
|
||||
/// CD / BC ∈ [1.13, 2.618] (D extends BC)
|
||||
/// AB ≈ CD (within 10%) (the two legs are equal — the defining symmetry)
|
||||
/// ```
|
||||
///
|
||||
/// Read from the last four confirmed pivots `A-B-C-D`. Output is `+1.0`
|
||||
/// (bullish, D a swing low), `-1.0` (bearish, D a swing high), or `0.0`; never
|
||||
/// `None`. See `crates/wickra-core/src/indicators/abcd.rs`.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Abcd {
|
||||
swing: SwingTracker,
|
||||
has_emitted: bool,
|
||||
}
|
||||
|
||||
impl Abcd {
|
||||
/// Construct a new AB=CD detector.
|
||||
pub const fn new() -> Self {
|
||||
Self {
|
||||
swing: SwingTracker::new(SWING_THRESHOLD, 4),
|
||||
has_emitted: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Abcd {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl Indicator for Abcd {
|
||||
type Input = Candle;
|
||||
type Output = f64;
|
||||
|
||||
fn update(&mut self, candle: Candle) -> Option<f64> {
|
||||
self.has_emitted = true;
|
||||
if !self.swing.update(candle) {
|
||||
return Some(0.0);
|
||||
}
|
||||
let pivots = self.swing.pivots();
|
||||
if pivots.len() < 4 {
|
||||
return Some(0.0);
|
||||
}
|
||||
let len = pivots.len();
|
||||
let pa = pivots[len - 4];
|
||||
let pb = pivots[len - 3];
|
||||
let pc = pivots[len - 2];
|
||||
let pd = pivots[len - 1];
|
||||
let ab = (pb.price - pa.price).abs();
|
||||
let bc = (pc.price - pb.price).abs();
|
||||
let cd = (pd.price - pc.price).abs();
|
||||
let ratios_ok = ratios_in(&[(bc / ab, 0.382, 0.886), (cd / bc, 1.13, 2.618)]);
|
||||
let legs_equal = approx_equal(ab, cd, 0.10);
|
||||
if ratios_ok && legs_equal {
|
||||
return Some(if pd.direction < 0.0 { 1.0 } else { -1.0 });
|
||||
}
|
||||
Some(0.0)
|
||||
}
|
||||
|
||||
fn reset(&mut self) {
|
||||
self.swing.reset();
|
||||
self.has_emitted = false;
|
||||
}
|
||||
|
||||
fn warmup_period(&self) -> usize {
|
||||
5
|
||||
}
|
||||
|
||||
fn is_ready(&self) -> bool {
|
||||
self.has_emitted
|
||||
}
|
||||
|
||||
fn name(&self) -> &'static str {
|
||||
"Abcd"
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::indicators::pattern_swing::candles_for_pivots;
|
||||
use crate::traits::BatchExt;
|
||||
|
||||
fn run(pivots: &[f64]) -> Vec<f64> {
|
||||
let mut indicator = Abcd::new();
|
||||
candles_for_pivots(pivots)
|
||||
.into_iter()
|
||||
.map(|c| indicator.update(c).unwrap())
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn accessors_and_metadata() {
|
||||
let indicator = Abcd::new();
|
||||
assert_eq!(indicator.name(), "Abcd");
|
||||
assert_eq!(indicator.warmup_period(), 5);
|
||||
assert!(!indicator.is_ready());
|
||||
assert!(!Abcd::default().is_ready());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bullish_abcd_is_plus_one() {
|
||||
// AB = 40 down, BC = 24.7 up (0.618), CD = 40 down → AB = CD.
|
||||
let out = run(&[140.0, 100.0, 124.7, 84.7]);
|
||||
assert_eq!(*out.last().unwrap(), 1.0);
|
||||
assert!(out[..out.len() - 1].iter().all(|&x| x == 0.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bearish_abcd_is_minus_one() {
|
||||
let out = run(&[150.0, 100.0, 140.0, 115.3, 155.3]);
|
||||
assert_eq!(*out.last().unwrap(), -1.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unequal_legs_do_not_trigger() {
|
||||
// CD (82) far longer than AB (40) → not an AB=CD.
|
||||
let out = run(&[150.0, 100.0, 140.0, 118.0, 200.0]);
|
||||
assert_eq!(*out.last().unwrap(), 0.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reset_clears_state() {
|
||||
let mut indicator = Abcd::new();
|
||||
for c in candles_for_pivots(&[140.0, 100.0, 124.7]) {
|
||||
let _ = indicator.update(c);
|
||||
}
|
||||
indicator.reset();
|
||||
assert!(!indicator.is_ready());
|
||||
let c = Candle::new(99.5, 100.0, 99.5, 99.5, 1.0, 0).unwrap();
|
||||
assert_eq!(indicator.update(c), Some(0.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn batch_equals_streaming() {
|
||||
let candles = candles_for_pivots(&[140.0, 100.0, 124.7, 84.7]);
|
||||
let mut a = Abcd::new();
|
||||
let mut b = Abcd::new();
|
||||
assert_eq!(
|
||||
a.batch(&candles),
|
||||
candles.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
//! Bat harmonic pattern.
|
||||
|
||||
use crate::indicators::pattern_swing::{ratios_in, xabcd, SwingTracker, SWING_THRESHOLD};
|
||||
use crate::ohlcv::Candle;
|
||||
use crate::traits::Indicator;
|
||||
|
||||
/// Bat — a 5-point (X-A-B-C-D) harmonic pattern with a shallow B and a deep
|
||||
/// `0.886` D completion:
|
||||
///
|
||||
/// ```text
|
||||
/// AB / XA ∈ [0.382, 0.50]
|
||||
/// BC / AB ∈ [0.382, 0.886]
|
||||
/// CD / BC ∈ [1.618, 2.618]
|
||||
/// AD / XA ∈ [0.84, 0.93] (≈ 0.886 — the defining D completion)
|
||||
/// ```
|
||||
///
|
||||
/// Output is `+1.0` (bullish, D a swing low), `-1.0` (bearish, D a swing high),
|
||||
/// or `0.0`; never `None`. See `crates/wickra-core/src/indicators/bat.rs`.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Bat {
|
||||
swing: SwingTracker,
|
||||
has_emitted: bool,
|
||||
}
|
||||
|
||||
impl Bat {
|
||||
/// Construct a new Bat detector.
|
||||
pub const fn new() -> Self {
|
||||
Self {
|
||||
swing: SwingTracker::new(SWING_THRESHOLD, 5),
|
||||
has_emitted: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Bat {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl Indicator for Bat {
|
||||
type Input = Candle;
|
||||
type Output = f64;
|
||||
|
||||
fn update(&mut self, candle: Candle) -> Option<f64> {
|
||||
self.has_emitted = true;
|
||||
if !self.swing.update(candle) {
|
||||
return Some(0.0);
|
||||
}
|
||||
let pivots = self.swing.pivots();
|
||||
if pivots.len() < 5 {
|
||||
return Some(0.0);
|
||||
}
|
||||
let p = xabcd(pivots);
|
||||
let xa = (p.a - p.x).abs();
|
||||
let ab = (p.b - p.a).abs();
|
||||
let bc = (p.c - p.b).abs();
|
||||
let cd = (p.d - p.c).abs();
|
||||
let ad = (p.d - p.a).abs();
|
||||
let matched = ratios_in(&[
|
||||
(ab / xa, 0.382, 0.50),
|
||||
(bc / ab, 0.382, 0.886),
|
||||
(cd / bc, 1.618, 2.618),
|
||||
(ad / xa, 0.84, 0.93),
|
||||
]);
|
||||
if matched {
|
||||
return Some(if p.bullish { 1.0 } else { -1.0 });
|
||||
}
|
||||
Some(0.0)
|
||||
}
|
||||
|
||||
fn reset(&mut self) {
|
||||
self.swing.reset();
|
||||
self.has_emitted = false;
|
||||
}
|
||||
|
||||
fn warmup_period(&self) -> usize {
|
||||
6
|
||||
}
|
||||
|
||||
fn is_ready(&self) -> bool {
|
||||
self.has_emitted
|
||||
}
|
||||
|
||||
fn name(&self) -> &'static str {
|
||||
"Bat"
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::indicators::pattern_swing::candles_for_pivots;
|
||||
use crate::traits::BatchExt;
|
||||
|
||||
fn run(pivots: &[f64]) -> Vec<f64> {
|
||||
let mut indicator = Bat::new();
|
||||
candles_for_pivots(pivots)
|
||||
.into_iter()
|
||||
.map(|c| indicator.update(c).unwrap())
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn accessors_and_metadata() {
|
||||
let indicator = Bat::new();
|
||||
assert_eq!(indicator.name(), "Bat");
|
||||
assert_eq!(indicator.warmup_period(), 6);
|
||||
assert!(!indicator.is_ready());
|
||||
assert!(!Bat::default().is_ready());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bullish_bat_is_plus_one() {
|
||||
let out = run(&[150.0, 100.0, 140.0, 122.0, 137.0, 104.56]);
|
||||
assert_eq!(*out.last().unwrap(), 1.0);
|
||||
assert!(out[..out.len() - 1].iter().all(|&x| x == 0.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bearish_bat_is_minus_one() {
|
||||
let out = run(&[150.0, 110.0, 128.0, 113.0, 145.44]);
|
||||
assert_eq!(*out.last().unwrap(), -1.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn out_of_ratio_does_not_trigger() {
|
||||
let out = run(&[150.0, 100.0, 140.0, 110.0, 135.0, 105.0]);
|
||||
assert_eq!(*out.last().unwrap(), 0.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reset_clears_state() {
|
||||
let mut indicator = Bat::new();
|
||||
for c in candles_for_pivots(&[150.0, 100.0, 140.0]) {
|
||||
let _ = indicator.update(c);
|
||||
}
|
||||
indicator.reset();
|
||||
assert!(!indicator.is_ready());
|
||||
let c = Candle::new(99.5, 100.0, 99.5, 99.5, 1.0, 0).unwrap();
|
||||
assert_eq!(indicator.update(c), Some(0.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn batch_equals_streaming() {
|
||||
let candles = candles_for_pivots(&[150.0, 100.0, 140.0, 122.0, 137.0, 104.56]);
|
||||
let mut a = Bat::new();
|
||||
let mut b = Bat::new();
|
||||
assert_eq!(
|
||||
a.batch(&candles),
|
||||
candles.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
//! Butterfly harmonic pattern.
|
||||
|
||||
use crate::indicators::pattern_swing::{ratios_in, xabcd, SwingTracker, SWING_THRESHOLD};
|
||||
use crate::ohlcv::Candle;
|
||||
use crate::traits::Indicator;
|
||||
|
||||
/// Butterfly — a 5-point (X-A-B-C-D) harmonic pattern with a `0.786` B and an
|
||||
/// **extended** D that overshoots X:
|
||||
///
|
||||
/// ```text
|
||||
/// AB / XA ∈ [0.74, 0.84] (≈ 0.786)
|
||||
/// BC / AB ∈ [0.382, 0.886]
|
||||
/// CD / BC ∈ [1.618, 2.618]
|
||||
/// AD / XA ∈ [1.27, 1.618] (the defining extended D completion)
|
||||
/// ```
|
||||
///
|
||||
/// Output is `+1.0` (bullish, D a swing low), `-1.0` (bearish, D a swing high),
|
||||
/// or `0.0`; never `None`. See `crates/wickra-core/src/indicators/butterfly.rs`.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Butterfly {
|
||||
swing: SwingTracker,
|
||||
has_emitted: bool,
|
||||
}
|
||||
|
||||
impl Butterfly {
|
||||
/// Construct a new Butterfly detector.
|
||||
pub const fn new() -> Self {
|
||||
Self {
|
||||
swing: SwingTracker::new(SWING_THRESHOLD, 5),
|
||||
has_emitted: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Butterfly {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl Indicator for Butterfly {
|
||||
type Input = Candle;
|
||||
type Output = f64;
|
||||
|
||||
fn update(&mut self, candle: Candle) -> Option<f64> {
|
||||
self.has_emitted = true;
|
||||
if !self.swing.update(candle) {
|
||||
return Some(0.0);
|
||||
}
|
||||
let pivots = self.swing.pivots();
|
||||
if pivots.len() < 5 {
|
||||
return Some(0.0);
|
||||
}
|
||||
let p = xabcd(pivots);
|
||||
let xa = (p.a - p.x).abs();
|
||||
let ab = (p.b - p.a).abs();
|
||||
let bc = (p.c - p.b).abs();
|
||||
let cd = (p.d - p.c).abs();
|
||||
let ad = (p.d - p.a).abs();
|
||||
let matched = ratios_in(&[
|
||||
(ab / xa, 0.74, 0.84),
|
||||
(bc / ab, 0.382, 0.886),
|
||||
(cd / bc, 1.618, 2.618),
|
||||
(ad / xa, 1.27, 1.618),
|
||||
]);
|
||||
if matched {
|
||||
return Some(if p.bullish { 1.0 } else { -1.0 });
|
||||
}
|
||||
Some(0.0)
|
||||
}
|
||||
|
||||
fn reset(&mut self) {
|
||||
self.swing.reset();
|
||||
self.has_emitted = false;
|
||||
}
|
||||
|
||||
fn warmup_period(&self) -> usize {
|
||||
6
|
||||
}
|
||||
|
||||
fn is_ready(&self) -> bool {
|
||||
self.has_emitted
|
||||
}
|
||||
|
||||
fn name(&self) -> &'static str {
|
||||
"Butterfly"
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::indicators::pattern_swing::candles_for_pivots;
|
||||
use crate::traits::BatchExt;
|
||||
|
||||
fn run(pivots: &[f64]) -> Vec<f64> {
|
||||
let mut indicator = Butterfly::new();
|
||||
candles_for_pivots(pivots)
|
||||
.into_iter()
|
||||
.map(|c| indicator.update(c).unwrap())
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn accessors_and_metadata() {
|
||||
let indicator = Butterfly::new();
|
||||
assert_eq!(indicator.name(), "Butterfly");
|
||||
assert_eq!(indicator.warmup_period(), 6);
|
||||
assert!(!indicator.is_ready());
|
||||
assert!(!Butterfly::default().is_ready());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bullish_butterfly_is_plus_one() {
|
||||
let out = run(&[150.0, 100.0, 140.0, 108.6, 128.0, 79.8]);
|
||||
assert_eq!(*out.last().unwrap(), 1.0);
|
||||
assert!(out[..out.len() - 1].iter().all(|&x| x == 0.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bearish_butterfly_is_minus_one() {
|
||||
let out = run(&[150.0, 110.0, 141.4, 121.4, 170.2]);
|
||||
assert_eq!(*out.last().unwrap(), -1.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn out_of_ratio_does_not_trigger() {
|
||||
let out = run(&[150.0, 100.0, 140.0, 110.0, 135.0, 105.0]);
|
||||
assert_eq!(*out.last().unwrap(), 0.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reset_clears_state() {
|
||||
let mut indicator = Butterfly::new();
|
||||
for c in candles_for_pivots(&[150.0, 100.0, 140.0]) {
|
||||
let _ = indicator.update(c);
|
||||
}
|
||||
indicator.reset();
|
||||
assert!(!indicator.is_ready());
|
||||
let c = Candle::new(99.5, 100.0, 99.5, 99.5, 1.0, 0).unwrap();
|
||||
assert_eq!(indicator.update(c), Some(0.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn batch_equals_streaming() {
|
||||
let candles = candles_for_pivots(&[150.0, 100.0, 140.0, 108.6, 128.0, 79.8]);
|
||||
let mut a = Butterfly::new();
|
||||
let mut b = Butterfly::new();
|
||||
assert_eq!(
|
||||
a.batch(&candles),
|
||||
candles.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
//! Crab harmonic pattern.
|
||||
|
||||
use crate::indicators::pattern_swing::{ratios_in, xabcd, SwingTracker, SWING_THRESHOLD};
|
||||
use crate::ohlcv::Candle;
|
||||
use crate::traits::Indicator;
|
||||
|
||||
/// Crab — a 5-point (X-A-B-C-D) harmonic pattern with the deepest D completion
|
||||
/// of the family, an `1.618` extension of XA:
|
||||
///
|
||||
/// ```text
|
||||
/// AB / XA ∈ [0.382, 0.618]
|
||||
/// BC / AB ∈ [0.382, 0.886]
|
||||
/// CD / BC ∈ [2.24, 3.618] (a very long terminal leg)
|
||||
/// AD / XA ∈ [1.55, 1.65] (≈ 1.618 — the defining D completion)
|
||||
/// ```
|
||||
///
|
||||
/// Output is `+1.0` (bullish, D a swing low), `-1.0` (bearish, D a swing high),
|
||||
/// or `0.0`; never `None`. See `crates/wickra-core/src/indicators/crab.rs`.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Crab {
|
||||
swing: SwingTracker,
|
||||
has_emitted: bool,
|
||||
}
|
||||
|
||||
impl Crab {
|
||||
/// Construct a new Crab detector.
|
||||
pub const fn new() -> Self {
|
||||
Self {
|
||||
swing: SwingTracker::new(SWING_THRESHOLD, 5),
|
||||
has_emitted: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Crab {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl Indicator for Crab {
|
||||
type Input = Candle;
|
||||
type Output = f64;
|
||||
|
||||
fn update(&mut self, candle: Candle) -> Option<f64> {
|
||||
self.has_emitted = true;
|
||||
if !self.swing.update(candle) {
|
||||
return Some(0.0);
|
||||
}
|
||||
let pivots = self.swing.pivots();
|
||||
if pivots.len() < 5 {
|
||||
return Some(0.0);
|
||||
}
|
||||
let p = xabcd(pivots);
|
||||
let xa = (p.a - p.x).abs();
|
||||
let ab = (p.b - p.a).abs();
|
||||
let bc = (p.c - p.b).abs();
|
||||
let cd = (p.d - p.c).abs();
|
||||
let ad = (p.d - p.a).abs();
|
||||
let matched = ratios_in(&[
|
||||
(ab / xa, 0.382, 0.618),
|
||||
(bc / ab, 0.382, 0.886),
|
||||
(cd / bc, 2.24, 3.618),
|
||||
(ad / xa, 1.55, 1.65),
|
||||
]);
|
||||
if matched {
|
||||
return Some(if p.bullish { 1.0 } else { -1.0 });
|
||||
}
|
||||
Some(0.0)
|
||||
}
|
||||
|
||||
fn reset(&mut self) {
|
||||
self.swing.reset();
|
||||
self.has_emitted = false;
|
||||
}
|
||||
|
||||
fn warmup_period(&self) -> usize {
|
||||
6
|
||||
}
|
||||
|
||||
fn is_ready(&self) -> bool {
|
||||
self.has_emitted
|
||||
}
|
||||
|
||||
fn name(&self) -> &'static str {
|
||||
"Crab"
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::indicators::pattern_swing::candles_for_pivots;
|
||||
use crate::traits::BatchExt;
|
||||
|
||||
fn run(pivots: &[f64]) -> Vec<f64> {
|
||||
let mut indicator = Crab::new();
|
||||
candles_for_pivots(pivots)
|
||||
.into_iter()
|
||||
.map(|c| indicator.update(c).unwrap())
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn accessors_and_metadata() {
|
||||
let indicator = Crab::new();
|
||||
assert_eq!(indicator.name(), "Crab");
|
||||
assert_eq!(indicator.warmup_period(), 6);
|
||||
assert!(!indicator.is_ready());
|
||||
assert!(!Crab::default().is_ready());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bullish_crab_is_plus_one() {
|
||||
let out = run(&[150.0, 100.0, 140.0, 120.0, 137.5, 75.3]);
|
||||
assert_eq!(*out.last().unwrap(), 1.0);
|
||||
assert!(out[..out.len() - 1].iter().all(|&x| x == 0.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bearish_crab_is_minus_one() {
|
||||
let out = run(&[150.0, 110.0, 130.0, 112.5, 174.7]);
|
||||
assert_eq!(*out.last().unwrap(), -1.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn out_of_ratio_does_not_trigger() {
|
||||
let out = run(&[150.0, 100.0, 140.0, 110.0, 135.0, 105.0]);
|
||||
assert_eq!(*out.last().unwrap(), 0.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reset_clears_state() {
|
||||
let mut indicator = Crab::new();
|
||||
for c in candles_for_pivots(&[150.0, 100.0, 140.0]) {
|
||||
let _ = indicator.update(c);
|
||||
}
|
||||
indicator.reset();
|
||||
assert!(!indicator.is_ready());
|
||||
let c = Candle::new(99.5, 100.0, 99.5, 99.5, 1.0, 0).unwrap();
|
||||
assert_eq!(indicator.update(c), Some(0.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn batch_equals_streaming() {
|
||||
let candles = candles_for_pivots(&[150.0, 100.0, 140.0, 120.0, 137.5, 75.3]);
|
||||
let mut a = Crab::new();
|
||||
let mut b = Crab::new();
|
||||
assert_eq!(
|
||||
a.batch(&candles),
|
||||
candles.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,176 @@
|
||||
//! Cup-and-Handle (and Inverse) continuation chart pattern.
|
||||
|
||||
use crate::indicators::pattern_swing::{
|
||||
approx_equal, SwingTracker, LEVEL_TOLERANCE, SWING_THRESHOLD,
|
||||
};
|
||||
use crate::ohlcv::Candle;
|
||||
use crate::traits::Indicator;
|
||||
|
||||
/// Cup-and-Handle / Inverse — a rounded base (the cup) followed by a shallow
|
||||
/// pullback (the handle) near the rim, then a breakout in the cup's direction.
|
||||
///
|
||||
/// Built on confirmed swing pivots ([`SWING_THRESHOLD`] = 5%) and read from the
|
||||
/// last four pivots:
|
||||
///
|
||||
/// ```text
|
||||
/// cup-and-handle (bullish, +1): Rim(high) , Cup(low) , Rim(high) , Handle(low)
|
||||
/// the two rims match (±3%) ; the handle low sits ABOVE the cup low (a shallow
|
||||
/// pullback) and below the right rim
|
||||
///
|
||||
/// inverse (bearish, -1): Rim(low) , Cap(high) , Rim(low) , Handle(high)
|
||||
/// the two rims match ; the handle high sits BELOW the cap high and above the
|
||||
/// right rim
|
||||
/// ```
|
||||
///
|
||||
/// The shallow handle (closer to the rim than the cup extreme) is what
|
||||
/// distinguishes a cup-and-handle from a plain double bottom/top. Output is
|
||||
/// `+1.0` / `-1.0` / `0.0`; never `None`.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct CupAndHandle {
|
||||
swing: SwingTracker,
|
||||
has_emitted: bool,
|
||||
}
|
||||
|
||||
impl CupAndHandle {
|
||||
/// Construct a new Cup-and-Handle detector.
|
||||
pub const fn new() -> Self {
|
||||
Self {
|
||||
swing: SwingTracker::new(SWING_THRESHOLD, 4),
|
||||
has_emitted: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for CupAndHandle {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl Indicator for CupAndHandle {
|
||||
type Input = Candle;
|
||||
type Output = f64;
|
||||
|
||||
fn update(&mut self, candle: Candle) -> Option<f64> {
|
||||
self.has_emitted = true;
|
||||
if !self.swing.update(candle) {
|
||||
return Some(0.0);
|
||||
}
|
||||
let pivots = self.swing.pivots();
|
||||
if pivots.len() < 4 {
|
||||
return Some(0.0);
|
||||
}
|
||||
let n = pivots.len();
|
||||
let rim_left = pivots[n - 4];
|
||||
let extreme = pivots[n - 3];
|
||||
let rim_right = pivots[n - 2];
|
||||
let handle = pivots[n - 1];
|
||||
let rims_match = approx_equal(rim_left.price, rim_right.price, LEVEL_TOLERANCE);
|
||||
|
||||
if handle.direction < 0.0 {
|
||||
// Bullish cup-and-handle: rims are highs, cup is the low between them,
|
||||
// handle is a shallow low above the cup but below the right rim.
|
||||
if rims_match && handle.price > extreme.price && handle.price < rim_right.price {
|
||||
return Some(1.0);
|
||||
}
|
||||
} else if rims_match && handle.price < extreme.price && handle.price > rim_right.price {
|
||||
// Inverse: rims are lows, cap is the high, handle a shallow high.
|
||||
return Some(-1.0);
|
||||
}
|
||||
Some(0.0)
|
||||
}
|
||||
|
||||
fn reset(&mut self) {
|
||||
self.swing.reset();
|
||||
self.has_emitted = false;
|
||||
}
|
||||
|
||||
fn warmup_period(&self) -> usize {
|
||||
// Four confirmed pivots; the earliest confirmation of the fourth is bar 5.
|
||||
5
|
||||
}
|
||||
|
||||
fn is_ready(&self) -> bool {
|
||||
self.has_emitted
|
||||
}
|
||||
|
||||
fn name(&self) -> &'static str {
|
||||
"CupAndHandle"
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::indicators::pattern_swing::candles_for_pivots;
|
||||
use crate::traits::BatchExt;
|
||||
|
||||
fn run(pivots: &[f64]) -> Vec<f64> {
|
||||
let mut indicator = CupAndHandle::new();
|
||||
candles_for_pivots(pivots)
|
||||
.into_iter()
|
||||
.map(|c| indicator.update(c).unwrap())
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn accessors_and_metadata() {
|
||||
let indicator = CupAndHandle::new();
|
||||
assert_eq!(indicator.name(), "CupAndHandle");
|
||||
assert_eq!(indicator.warmup_period(), 5);
|
||||
assert!(!indicator.is_ready());
|
||||
assert!(!CupAndHandle::default().is_ready());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cup_and_handle_is_plus_one() {
|
||||
// Rims 120/121, cup 90 (deep), handle 110 (shallow, above the cup).
|
||||
let out = run(&[120.0, 90.0, 121.0, 110.0]);
|
||||
assert_eq!(*out.last().unwrap(), 1.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn inverse_cup_and_handle_is_minus_one() {
|
||||
// Lead high then rims 100/101, cap 130, handle 110 (below cap, above rim).
|
||||
let out = run(&[140.0, 100.0, 130.0, 101.0, 110.0]);
|
||||
assert_eq!(*out.last().unwrap(), -1.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deep_handle_is_not_cup_and_handle() {
|
||||
// Handle (85) below the cup low (90) → a double bottom, not cup-and-handle.
|
||||
let out = run(&[120.0, 90.0, 121.0, 85.0]);
|
||||
assert_eq!(*out.last().unwrap(), 0.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn inverse_with_mismatched_rims_does_not_trigger() {
|
||||
// Inverse shape (ends high) but the rims (100 / 90) diverge → enters the
|
||||
// inverse branch yet reports no pattern.
|
||||
let out = run(&[140.0, 100.0, 130.0, 90.0, 110.0]);
|
||||
assert_eq!(*out.last().unwrap(), 0.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reset_clears_state() {
|
||||
let mut indicator = CupAndHandle::new();
|
||||
for c in candles_for_pivots(&[120.0, 90.0, 121.0]) {
|
||||
let _ = indicator.update(c);
|
||||
}
|
||||
indicator.reset();
|
||||
assert!(!indicator.is_ready());
|
||||
let c = Candle::new(99.5, 100.0, 99.5, 99.5, 1.0, 0).unwrap();
|
||||
assert_eq!(indicator.update(c), Some(0.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn batch_equals_streaming() {
|
||||
let candles = candles_for_pivots(&[120.0, 90.0, 121.0, 110.0]);
|
||||
let mut a = CupAndHandle::new();
|
||||
let mut b = CupAndHandle::new();
|
||||
assert_eq!(
|
||||
a.batch(&candles),
|
||||
candles.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
//! Cypher harmonic pattern.
|
||||
|
||||
use crate::indicators::pattern_swing::{ratios_in, xabcd, SwingTracker, SWING_THRESHOLD};
|
||||
use crate::ohlcv::Candle;
|
||||
use crate::traits::Indicator;
|
||||
|
||||
/// Cypher — a 5-point (X-A-B-C-D) harmonic pattern whose C leg is measured
|
||||
/// against XA (not AB) and whose D retraces the XC leg by `0.786`:
|
||||
///
|
||||
/// ```text
|
||||
/// AB / XA ∈ [0.382, 0.618]
|
||||
/// BC / XA ∈ [1.13, 1.414] (C extends beyond A, measured on XA)
|
||||
/// CD / XC ∈ [0.74, 0.83] (≈ 0.786 retracement of XC — the D completion)
|
||||
/// ```
|
||||
///
|
||||
/// Output is `+1.0` (bullish, D a swing low), `-1.0` (bearish, D a swing high),
|
||||
/// or `0.0`; never `None`. See `crates/wickra-core/src/indicators/cypher.rs`.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Cypher {
|
||||
swing: SwingTracker,
|
||||
has_emitted: bool,
|
||||
}
|
||||
|
||||
impl Cypher {
|
||||
/// Construct a new Cypher detector.
|
||||
pub const fn new() -> Self {
|
||||
Self {
|
||||
swing: SwingTracker::new(SWING_THRESHOLD, 5),
|
||||
has_emitted: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Cypher {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl Indicator for Cypher {
|
||||
type Input = Candle;
|
||||
type Output = f64;
|
||||
|
||||
fn update(&mut self, candle: Candle) -> Option<f64> {
|
||||
self.has_emitted = true;
|
||||
if !self.swing.update(candle) {
|
||||
return Some(0.0);
|
||||
}
|
||||
let pivots = self.swing.pivots();
|
||||
if pivots.len() < 5 {
|
||||
return Some(0.0);
|
||||
}
|
||||
let p = xabcd(pivots);
|
||||
let xa = (p.a - p.x).abs();
|
||||
let ab = (p.b - p.a).abs();
|
||||
let bc = (p.c - p.b).abs();
|
||||
let xc = (p.c - p.x).abs();
|
||||
let cd = (p.d - p.c).abs();
|
||||
let matched = ratios_in(&[
|
||||
(ab / xa, 0.382, 0.618),
|
||||
(bc / xa, 1.13, 1.414),
|
||||
(cd / xc, 0.74, 0.83),
|
||||
]);
|
||||
if matched {
|
||||
return Some(if p.bullish { 1.0 } else { -1.0 });
|
||||
}
|
||||
Some(0.0)
|
||||
}
|
||||
|
||||
fn reset(&mut self) {
|
||||
self.swing.reset();
|
||||
self.has_emitted = false;
|
||||
}
|
||||
|
||||
fn warmup_period(&self) -> usize {
|
||||
6
|
||||
}
|
||||
|
||||
fn is_ready(&self) -> bool {
|
||||
self.has_emitted
|
||||
}
|
||||
|
||||
fn name(&self) -> &'static str {
|
||||
"Cypher"
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::indicators::pattern_swing::candles_for_pivots;
|
||||
use crate::traits::BatchExt;
|
||||
|
||||
fn run(pivots: &[f64]) -> Vec<f64> {
|
||||
let mut indicator = Cypher::new();
|
||||
candles_for_pivots(pivots)
|
||||
.into_iter()
|
||||
.map(|c| indicator.update(c).unwrap())
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn accessors_and_metadata() {
|
||||
let indicator = Cypher::new();
|
||||
assert_eq!(indicator.name(), "Cypher");
|
||||
assert_eq!(indicator.warmup_period(), 6);
|
||||
assert!(!indicator.is_ready());
|
||||
assert!(!Cypher::default().is_ready());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bullish_cypher_is_plus_one() {
|
||||
let out = run(&[150.0, 100.0, 140.0, 120.0, 168.0, 114.55]);
|
||||
assert_eq!(*out.last().unwrap(), 1.0);
|
||||
assert!(out[..out.len() - 1].iter().all(|&x| x == 0.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bearish_cypher_is_minus_one() {
|
||||
let out = run(&[150.0, 110.0, 130.0, 82.0, 135.45]);
|
||||
assert_eq!(*out.last().unwrap(), -1.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn out_of_ratio_does_not_trigger() {
|
||||
let out = run(&[150.0, 100.0, 140.0, 110.0, 135.0, 105.0]);
|
||||
assert_eq!(*out.last().unwrap(), 0.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reset_clears_state() {
|
||||
let mut indicator = Cypher::new();
|
||||
for c in candles_for_pivots(&[150.0, 100.0, 140.0]) {
|
||||
let _ = indicator.update(c);
|
||||
}
|
||||
indicator.reset();
|
||||
assert!(!indicator.is_ready());
|
||||
let c = Candle::new(99.5, 100.0, 99.5, 99.5, 1.0, 0).unwrap();
|
||||
assert_eq!(indicator.update(c), Some(0.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn batch_equals_streaming() {
|
||||
let candles = candles_for_pivots(&[150.0, 100.0, 140.0, 120.0, 168.0, 114.55]);
|
||||
let mut a = Cypher::new();
|
||||
let mut b = Cypher::new();
|
||||
assert_eq!(
|
||||
a.batch(&candles),
|
||||
candles.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,188 @@
|
||||
//! Double Top / Double Bottom reversal chart pattern.
|
||||
|
||||
use crate::indicators::pattern_swing::{
|
||||
approx_equal, SwingTracker, LEVEL_TOLERANCE, SWING_THRESHOLD,
|
||||
};
|
||||
use crate::ohlcv::Candle;
|
||||
use crate::traits::Indicator;
|
||||
|
||||
/// Double Top / Double Bottom — a two-peak (or two-trough) reversal pattern.
|
||||
///
|
||||
/// The detector tracks confirmed swing pivots (a non-repainting percent-threshold
|
||||
/// zig-zag, [`SWING_THRESHOLD`] = 5%). A pattern is recognised on the bar that
|
||||
/// confirms the **second** matching extreme:
|
||||
///
|
||||
/// ```text
|
||||
/// double top : … High₁ , Low , High₂ with High₁ ≈ High₂ → -1 (bearish)
|
||||
/// double bottom : … Low₁ , High , Low₂ with Low₁ ≈ Low₂ → +1 (bullish)
|
||||
/// ```
|
||||
///
|
||||
/// Two extremes count as the same level when they are within
|
||||
/// [`LEVEL_TOLERANCE`] (3%) of each other. Because pivots strictly alternate
|
||||
/// high/low, the trough between the twin tops (or the peak between the twin
|
||||
/// bottoms) is guaranteed to sit beyond both, so no extra separation check is
|
||||
/// needed.
|
||||
///
|
||||
/// Output is `+1.0` for a double bottom, `-1.0` for a double top, and `0.0` on
|
||||
/// every other bar (including warmup and bars that confirm a pivot which does
|
||||
/// not complete the pattern). Like the candlestick family this detector never
|
||||
/// returns `None`.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// use wickra_core::{Candle, DoubleTopBottom, Indicator};
|
||||
///
|
||||
/// let mut indicator = DoubleTopBottom::new();
|
||||
/// for (i, &(high, low)) in [
|
||||
/// (100.0, 99.5),
|
||||
/// (120.0, 119.5),
|
||||
/// (110.0, 100.0), // confirms the first top at 120
|
||||
/// (120.0, 119.0), // confirms the trough at 100
|
||||
/// (115.0, 110.0), // confirms the second top at 120 → double top
|
||||
/// ]
|
||||
/// .iter()
|
||||
/// .enumerate()
|
||||
/// {
|
||||
/// let c = Candle::new(low, high, low, low, 1.0, i as i64).unwrap();
|
||||
/// let signal = indicator.update(c).unwrap();
|
||||
/// if i == 4 {
|
||||
/// assert_eq!(signal, -1.0);
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct DoubleTopBottom {
|
||||
swing: SwingTracker,
|
||||
has_emitted: bool,
|
||||
}
|
||||
|
||||
impl DoubleTopBottom {
|
||||
/// Construct a new Double Top / Double Bottom detector.
|
||||
pub const fn new() -> Self {
|
||||
Self {
|
||||
swing: SwingTracker::new(SWING_THRESHOLD, 3),
|
||||
has_emitted: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for DoubleTopBottom {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl Indicator for DoubleTopBottom {
|
||||
type Input = Candle;
|
||||
type Output = f64;
|
||||
|
||||
fn update(&mut self, candle: Candle) -> Option<f64> {
|
||||
self.has_emitted = true;
|
||||
if !self.swing.update(candle) {
|
||||
return Some(0.0);
|
||||
}
|
||||
let pivots = self.swing.pivots();
|
||||
if pivots.len() < 3 {
|
||||
return Some(0.0);
|
||||
}
|
||||
let first = pivots[pivots.len() - 3];
|
||||
let last = pivots[pivots.len() - 1];
|
||||
if approx_equal(first.price, last.price, LEVEL_TOLERANCE) {
|
||||
// `last` is the just-confirmed extreme: a high → double top (bearish),
|
||||
// a low → double bottom (bullish).
|
||||
return Some(if last.direction > 0.0 { -1.0 } else { 1.0 });
|
||||
}
|
||||
Some(0.0)
|
||||
}
|
||||
|
||||
fn reset(&mut self) {
|
||||
self.swing.reset();
|
||||
self.has_emitted = false;
|
||||
}
|
||||
|
||||
fn warmup_period(&self) -> usize {
|
||||
// The first complete pattern needs three confirmed pivots; the earliest
|
||||
// bar that can confirm a third pivot is the fifth.
|
||||
5
|
||||
}
|
||||
|
||||
fn is_ready(&self) -> bool {
|
||||
self.has_emitted
|
||||
}
|
||||
|
||||
fn name(&self) -> &'static str {
|
||||
"DoubleTopBottom"
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::indicators::pattern_swing::candles_for_pivots;
|
||||
use crate::traits::BatchExt;
|
||||
|
||||
fn run(pivots: &[f64]) -> Vec<f64> {
|
||||
let mut indicator = DoubleTopBottom::new();
|
||||
candles_for_pivots(pivots)
|
||||
.into_iter()
|
||||
.map(|c| indicator.update(c).unwrap())
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn accessors_and_metadata() {
|
||||
let indicator = DoubleTopBottom::new();
|
||||
assert_eq!(indicator.name(), "DoubleTopBottom");
|
||||
assert_eq!(indicator.warmup_period(), 5);
|
||||
assert!(!indicator.is_ready());
|
||||
assert!(!DoubleTopBottom::default().is_ready());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn double_top_is_minus_one() {
|
||||
// Twin highs 120 / 120 with a 100 trough → double top on the second.
|
||||
let out = run(&[120.0, 100.0, 120.0]);
|
||||
assert_eq!(*out.last().unwrap(), -1.0);
|
||||
// All earlier bars are warmup / non-completing.
|
||||
assert!(out[..out.len() - 1].iter().all(|&x| x == 0.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn double_bottom_is_plus_one() {
|
||||
// Lead high, then twin lows 100 / 99 around a 120 peak → double bottom.
|
||||
let out = run(&[130.0, 100.0, 120.0, 99.0]);
|
||||
assert_eq!(*out.last().unwrap(), 1.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unequal_tops_do_not_trigger() {
|
||||
// Second top 140 diverges from the first (120) → no pattern.
|
||||
let out = run(&[120.0, 100.0, 140.0]);
|
||||
assert_eq!(*out.last().unwrap(), 0.0);
|
||||
assert!(out.iter().all(|&x| x == 0.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reset_clears_state() {
|
||||
let mut indicator = DoubleTopBottom::new();
|
||||
for c in candles_for_pivots(&[120.0, 100.0, 120.0]) {
|
||||
let _ = indicator.update(c);
|
||||
}
|
||||
indicator.reset();
|
||||
assert!(!indicator.is_ready());
|
||||
let c = Candle::new(99.5, 100.0, 99.5, 99.5, 1.0, 0).unwrap();
|
||||
assert_eq!(indicator.update(c), Some(0.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn batch_equals_streaming() {
|
||||
let candles = candles_for_pivots(&[120.0, 100.0, 120.0]);
|
||||
let mut a = DoubleTopBottom::new();
|
||||
let mut b = DoubleTopBottom::new();
|
||||
assert_eq!(
|
||||
a.batch(&candles),
|
||||
candles.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
//! Flag / Pennant continuation chart pattern.
|
||||
|
||||
use crate::indicators::pattern_swing::{SwingTracker, SWING_THRESHOLD};
|
||||
use crate::ohlcv::Candle;
|
||||
use crate::traits::Indicator;
|
||||
|
||||
/// Maximum size of the consolidation swing relative to the pole for a
|
||||
/// flag/pennant to qualify — the pullback must retrace less than half the pole.
|
||||
const MAX_RETRACE_FRACTION: f64 = 0.5;
|
||||
|
||||
/// Flag / Pennant — a brief consolidation against a sharp prior move (the
|
||||
/// "pole"), resolving in the pole's direction.
|
||||
///
|
||||
/// Built on confirmed swing pivots ([`SWING_THRESHOLD`] = 5%); evaluated from the
|
||||
/// last three pivots `pole_start → pole_end → consolidation`:
|
||||
///
|
||||
/// ```text
|
||||
/// pole = |pole_end − pole_start| (the sharp impulse)
|
||||
/// pullback = |consolidation − pole_end| (the shallow counter-move)
|
||||
/// qualifies when pullback < 0.5 · pole
|
||||
/// bull flag : pole_end is a swing high → +1 (up-pole, continuation up)
|
||||
/// bear flag : pole_end is a swing low → -1 (down-pole, continuation down)
|
||||
/// ```
|
||||
///
|
||||
/// The detector fires on the bar that confirms the consolidation pivot (the flag
|
||||
/// is complete; the breakout is expected to follow). Output is `+1.0` / `-1.0` /
|
||||
/// `0.0`; never `None`.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct FlagPennant {
|
||||
swing: SwingTracker,
|
||||
has_emitted: bool,
|
||||
}
|
||||
|
||||
impl FlagPennant {
|
||||
/// Construct a new Flag / Pennant detector.
|
||||
pub const fn new() -> Self {
|
||||
Self {
|
||||
swing: SwingTracker::new(SWING_THRESHOLD, 3),
|
||||
has_emitted: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for FlagPennant {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl Indicator for FlagPennant {
|
||||
type Input = Candle;
|
||||
type Output = f64;
|
||||
|
||||
fn update(&mut self, candle: Candle) -> Option<f64> {
|
||||
self.has_emitted = true;
|
||||
if !self.swing.update(candle) {
|
||||
return Some(0.0);
|
||||
}
|
||||
let pivots = self.swing.pivots();
|
||||
if pivots.len() < 3 {
|
||||
return Some(0.0);
|
||||
}
|
||||
let n = pivots.len();
|
||||
let pole_start = pivots[n - 3];
|
||||
let pole_end = pivots[n - 2];
|
||||
let consolidation = pivots[n - 1];
|
||||
let pole = (pole_end.price - pole_start.price).abs();
|
||||
let pullback = (consolidation.price - pole_end.price).abs();
|
||||
|
||||
if pole > 0.0 && pullback < MAX_RETRACE_FRACTION * pole {
|
||||
// pole_end a high → up-pole → bull flag; a low → bear flag.
|
||||
return Some(if pole_end.direction > 0.0 { 1.0 } else { -1.0 });
|
||||
}
|
||||
Some(0.0)
|
||||
}
|
||||
|
||||
fn reset(&mut self) {
|
||||
self.swing.reset();
|
||||
self.has_emitted = false;
|
||||
}
|
||||
|
||||
fn warmup_period(&self) -> usize {
|
||||
// Three confirmed pivots; the earliest confirmation of the third is bar 4.
|
||||
4
|
||||
}
|
||||
|
||||
fn is_ready(&self) -> bool {
|
||||
self.has_emitted
|
||||
}
|
||||
|
||||
fn name(&self) -> &'static str {
|
||||
"FlagPennant"
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::indicators::pattern_swing::candles_for_pivots;
|
||||
use crate::traits::BatchExt;
|
||||
|
||||
fn run(pivots: &[f64]) -> Vec<f64> {
|
||||
let mut indicator = FlagPennant::new();
|
||||
candles_for_pivots(pivots)
|
||||
.into_iter()
|
||||
.map(|c| indicator.update(c).unwrap())
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn accessors_and_metadata() {
|
||||
let indicator = FlagPennant::new();
|
||||
assert_eq!(indicator.name(), "FlagPennant");
|
||||
assert_eq!(indicator.warmup_period(), 4);
|
||||
assert!(!indicator.is_ready());
|
||||
assert!(!FlagPennant::default().is_ready());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bull_flag_is_plus_one() {
|
||||
// Up-pole 100 → 140 (40), shallow pullback to 130 (10 < 20) → bull flag.
|
||||
let out = run(&[150.0, 100.0, 140.0, 130.0]);
|
||||
assert_eq!(*out.last().unwrap(), 1.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bear_flag_is_minus_one() {
|
||||
// Down-pole 140 → 100 (40), shallow pullback to 110 (10 < 20) → bear flag.
|
||||
let out = run(&[140.0, 100.0, 110.0]);
|
||||
assert_eq!(*out.last().unwrap(), -1.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deep_pullback_is_not_a_flag() {
|
||||
// Pole 100 → 140 (40) but pullback to 104 (36 > 20) → not a flag.
|
||||
let out = run(&[150.0, 100.0, 140.0, 104.0]);
|
||||
assert_eq!(*out.last().unwrap(), 0.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reset_clears_state() {
|
||||
let mut indicator = FlagPennant::new();
|
||||
for c in candles_for_pivots(&[150.0, 100.0, 140.0]) {
|
||||
let _ = indicator.update(c);
|
||||
}
|
||||
indicator.reset();
|
||||
assert!(!indicator.is_ready());
|
||||
let c = Candle::new(99.5, 100.0, 99.5, 99.5, 1.0, 0).unwrap();
|
||||
assert_eq!(indicator.update(c), Some(0.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn batch_equals_streaming() {
|
||||
let candles = candles_for_pivots(&[150.0, 100.0, 140.0, 130.0]);
|
||||
let mut a = FlagPennant::new();
|
||||
let mut b = FlagPennant::new();
|
||||
assert_eq!(
|
||||
a.batch(&candles),
|
||||
candles.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,157 @@
|
||||
//! Gartley harmonic pattern.
|
||||
|
||||
use crate::indicators::pattern_swing::{ratios_in, xabcd, SwingTracker, SWING_THRESHOLD};
|
||||
use crate::ohlcv::Candle;
|
||||
use crate::traits::Indicator;
|
||||
|
||||
/// Gartley — the classic 5-point (X-A-B-C-D) harmonic pattern, recognised from
|
||||
/// confirmed swing pivots when the legs fall inside the Gartley Fibonacci
|
||||
/// windows:
|
||||
///
|
||||
/// ```text
|
||||
/// AB / XA ∈ [0.55, 0.70] (≈ 0.618 retracement of XA)
|
||||
/// BC / AB ∈ [0.382, 0.886]
|
||||
/// CD / BC ∈ [1.13, 1.618]
|
||||
/// AD / XA ∈ [0.74, 0.84] (≈ 0.786 — the defining D completion)
|
||||
/// ```
|
||||
///
|
||||
/// Output is `+1.0` when the terminal point D is a swing low (bullish
|
||||
/// completion), `-1.0` when D is a swing high (bearish), and `0.0` otherwise;
|
||||
/// never `None`. See `crates/wickra-core/src/indicators/gartley.rs`.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Gartley {
|
||||
swing: SwingTracker,
|
||||
has_emitted: bool,
|
||||
}
|
||||
|
||||
impl Gartley {
|
||||
/// Construct a new Gartley detector.
|
||||
pub const fn new() -> Self {
|
||||
Self {
|
||||
swing: SwingTracker::new(SWING_THRESHOLD, 5),
|
||||
has_emitted: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Gartley {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl Indicator for Gartley {
|
||||
type Input = Candle;
|
||||
type Output = f64;
|
||||
|
||||
fn update(&mut self, candle: Candle) -> Option<f64> {
|
||||
self.has_emitted = true;
|
||||
if !self.swing.update(candle) {
|
||||
return Some(0.0);
|
||||
}
|
||||
let pivots = self.swing.pivots();
|
||||
if pivots.len() < 5 {
|
||||
return Some(0.0);
|
||||
}
|
||||
let p = xabcd(pivots);
|
||||
let xa = (p.a - p.x).abs();
|
||||
let ab = (p.b - p.a).abs();
|
||||
let bc = (p.c - p.b).abs();
|
||||
let cd = (p.d - p.c).abs();
|
||||
let ad = (p.d - p.a).abs();
|
||||
let matched = ratios_in(&[
|
||||
(ab / xa, 0.55, 0.70),
|
||||
(bc / ab, 0.382, 0.886),
|
||||
(cd / bc, 1.13, 1.618),
|
||||
(ad / xa, 0.74, 0.84),
|
||||
]);
|
||||
if matched {
|
||||
return Some(if p.bullish { 1.0 } else { -1.0 });
|
||||
}
|
||||
Some(0.0)
|
||||
}
|
||||
|
||||
fn reset(&mut self) {
|
||||
self.swing.reset();
|
||||
self.has_emitted = false;
|
||||
}
|
||||
|
||||
fn warmup_period(&self) -> usize {
|
||||
6
|
||||
}
|
||||
|
||||
fn is_ready(&self) -> bool {
|
||||
self.has_emitted
|
||||
}
|
||||
|
||||
fn name(&self) -> &'static str {
|
||||
"Gartley"
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::indicators::pattern_swing::candles_for_pivots;
|
||||
use crate::traits::BatchExt;
|
||||
|
||||
fn run(pivots: &[f64]) -> Vec<f64> {
|
||||
let mut indicator = Gartley::new();
|
||||
candles_for_pivots(pivots)
|
||||
.into_iter()
|
||||
.map(|c| indicator.update(c).unwrap())
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn accessors_and_metadata() {
|
||||
let indicator = Gartley::new();
|
||||
assert_eq!(indicator.name(), "Gartley");
|
||||
assert_eq!(indicator.warmup_period(), 6);
|
||||
assert!(!indicator.is_ready());
|
||||
assert!(!Gartley::default().is_ready());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bullish_gartley_is_plus_one() {
|
||||
let out = run(&[150.0, 100.0, 140.0, 115.3, 127.65, 108.56]);
|
||||
assert_eq!(*out.last().unwrap(), 1.0);
|
||||
assert!(out[..out.len() - 1].iter().all(|&x| x == 0.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bearish_gartley_is_minus_one() {
|
||||
let out = run(&[150.0, 110.0, 134.7, 122.35, 141.44]);
|
||||
assert_eq!(*out.last().unwrap(), -1.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn out_of_ratio_does_not_trigger() {
|
||||
// Five pivots but the D completion (AD/XA ≈ 0.25) is far from 0.786.
|
||||
let out = run(&[150.0, 100.0, 140.0, 110.0, 135.0, 105.0]);
|
||||
assert_eq!(*out.last().unwrap(), 0.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reset_clears_state() {
|
||||
let mut indicator = Gartley::new();
|
||||
for c in candles_for_pivots(&[150.0, 100.0, 140.0]) {
|
||||
let _ = indicator.update(c);
|
||||
}
|
||||
indicator.reset();
|
||||
assert!(!indicator.is_ready());
|
||||
let c = Candle::new(99.5, 100.0, 99.5, 99.5, 1.0, 0).unwrap();
|
||||
assert_eq!(indicator.update(c), Some(0.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn batch_equals_streaming() {
|
||||
let candles = candles_for_pivots(&[150.0, 100.0, 140.0, 115.3, 127.65, 108.56]);
|
||||
let mut a = Gartley::new();
|
||||
let mut b = Gartley::new();
|
||||
assert_eq!(
|
||||
a.batch(&candles),
|
||||
candles.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,191 @@
|
||||
//! Head-and-Shoulders (and Inverse) reversal chart pattern.
|
||||
|
||||
use crate::indicators::pattern_swing::{
|
||||
approx_equal, SwingTracker, LEVEL_TOLERANCE, SWING_THRESHOLD,
|
||||
};
|
||||
use crate::ohlcv::Candle;
|
||||
use crate::traits::Indicator;
|
||||
|
||||
/// Head-and-Shoulders / Inverse Head-and-Shoulders — a five-pivot reversal
|
||||
/// pattern with a central extreme (the head) flanked by two lower/higher
|
||||
/// shoulders at a similar level, joined by a roughly horizontal neckline.
|
||||
///
|
||||
/// Built on confirmed swing pivots ([`SWING_THRESHOLD`] = 5%); recognised on the
|
||||
/// bar that confirms the right shoulder:
|
||||
///
|
||||
/// ```text
|
||||
/// head-and-shoulders top (bearish, -1):
|
||||
/// LeftShoulder(high) , Trough , Head(high) , Trough , RightShoulder(high)
|
||||
/// Head > both shoulders ; LeftShoulder ≈ RightShoulder ; Trough₁ ≈ Trough₂
|
||||
///
|
||||
/// inverse head-and-shoulders (bullish, +1):
|
||||
/// LeftShoulder(low) , Peak , Head(low) , Peak , RightShoulder(low)
|
||||
/// Head < both shoulders ; LeftShoulder ≈ RightShoulder ; Peak₁ ≈ Peak₂
|
||||
/// ```
|
||||
///
|
||||
/// The shoulders must match within [`LEVEL_TOLERANCE`] (3%) and the two neckline
|
||||
/// points within the same tolerance. Output is `-1.0` for a top, `+1.0` for an
|
||||
/// inverse, `0.0` otherwise; never `None`.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct HeadAndShoulders {
|
||||
swing: SwingTracker,
|
||||
has_emitted: bool,
|
||||
}
|
||||
|
||||
impl HeadAndShoulders {
|
||||
/// Construct a new Head-and-Shoulders detector.
|
||||
pub const fn new() -> Self {
|
||||
Self {
|
||||
swing: SwingTracker::new(SWING_THRESHOLD, 5),
|
||||
has_emitted: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for HeadAndShoulders {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl Indicator for HeadAndShoulders {
|
||||
type Input = Candle;
|
||||
type Output = f64;
|
||||
|
||||
fn update(&mut self, candle: Candle) -> Option<f64> {
|
||||
self.has_emitted = true;
|
||||
if !self.swing.update(candle) {
|
||||
return Some(0.0);
|
||||
}
|
||||
let pivots = self.swing.pivots();
|
||||
if pivots.len() < 5 {
|
||||
return Some(0.0);
|
||||
}
|
||||
let n = pivots.len();
|
||||
let left_shoulder = pivots[n - 5];
|
||||
let neck_1 = pivots[n - 4];
|
||||
let head = pivots[n - 3];
|
||||
let neck_2 = pivots[n - 2];
|
||||
let right_shoulder = pivots[n - 1];
|
||||
|
||||
let shoulders_match =
|
||||
approx_equal(left_shoulder.price, right_shoulder.price, LEVEL_TOLERANCE);
|
||||
let neckline_flat = approx_equal(neck_1.price, neck_2.price, LEVEL_TOLERANCE);
|
||||
let head_is_peak = head.price > left_shoulder.price && head.price > right_shoulder.price;
|
||||
let head_is_trough = head.price < left_shoulder.price && head.price < right_shoulder.price;
|
||||
let frame_matches = shoulders_match && neckline_flat;
|
||||
|
||||
if right_shoulder.direction > 0.0 {
|
||||
// Head-and-shoulders top: head is the highest of the three highs.
|
||||
if head_is_peak && frame_matches {
|
||||
return Some(-1.0);
|
||||
}
|
||||
} else if head_is_trough && frame_matches {
|
||||
// Inverse: head is the lowest of the three lows.
|
||||
return Some(1.0);
|
||||
}
|
||||
Some(0.0)
|
||||
}
|
||||
|
||||
fn reset(&mut self) {
|
||||
self.swing.reset();
|
||||
self.has_emitted = false;
|
||||
}
|
||||
|
||||
fn warmup_period(&self) -> usize {
|
||||
// Five confirmed pivots; the earliest confirmation of the fifth is bar 6.
|
||||
6
|
||||
}
|
||||
|
||||
fn is_ready(&self) -> bool {
|
||||
self.has_emitted
|
||||
}
|
||||
|
||||
fn name(&self) -> &'static str {
|
||||
"HeadAndShoulders"
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::indicators::pattern_swing::candles_for_pivots;
|
||||
use crate::traits::BatchExt;
|
||||
|
||||
fn run(pivots: &[f64]) -> Vec<f64> {
|
||||
let mut indicator = HeadAndShoulders::new();
|
||||
candles_for_pivots(pivots)
|
||||
.into_iter()
|
||||
.map(|c| indicator.update(c).unwrap())
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn accessors_and_metadata() {
|
||||
let indicator = HeadAndShoulders::new();
|
||||
assert_eq!(indicator.name(), "HeadAndShoulders");
|
||||
assert_eq!(indicator.warmup_period(), 6);
|
||||
assert!(!indicator.is_ready());
|
||||
assert!(!HeadAndShoulders::default().is_ready());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn head_and_shoulders_top_is_minus_one() {
|
||||
// LS 100, trough 90, head 120, trough 92, RS 101.
|
||||
let out = run(&[100.0, 90.0, 120.0, 92.0, 101.0]);
|
||||
assert_eq!(*out.last().unwrap(), -1.0);
|
||||
assert!(out[..out.len() - 1].iter().all(|&x| x == 0.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn inverse_head_and_shoulders_is_plus_one() {
|
||||
// Lead high then LS 100, peak 110, head 80, peak 108, RS 101.
|
||||
let out = run(&[130.0, 100.0, 110.0, 80.0, 108.0, 101.0]);
|
||||
assert_eq!(*out.last().unwrap(), 1.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mismatched_shoulders_do_not_trigger() {
|
||||
// Right shoulder (115) far from left (100) → no pattern.
|
||||
let out = run(&[100.0, 90.0, 130.0, 92.0, 115.0]);
|
||||
assert_eq!(*out.last().unwrap(), 0.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn inverse_mismatched_shoulders_do_not_trigger() {
|
||||
// Inverse shape (ends on a low) but the right shoulder (90) diverges from
|
||||
// the left (100) → enters the inverse branch yet reports no pattern.
|
||||
let out = run(&[130.0, 100.0, 110.0, 80.0, 108.0, 90.0]);
|
||||
assert_eq!(*out.last().unwrap(), 0.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn equal_highs_without_taller_head_do_not_trigger() {
|
||||
// Three equal highs (no dominant head) → not H&S (that is a triple top).
|
||||
let out = run(&[120.0, 90.0, 120.0, 92.0, 120.0]);
|
||||
assert_eq!(*out.last().unwrap(), 0.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reset_clears_state() {
|
||||
let mut indicator = HeadAndShoulders::new();
|
||||
for c in candles_for_pivots(&[100.0, 90.0, 120.0]) {
|
||||
let _ = indicator.update(c);
|
||||
}
|
||||
indicator.reset();
|
||||
assert!(!indicator.is_ready());
|
||||
let c = Candle::new(99.5, 100.0, 99.5, 99.5, 1.0, 0).unwrap();
|
||||
assert_eq!(indicator.update(c), Some(0.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn batch_equals_streaming() {
|
||||
let candles = candles_for_pivots(&[100.0, 90.0, 120.0, 92.0, 101.0]);
|
||||
let mut a = HeadAndShoulders::new();
|
||||
let mut b = HeadAndShoulders::new();
|
||||
assert_eq!(
|
||||
a.batch(&candles),
|
||||
candles.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,13 @@
|
||||
//! [`FAMILIES`]. Every public name is re-exported flat from this module and
|
||||
//! from the crate root for convenience.
|
||||
|
||||
// Internal shared building block for the chart- and harmonic-pattern detectors.
|
||||
// Declared `pub(crate)` (not `mod`) so it is excluded from the public-catalogue
|
||||
// counter (`grep -c '^mod '`) and re-exported nowhere.
|
||||
pub(crate) mod pattern_swing;
|
||||
|
||||
mod abandoned_baby;
|
||||
mod abcd;
|
||||
mod absolute_breadth_index;
|
||||
mod acceleration_bands;
|
||||
mod accelerator_oscillator;
|
||||
@@ -35,6 +41,7 @@ mod avg_price;
|
||||
mod awesome_oscillator;
|
||||
mod awesome_oscillator_histogram;
|
||||
mod balance_of_power;
|
||||
mod bat;
|
||||
mod belt_hold;
|
||||
mod beta;
|
||||
mod beta_neutral_spread;
|
||||
@@ -43,6 +50,7 @@ mod bollinger_bandwidth;
|
||||
mod breadth_thrust;
|
||||
mod breakaway;
|
||||
mod bullish_percent_index;
|
||||
mod butterfly;
|
||||
mod calendar_spread;
|
||||
mod calmar_ratio;
|
||||
mod camarilla_pivots;
|
||||
@@ -65,9 +73,12 @@ mod conditional_value_at_risk;
|
||||
mod connors_rsi;
|
||||
mod coppock;
|
||||
mod counterattack;
|
||||
mod crab;
|
||||
mod cumulative_volume_index;
|
||||
mod cup_and_handle;
|
||||
mod cvd;
|
||||
mod cybernetic_cycle;
|
||||
mod cypher;
|
||||
mod day_of_week_profile;
|
||||
mod decycler;
|
||||
mod decycler_oscillator;
|
||||
@@ -82,6 +93,7 @@ mod doji_star;
|
||||
mod donchian;
|
||||
mod donchian_stop;
|
||||
mod double_bollinger;
|
||||
mod double_top_bottom;
|
||||
mod downside_gap_three_methods;
|
||||
mod dpo;
|
||||
mod dragonfly_doji;
|
||||
@@ -100,6 +112,7 @@ mod falling_three_methods;
|
||||
mod fama;
|
||||
mod fibonacci_pivots;
|
||||
mod fisher_transform;
|
||||
mod flag_pennant;
|
||||
mod footprint;
|
||||
mod force_index;
|
||||
mod fractal_chaos_bands;
|
||||
@@ -111,11 +124,13 @@ mod funding_rate_zscore;
|
||||
mod gain_loss_ratio;
|
||||
mod gap_side_by_side_white;
|
||||
mod garman_klass;
|
||||
mod gartley;
|
||||
mod granger_causality;
|
||||
mod gravestone_doji;
|
||||
mod hammer;
|
||||
mod hanging_man;
|
||||
mod harami;
|
||||
mod head_and_shoulders;
|
||||
mod heikin_ashi;
|
||||
mod high_low_index;
|
||||
mod high_wave;
|
||||
@@ -229,6 +244,7 @@ mod quoted_spread;
|
||||
mod r_squared;
|
||||
mod realized_spread;
|
||||
mod recovery_factor;
|
||||
mod rectangle_range;
|
||||
mod relative_strength_ab;
|
||||
mod renko_bars;
|
||||
mod renko_trailing_stop;
|
||||
@@ -252,6 +268,7 @@ mod separating_lines;
|
||||
mod session_high_low;
|
||||
mod session_range;
|
||||
mod session_vwap;
|
||||
mod shark;
|
||||
mod sharpe_ratio;
|
||||
mod shooting_star;
|
||||
mod short_line;
|
||||
@@ -296,6 +313,7 @@ mod td_sequential;
|
||||
mod td_setup;
|
||||
mod tema;
|
||||
mod term_structure_basis;
|
||||
mod three_drives;
|
||||
mod three_inside;
|
||||
mod three_line_strike;
|
||||
mod three_outside;
|
||||
@@ -308,8 +326,10 @@ mod time_of_day_return_profile;
|
||||
mod tpo_profile;
|
||||
mod trade_imbalance;
|
||||
mod treynor_ratio;
|
||||
mod triangle;
|
||||
mod trima;
|
||||
mod trin;
|
||||
mod triple_top_bottom;
|
||||
mod trix;
|
||||
mod true_range;
|
||||
mod tsf;
|
||||
@@ -343,6 +363,7 @@ mod vwap_stddev_bands;
|
||||
mod vwma;
|
||||
mod vzo;
|
||||
mod wave_trend;
|
||||
mod wedge;
|
||||
mod weighted_close;
|
||||
mod williams_fractals;
|
||||
mod williams_r;
|
||||
@@ -356,6 +377,7 @@ mod zig_zag;
|
||||
mod zlema;
|
||||
|
||||
pub use abandoned_baby::AbandonedBaby;
|
||||
pub use abcd::Abcd;
|
||||
pub use absolute_breadth_index::AbsoluteBreadthIndex;
|
||||
pub use acceleration_bands::{AccelerationBands, AccelerationBandsOutput};
|
||||
pub use accelerator_oscillator::AcceleratorOscillator;
|
||||
@@ -386,6 +408,7 @@ pub use avg_price::AvgPrice;
|
||||
pub use awesome_oscillator::AwesomeOscillator;
|
||||
pub use awesome_oscillator_histogram::AwesomeOscillatorHistogram;
|
||||
pub use balance_of_power::BalanceOfPower;
|
||||
pub use bat::Bat;
|
||||
pub use belt_hold::BeltHold;
|
||||
pub use beta::Beta;
|
||||
pub use beta_neutral_spread::BetaNeutralSpread;
|
||||
@@ -394,6 +417,7 @@ pub use bollinger_bandwidth::BollingerBandwidth;
|
||||
pub use breadth_thrust::BreadthThrust;
|
||||
pub use breakaway::Breakaway;
|
||||
pub use bullish_percent_index::BullishPercentIndex;
|
||||
pub use butterfly::Butterfly;
|
||||
pub use calendar_spread::CalendarSpread;
|
||||
pub use calmar_ratio::CalmarRatio;
|
||||
pub use camarilla_pivots::{Camarilla, CamarillaPivotsOutput};
|
||||
@@ -416,9 +440,12 @@ pub use conditional_value_at_risk::ConditionalValueAtRisk;
|
||||
pub use connors_rsi::ConnorsRsi;
|
||||
pub use coppock::Coppock;
|
||||
pub use counterattack::Counterattack;
|
||||
pub use crab::Crab;
|
||||
pub use cumulative_volume_index::CumulativeVolumeIndex;
|
||||
pub use cup_and_handle::CupAndHandle;
|
||||
pub use cvd::CumulativeVolumeDelta;
|
||||
pub use cybernetic_cycle::CyberneticCycle;
|
||||
pub use cypher::Cypher;
|
||||
pub use day_of_week_profile::{DayOfWeekProfile, DayOfWeekProfileOutput};
|
||||
pub use decycler::Decycler;
|
||||
pub use decycler_oscillator::DecyclerOscillator;
|
||||
@@ -433,6 +460,7 @@ pub use doji_star::DojiStar;
|
||||
pub use donchian::{Donchian, DonchianOutput};
|
||||
pub use donchian_stop::{DonchianStop, DonchianStopOutput};
|
||||
pub use double_bollinger::{DoubleBollinger, DoubleBollingerOutput};
|
||||
pub use double_top_bottom::DoubleTopBottom;
|
||||
pub use downside_gap_three_methods::DownsideGapThreeMethods;
|
||||
pub use dpo::Dpo;
|
||||
pub use dragonfly_doji::DragonflyDoji;
|
||||
@@ -451,6 +479,7 @@ pub use falling_three_methods::FallingThreeMethods;
|
||||
pub use fama::Fama;
|
||||
pub use fibonacci_pivots::{FibonacciPivots, FibonacciPivotsOutput};
|
||||
pub use fisher_transform::FisherTransform;
|
||||
pub use flag_pennant::FlagPennant;
|
||||
pub use footprint::{Footprint, FootprintLevel, FootprintOutput};
|
||||
pub use force_index::ForceIndex;
|
||||
pub use fractal_chaos_bands::{FractalChaosBands, FractalChaosBandsOutput};
|
||||
@@ -462,11 +491,13 @@ pub use funding_rate_zscore::FundingRateZScore;
|
||||
pub use gain_loss_ratio::GainLossRatio;
|
||||
pub use gap_side_by_side_white::GapSideBySideWhite;
|
||||
pub use garman_klass::GarmanKlassVolatility;
|
||||
pub use gartley::Gartley;
|
||||
pub use granger_causality::GrangerCausality;
|
||||
pub use gravestone_doji::GravestoneDoji;
|
||||
pub use hammer::Hammer;
|
||||
pub use hanging_man::HangingMan;
|
||||
pub use harami::Harami;
|
||||
pub use head_and_shoulders::HeadAndShoulders;
|
||||
pub use heikin_ashi::{HeikinAshi, HeikinAshiOutput};
|
||||
pub use high_low_index::HighLowIndex;
|
||||
pub use high_wave::HighWave;
|
||||
@@ -580,6 +611,7 @@ pub use quoted_spread::QuotedSpread;
|
||||
pub use r_squared::RSquared;
|
||||
pub use realized_spread::RealizedSpread;
|
||||
pub use recovery_factor::RecoveryFactor;
|
||||
pub use rectangle_range::RectangleRange;
|
||||
pub use relative_strength_ab::{RelativeStrengthAB, RelativeStrengthOutput};
|
||||
pub use renko_bars::{RenkoBars, RenkoBrick};
|
||||
pub use renko_trailing_stop::RenkoTrailingStop;
|
||||
@@ -603,6 +635,7 @@ pub use separating_lines::SeparatingLines;
|
||||
pub use session_high_low::{SessionHighLow, SessionHighLowOutput};
|
||||
pub use session_range::{SessionRange, SessionRangeOutput};
|
||||
pub use session_vwap::SessionVwap;
|
||||
pub use shark::Shark;
|
||||
pub use sharpe_ratio::SharpeRatio;
|
||||
pub use shooting_star::ShootingStar;
|
||||
pub use short_line::ShortLine;
|
||||
@@ -647,6 +680,7 @@ pub use td_sequential::{TdSequential, TdSequentialOutput};
|
||||
pub use td_setup::TdSetup;
|
||||
pub use tema::Tema;
|
||||
pub use term_structure_basis::TermStructureBasis;
|
||||
pub use three_drives::ThreeDrives;
|
||||
pub use three_inside::ThreeInside;
|
||||
pub use three_line_strike::ThreeLineStrike;
|
||||
pub use three_outside::ThreeOutside;
|
||||
@@ -659,8 +693,10 @@ pub use time_of_day_return_profile::{TimeOfDayReturnProfile, TimeOfDayReturnProf
|
||||
pub use tpo_profile::{TpoProfile, TpoProfileOutput};
|
||||
pub use trade_imbalance::TradeImbalance;
|
||||
pub use treynor_ratio::TreynorRatio;
|
||||
pub use triangle::Triangle;
|
||||
pub use trima::Trima;
|
||||
pub use trin::Trin;
|
||||
pub use triple_top_bottom::TripleTopBottom;
|
||||
pub use trix::Trix;
|
||||
pub use true_range::TrueRange;
|
||||
pub use tsf::Tsf;
|
||||
@@ -694,6 +730,7 @@ pub use vwap_stddev_bands::{VwapStdDevBands, VwapStdDevBandsOutput};
|
||||
pub use vwma::Vwma;
|
||||
pub use vzo::Vzo;
|
||||
pub use wave_trend::{WaveTrend, WaveTrendOutput};
|
||||
pub use wedge::Wedge;
|
||||
pub use weighted_close::WeightedClose;
|
||||
pub use williams_fractals::{WilliamsFractals, WilliamsFractalsOutput};
|
||||
pub use williams_r::WilliamsR;
|
||||
@@ -1159,6 +1196,32 @@ pub const FAMILIES: &[(&str, &[&str])] = &[
|
||||
"VolumeByTimeProfile",
|
||||
],
|
||||
),
|
||||
(
|
||||
"Chart Patterns",
|
||||
&[
|
||||
"DoubleTopBottom",
|
||||
"TripleTopBottom",
|
||||
"HeadAndShoulders",
|
||||
"Triangle",
|
||||
"Wedge",
|
||||
"FlagPennant",
|
||||
"RectangleRange",
|
||||
"CupAndHandle",
|
||||
],
|
||||
),
|
||||
(
|
||||
"Harmonic Patterns",
|
||||
&[
|
||||
"Abcd",
|
||||
"Gartley",
|
||||
"Butterfly",
|
||||
"Bat",
|
||||
"Crab",
|
||||
"Shark",
|
||||
"Cypher",
|
||||
"ThreeDrives",
|
||||
],
|
||||
),
|
||||
];
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -1187,6 +1250,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, 351, "FAMILIES total drifted from indicator count");
|
||||
assert_eq!(total, 367, "FAMILIES total drifted from indicator count");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,486 @@
|
||||
//! Internal swing-pivot tracker shared by the chart-pattern and harmonic-pattern
|
||||
//! detectors. Not a public indicator — it carries no `Indicator` impl and is
|
||||
//! re-exported nowhere, so it is excluded from the public catalogue counter.
|
||||
//!
|
||||
//! The tracker mirrors [`crate::indicators::ZigZag`]'s non-repainting
|
||||
//! percent-threshold confirmation logic, but differs in two ways that make it a
|
||||
//! reusable building block rather than a standalone indicator:
|
||||
//!
|
||||
//! * It is **parameter-free at the call site** — the reversal threshold is baked
|
||||
//! in by each detector as a compile-time constant, so construction is
|
||||
//! infallible (`const fn new`) and there is no user-facing validation branch.
|
||||
//! * It **accumulates a bounded history** of the most recently confirmed pivots
|
||||
//! (capped at `cap`), so a detector can inspect the last few swings to match a
|
||||
//! geometric template (double top, head-and-shoulders, the XABCD legs of a
|
||||
//! harmonic pattern, …).
|
||||
|
||||
use crate::ohlcv::Candle;
|
||||
|
||||
/// Default fractional reversal threshold for pattern swing detection (5%). A
|
||||
/// pivot is confirmed once price reverses by this fraction away from the running
|
||||
/// extreme. Baked in so the pattern detectors stay parameter-free, mirroring the
|
||||
/// candlestick-pattern family's fixed geometric thresholds.
|
||||
pub(crate) const SWING_THRESHOLD: f64 = 0.05;
|
||||
|
||||
/// Default relative tolerance for two swing levels to count as "equal" (3%) —
|
||||
/// the twin tops of a double top, the shoulders of a head-and-shoulders, the
|
||||
/// flat boundary of a rectangle.
|
||||
pub(crate) const LEVEL_TOLERANCE: f64 = 0.03;
|
||||
|
||||
/// A confirmed swing pivot: the extreme price the swing turned from and its
|
||||
/// direction (`+1.0` for a swing high, `-1.0` for a swing low).
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub(crate) struct Pivot {
|
||||
/// Price of the confirmed swing extreme.
|
||||
pub price: f64,
|
||||
/// `+1.0` if the pivot is a swing high, `-1.0` if it is a swing low.
|
||||
pub direction: f64,
|
||||
}
|
||||
|
||||
/// Non-repainting percent-threshold swing tracker with a bounded pivot history.
|
||||
///
|
||||
/// Feeding a candle returns `true` exactly on the bar where a new pivot is
|
||||
/// confirmed (price has reversed by the configured fraction away from the
|
||||
/// running extreme); the newly confirmed pivot is appended to [`pivots`] and the
|
||||
/// oldest is dropped once the cap is exceeded. Bars that merely extend the
|
||||
/// running extreme, or that move less than the threshold, return `false`.
|
||||
///
|
||||
/// [`pivots`]: SwingTracker::pivots
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct SwingTracker {
|
||||
threshold: f64,
|
||||
cap: usize,
|
||||
state: Option<State>,
|
||||
pivots: Vec<Pivot>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
struct State {
|
||||
/// `+1.0` while tracking a candidate high (uptrend), `-1.0` while tracking a
|
||||
/// candidate low (downtrend).
|
||||
direction: f64,
|
||||
/// The running candidate extreme price.
|
||||
extreme: f64,
|
||||
}
|
||||
|
||||
impl SwingTracker {
|
||||
/// Construct a tracker with a fractional reversal `threshold` (e.g. `0.05`
|
||||
/// for 5%) and a pivot history capped at `cap` entries.
|
||||
///
|
||||
/// The threshold is supplied by the detectors as a compile-time constant in
|
||||
/// `(0, 1)`, so no runtime validation is performed — an out-of-range
|
||||
/// constant would be a library bug caught by the unit tests, not invalid
|
||||
/// caller input.
|
||||
pub(crate) const fn new(threshold: f64, cap: usize) -> Self {
|
||||
Self {
|
||||
threshold,
|
||||
cap,
|
||||
state: None,
|
||||
pivots: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Feed one candle. Returns `true` when a new pivot was confirmed this bar.
|
||||
pub(crate) fn update(&mut self, candle: Candle) -> bool {
|
||||
let Some(s) = self.state else {
|
||||
// Bootstrap: seed an uptrend tracking the first candle's high.
|
||||
self.state = Some(State {
|
||||
direction: 1.0,
|
||||
extreme: candle.high,
|
||||
});
|
||||
return false;
|
||||
};
|
||||
|
||||
if s.direction > 0.0 {
|
||||
if candle.high > s.extreme {
|
||||
// Extend the candidate high.
|
||||
self.state = Some(State {
|
||||
direction: 1.0,
|
||||
extreme: candle.high,
|
||||
});
|
||||
return false;
|
||||
}
|
||||
if candle.low <= s.extreme * (1.0 - self.threshold) {
|
||||
// Confirm the swing high; flip to tracking this bar's low.
|
||||
self.push(Pivot {
|
||||
price: s.extreme,
|
||||
direction: 1.0,
|
||||
});
|
||||
self.state = Some(State {
|
||||
direction: -1.0,
|
||||
extreme: candle.low,
|
||||
});
|
||||
return true;
|
||||
}
|
||||
false
|
||||
} else {
|
||||
if candle.low < s.extreme {
|
||||
// Extend the candidate low.
|
||||
self.state = Some(State {
|
||||
direction: -1.0,
|
||||
extreme: candle.low,
|
||||
});
|
||||
return false;
|
||||
}
|
||||
if candle.high >= s.extreme * (1.0 + self.threshold) {
|
||||
// Confirm the swing low; flip to tracking this bar's high.
|
||||
self.push(Pivot {
|
||||
price: s.extreme,
|
||||
direction: -1.0,
|
||||
});
|
||||
self.state = Some(State {
|
||||
direction: 1.0,
|
||||
extreme: candle.high,
|
||||
});
|
||||
return true;
|
||||
}
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
fn push(&mut self, pivot: Pivot) {
|
||||
self.pivots.push(pivot);
|
||||
if self.pivots.len() > self.cap {
|
||||
self.pivots.remove(0);
|
||||
}
|
||||
}
|
||||
|
||||
/// The confirmed pivots in chronological order (oldest first, newest last).
|
||||
pub(crate) fn pivots(&self) -> &[Pivot] {
|
||||
&self.pivots
|
||||
}
|
||||
|
||||
/// Clear all state, returning the tracker to its just-constructed condition.
|
||||
pub(crate) fn reset(&mut self) {
|
||||
self.state = None;
|
||||
self.pivots.clear();
|
||||
}
|
||||
}
|
||||
|
||||
/// The two most recent swing highs and lows from the last four (strictly
|
||||
/// alternating) pivots, returned as `(high_old, high_new, low_old, low_new)`.
|
||||
/// Used by the converging/diverging trendline patterns (triangle, wedge,
|
||||
/// rectangle). The slice must hold at least four pivots.
|
||||
pub(crate) fn recent_legs(pivots: &[Pivot]) -> (f64, f64, f64, f64) {
|
||||
let n = pivots.len();
|
||||
if pivots[n - 1].direction > 0.0 {
|
||||
// … low_old, high_old, low_new, high_new (newest is a high)
|
||||
(
|
||||
pivots[n - 3].price,
|
||||
pivots[n - 1].price,
|
||||
pivots[n - 4].price,
|
||||
pivots[n - 2].price,
|
||||
)
|
||||
} else {
|
||||
// … high_old, low_old, high_new, low_new (newest is a low)
|
||||
(
|
||||
pivots[n - 4].price,
|
||||
pivots[n - 2].price,
|
||||
pivots[n - 3].price,
|
||||
pivots[n - 1].price,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Relative-tolerance equality: `true` when `a` and `b` are within `tol`
|
||||
/// (a fraction) of the larger magnitude. Used to decide whether two swing
|
||||
/// levels (the twin highs of a double top, the shoulders of a head-and-shoulders,
|
||||
/// a harmonic Fibonacci ratio) count as "the same".
|
||||
pub(crate) fn approx_equal(a: f64, b: f64, tol: f64) -> bool {
|
||||
let scale = a.abs().max(b.abs()).max(f64::MIN_POSITIVE);
|
||||
(a - b).abs() <= tol * scale
|
||||
}
|
||||
|
||||
/// The five most recent pivots interpreted as the X-A-B-C-D points of a harmonic
|
||||
/// pattern, with the terminal direction. The slice must hold at least five
|
||||
/// pivots. Each detector derives the leg lengths and Fibonacci ratios it needs
|
||||
/// from these five prices.
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub(crate) struct Xabcd {
|
||||
pub x: f64,
|
||||
pub a: f64,
|
||||
pub b: f64,
|
||||
pub c: f64,
|
||||
pub d: f64,
|
||||
/// `true` when the terminal point D is a swing low (a bullish, buy-side
|
||||
/// completion); `false` when D is a swing high (bearish).
|
||||
pub bullish: bool,
|
||||
}
|
||||
|
||||
/// Read the last five pivots as an [`Xabcd`]. Pivots are guaranteed nonzero-leg
|
||||
/// (the swing tracker only confirms moves of at least the threshold), so the
|
||||
/// leg-ratio divisions in the detectors never divide by zero.
|
||||
pub(crate) fn xabcd(pivots: &[Pivot]) -> Xabcd {
|
||||
let n = pivots.len();
|
||||
Xabcd {
|
||||
x: pivots[n - 5].price,
|
||||
a: pivots[n - 4].price,
|
||||
b: pivots[n - 3].price,
|
||||
c: pivots[n - 2].price,
|
||||
d: pivots[n - 1].price,
|
||||
bullish: pivots[n - 1].direction < 0.0,
|
||||
}
|
||||
}
|
||||
|
||||
/// `true` when every `(value, low, high)` triple satisfies `low <= value <= high`.
|
||||
/// Harmonic detectors express their Fibonacci windows as a list of these triples;
|
||||
/// evaluating them in one expression keeps the per-triple comparison on a single
|
||||
/// line (no multi-line `&&` coverage gaps).
|
||||
pub(crate) fn ratios_in(checks: &[(f64, f64, f64)]) -> bool {
|
||||
checks
|
||||
.iter()
|
||||
.all(|&(value, low, high)| value >= low && value <= high)
|
||||
}
|
||||
|
||||
/// Build a candle sequence that drives a `SwingTracker` (or any detector built
|
||||
/// on one) to confirm exactly the given alternating pivot prices, in order.
|
||||
///
|
||||
/// `pivots` must start with a **high** and strictly alternate high/low, with
|
||||
/// each consecutive pair differing by at least the swing threshold (5%) in the
|
||||
/// correct direction (`high > adjacent low * 1.05`). The returned vector has one
|
||||
/// seed candle plus one confirming candle per pivot; pivot `k` is confirmed by
|
||||
/// candle `k + 1`. Only the high/low of each candle is meaningful — the pattern
|
||||
/// detectors read swings, not bodies.
|
||||
#[cfg(test)]
|
||||
pub(crate) fn candles_for_pivots(pivots: &[f64]) -> Vec<Candle> {
|
||||
fn bar(high: f64, low: f64, ts: i64) -> Candle {
|
||||
Candle::new(low, high, low, low, 1.0, ts).unwrap()
|
||||
}
|
||||
let mut out = vec![bar(pivots[0], pivots[0] * 0.999, 0)];
|
||||
let mut ts: i64 = 0;
|
||||
for (k, &price) in pivots.iter().enumerate() {
|
||||
ts += 1;
|
||||
let is_high = k % 2 == 0;
|
||||
let next = if k + 1 < pivots.len() {
|
||||
pivots[k + 1]
|
||||
} else if is_high {
|
||||
price * 0.90
|
||||
} else {
|
||||
price * 1.10
|
||||
};
|
||||
let candle = if is_high {
|
||||
// Reverse down from the candidate high `price` to confirm it.
|
||||
bar(price * 0.99, next, ts)
|
||||
} else {
|
||||
// Reverse up from the candidate low `price` to confirm it.
|
||||
bar(next, price * 1.01, ts)
|
||||
};
|
||||
out.push(candle);
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn c_hl(high: f64, low: f64, ts: i64) -> Candle {
|
||||
Candle::new(low, high, low, low, 1.0, ts).unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn first_bar_only_bootstraps_no_pivot() {
|
||||
let mut t = SwingTracker::new(0.05, 6);
|
||||
assert!(!t.update(c_hl(100.0, 99.5, 0)));
|
||||
assert!(t.pivots().is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn extends_candidate_high_without_confirming() {
|
||||
let mut t = SwingTracker::new(0.10, 6);
|
||||
assert!(!t.update(c_hl(100.0, 99.5, 0)));
|
||||
// A higher high merely raises the candidate — no pivot yet.
|
||||
assert!(!t.update(c_hl(110.0, 109.0, 1)));
|
||||
assert!(t.pivots().is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn uptrend_small_move_does_not_confirm() {
|
||||
let mut t = SwingTracker::new(0.10, 6);
|
||||
let _ = t.update(c_hl(100.0, 99.5, 0));
|
||||
// A 1% dip is below the 10% threshold — neither extends nor confirms.
|
||||
assert!(!t.update(c_hl(99.8, 99.0, 1)));
|
||||
assert!(t.pivots().is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn confirms_high_then_low_alternating() {
|
||||
let mut t = SwingTracker::new(0.10, 6);
|
||||
let _ = t.update(c_hl(100.0, 99.5, 0)); // seed uptrend
|
||||
let _ = t.update(c_hl(120.0, 119.5, 1)); // raise candidate high to 120
|
||||
// Drop ≥10% below 120 → confirm the high at 120, flip to downtrend.
|
||||
assert!(t.update(c_hl(101.0, 100.0, 2)));
|
||||
assert_eq!(
|
||||
t.pivots().last().copied(),
|
||||
Some(Pivot {
|
||||
price: 120.0,
|
||||
direction: 1.0,
|
||||
})
|
||||
);
|
||||
// Now in a downtrend: a lower low extends the candidate low.
|
||||
assert!(!t.update(c_hl(100.5, 90.0, 3)));
|
||||
// Rise ≥10% above 90 → confirm the low at 90.
|
||||
assert!(t.update(c_hl(100.0, 99.0, 4)));
|
||||
assert_eq!(
|
||||
t.pivots().last().copied(),
|
||||
Some(Pivot {
|
||||
price: 90.0,
|
||||
direction: -1.0,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn downtrend_tiny_rise_does_not_confirm() {
|
||||
let mut t = SwingTracker::new(0.10, 6);
|
||||
let _ = t.update(c_hl(100.0, 99.5, 0));
|
||||
let _ = t.update(c_hl(120.0, 119.5, 1));
|
||||
let _ = t.update(c_hl(101.0, 90.0, 2)); // confirm high, now downtrend at 90
|
||||
// A 1% bounce is below threshold — no confirmation, no new candidate low.
|
||||
assert!(!t.update(c_hl(91.0, 90.5, 3)));
|
||||
assert_eq!(t.pivots().len(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn history_is_capped() {
|
||||
let mut t = SwingTracker::new(0.10, 2);
|
||||
// Drive an oscillation that confirms several pivots; only the last 2 stay.
|
||||
let path = [
|
||||
(100.0, 99.5),
|
||||
(120.0, 119.5),
|
||||
(101.0, 90.0), // confirm 120 (high)
|
||||
(91.0, 90.5),
|
||||
(110.0, 109.0), // confirm 90 (low)
|
||||
(109.0, 95.0), // confirm 110 (high)
|
||||
];
|
||||
for (i, (h, l)) in path.iter().enumerate() {
|
||||
let _ = t.update(c_hl(*h, *l, i64::try_from(i).unwrap()));
|
||||
}
|
||||
assert_eq!(t.pivots().len(), 2);
|
||||
// The two most recent confirmations: low 90 then high 110.
|
||||
assert_eq!(t.pivots()[0].price, 90.0);
|
||||
assert_eq!(t.pivots()[1].price, 110.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reset_clears_state_and_history() {
|
||||
let mut t = SwingTracker::new(0.10, 6);
|
||||
let _ = t.update(c_hl(100.0, 99.5, 0));
|
||||
let _ = t.update(c_hl(120.0, 119.5, 1));
|
||||
let _ = t.update(c_hl(101.0, 90.0, 2));
|
||||
assert_eq!(t.pivots().len(), 1);
|
||||
t.reset();
|
||||
assert!(t.pivots().is_empty());
|
||||
// After reset the next bar bootstraps again (returns false).
|
||||
assert!(!t.update(c_hl(100.0, 99.5, 0)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn recent_legs_extracts_highs_and_lows_either_ending() {
|
||||
// Newest pivot a high: [low_old, high_old, low_new, high_new].
|
||||
let ending_high = [
|
||||
Pivot {
|
||||
price: 100.0,
|
||||
direction: -1.0,
|
||||
},
|
||||
Pivot {
|
||||
price: 120.0,
|
||||
direction: 1.0,
|
||||
},
|
||||
Pivot {
|
||||
price: 110.0,
|
||||
direction: -1.0,
|
||||
},
|
||||
Pivot {
|
||||
price: 121.0,
|
||||
direction: 1.0,
|
||||
},
|
||||
];
|
||||
assert_eq!(recent_legs(&ending_high), (120.0, 121.0, 100.0, 110.0));
|
||||
// Newest pivot a low: [high_old, low_old, high_new, low_new].
|
||||
let ending_low = [
|
||||
Pivot {
|
||||
price: 120.0,
|
||||
direction: 1.0,
|
||||
},
|
||||
Pivot {
|
||||
price: 100.0,
|
||||
direction: -1.0,
|
||||
},
|
||||
Pivot {
|
||||
price: 110.0,
|
||||
direction: 1.0,
|
||||
},
|
||||
Pivot {
|
||||
price: 99.0,
|
||||
direction: -1.0,
|
||||
},
|
||||
];
|
||||
assert_eq!(recent_legs(&ending_low), (120.0, 110.0, 100.0, 99.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn xabcd_reads_last_five_pivots_and_direction() {
|
||||
let pivots = [
|
||||
Pivot {
|
||||
price: 50.0,
|
||||
direction: 1.0,
|
||||
},
|
||||
Pivot {
|
||||
price: 100.0,
|
||||
direction: -1.0,
|
||||
}, // X
|
||||
Pivot {
|
||||
price: 140.0,
|
||||
direction: 1.0,
|
||||
}, // A
|
||||
Pivot {
|
||||
price: 115.0,
|
||||
direction: -1.0,
|
||||
}, // B
|
||||
Pivot {
|
||||
price: 128.0,
|
||||
direction: 1.0,
|
||||
}, // C
|
||||
Pivot {
|
||||
price: 108.0,
|
||||
direction: -1.0,
|
||||
}, // D (low → bullish)
|
||||
];
|
||||
let p = xabcd(&pivots);
|
||||
assert_eq!(
|
||||
(p.x, p.a, p.b, p.c, p.d),
|
||||
(100.0, 140.0, 115.0, 128.0, 108.0)
|
||||
);
|
||||
assert!(p.bullish);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ratios_in_checks_every_window() {
|
||||
assert!(ratios_in(&[(0.6, 0.5, 0.7), (1.5, 1.0, 2.0)]));
|
||||
assert!(!ratios_in(&[(0.6, 0.5, 0.7), (3.0, 1.0, 2.0)])); // second out of range
|
||||
assert!(!ratios_in(&[(0.4, 0.5, 0.7)])); // below the window
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn candles_for_pivots_realizes_the_requested_swings() {
|
||||
let want = [120.0, 100.0, 125.0, 95.0];
|
||||
let mut t = SwingTracker::new(0.05, 6);
|
||||
for candle in candles_for_pivots(&want) {
|
||||
let _ = t.update(candle);
|
||||
}
|
||||
let got: Vec<f64> = t.pivots().iter().map(|p| p.price).collect();
|
||||
assert_eq!(got, want);
|
||||
// Directions alternate starting from a high.
|
||||
assert_eq!(t.pivots()[0].direction, 1.0);
|
||||
assert_eq!(t.pivots()[1].direction, -1.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn approx_equal_relative_tolerance() {
|
||||
assert!(approx_equal(100.0, 102.0, 0.03)); // 2% apart, within 3%
|
||||
assert!(!approx_equal(100.0, 110.0, 0.03)); // 10% apart, outside 3%
|
||||
assert!(approx_equal(0.0, 0.0, 0.01)); // both zero
|
||||
assert!(approx_equal(-50.0, -49.0, 0.05)); // negative magnitudes
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,155 @@
|
||||
//! Rectangle / Range chart pattern.
|
||||
|
||||
use crate::indicators::pattern_swing::{
|
||||
approx_equal, recent_legs, SwingTracker, LEVEL_TOLERANCE, SWING_THRESHOLD,
|
||||
};
|
||||
use crate::ohlcv::Candle;
|
||||
use crate::traits::Indicator;
|
||||
|
||||
/// Rectangle / Range — price oscillating between a roughly horizontal support
|
||||
/// and resistance, a mean-reversion (range-trading) structure.
|
||||
///
|
||||
/// Built on confirmed swing pivots ([`SWING_THRESHOLD`] = 5%); recognised when the
|
||||
/// last two highs and the last two lows are each flat within [`LEVEL_TOLERANCE`]
|
||||
/// (3%):
|
||||
///
|
||||
/// ```text
|
||||
/// flat highs (resistance) AND flat lows (support):
|
||||
/// last pivot a low → +1 (a bounce off support — buy the range)
|
||||
/// last pivot a high → -1 (a rejection at resistance — sell the range)
|
||||
/// ```
|
||||
///
|
||||
/// Unlike the breakout patterns the rectangle is range-bound, so the sign
|
||||
/// encodes the actionable mean-reversion direction of the just-confirmed touch.
|
||||
/// Output is `+1.0` / `-1.0` / `0.0`; never `None`.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct RectangleRange {
|
||||
swing: SwingTracker,
|
||||
has_emitted: bool,
|
||||
}
|
||||
|
||||
impl RectangleRange {
|
||||
/// Construct a new Rectangle / Range detector.
|
||||
pub const fn new() -> Self {
|
||||
Self {
|
||||
swing: SwingTracker::new(SWING_THRESHOLD, 4),
|
||||
has_emitted: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for RectangleRange {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl Indicator for RectangleRange {
|
||||
type Input = Candle;
|
||||
type Output = f64;
|
||||
|
||||
fn update(&mut self, candle: Candle) -> Option<f64> {
|
||||
self.has_emitted = true;
|
||||
if !self.swing.update(candle) {
|
||||
return Some(0.0);
|
||||
}
|
||||
let pivots = self.swing.pivots();
|
||||
if pivots.len() < 4 {
|
||||
return Some(0.0);
|
||||
}
|
||||
let (high_old, high_new, low_old, low_new) = recent_legs(pivots);
|
||||
let flat_highs = approx_equal(high_old, high_new, LEVEL_TOLERANCE);
|
||||
let flat_lows = approx_equal(low_old, low_new, LEVEL_TOLERANCE);
|
||||
if flat_highs && flat_lows {
|
||||
let last_is_high = pivots[pivots.len() - 1].direction > 0.0;
|
||||
return Some(if last_is_high { -1.0 } else { 1.0 });
|
||||
}
|
||||
Some(0.0)
|
||||
}
|
||||
|
||||
fn reset(&mut self) {
|
||||
self.swing.reset();
|
||||
self.has_emitted = false;
|
||||
}
|
||||
|
||||
fn warmup_period(&self) -> usize {
|
||||
// Four confirmed pivots; the earliest confirmation of the fourth is bar 5.
|
||||
5
|
||||
}
|
||||
|
||||
fn is_ready(&self) -> bool {
|
||||
self.has_emitted
|
||||
}
|
||||
|
||||
fn name(&self) -> &'static str {
|
||||
"RectangleRange"
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::indicators::pattern_swing::candles_for_pivots;
|
||||
use crate::traits::BatchExt;
|
||||
|
||||
fn run(pivots: &[f64]) -> Vec<f64> {
|
||||
let mut indicator = RectangleRange::new();
|
||||
candles_for_pivots(pivots)
|
||||
.into_iter()
|
||||
.map(|c| indicator.update(c).unwrap())
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn accessors_and_metadata() {
|
||||
let indicator = RectangleRange::new();
|
||||
assert_eq!(indicator.name(), "RectangleRange");
|
||||
assert_eq!(indicator.warmup_period(), 5);
|
||||
assert!(!indicator.is_ready());
|
||||
assert!(!RectangleRange::default().is_ready());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn range_bounce_off_support_is_plus_one() {
|
||||
// Flat highs (120, 121), flat lows (100, 99); last pivot a low → +1.
|
||||
let out = run(&[120.0, 100.0, 121.0, 99.0]);
|
||||
assert_eq!(*out.last().unwrap(), 1.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn range_rejection_at_resistance_is_minus_one() {
|
||||
// Same range but ending on a high pivot → -1.
|
||||
let out = run(&[130.0, 100.0, 120.0, 99.0, 121.0]);
|
||||
assert_eq!(*out.last().unwrap(), -1.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn trending_highs_are_not_a_rectangle() {
|
||||
// Rising highs break the flat-resistance requirement → no rectangle.
|
||||
let out = run(&[120.0, 100.0, 140.0, 99.0]);
|
||||
assert_eq!(*out.last().unwrap(), 0.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reset_clears_state() {
|
||||
let mut indicator = RectangleRange::new();
|
||||
for c in candles_for_pivots(&[120.0, 100.0, 121.0]) {
|
||||
let _ = indicator.update(c);
|
||||
}
|
||||
indicator.reset();
|
||||
assert!(!indicator.is_ready());
|
||||
let c = Candle::new(99.5, 100.0, 99.5, 99.5, 1.0, 0).unwrap();
|
||||
assert_eq!(indicator.update(c), Some(0.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn batch_equals_streaming() {
|
||||
let candles = candles_for_pivots(&[120.0, 100.0, 121.0, 99.0]);
|
||||
let mut a = RectangleRange::new();
|
||||
let mut b = RectangleRange::new();
|
||||
assert_eq!(
|
||||
a.batch(&candles),
|
||||
candles.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,155 @@
|
||||
//! Shark harmonic pattern.
|
||||
|
||||
use crate::indicators::pattern_swing::{ratios_in, xabcd, SwingTracker, SWING_THRESHOLD};
|
||||
use crate::ohlcv::Candle;
|
||||
use crate::traits::Indicator;
|
||||
|
||||
/// Shark — a 5-point (X-A-B-C-D) harmonic pattern characterised by an
|
||||
/// **expansion** leg (AB longer than XA) and a `0.886`–`1.13` D completion:
|
||||
///
|
||||
/// ```text
|
||||
/// AB / XA ∈ [1.13, 1.618] (expansion — B overshoots X)
|
||||
/// BC / AB ∈ [1.618, 2.24]
|
||||
/// CD / BC ∈ [0.382, 0.886]
|
||||
/// AD / XA ∈ [0.886, 1.13] (the defining D completion near A)
|
||||
/// ```
|
||||
///
|
||||
/// This is the 5-point reading of the Shark; output is `+1.0` (bullish, D a
|
||||
/// swing low), `-1.0` (bearish, D a swing high), or `0.0`; never `None`. See
|
||||
/// `crates/wickra-core/src/indicators/shark.rs`.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Shark {
|
||||
swing: SwingTracker,
|
||||
has_emitted: bool,
|
||||
}
|
||||
|
||||
impl Shark {
|
||||
/// Construct a new Shark detector.
|
||||
pub const fn new() -> Self {
|
||||
Self {
|
||||
swing: SwingTracker::new(SWING_THRESHOLD, 5),
|
||||
has_emitted: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Shark {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl Indicator for Shark {
|
||||
type Input = Candle;
|
||||
type Output = f64;
|
||||
|
||||
fn update(&mut self, candle: Candle) -> Option<f64> {
|
||||
self.has_emitted = true;
|
||||
if !self.swing.update(candle) {
|
||||
return Some(0.0);
|
||||
}
|
||||
let pivots = self.swing.pivots();
|
||||
if pivots.len() < 5 {
|
||||
return Some(0.0);
|
||||
}
|
||||
let p = xabcd(pivots);
|
||||
let xa = (p.a - p.x).abs();
|
||||
let ab = (p.b - p.a).abs();
|
||||
let bc = (p.c - p.b).abs();
|
||||
let cd = (p.d - p.c).abs();
|
||||
let ad = (p.d - p.a).abs();
|
||||
let matched = ratios_in(&[
|
||||
(ab / xa, 1.13, 1.618),
|
||||
(bc / ab, 1.618, 2.24),
|
||||
(cd / bc, 0.382, 0.886),
|
||||
(ad / xa, 0.886, 1.13),
|
||||
]);
|
||||
if matched {
|
||||
return Some(if p.bullish { 1.0 } else { -1.0 });
|
||||
}
|
||||
Some(0.0)
|
||||
}
|
||||
|
||||
fn reset(&mut self) {
|
||||
self.swing.reset();
|
||||
self.has_emitted = false;
|
||||
}
|
||||
|
||||
fn warmup_period(&self) -> usize {
|
||||
6
|
||||
}
|
||||
|
||||
fn is_ready(&self) -> bool {
|
||||
self.has_emitted
|
||||
}
|
||||
|
||||
fn name(&self) -> &'static str {
|
||||
"Shark"
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::indicators::pattern_swing::candles_for_pivots;
|
||||
use crate::traits::BatchExt;
|
||||
|
||||
fn run(pivots: &[f64]) -> Vec<f64> {
|
||||
let mut indicator = Shark::new();
|
||||
candles_for_pivots(pivots)
|
||||
.into_iter()
|
||||
.map(|c| indicator.update(c).unwrap())
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn accessors_and_metadata() {
|
||||
let indicator = Shark::new();
|
||||
assert_eq!(indicator.name(), "Shark");
|
||||
assert_eq!(indicator.warmup_period(), 6);
|
||||
assert!(!indicator.is_ready());
|
||||
assert!(!Shark::default().is_ready());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bullish_shark_is_plus_one() {
|
||||
let out = run(&[150.0, 100.0, 140.0, 88.0, 186.8, 100.0]);
|
||||
assert_eq!(*out.last().unwrap(), 1.0);
|
||||
assert!(out[..out.len() - 1].iter().all(|&x| x == 0.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bearish_shark_is_minus_one() {
|
||||
let out = run(&[150.0, 110.0, 162.0, 60.2, 150.0]);
|
||||
assert_eq!(*out.last().unwrap(), -1.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn out_of_ratio_does_not_trigger() {
|
||||
let out = run(&[150.0, 100.0, 140.0, 110.0, 135.0, 105.0]);
|
||||
assert_eq!(*out.last().unwrap(), 0.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reset_clears_state() {
|
||||
let mut indicator = Shark::new();
|
||||
for c in candles_for_pivots(&[150.0, 100.0, 140.0]) {
|
||||
let _ = indicator.update(c);
|
||||
}
|
||||
indicator.reset();
|
||||
assert!(!indicator.is_ready());
|
||||
let c = Candle::new(99.5, 100.0, 99.5, 99.5, 1.0, 0).unwrap();
|
||||
assert_eq!(indicator.update(c), Some(0.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn batch_equals_streaming() {
|
||||
let candles = candles_for_pivots(&[150.0, 100.0, 140.0, 88.0, 186.8, 100.0]);
|
||||
let mut a = Shark::new();
|
||||
let mut b = Shark::new();
|
||||
assert_eq!(
|
||||
a.batch(&candles),
|
||||
candles.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,155 @@
|
||||
//! Three Drives harmonic pattern.
|
||||
|
||||
use crate::indicators::pattern_swing::{
|
||||
approx_equal, ratios_in, xabcd, SwingTracker, SWING_THRESHOLD,
|
||||
};
|
||||
use crate::ohlcv::Candle;
|
||||
use crate::traits::Indicator;
|
||||
|
||||
/// Three Drives — a symmetric harmonic pattern of two visible drives separated
|
||||
/// by two retracements, read from the last five pivots `X-A-B-C-D` (the two
|
||||
/// drive legs are `A→B` and `C→D`):
|
||||
///
|
||||
/// ```text
|
||||
/// AB / XA ∈ [1.13, 1.75] (drive 1 extends the prior retracement)
|
||||
/// CD / BC ∈ [1.13, 1.75] (drive 2 extends symmetrically)
|
||||
/// AB ≈ CD (within 20%) (the two drives are similar in size)
|
||||
/// XA ≈ BC (within 30%) (the two retracements are similar)
|
||||
/// ```
|
||||
///
|
||||
/// Output is `+1.0` (bullish, terminal D a swing low — drives down), `-1.0`
|
||||
/// (bearish, drives up), or `0.0`; never `None`. See
|
||||
/// `crates/wickra-core/src/indicators/three_drives.rs`.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ThreeDrives {
|
||||
swing: SwingTracker,
|
||||
has_emitted: bool,
|
||||
}
|
||||
|
||||
impl ThreeDrives {
|
||||
/// Construct a new Three Drives detector.
|
||||
pub const fn new() -> Self {
|
||||
Self {
|
||||
swing: SwingTracker::new(SWING_THRESHOLD, 5),
|
||||
has_emitted: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for ThreeDrives {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl Indicator for ThreeDrives {
|
||||
type Input = Candle;
|
||||
type Output = f64;
|
||||
|
||||
fn update(&mut self, candle: Candle) -> Option<f64> {
|
||||
self.has_emitted = true;
|
||||
if !self.swing.update(candle) {
|
||||
return Some(0.0);
|
||||
}
|
||||
let pivots = self.swing.pivots();
|
||||
if pivots.len() < 5 {
|
||||
return Some(0.0);
|
||||
}
|
||||
let p = xabcd(pivots);
|
||||
let xa = (p.a - p.x).abs();
|
||||
let ab = (p.b - p.a).abs();
|
||||
let bc = (p.c - p.b).abs();
|
||||
let cd = (p.d - p.c).abs();
|
||||
let extensions = ratios_in(&[(ab / xa, 1.13, 1.75), (cd / bc, 1.13, 1.75)]);
|
||||
let symmetric = approx_equal(ab, cd, 0.20) && approx_equal(xa, bc, 0.30);
|
||||
if extensions && symmetric {
|
||||
return Some(if p.bullish { 1.0 } else { -1.0 });
|
||||
}
|
||||
Some(0.0)
|
||||
}
|
||||
|
||||
fn reset(&mut self) {
|
||||
self.swing.reset();
|
||||
self.has_emitted = false;
|
||||
}
|
||||
|
||||
fn warmup_period(&self) -> usize {
|
||||
6
|
||||
}
|
||||
|
||||
fn is_ready(&self) -> bool {
|
||||
self.has_emitted
|
||||
}
|
||||
|
||||
fn name(&self) -> &'static str {
|
||||
"ThreeDrives"
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::indicators::pattern_swing::candles_for_pivots;
|
||||
use crate::traits::BatchExt;
|
||||
|
||||
fn run(pivots: &[f64]) -> Vec<f64> {
|
||||
let mut indicator = ThreeDrives::new();
|
||||
candles_for_pivots(pivots)
|
||||
.into_iter()
|
||||
.map(|c| indicator.update(c).unwrap())
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn accessors_and_metadata() {
|
||||
let indicator = ThreeDrives::new();
|
||||
assert_eq!(indicator.name(), "ThreeDrives");
|
||||
assert_eq!(indicator.warmup_period(), 6);
|
||||
assert!(!indicator.is_ready());
|
||||
assert!(!ThreeDrives::default().is_ready());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bearish_three_drives_is_minus_one() {
|
||||
// Three rising drives (120, 128, 136) → bearish exhaustion.
|
||||
let out = run(&[120.0, 100.0, 128.0, 108.0, 136.0]);
|
||||
assert_eq!(*out.last().unwrap(), -1.0);
|
||||
assert!(out[..out.len() - 1].iter().all(|&x| x == 0.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bullish_three_drives_is_plus_one() {
|
||||
// Three falling drives → bullish exhaustion.
|
||||
let out = run(&[150.0, 120.0, 140.0, 112.0, 132.0, 104.0]);
|
||||
assert_eq!(*out.last().unwrap(), 1.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn asymmetric_drives_do_not_trigger() {
|
||||
let out = run(&[150.0, 100.0, 140.0, 110.0, 135.0, 105.0]);
|
||||
assert_eq!(*out.last().unwrap(), 0.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reset_clears_state() {
|
||||
let mut indicator = ThreeDrives::new();
|
||||
for c in candles_for_pivots(&[120.0, 100.0, 128.0]) {
|
||||
let _ = indicator.update(c);
|
||||
}
|
||||
indicator.reset();
|
||||
assert!(!indicator.is_ready());
|
||||
let c = Candle::new(99.5, 100.0, 99.5, 99.5, 1.0, 0).unwrap();
|
||||
assert_eq!(indicator.update(c), Some(0.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn batch_equals_streaming() {
|
||||
let candles = candles_for_pivots(&[120.0, 100.0, 128.0, 108.0, 136.0]);
|
||||
let mut a = ThreeDrives::new();
|
||||
let mut b = ThreeDrives::new();
|
||||
assert_eq!(
|
||||
a.batch(&candles),
|
||||
candles.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,181 @@
|
||||
//! Triangle (ascending / descending / symmetrical) chart pattern.
|
||||
|
||||
use crate::indicators::pattern_swing::{
|
||||
approx_equal, recent_legs, SwingTracker, LEVEL_TOLERANCE, SWING_THRESHOLD,
|
||||
};
|
||||
use crate::ohlcv::Candle;
|
||||
use crate::traits::Indicator;
|
||||
|
||||
/// Triangle — a consolidation pattern bounded by two converging trendlines,
|
||||
/// detected from the two most recent swing highs and lows.
|
||||
///
|
||||
/// Built on confirmed swing pivots ([`SWING_THRESHOLD`] = 5%); evaluated on every
|
||||
/// bar that confirms a new pivot once four pivots exist:
|
||||
///
|
||||
/// ```text
|
||||
/// ascending : flat highs + rising lows → +1 (bullish bias)
|
||||
/// descending : falling highs + flat lows → -1 (bearish bias)
|
||||
/// symmetrical : falling highs + rising lows → +1 if the last pivot is a low
|
||||
/// (an up-bounce), else -1
|
||||
/// ```
|
||||
///
|
||||
/// "Flat" means the two highs (or lows) are within [`LEVEL_TOLERANCE`] (3%) of
|
||||
/// each other; "rising"/"falling" means they differ by more than that tolerance.
|
||||
/// The symmetrical case is directionally neutral, so its sign follows the
|
||||
/// momentum of the most recently confirmed swing. Output is `+1.0` / `-1.0` /
|
||||
/// `0.0`; never `None`.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Triangle {
|
||||
swing: SwingTracker,
|
||||
has_emitted: bool,
|
||||
}
|
||||
|
||||
impl Triangle {
|
||||
/// Construct a new Triangle detector.
|
||||
pub const fn new() -> Self {
|
||||
Self {
|
||||
swing: SwingTracker::new(SWING_THRESHOLD, 4),
|
||||
has_emitted: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Triangle {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl Indicator for Triangle {
|
||||
type Input = Candle;
|
||||
type Output = f64;
|
||||
|
||||
fn update(&mut self, candle: Candle) -> Option<f64> {
|
||||
self.has_emitted = true;
|
||||
if !self.swing.update(candle) {
|
||||
return Some(0.0);
|
||||
}
|
||||
let pivots = self.swing.pivots();
|
||||
if pivots.len() < 4 {
|
||||
return Some(0.0);
|
||||
}
|
||||
let (high_old, high_new, low_old, low_new) = recent_legs(pivots);
|
||||
let flat_highs = approx_equal(high_old, high_new, LEVEL_TOLERANCE);
|
||||
let flat_lows = approx_equal(low_old, low_new, LEVEL_TOLERANCE);
|
||||
let rising_lows = low_new > low_old * (1.0 + LEVEL_TOLERANCE);
|
||||
let falling_highs = high_new < high_old * (1.0 - LEVEL_TOLERANCE);
|
||||
let last_is_high = pivots[pivots.len() - 1].direction > 0.0;
|
||||
|
||||
if flat_highs && rising_lows {
|
||||
return Some(1.0); // ascending
|
||||
}
|
||||
if falling_highs && flat_lows {
|
||||
return Some(-1.0); // descending
|
||||
}
|
||||
if falling_highs && rising_lows {
|
||||
// symmetrical: lean with the latest swing's momentum.
|
||||
return Some(if last_is_high { -1.0 } else { 1.0 });
|
||||
}
|
||||
Some(0.0)
|
||||
}
|
||||
|
||||
fn reset(&mut self) {
|
||||
self.swing.reset();
|
||||
self.has_emitted = false;
|
||||
}
|
||||
|
||||
fn warmup_period(&self) -> usize {
|
||||
// Four confirmed pivots; the earliest confirmation of the fourth is bar 5.
|
||||
5
|
||||
}
|
||||
|
||||
fn is_ready(&self) -> bool {
|
||||
self.has_emitted
|
||||
}
|
||||
|
||||
fn name(&self) -> &'static str {
|
||||
"Triangle"
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::indicators::pattern_swing::candles_for_pivots;
|
||||
use crate::traits::BatchExt;
|
||||
|
||||
fn run(pivots: &[f64]) -> Vec<f64> {
|
||||
let mut indicator = Triangle::new();
|
||||
candles_for_pivots(pivots)
|
||||
.into_iter()
|
||||
.map(|c| indicator.update(c).unwrap())
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn accessors_and_metadata() {
|
||||
let indicator = Triangle::new();
|
||||
assert_eq!(indicator.name(), "Triangle");
|
||||
assert_eq!(indicator.warmup_period(), 5);
|
||||
assert!(!indicator.is_ready());
|
||||
assert!(!Triangle::default().is_ready());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ascending_triangle_is_plus_one() {
|
||||
// Flat highs (120, 120), rising lows (100 → 110).
|
||||
let out = run(&[130.0, 100.0, 120.0, 110.0, 120.0]);
|
||||
assert_eq!(*out.last().unwrap(), 1.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn descending_triangle_is_minus_one() {
|
||||
// Falling highs (120 → 110), flat lows (100, 99).
|
||||
let out = run(&[120.0, 100.0, 110.0, 99.0]);
|
||||
assert_eq!(*out.last().unwrap(), -1.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn symmetrical_triangle_ending_low_is_plus_one() {
|
||||
// Falling highs (120 → 113), rising lows (100 → 106); last pivot a low.
|
||||
let out = run(&[120.0, 100.0, 113.0, 106.0]);
|
||||
assert_eq!(*out.last().unwrap(), 1.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn symmetrical_triangle_ending_high_is_minus_one() {
|
||||
// Same convergence but ending on a high pivot.
|
||||
let out = run(&[130.0, 100.0, 120.0, 106.0, 113.0]);
|
||||
assert_eq!(*out.last().unwrap(), -1.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn expanding_swings_are_not_a_triangle() {
|
||||
// Rising highs and falling lows (broadening) → no converging triangle.
|
||||
let out = run(&[110.0, 100.0, 130.0, 80.0]);
|
||||
assert_eq!(*out.last().unwrap(), 0.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reset_clears_state() {
|
||||
let mut indicator = Triangle::new();
|
||||
for c in candles_for_pivots(&[130.0, 100.0, 120.0]) {
|
||||
let _ = indicator.update(c);
|
||||
}
|
||||
indicator.reset();
|
||||
assert!(!indicator.is_ready());
|
||||
let c = Candle::new(99.5, 100.0, 99.5, 99.5, 1.0, 0).unwrap();
|
||||
assert_eq!(indicator.update(c), Some(0.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn batch_equals_streaming() {
|
||||
let candles = candles_for_pivots(&[130.0, 100.0, 120.0, 110.0, 120.0]);
|
||||
let mut a = Triangle::new();
|
||||
let mut b = Triangle::new();
|
||||
assert_eq!(
|
||||
a.batch(&candles),
|
||||
candles.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,160 @@
|
||||
//! Triple Top / Triple Bottom reversal chart pattern.
|
||||
|
||||
use crate::indicators::pattern_swing::{
|
||||
approx_equal, SwingTracker, LEVEL_TOLERANCE, SWING_THRESHOLD,
|
||||
};
|
||||
use crate::ohlcv::Candle;
|
||||
use crate::traits::Indicator;
|
||||
|
||||
/// Triple Top / Triple Bottom — a three-peak (or three-trough) reversal pattern,
|
||||
/// a stronger variant of the double top/bottom.
|
||||
///
|
||||
/// Built on confirmed swing pivots ([`SWING_THRESHOLD`] = 5%). A pattern is
|
||||
/// recognised on the bar that confirms the **third** matching extreme:
|
||||
///
|
||||
/// ```text
|
||||
/// triple top : High₁ , Low , High₂ , Low , High₃ High₁ ≈ High₂ ≈ High₃ → -1
|
||||
/// triple bottom : Low₁ , High, Low₂ , High, Low₃ Low₁ ≈ Low₂ ≈ Low₃ → +1
|
||||
/// ```
|
||||
///
|
||||
/// The three same-direction extremes (positions `n-5`, `n-3`, `n-1` in the pivot
|
||||
/// history) must all lie within [`LEVEL_TOLERANCE`] (3%) of one another.
|
||||
///
|
||||
/// Output is `+1.0` for a triple bottom, `-1.0` for a triple top, and `0.0`
|
||||
/// otherwise; never `None`.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct TripleTopBottom {
|
||||
swing: SwingTracker,
|
||||
has_emitted: bool,
|
||||
}
|
||||
|
||||
impl TripleTopBottom {
|
||||
/// Construct a new Triple Top / Triple Bottom detector.
|
||||
pub const fn new() -> Self {
|
||||
Self {
|
||||
swing: SwingTracker::new(SWING_THRESHOLD, 5),
|
||||
has_emitted: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for TripleTopBottom {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl Indicator for TripleTopBottom {
|
||||
type Input = Candle;
|
||||
type Output = f64;
|
||||
|
||||
fn update(&mut self, candle: Candle) -> Option<f64> {
|
||||
self.has_emitted = true;
|
||||
if !self.swing.update(candle) {
|
||||
return Some(0.0);
|
||||
}
|
||||
let pivots = self.swing.pivots();
|
||||
if pivots.len() < 5 {
|
||||
return Some(0.0);
|
||||
}
|
||||
let n = pivots.len();
|
||||
let first = pivots[n - 5];
|
||||
let middle = pivots[n - 3];
|
||||
let last = pivots[n - 1];
|
||||
let outer_match = approx_equal(first.price, middle.price, LEVEL_TOLERANCE);
|
||||
let inner_match = approx_equal(middle.price, last.price, LEVEL_TOLERANCE);
|
||||
if outer_match && inner_match {
|
||||
return Some(if last.direction > 0.0 { -1.0 } else { 1.0 });
|
||||
}
|
||||
Some(0.0)
|
||||
}
|
||||
|
||||
fn reset(&mut self) {
|
||||
self.swing.reset();
|
||||
self.has_emitted = false;
|
||||
}
|
||||
|
||||
fn warmup_period(&self) -> usize {
|
||||
// Five confirmed pivots are needed; the earliest bar that can confirm a
|
||||
// fifth pivot is the sixth.
|
||||
6
|
||||
}
|
||||
|
||||
fn is_ready(&self) -> bool {
|
||||
self.has_emitted
|
||||
}
|
||||
|
||||
fn name(&self) -> &'static str {
|
||||
"TripleTopBottom"
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::indicators::pattern_swing::candles_for_pivots;
|
||||
use crate::traits::BatchExt;
|
||||
|
||||
fn run(pivots: &[f64]) -> Vec<f64> {
|
||||
let mut indicator = TripleTopBottom::new();
|
||||
candles_for_pivots(pivots)
|
||||
.into_iter()
|
||||
.map(|c| indicator.update(c).unwrap())
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn accessors_and_metadata() {
|
||||
let indicator = TripleTopBottom::new();
|
||||
assert_eq!(indicator.name(), "TripleTopBottom");
|
||||
assert_eq!(indicator.warmup_period(), 6);
|
||||
assert!(!indicator.is_ready());
|
||||
assert!(!TripleTopBottom::default().is_ready());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn triple_top_is_minus_one() {
|
||||
// Three ~equal highs (120, 121, 119) → triple top on the third.
|
||||
let out = run(&[120.0, 100.0, 121.0, 99.0, 119.0]);
|
||||
assert_eq!(*out.last().unwrap(), -1.0);
|
||||
assert!(out[..out.len() - 1].iter().all(|&x| x == 0.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn triple_bottom_is_plus_one() {
|
||||
// Lead high then three ~equal lows (100, 99, 101) → triple bottom.
|
||||
let out = run(&[130.0, 100.0, 120.0, 99.0, 122.0, 101.0]);
|
||||
assert_eq!(*out.last().unwrap(), 1.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unequal_third_peak_does_not_trigger() {
|
||||
// Third high (140) diverges from the first two (120, 121) → no pattern.
|
||||
let out = run(&[120.0, 100.0, 121.0, 99.0, 140.0]);
|
||||
assert_eq!(*out.last().unwrap(), 0.0);
|
||||
assert!(out.iter().all(|&x| x == 0.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reset_clears_state() {
|
||||
let mut indicator = TripleTopBottom::new();
|
||||
for c in candles_for_pivots(&[120.0, 100.0, 121.0]) {
|
||||
let _ = indicator.update(c);
|
||||
}
|
||||
indicator.reset();
|
||||
assert!(!indicator.is_ready());
|
||||
let c = Candle::new(99.5, 100.0, 99.5, 99.5, 1.0, 0).unwrap();
|
||||
assert_eq!(indicator.update(c), Some(0.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn batch_equals_streaming() {
|
||||
let candles = candles_for_pivots(&[120.0, 100.0, 121.0, 99.0, 119.0]);
|
||||
let mut a = TripleTopBottom::new();
|
||||
let mut b = TripleTopBottom::new();
|
||||
assert_eq!(
|
||||
a.batch(&candles),
|
||||
candles.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,157 @@
|
||||
//! Wedge (rising / falling) reversal chart pattern.
|
||||
|
||||
use crate::indicators::pattern_swing::{recent_legs, SwingTracker, SWING_THRESHOLD};
|
||||
use crate::ohlcv::Candle;
|
||||
use crate::traits::Indicator;
|
||||
|
||||
/// Wedge — a pattern where both trendlines slope the same way but converge,
|
||||
/// signalling exhaustion of the prevailing move.
|
||||
///
|
||||
/// Built on confirmed swing pivots ([`SWING_THRESHOLD`] = 5%); evaluated from the
|
||||
/// last two swing highs and lows:
|
||||
///
|
||||
/// ```text
|
||||
/// rising wedge : highs rising AND lows rising, lows rising faster → -1 (bearish)
|
||||
/// falling wedge : highs falling AND lows falling, highs falling faster → +1 (bullish)
|
||||
/// ```
|
||||
///
|
||||
/// Convergence is the key: in a rising wedge the lower trendline climbs faster
|
||||
/// than the upper (the range narrows from below); in a falling wedge the upper
|
||||
/// trendline drops faster than the lower. Output is `+1.0` / `-1.0` / `0.0`;
|
||||
/// never `None`.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Wedge {
|
||||
swing: SwingTracker,
|
||||
has_emitted: bool,
|
||||
}
|
||||
|
||||
impl Wedge {
|
||||
/// Construct a new Wedge detector.
|
||||
pub const fn new() -> Self {
|
||||
Self {
|
||||
swing: SwingTracker::new(SWING_THRESHOLD, 4),
|
||||
has_emitted: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Wedge {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl Indicator for Wedge {
|
||||
type Input = Candle;
|
||||
type Output = f64;
|
||||
|
||||
fn update(&mut self, candle: Candle) -> Option<f64> {
|
||||
self.has_emitted = true;
|
||||
if !self.swing.update(candle) {
|
||||
return Some(0.0);
|
||||
}
|
||||
let pivots = self.swing.pivots();
|
||||
if pivots.len() < 4 {
|
||||
return Some(0.0);
|
||||
}
|
||||
let (high_old, high_new, low_old, low_new) = recent_legs(pivots);
|
||||
let high_slope = high_new - high_old;
|
||||
let low_slope = low_new - low_old;
|
||||
|
||||
// Rising wedge: both lines slope up, lower line steeper (converging) → bearish.
|
||||
if high_slope > 0.0 && low_slope > 0.0 && low_slope > high_slope {
|
||||
return Some(-1.0);
|
||||
}
|
||||
// Falling wedge: both lines slope down, upper line steeper → bullish.
|
||||
if high_slope < 0.0 && low_slope < 0.0 && high_slope < low_slope {
|
||||
return Some(1.0);
|
||||
}
|
||||
Some(0.0)
|
||||
}
|
||||
|
||||
fn reset(&mut self) {
|
||||
self.swing.reset();
|
||||
self.has_emitted = false;
|
||||
}
|
||||
|
||||
fn warmup_period(&self) -> usize {
|
||||
// Four confirmed pivots; the earliest confirmation of the fourth is bar 5.
|
||||
5
|
||||
}
|
||||
|
||||
fn is_ready(&self) -> bool {
|
||||
self.has_emitted
|
||||
}
|
||||
|
||||
fn name(&self) -> &'static str {
|
||||
"Wedge"
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::indicators::pattern_swing::candles_for_pivots;
|
||||
use crate::traits::BatchExt;
|
||||
|
||||
fn run(pivots: &[f64]) -> Vec<f64> {
|
||||
let mut indicator = Wedge::new();
|
||||
candles_for_pivots(pivots)
|
||||
.into_iter()
|
||||
.map(|c| indicator.update(c).unwrap())
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn accessors_and_metadata() {
|
||||
let indicator = Wedge::new();
|
||||
assert_eq!(indicator.name(), "Wedge");
|
||||
assert_eq!(indicator.warmup_period(), 5);
|
||||
assert!(!indicator.is_ready());
|
||||
assert!(!Wedge::default().is_ready());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rising_wedge_is_minus_one() {
|
||||
// Highs 100 → 103 (+3), lows 90 → 94 (+4, steeper) → rising wedge.
|
||||
let out = run(&[110.0, 90.0, 100.0, 94.0, 103.0]);
|
||||
assert_eq!(*out.last().unwrap(), -1.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn falling_wedge_is_plus_one() {
|
||||
// Highs 120 → 106 (-14, steeper), lows 100 → 99 (-1) → falling wedge.
|
||||
let out = run(&[120.0, 100.0, 106.0, 99.0]);
|
||||
assert_eq!(*out.last().unwrap(), 1.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn diverging_swings_are_not_a_wedge() {
|
||||
// Rising highs but falling lows (broadening) → no wedge.
|
||||
let out = run(&[110.0, 100.0, 130.0, 80.0]);
|
||||
assert_eq!(*out.last().unwrap(), 0.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reset_clears_state() {
|
||||
let mut indicator = Wedge::new();
|
||||
for c in candles_for_pivots(&[110.0, 90.0, 100.0]) {
|
||||
let _ = indicator.update(c);
|
||||
}
|
||||
indicator.reset();
|
||||
assert!(!indicator.is_ready());
|
||||
let c = Candle::new(99.5, 100.0, 99.5, 99.5, 1.0, 0).unwrap();
|
||||
assert_eq!(indicator.update(c), Some(0.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn batch_equals_streaming() {
|
||||
let candles = candles_for_pivots(&[110.0, 90.0, 100.0, 94.0, 103.0]);
|
||||
let mut a = Wedge::new();
|
||||
let mut b = Wedge::new();
|
||||
assert_eq!(
|
||||
a.batch(&candles),
|
||||
candles.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -56,55 +56,57 @@ pub use cross_section::{CrossSection, Member};
|
||||
pub use derivatives::DerivativesTick;
|
||||
pub use error::{Error, Result};
|
||||
pub use indicators::{
|
||||
AbandonedBaby, AbsoluteBreadthIndex, AccelerationBands, AccelerationBandsOutput,
|
||||
AbandonedBaby, Abcd, AbsoluteBreadthIndex, AccelerationBands, AccelerationBandsOutput,
|
||||
AcceleratorOscillator, AdOscillator, AdVolumeLine, AdaptiveCycle, Adl, AdvanceBlock,
|
||||
AdvanceDecline, AdvanceDeclineRatio, Adx, AdxOutput, Adxr, Alligator, AlligatorOutput, Alma,
|
||||
Alpha, AnchoredRsi, AnchoredVwap, Apo, Aroon, AroonOscillator, AroonOutput, Atr, AtrBands,
|
||||
AtrBandsOutput, AtrTrailingStop, Autocorrelation, AverageDailyRange, AverageDrawdown, AvgPrice,
|
||||
AwesomeOscillator, AwesomeOscillatorHistogram, BalanceOfPower, BeltHold, Beta,
|
||||
AwesomeOscillator, AwesomeOscillatorHistogram, BalanceOfPower, Bat, BeltHold, Beta,
|
||||
BetaNeutralSpread, BollingerBands, BollingerBandwidth, BollingerOutput, BreadthThrust,
|
||||
Breakaway, BullishPercentIndex, CalendarSpread, CalmarRatio, Camarilla, CamarillaPivotsOutput,
|
||||
Cci, CenterOfGravity, Cfo, ChaikinMoneyFlow, ChaikinOscillator, ChaikinVolatility,
|
||||
ChandeKrollStop, ChandeKrollStopOutput, ChandelierExit, ChandelierExitOutput, ChoppinessIndex,
|
||||
ClassicPivots, ClassicPivotsOutput, ClosingMarubozu, Cmo, CoefficientOfVariation,
|
||||
Cointegration, CointegrationOutput, ConcealingBabySwallow, ConditionalValueAtRisk, ConnorsRsi,
|
||||
Coppock, Counterattack, CumulativeVolumeDelta, CumulativeVolumeIndex, CyberneticCycle,
|
||||
DayOfWeekProfile, DayOfWeekProfileOutput, Decycler, DecyclerOscillator, Dema, DemandIndex,
|
||||
DemarkPivots, DemarkPivotsOutput, DepthSlope, DetrendedStdDev, DistanceSsd, Doji, DojiStar,
|
||||
Donchian, DonchianOutput, DonchianStop, DonchianStopOutput, DoubleBollinger,
|
||||
DoubleBollingerOutput, DownsideGapThreeMethods, Dpo, DragonflyDoji, DrawdownDuration, Dx,
|
||||
Breakaway, BullishPercentIndex, Butterfly, CalendarSpread, CalmarRatio, Camarilla,
|
||||
CamarillaPivotsOutput, Cci, CenterOfGravity, Cfo, ChaikinMoneyFlow, ChaikinOscillator,
|
||||
ChaikinVolatility, ChandeKrollStop, ChandeKrollStopOutput, ChandelierExit,
|
||||
ChandelierExitOutput, ChoppinessIndex, ClassicPivots, ClassicPivotsOutput, ClosingMarubozu,
|
||||
Cmo, CoefficientOfVariation, Cointegration, CointegrationOutput, ConcealingBabySwallow,
|
||||
ConditionalValueAtRisk, ConnorsRsi, Coppock, Counterattack, Crab, CumulativeVolumeDelta,
|
||||
CumulativeVolumeIndex, CupAndHandle, CyberneticCycle, Cypher, DayOfWeekProfile,
|
||||
DayOfWeekProfileOutput, Decycler, DecyclerOscillator, Dema, DemandIndex, DemarkPivots,
|
||||
DemarkPivotsOutput, DepthSlope, DetrendedStdDev, DistanceSsd, Doji, DojiStar, Donchian,
|
||||
DonchianOutput, DonchianStop, DonchianStopOutput, DoubleBollinger, DoubleBollingerOutput,
|
||||
DoubleTopBottom, DownsideGapThreeMethods, Dpo, DragonflyDoji, DrawdownDuration, Dx,
|
||||
EaseOfMovement, EffectiveSpread, EhlersStochastic, ElderImpulse, Ema,
|
||||
EmpiricalModeDecomposition, Engulfing, EveningDojiStar, Evwma, FallingThreeMethods, Fama,
|
||||
FibonacciPivots, FibonacciPivotsOutput, FisherTransform, Footprint, FootprintOutput,
|
||||
ForceIndex, FractalChaosBands, FractalChaosBandsOutput, Frama, FundingBasis, FundingRate,
|
||||
FundingRateMean, FundingRateZScore, GainLossRatio, GapSideBySideWhite, GarmanKlassVolatility,
|
||||
GrangerCausality, GravestoneDoji, Hammer, HangingMan, Harami, HeikinAshi, HeikinAshiOutput,
|
||||
HiLoActivator, HighLowIndex, HighWave, Hikkake, HikkakeModified, HilbertDominantCycle,
|
||||
HistoricalVolatility, Hma, HomingPigeon, HtDcPhase, HtPhasor, HtPhasorOutput, HtTrendMode,
|
||||
HurstChannel, HurstChannelOutput, HurstExponent, Ichimoku, IchimokuOutput, IdenticalThreeCrows,
|
||||
InNeck, Inertia, InformationRatio, InitialBalance, InitialBalanceOutput,
|
||||
InstantaneousTrendline, IntradayVolatilityProfile, IntradayVolatilityProfileOutput,
|
||||
InverseFisherTransform, InvertedHammer, Jma, KagiBars, KalmanHedgeRatio,
|
||||
KalmanHedgeRatioOutput, Kama, KellyCriterion, Keltner, KeltnerOutput, Kicking, KickingByLength,
|
||||
Kst, KstOutput, Kurtosis, Kvo, KylesLambda, LadderBottom, LaguerreRsi, LeadLagCrossCorrelation,
|
||||
LeadLagCrossCorrelationOutput, LinRegAngle, LinRegChannel, LinRegChannelOutput,
|
||||
LinRegIntercept, LinRegSlope, LinearRegression, LiquidationFeatures, LiquidationFeaturesOutput,
|
||||
LongLeggedDoji, LongLine, LongShortRatio, MaEnvelope, MaEnvelopeOutput, MacdExt, MacdFix,
|
||||
MacdIndicator, MacdOutput, Mama, MamaOutput, MarketFacilitationIndex, Marubozu, MassIndex,
|
||||
MatHold, MatchingLow, MaxDrawdown, McClellanOscillator, McClellanSummationIndex,
|
||||
McGinleyDynamic, MedianAbsoluteDeviation, MedianPrice, Mfi, Microprice, MidPoint, MidPrice,
|
||||
MinusDi, MinusDm, Mom, MorningDojiStar, MorningEveningStar, Natr, NewHighsNewLows, Nvi,
|
||||
OIPriceDivergence, OIWeighted, Obv, OmegaRatio, OnNeck, OpenInterestDelta, OpeningMarubozu,
|
||||
OpeningRange, OpeningRangeOutput, OrderBookImbalanceFull, OrderBookImbalanceTop1,
|
||||
OrderBookImbalanceTopN, OuHalfLife, OvernightGap, OvernightIntradayReturn,
|
||||
OvernightIntradayReturnOutput, PainIndex, PairSpreadZScore, PairwiseBeta, ParkinsonVolatility,
|
||||
PearsonCorrelation, PercentAboveMa, PercentB, PercentageTrailingStop, Pgo, PiercingDarkCloud,
|
||||
PlusDi, PlusDm, Pmo, PointAndFigureBars, Ppo, ProfitFactor, Psar, Pvi, QuotedSpread, RSquared,
|
||||
RealizedSpread, RecoveryFactor, RelativeStrengthAB, RelativeStrengthOutput, RenkoBars,
|
||||
FibonacciPivots, FibonacciPivotsOutput, FisherTransform, FlagPennant, Footprint,
|
||||
FootprintOutput, ForceIndex, FractalChaosBands, FractalChaosBandsOutput, Frama, FundingBasis,
|
||||
FundingRate, FundingRateMean, FundingRateZScore, GainLossRatio, GapSideBySideWhite,
|
||||
GarmanKlassVolatility, Gartley, GrangerCausality, GravestoneDoji, Hammer, HangingMan, Harami,
|
||||
HeadAndShoulders, HeikinAshi, HeikinAshiOutput, HiLoActivator, HighLowIndex, HighWave, Hikkake,
|
||||
HikkakeModified, HilbertDominantCycle, HistoricalVolatility, Hma, HomingPigeon, HtDcPhase,
|
||||
HtPhasor, HtPhasorOutput, HtTrendMode, HurstChannel, HurstChannelOutput, HurstExponent,
|
||||
Ichimoku, IchimokuOutput, IdenticalThreeCrows, InNeck, Inertia, InformationRatio,
|
||||
InitialBalance, InitialBalanceOutput, InstantaneousTrendline, IntradayVolatilityProfile,
|
||||
IntradayVolatilityProfileOutput, InverseFisherTransform, InvertedHammer, Jma, KagiBars,
|
||||
KalmanHedgeRatio, KalmanHedgeRatioOutput, Kama, KellyCriterion, Keltner, KeltnerOutput,
|
||||
Kicking, KickingByLength, Kst, KstOutput, Kurtosis, Kvo, KylesLambda, LadderBottom,
|
||||
LaguerreRsi, LeadLagCrossCorrelation, LeadLagCrossCorrelationOutput, LinRegAngle,
|
||||
LinRegChannel, LinRegChannelOutput, LinRegIntercept, LinRegSlope, LinearRegression,
|
||||
LiquidationFeatures, LiquidationFeaturesOutput, LongLeggedDoji, LongLine, LongShortRatio,
|
||||
MaEnvelope, MaEnvelopeOutput, MacdExt, MacdFix, MacdIndicator, MacdOutput, Mama, MamaOutput,
|
||||
MarketFacilitationIndex, Marubozu, MassIndex, MatHold, MatchingLow, MaxDrawdown,
|
||||
McClellanOscillator, McClellanSummationIndex, McGinleyDynamic, MedianAbsoluteDeviation,
|
||||
MedianPrice, Mfi, Microprice, MidPoint, MidPrice, MinusDi, MinusDm, Mom, MorningDojiStar,
|
||||
MorningEveningStar, Natr, NewHighsNewLows, Nvi, OIPriceDivergence, OIWeighted, Obv, OmegaRatio,
|
||||
OnNeck, OpenInterestDelta, OpeningMarubozu, OpeningRange, OpeningRangeOutput,
|
||||
OrderBookImbalanceFull, OrderBookImbalanceTop1, OrderBookImbalanceTopN, OuHalfLife,
|
||||
OvernightGap, OvernightIntradayReturn, OvernightIntradayReturnOutput, PainIndex,
|
||||
PairSpreadZScore, PairwiseBeta, ParkinsonVolatility, PearsonCorrelation, PercentAboveMa,
|
||||
PercentB, PercentageTrailingStop, Pgo, PiercingDarkCloud, PlusDi, PlusDm, Pmo,
|
||||
PointAndFigureBars, Ppo, ProfitFactor, Psar, Pvi, QuotedSpread, RSquared, RealizedSpread,
|
||||
RecoveryFactor, RectangleRange, RelativeStrengthAB, RelativeStrengthOutput, RenkoBars,
|
||||
RenkoTrailingStop, RickshawMan, RisingThreeMethods, Roc, Rocp, Rocr, Rocr100,
|
||||
RogersSatchellVolatility, RollingCorrelation, RollingCovariance, RollingVwap, RoofingFilter,
|
||||
Rsi, Rvi, RviVolatility, Rwi, RwiOutput, SarExt, SeasonalZScore, SeparatingLines,
|
||||
SessionHighLow, SessionHighLowOutput, SessionRange, SessionRangeOutput, SessionVwap,
|
||||
SessionHighLow, SessionHighLowOutput, SessionRange, SessionRangeOutput, SessionVwap, Shark,
|
||||
SharpeRatio, ShootingStar, ShortLine, SignedVolume, SineWave, Skewness, Sma, Smi, Smma,
|
||||
SortinoRatio, SpearmanCorrelation, SpinningTop, SpreadBollingerBands,
|
||||
SpreadBollingerBandsOutput, SpreadHurst, StalledPattern, StandardError, StandardErrorBands,
|
||||
@@ -113,18 +115,19 @@ pub use indicators::{
|
||||
SuperTrendOutput, TakerBuySellRatio, Takuri, TasukiGap, TdCombo, TdCountdown, TdDeMarker,
|
||||
TdDifferential, TdLines, TdLinesOutput, TdOpen, TdPressure, TdRangeProjection,
|
||||
TdRangeProjectionOutput, TdRei, TdRiskLevel, TdRiskLevelOutput, TdSequential,
|
||||
TdSequentialOutput, TdSetup, Tema, TermStructureBasis, ThreeInside, ThreeLineStrike,
|
||||
ThreeOutside, ThreeSoldiersOrCrows, ThreeStarsInSouth, Thrusting, TickIndex, Tii,
|
||||
TimeOfDayReturnProfile, TimeOfDayReturnProfileOutput, TpoProfile, TpoProfileOutput,
|
||||
TradeImbalance, TreynorRatio, Trima, Trin, Trix, TrueRange, Tsf, Tsi, Tsv, TtmSqueeze,
|
||||
TtmSqueezeOutput, TurnOfMonth, Tweezer, TwoCrows, TypicalPrice, UlcerIndex, UltimateOscillator,
|
||||
UniqueThreeRiver, UpDownVolumeRatio, UpsideGapThreeMethods, UpsideGapTwoCrows, ValueArea,
|
||||
ValueAreaOutput, ValueAtRisk, Variance, VarianceRatio, VerticalHorizontalFilter, Vidya,
|
||||
VoltyStop, VolumeByTimeProfile, VolumeByTimeProfileOutput, VolumeOscillator, VolumePriceTrend,
|
||||
VolumeProfile, VolumeProfileOutput, Vortex, VortexOutput, Vwap, VwapStdDevBands,
|
||||
VwapStdDevBandsOutput, Vwma, Vzo, WaveTrend, WaveTrendOutput, WeightedClose, WilliamsFractals,
|
||||
WilliamsFractalsOutput, WilliamsR, Wma, WoodiePivots, WoodiePivotsOutput, YangZhangVolatility,
|
||||
YoyoExit, ZScore, ZeroLagMacd, ZeroLagMacdOutput, ZigZag, ZigZagOutput, Zlema, FAMILIES, T3,
|
||||
TdSequentialOutput, TdSetup, Tema, TermStructureBasis, ThreeDrives, ThreeInside,
|
||||
ThreeLineStrike, ThreeOutside, ThreeSoldiersOrCrows, ThreeStarsInSouth, Thrusting, TickIndex,
|
||||
Tii, TimeOfDayReturnProfile, TimeOfDayReturnProfileOutput, TpoProfile, TpoProfileOutput,
|
||||
TradeImbalance, TreynorRatio, Triangle, Trima, Trin, TripleTopBottom, Trix, TrueRange, Tsf,
|
||||
Tsi, Tsv, TtmSqueeze, TtmSqueezeOutput, TurnOfMonth, Tweezer, TwoCrows, TypicalPrice,
|
||||
UlcerIndex, UltimateOscillator, UniqueThreeRiver, UpDownVolumeRatio, UpsideGapThreeMethods,
|
||||
UpsideGapTwoCrows, ValueArea, ValueAreaOutput, ValueAtRisk, Variance, VarianceRatio,
|
||||
VerticalHorizontalFilter, Vidya, VoltyStop, VolumeByTimeProfile, VolumeByTimeProfileOutput,
|
||||
VolumeOscillator, VolumePriceTrend, VolumeProfile, VolumeProfileOutput, Vortex, VortexOutput,
|
||||
Vwap, VwapStdDevBands, VwapStdDevBandsOutput, Vwma, Vzo, WaveTrend, WaveTrendOutput, Wedge,
|
||||
WeightedClose, WilliamsFractals, WilliamsFractalsOutput, WilliamsR, Wma, WoodiePivots,
|
||||
WoodiePivotsOutput, YangZhangVolatility, YoyoExit, ZScore, ZeroLagMacd, ZeroLagMacdOutput,
|
||||
ZigZag, ZigZagOutput, Zlema, FAMILIES, T3,
|
||||
};
|
||||
// `FootprintLevel` is a row element of `FootprintOutput`, re-exported on its own
|
||||
// line so the indicator-count tooling (which scans the braced block above and
|
||||
|
||||
+1
-1
@@ -8,7 +8,7 @@ That includes:
|
||||
[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 **351 indicators** across
|
||||
- A per-indicator deep dive for every one of the **367 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 &
|
||||
|
||||
Generated
+7
-7
@@ -17,7 +17,7 @@
|
||||
},
|
||||
"../../bindings/node": {
|
||||
"name": "wickra",
|
||||
"version": "0.5.1",
|
||||
"version": "0.5.2",
|
||||
"license": "MIT OR Apache-2.0",
|
||||
"devDependencies": {
|
||||
"@napi-rs/cli": "^2.18.0"
|
||||
@@ -26,12 +26,12 @@
|
||||
"node": ">= 18"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"wickra-darwin-arm64": "0.5.1",
|
||||
"wickra-darwin-x64": "0.5.1",
|
||||
"wickra-linux-arm64-gnu": "0.5.1",
|
||||
"wickra-linux-x64-gnu": "0.5.1",
|
||||
"wickra-win32-arm64-msvc": "0.5.1",
|
||||
"wickra-win32-x64-msvc": "0.5.1"
|
||||
"wickra-darwin-arm64": "0.5.2",
|
||||
"wickra-darwin-x64": "0.5.2",
|
||||
"wickra-linux-arm64-gnu": "0.5.2",
|
||||
"wickra-linux-x64-gnu": "0.5.2",
|
||||
"wickra-win32-arm64-msvc": "0.5.2",
|
||||
"wickra-win32-x64-msvc": "0.5.2"
|
||||
}
|
||||
},
|
||||
"node_modules/wickra": {
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
//! WeightedClose.
|
||||
|
||||
use libfuzzer_sys::fuzz_target;
|
||||
use wickra_core::{AbandonedBaby, AccelerationBands, AcceleratorOscillator, AdOscillator, Adl, AdvanceBlock, Adx, Adxr, Alligator, AnchoredVwap, Aroon, AroonOscillator, Atr, AtrBands, AtrTrailingStop, AverageDailyRange, AwesomeOscillator, AwesomeOscillatorHistogram, BalanceOfPower, BatchExt, BeltHold, Breakaway, Camarilla, Candle, Cci, ChaikinMoneyFlow, ChaikinOscillator, ChaikinVolatility, ChandeKrollStop, ChandelierExit, ChoppinessIndex, ClassicPivots, ClosingMarubozu, ConcealingBabySwallow, Counterattack, DayOfWeekProfile, DemandIndex, DemarkPivots, Doji, DojiStar, Donchian, DonchianStop, DownsideGapThreeMethods, DragonflyDoji, Dx, EaseOfMovement, Engulfing, EveningDojiStar, Evwma, FallingThreeMethods, FibonacciPivots, ForceIndex, FractalChaosBands, GapSideBySideWhite, GarmanKlassVolatility, GravestoneDoji, Hammer, HangingMan, Harami, HeikinAshi, HiLoActivator, HighWave, Hikkake, HikkakeModified, HomingPigeon, HurstChannel, Ichimoku, IdenticalThreeCrows, InNeck, Indicator, Inertia, InitialBalance, IntradayVolatilityProfile, InvertedHammer, Keltner, Kicking, KickingByLength, Kvo, LadderBottom, LongLeggedDoji, LongLine, MarketFacilitationIndex, Marubozu, MassIndex, MatHold, MatchingLow, AvgPrice, MedianPrice, Mfi, MidPrice, MinusDi, MinusDm, MorningDojiStar, MorningEveningStar, Natr, Nvi, Obv, OnNeck, OpeningMarubozu, OpeningRange, OvernightGap, OvernightIntradayReturn, ParkinsonVolatility, Pgo, PiercingDarkCloud, PlusDi, PlusDm, Psar, Pvi, RickshawMan, RisingThreeMethods, RogersSatchellVolatility, RollingVwap, Rvi, Rwi, SarExt, SeasonalZScore, SeparatingLines, SessionHighLow, SessionRange, SessionVwap, ShootingStar, ShortLine, Smi, SpinningTop, StalledPattern, StarcBands, StickSandwich, Stochastic, SuperTrend, Takuri, TasukiGap, TdCombo, TdCountdown, TdDeMarker, TdDifferential, TdLines, TdOpen, TdPressure, TdRangeProjection, TdRei, TdRiskLevel, TdSequential, TdSetup, ThreeInside, ThreeLineStrike, ThreeOutside, ThreeSoldiersOrCrows, ThreeStarsInSouth, Thrusting, TimeOfDayReturnProfile, TpoProfile, TrueRange, Tsv, TtmSqueeze, TurnOfMonth, Tweezer, TwoCrows, TypicalPrice, UltimateOscillator, UniqueThreeRiver, UpsideGapThreeMethods, UpsideGapTwoCrows, ValueArea, VoltyStop, VolumeByTimeProfile, VolumeOscillator, VolumePriceTrend, VolumeProfile, Vortex, Vwap, VwapStdDevBands, Vwma, Vzo, WaveTrend, WeightedClose, WilliamsFractals, WilliamsR, WoodiePivots, YangZhangVolatility, YoyoExit, ZigZag};
|
||||
use wickra_core::{AbandonedBaby, Abcd, AccelerationBands, AcceleratorOscillator, AdOscillator, Adl, AdvanceBlock, Adx, Adxr, Alligator, AnchoredVwap, Aroon, AroonOscillator, Atr, AtrBands, AtrTrailingStop, AverageDailyRange, AwesomeOscillator, AwesomeOscillatorHistogram, BalanceOfPower, Bat, BatchExt, BeltHold, Breakaway, Butterfly, Camarilla, Candle, Cci, ChaikinMoneyFlow, ChaikinOscillator, ChaikinVolatility, ChandeKrollStop, ChandelierExit, ChoppinessIndex, ClassicPivots, ClosingMarubozu, ConcealingBabySwallow, Counterattack, Crab, CupAndHandle, Cypher, DayOfWeekProfile, DemandIndex, DemarkPivots, Doji, DojiStar, Donchian, DonchianStop, DoubleTopBottom, DownsideGapThreeMethods, DragonflyDoji, Dx, EaseOfMovement, Engulfing, EveningDojiStar, Evwma, FallingThreeMethods, FibonacciPivots, FlagPennant, ForceIndex, FractalChaosBands, GapSideBySideWhite, GarmanKlassVolatility, Gartley, GravestoneDoji, Hammer, HangingMan, Harami, HeadAndShoulders, HeikinAshi, HiLoActivator, HighWave, Hikkake, HikkakeModified, HomingPigeon, HurstChannel, Ichimoku, IdenticalThreeCrows, InNeck, Indicator, Inertia, InitialBalance, IntradayVolatilityProfile, InvertedHammer, Keltner, Kicking, KickingByLength, Kvo, LadderBottom, LongLeggedDoji, LongLine, MarketFacilitationIndex, Marubozu, MassIndex, MatHold, MatchingLow, AvgPrice, MedianPrice, Mfi, MidPrice, MinusDi, MinusDm, MorningDojiStar, MorningEveningStar, Natr, Nvi, Obv, OnNeck, OpeningMarubozu, OpeningRange, OvernightGap, OvernightIntradayReturn, ParkinsonVolatility, Pgo, PiercingDarkCloud, PlusDi, PlusDm, Psar, Pvi, RectangleRange, RickshawMan, RisingThreeMethods, RogersSatchellVolatility, RollingVwap, Rvi, Rwi, SarExt, SeasonalZScore, SeparatingLines, SessionHighLow, SessionRange, SessionVwap, Shark, ShootingStar, ShortLine, Smi, SpinningTop, StalledPattern, StarcBands, StickSandwich, Stochastic, SuperTrend, Takuri, TasukiGap, TdCombo, TdCountdown, TdDeMarker, TdDifferential, TdLines, TdOpen, TdPressure, TdRangeProjection, TdRei, TdRiskLevel, TdSequential, TdSetup, ThreeDrives, ThreeInside, ThreeLineStrike, ThreeOutside, ThreeSoldiersOrCrows, ThreeStarsInSouth, Thrusting, TimeOfDayReturnProfile, TpoProfile, Triangle, TripleTopBottom, TrueRange, Tsv, TtmSqueeze, TurnOfMonth, Tweezer, TwoCrows, TypicalPrice, UltimateOscillator, UniqueThreeRiver, UpsideGapThreeMethods, UpsideGapTwoCrows, ValueArea, VoltyStop, VolumeByTimeProfile, VolumeOscillator, VolumePriceTrend, VolumeProfile, Vortex, Vwap, VwapStdDevBands, Vwma, Vzo, WaveTrend, Wedge, WeightedClose, WilliamsFractals, WilliamsR, WoodiePivots, YangZhangVolatility, YoyoExit, ZigZag};
|
||||
|
||||
/// Convert a flat `f64` stream into a `Vec<Candle>` by chunking it into
|
||||
/// `[open, high, low, close, volume]` groups. Tuples that fail OHLCV
|
||||
@@ -372,4 +372,24 @@ fuzz_target!(|data: Vec<f64>| {
|
||||
|
||||
drive(|| SessionVwap::new(0), &candles);
|
||||
|
||||
// --- Chart Patterns ---
|
||||
drive(CupAndHandle::new, &candles);
|
||||
drive(RectangleRange::new, &candles);
|
||||
drive(FlagPennant::new, &candles);
|
||||
drive(Wedge::new, &candles);
|
||||
drive(Triangle::new, &candles);
|
||||
drive(HeadAndShoulders::new, &candles);
|
||||
drive(TripleTopBottom::new, &candles);
|
||||
drive(DoubleTopBottom::new, &candles);
|
||||
|
||||
// --- Harmonic Patterns ---
|
||||
drive(ThreeDrives::new, &candles);
|
||||
drive(Cypher::new, &candles);
|
||||
drive(Shark::new, &candles);
|
||||
drive(Crab::new, &candles);
|
||||
drive(Bat::new, &candles);
|
||||
drive(Butterfly::new, &candles);
|
||||
drive(Gartley::new, &candles);
|
||||
drive(Abcd::new, &candles);
|
||||
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user