Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
099ae66b57 | ||
|
|
11dd659b5f | ||
|
|
c096943bdf |
@@ -60,7 +60,7 @@ Closes #
|
|||||||
- [ ] Public API changes are reflected in `CHANGELOG.md`
|
- [ ] Public API changes are reflected in `CHANGELOG.md`
|
||||||
- [ ] Public API changes are reflected in rustdoc / README / examples
|
- [ ] Public API changes are reflected in rustdoc / README / examples
|
||||||
- [ ] No `todo*.md` or other local-only notes are staged
|
- [ ] No `todo*.md` or other local-only notes are staged
|
||||||
- [ ] License header / `LICENSE` reference unchanged (PolyForm-NC-1.0.0)
|
- [ ] License header / `LICENSE` reference unchanged (MIT OR Apache-2.0)
|
||||||
|
|
||||||
## Notes for reviewers
|
## Notes for reviewers
|
||||||
|
|
||||||
|
|||||||
@@ -536,7 +536,7 @@ jobs:
|
|||||||
pkg.repository = { type: 'git', url: 'https://github.com/wickra-lib/wickra' };
|
pkg.repository = { type: 'git', url: 'https://github.com/wickra-lib/wickra' };
|
||||||
pkg.homepage = 'https://github.com/wickra-lib/wickra';
|
pkg.homepage = 'https://github.com/wickra-lib/wickra';
|
||||||
pkg.bugs = { url: 'https://github.com/wickra-lib/wickra/issues' };
|
pkg.bugs = { url: 'https://github.com/wickra-lib/wickra/issues' };
|
||||||
pkg.license = 'PolyForm-Noncommercial-1.0.0';
|
pkg.license = 'MIT OR Apache-2.0';
|
||||||
fs.writeFileSync('package.json', JSON.stringify(pkg, null, 2));
|
fs.writeFileSync('package.json', JSON.stringify(pkg, null, 2));
|
||||||
"
|
"
|
||||||
|
|
||||||
|
|||||||
+23
-1
@@ -7,6 +7,27 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
## [0.5.0] - 2026-06-03
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- **TICK Index** — instantaneous net advancing-minus-declining issues (`TICK_INDEX`).
|
||||||
|
- **Absolute Breadth Index** — absolute value of net advancing-minus-declining issues (`ABSOLUTE_BREADTH_INDEX`).
|
||||||
|
- **Cumulative Volume Index** — running total of volume-normalised net advancing volume (`CUMULATIVE_VOLUME_INDEX`).
|
||||||
|
- **Bullish Percent Index** — percentage of the universe on a point-and-figure buy signal (`BULLISH_PERCENT_INDEX`).
|
||||||
|
- **Up/Down Volume Ratio** — advancing volume divided by declining volume (`UP_DOWN_VOLUME_RATIO`).
|
||||||
|
- **Percent Above Moving Average** — percentage of the universe trading above its reference moving average (`PERCENT_ABOVE_MA`).
|
||||||
|
- **High-Low Index** — moving average of the record-high percentage (`HIGH_LOW_INDEX`).
|
||||||
|
- **New Highs - New Lows** — net count of new period highs minus new period lows (`NEW_HIGHS_NEW_LOWS`).
|
||||||
|
- **Breadth Thrust** — moving average of the advancing-issues share (Zweig) (`BREADTH_THRUST`).
|
||||||
|
- **TRIN / Arms Index** — advance-decline ratio divided by the up-down volume ratio (`TRIN`).
|
||||||
|
- **McClellan Summation Index** — running cumulative total of the McClellan Oscillator (`MCCLELLAN_SUMMATION_INDEX`).
|
||||||
|
- **McClellan Oscillator** — spread between a 19- and 39-period EMA of ratio-adjusted net advances (`MCCLELLAN_OSCILLATOR`).
|
||||||
|
- **Advance/Decline Volume Line** — cumulative net advancing-minus-declining volume across the universe (`AD_VOLUME_LINE`).
|
||||||
|
- **Advance/Decline Ratio** — advancing issues divided by declining issues across the universe (`ADVANCE_DECLINE_RATIO`).
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- **Relicensed** from PolyForm Noncommercial 1.0.0 to dual **MIT OR Apache-2.0**. Wickra is now OSI-approved, permissive open source; commercial use is permitted under either license. See [`LICENSE-MIT`](LICENSE-MIT) and [`LICENSE-APACHE`](LICENSE-APACHE).
|
||||||
|
|
||||||
## [0.4.7] - 2026-06-03
|
## [0.4.7] - 2026-06-03
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
@@ -1147,7 +1168,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
optional Binance live feed.
|
optional Binance live feed.
|
||||||
- Bindings for Python, Node.js, and WebAssembly.
|
- Bindings for Python, Node.js, and WebAssembly.
|
||||||
|
|
||||||
[Unreleased]: https://github.com/wickra-lib/wickra/compare/v0.4.7...HEAD
|
[Unreleased]: https://github.com/wickra-lib/wickra/compare/v0.5.0...HEAD
|
||||||
|
[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
|
[0.4.7]: https://github.com/wickra-lib/wickra/compare/v0.4.6...v0.4.7
|
||||||
[0.4.6]: https://github.com/wickra-lib/wickra/compare/v0.4.5...v0.4.6
|
[0.4.6]: https://github.com/wickra-lib/wickra/compare/v0.4.5...v0.4.6
|
||||||
[0.4.5]: https://github.com/wickra-lib/wickra/compare/v0.4.4...v0.4.5
|
[0.4.5]: https://github.com/wickra-lib/wickra/compare/v0.4.4...v0.4.5
|
||||||
|
|||||||
+3
-1
@@ -26,4 +26,6 @@ keywords:
|
|||||||
- quantitative-finance
|
- quantitative-finance
|
||||||
- rust
|
- rust
|
||||||
- time-series
|
- time-series
|
||||||
license: PolyForm-Noncommercial-1.0.0
|
license:
|
||||||
|
- MIT
|
||||||
|
- Apache-2.0
|
||||||
|
|||||||
+5
-5
@@ -5,11 +5,11 @@ build the project, the standards a change must meet, and how to get it merged.
|
|||||||
|
|
||||||
## License of contributions
|
## License of contributions
|
||||||
|
|
||||||
Wickra is licensed under the **PolyForm Noncommercial License 1.0.0** (see
|
Wickra is dual-licensed under the [MIT](LICENSE-MIT) and
|
||||||
[`LICENSE`](LICENSE)). By submitting a contribution you agree that it is
|
[Apache-2.0](LICENSE-APACHE) licenses; users may choose either. Unless you
|
||||||
licensed to the project under those same terms. The Noncommercial license
|
explicitly state otherwise, any contribution you intentionally submit for
|
||||||
permits use for any purpose **other than** a commercial one; keep that in mind
|
inclusion in the work, as defined in the Apache-2.0 license, shall be dual
|
||||||
when proposing features or depending on Wickra elsewhere.
|
licensed as above, without any additional terms or conditions.
|
||||||
|
|
||||||
## Project layout
|
## Project layout
|
||||||
|
|
||||||
|
|||||||
Generated
+6
-6
@@ -1867,7 +1867,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wickra"
|
name = "wickra"
|
||||||
version = "0.4.7"
|
version = "0.5.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"approx",
|
"approx",
|
||||||
"criterion",
|
"criterion",
|
||||||
@@ -1878,7 +1878,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wickra-core"
|
name = "wickra-core"
|
||||||
version = "0.4.7"
|
version = "0.5.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"approx",
|
"approx",
|
||||||
"proptest",
|
"proptest",
|
||||||
@@ -1888,7 +1888,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wickra-data"
|
name = "wickra-data"
|
||||||
version = "0.4.7"
|
version = "0.5.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"approx",
|
"approx",
|
||||||
"csv",
|
"csv",
|
||||||
@@ -1915,7 +1915,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wickra-node"
|
name = "wickra-node"
|
||||||
version = "0.4.7"
|
version = "0.5.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"napi",
|
"napi",
|
||||||
"napi-build",
|
"napi-build",
|
||||||
@@ -1925,7 +1925,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wickra-python"
|
name = "wickra-python"
|
||||||
version = "0.4.7"
|
version = "0.5.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"numpy",
|
"numpy",
|
||||||
"pyo3",
|
"pyo3",
|
||||||
@@ -1934,7 +1934,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wickra-wasm"
|
name = "wickra-wasm"
|
||||||
version = "0.4.7"
|
version = "0.5.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"console_error_panic_hook",
|
"console_error_panic_hook",
|
||||||
"js-sys",
|
"js-sys",
|
||||||
|
|||||||
+3
-3
@@ -12,11 +12,11 @@ members = [
|
|||||||
exclude = ["fuzz"]
|
exclude = ["fuzz"]
|
||||||
|
|
||||||
[workspace.package]
|
[workspace.package]
|
||||||
version = "0.4.7"
|
version = "0.5.0"
|
||||||
authors = ["kingchenc <support@wickra.org>"]
|
authors = ["kingchenc <support@wickra.org>"]
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
rust-version = "1.86"
|
rust-version = "1.86"
|
||||||
license-file = "LICENSE"
|
license = "MIT OR Apache-2.0"
|
||||||
repository = "https://github.com/wickra-lib/wickra"
|
repository = "https://github.com/wickra-lib/wickra"
|
||||||
homepage = "https://github.com/wickra-lib/wickra"
|
homepage = "https://github.com/wickra-lib/wickra"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
@@ -24,7 +24,7 @@ keywords = ["finance", "trading", "indicators", "technical-analysis", "ta"]
|
|||||||
categories = ["finance", "mathematics", "science"]
|
categories = ["finance", "mathematics", "science"]
|
||||||
|
|
||||||
[workspace.dependencies]
|
[workspace.dependencies]
|
||||||
wickra-core = { path = "crates/wickra-core", version = "0.4.7" }
|
wickra-core = { path = "crates/wickra-core", version = "0.5.0" }
|
||||||
|
|
||||||
thiserror = "2"
|
thiserror = "2"
|
||||||
rayon = "1.10"
|
rayon = "1.10"
|
||||||
|
|||||||
@@ -1,161 +0,0 @@
|
|||||||
# PolyForm Noncommercial License 1.0.0
|
|
||||||
|
|
||||||
<https://polyformproject.org/licenses/noncommercial/1.0.0>
|
|
||||||
|
|
||||||
## Acceptance
|
|
||||||
|
|
||||||
In order to get any license under these terms, you must agree
|
|
||||||
to them as both strict obligations and conditions to all
|
|
||||||
your licenses.
|
|
||||||
|
|
||||||
## Copyright License
|
|
||||||
|
|
||||||
The licensor grants you a copyright license for the
|
|
||||||
software to do everything you might do with the software
|
|
||||||
that would otherwise infringe the licensor's copyright
|
|
||||||
in it for any permitted purpose. However, you may
|
|
||||||
only distribute the software according to [Distribution
|
|
||||||
License](#distribution-license) and make changes or new works
|
|
||||||
based on the software according to [Changes and New Works
|
|
||||||
License](#changes-and-new-works-license).
|
|
||||||
|
|
||||||
## Distribution License
|
|
||||||
|
|
||||||
The licensor grants you an additional copyright license
|
|
||||||
to distribute copies of the software. Your license to
|
|
||||||
distribute covers distributing the software with changes
|
|
||||||
and new works permitted by [Changes and New Works
|
|
||||||
License](#changes-and-new-works-license).
|
|
||||||
|
|
||||||
## Notices
|
|
||||||
|
|
||||||
You must ensure that anyone who gets a copy of any part of
|
|
||||||
the software from you also gets a copy of these terms or the
|
|
||||||
URL for them above, as well as copies of any plain-text lines
|
|
||||||
beginning with `Required Notice:` that the licensor provided
|
|
||||||
with the software. For example:
|
|
||||||
|
|
||||||
> Required Notice: Copyright 2026 kingchenc (https://github.com/wickra-lib/wickra)
|
|
||||||
|
|
||||||
## Changes and New Works License
|
|
||||||
|
|
||||||
The licensor grants you an additional copyright license
|
|
||||||
to make changes and new works based on the software for any
|
|
||||||
permitted purpose.
|
|
||||||
|
|
||||||
## Patent License
|
|
||||||
|
|
||||||
The licensor grants you a patent license for the software that
|
|
||||||
covers patent claims the licensor can license, or becomes able
|
|
||||||
to license, that you would infringe by using the software.
|
|
||||||
|
|
||||||
## Noncommercial Purposes
|
|
||||||
|
|
||||||
Any noncommercial purpose is a permitted purpose.
|
|
||||||
|
|
||||||
## Personal Uses
|
|
||||||
|
|
||||||
Personal use for research, experiment, and testing for
|
|
||||||
the benefit of public knowledge, personal study, private
|
|
||||||
entertainment, hobby projects, amateur pursuits, or religious
|
|
||||||
observance, without any anticipated commercial application,
|
|
||||||
is use for a permitted purpose.
|
|
||||||
|
|
||||||
## Noncommercial Organizations
|
|
||||||
|
|
||||||
Use by any charitable organization, educational institution,
|
|
||||||
public research organization, public safety or health
|
|
||||||
organization, environmental protection organization, or
|
|
||||||
government institution is use for a permitted purpose regardless
|
|
||||||
of the source of funding or obligations resulting from the
|
|
||||||
funding.
|
|
||||||
|
|
||||||
## Fair Use
|
|
||||||
|
|
||||||
You may have "fair use" rights for the software under the
|
|
||||||
law. These terms do not limit them.
|
|
||||||
|
|
||||||
## No Other Rights
|
|
||||||
|
|
||||||
These terms do not allow you to sublicense or transfer any of
|
|
||||||
your licenses to anyone else, or prevent the licensor from
|
|
||||||
granting licenses to anyone else. These terms do not imply
|
|
||||||
any other licenses.
|
|
||||||
|
|
||||||
## Patent Defense
|
|
||||||
|
|
||||||
If you make any written claim that the software infringes or
|
|
||||||
contributes to infringement of any patent, your patent license
|
|
||||||
for the software granted under these terms ends immediately. If
|
|
||||||
your company makes such a claim, your patent license ends
|
|
||||||
immediately for work on behalf of your company.
|
|
||||||
|
|
||||||
## Violations
|
|
||||||
|
|
||||||
The first time you are notified in writing that you have
|
|
||||||
violated any of these terms, or done anything with the software
|
|
||||||
not covered by your licenses, your licenses can nonetheless
|
|
||||||
continue if you come into full compliance with these terms,
|
|
||||||
and take practical steps to correct past violations, within 32
|
|
||||||
days of receiving notice. Otherwise, all your licenses end
|
|
||||||
immediately.
|
|
||||||
|
|
||||||
## No Liability
|
|
||||||
|
|
||||||
***As far as the law allows, the software comes as is, without
|
|
||||||
any warranty or condition, and the licensor will not be liable
|
|
||||||
to you for any damages arising out of these terms or the use
|
|
||||||
or nature of the software, under any kind of legal claim.***
|
|
||||||
|
|
||||||
## Definitions
|
|
||||||
|
|
||||||
The **licensor** is the individual or entity offering these
|
|
||||||
terms, and the **software** is the software the licensor makes
|
|
||||||
available under these terms.
|
|
||||||
|
|
||||||
**You** refers to the individual or entity agreeing to these
|
|
||||||
terms.
|
|
||||||
|
|
||||||
**Your company** is any legal entity, sole proprietorship,
|
|
||||||
or other kind of organization that you work for, plus all
|
|
||||||
organizations that have control over, are under the control
|
|
||||||
of, or are under common control with that organization.
|
|
||||||
**Control** means ownership of substantially all the assets
|
|
||||||
of an entity, or the power to direct its management and
|
|
||||||
policies by vote, contract, or otherwise. Control can be
|
|
||||||
direct or indirect.
|
|
||||||
|
|
||||||
**Your licenses** are all the licenses granted to you for the
|
|
||||||
software under these terms.
|
|
||||||
|
|
||||||
**Use** means anything you do with the software requiring one
|
|
||||||
of your licenses.
|
|
||||||
|
|
||||||
## Additional Permissions Granted by the Licensor
|
|
||||||
|
|
||||||
These additional permissions supplement the PolyForm Noncommercial
|
|
||||||
License 1.0.0 above. They only broaden, and never narrow, the
|
|
||||||
licenses granted to you. The text of the PolyForm Noncommercial
|
|
||||||
License 1.0.0 above is unmodified.
|
|
||||||
|
|
||||||
Use by a natural person, acting for their own personal account and
|
|
||||||
not on behalf of any third party, is use for a permitted purpose.
|
|
||||||
This includes operating an automated trading bot or trading strategy
|
|
||||||
on that person's own capital, whether or not it earns that person
|
|
||||||
money.
|
|
||||||
|
|
||||||
For the avoidance of doubt, the licenses above already let you use,
|
|
||||||
fork, modify, and redistribute the software, and file issues and
|
|
||||||
contribute changes, for any permitted purpose. Personal projects,
|
|
||||||
research, education, nonprofit organizations, government use, and
|
|
||||||
hobby trading bots are permitted purposes.
|
|
||||||
|
|
||||||
Any other commercial use — in particular the commercial sale of the
|
|
||||||
software itself, or the commercial sale of services built around it —
|
|
||||||
requires a separate commercial license from the licensor. If you want
|
|
||||||
to use Wickra commercially, get in touch about a license at
|
|
||||||
<https://github.com/wickra-lib/wickra>.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
Required Notice: Copyright 2026 kingchenc (https://github.com/wickra-lib/wickra)
|
|
||||||
+201
@@ -0,0 +1,201 @@
|
|||||||
|
Apache License
|
||||||
|
Version 2.0, January 2004
|
||||||
|
http://www.apache.org/licenses/
|
||||||
|
|
||||||
|
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||||
|
|
||||||
|
1. Definitions.
|
||||||
|
|
||||||
|
"License" shall mean the terms and conditions for use, reproduction,
|
||||||
|
and distribution as defined by Sections 1 through 9 of this document.
|
||||||
|
|
||||||
|
"Licensor" shall mean the copyright owner or entity authorized by
|
||||||
|
the copyright owner that is granting the License.
|
||||||
|
|
||||||
|
"Legal Entity" shall mean the union of the acting entity and all
|
||||||
|
other entities that control, are controlled by, or are under common
|
||||||
|
control with that entity. For the purposes of this definition,
|
||||||
|
"control" means (i) the power, direct or indirect, to cause the
|
||||||
|
direction or management of such entity, whether by contract or
|
||||||
|
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||||
|
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||||
|
|
||||||
|
"You" (or "Your") shall mean an individual or Legal Entity
|
||||||
|
exercising permissions granted by this License.
|
||||||
|
|
||||||
|
"Source" form shall mean the preferred form for making modifications,
|
||||||
|
including but not limited to software source code, documentation
|
||||||
|
source, and configuration files.
|
||||||
|
|
||||||
|
"Object" form shall mean any form resulting from mechanical
|
||||||
|
transformation or translation of a Source form, including but
|
||||||
|
not limited to compiled object code, generated documentation,
|
||||||
|
and conversions to other media types.
|
||||||
|
|
||||||
|
"Work" shall mean the work of authorship, whether in Source or
|
||||||
|
Object form, made available under the License, as indicated by a
|
||||||
|
copyright notice that is included in or attached to the work
|
||||||
|
(an example is provided in the Appendix below).
|
||||||
|
|
||||||
|
"Derivative Works" shall mean any work, whether in Source or Object
|
||||||
|
form, that is based on (or derived from) the Work and for which the
|
||||||
|
editorial revisions, annotations, elaborations, or other modifications
|
||||||
|
represent, as a whole, an original work of authorship. For the purposes
|
||||||
|
of this License, Derivative Works shall not include works that remain
|
||||||
|
separable from, or merely link (or bind by name) to the interfaces of,
|
||||||
|
the Work and Derivative Works thereof.
|
||||||
|
|
||||||
|
"Contribution" shall mean any work of authorship, including
|
||||||
|
the original version of the Work and any modifications or additions
|
||||||
|
to that Work or Derivative Works thereof, that is intentionally
|
||||||
|
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||||
|
or by an individual or Legal Entity authorized to submit on behalf of
|
||||||
|
the copyright owner. For the purposes of this definition, "submitted"
|
||||||
|
means any form of electronic, verbal, or written communication sent
|
||||||
|
to the Licensor or its representatives, including but not limited to
|
||||||
|
communication on electronic mailing lists, source code control systems,
|
||||||
|
and issue tracking systems that are managed by, or on behalf of, the
|
||||||
|
Licensor for the purpose of discussing and improving the Work, but
|
||||||
|
excluding communication that is conspicuously marked or otherwise
|
||||||
|
designated in writing by the copyright owner as "Not a Contribution."
|
||||||
|
|
||||||
|
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||||
|
on behalf of whom a Contribution has been received by Licensor and
|
||||||
|
subsequently incorporated within the Work.
|
||||||
|
|
||||||
|
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
copyright license to reproduce, prepare Derivative Works of,
|
||||||
|
publicly display, publicly perform, sublicense, and distribute the
|
||||||
|
Work and such Derivative Works in Source or Object form.
|
||||||
|
|
||||||
|
3. Grant of Patent License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
(except as stated in this section) patent license to make, have made,
|
||||||
|
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||||
|
where such license applies only to those patent claims licensable
|
||||||
|
by such Contributor that are necessarily infringed by their
|
||||||
|
Contribution(s) alone or by combination of their Contribution(s)
|
||||||
|
with the Work to which such Contribution(s) was submitted. If You
|
||||||
|
institute patent litigation against any entity (including a
|
||||||
|
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||||
|
or a Contribution incorporated within the Work constitutes direct
|
||||||
|
or contributory patent infringement, then any patent licenses
|
||||||
|
granted to You under this License for that Work shall terminate
|
||||||
|
as of the date such litigation is filed.
|
||||||
|
|
||||||
|
4. Redistribution. You may reproduce and distribute copies of the
|
||||||
|
Work or Derivative Works thereof in any medium, with or without
|
||||||
|
modifications, and in Source or Object form, provided that You
|
||||||
|
meet the following conditions:
|
||||||
|
|
||||||
|
(a) You must give any other recipients of the Work or Derivative
|
||||||
|
Works a copy of this License; and
|
||||||
|
|
||||||
|
(b) You must cause any modified files to carry prominent notices
|
||||||
|
stating that You changed the files; and
|
||||||
|
|
||||||
|
(c) You must retain, in the Source form of any Derivative Works
|
||||||
|
that You distribute, all copyright, patent, trademark, and
|
||||||
|
attribution notices from the Source form of the Work,
|
||||||
|
excluding those notices that do not pertain to any part of
|
||||||
|
the Derivative Works; and
|
||||||
|
|
||||||
|
(d) If the Work includes a "NOTICE" text file as part of its
|
||||||
|
distribution, then any Derivative Works that You distribute must
|
||||||
|
include a readable copy of the attribution notices contained
|
||||||
|
within such NOTICE file, excluding those notices that do not
|
||||||
|
pertain to any part of the Derivative Works, in at least one
|
||||||
|
of the following places: within a NOTICE text file distributed
|
||||||
|
as part of the Derivative Works; within the Source form or
|
||||||
|
documentation, if provided along with the Derivative Works; or,
|
||||||
|
within a display generated by the Derivative Works, if and
|
||||||
|
wherever such third-party notices normally appear. The contents
|
||||||
|
of the NOTICE file are for informational purposes only and
|
||||||
|
do not modify the License. You may add Your own attribution
|
||||||
|
notices within Derivative Works that You distribute, alongside
|
||||||
|
or as an addendum to the NOTICE text from the Work, provided
|
||||||
|
that such additional attribution notices cannot be construed
|
||||||
|
as modifying the License.
|
||||||
|
|
||||||
|
You may add Your own copyright statement to Your modifications and
|
||||||
|
may provide additional or different license terms and conditions
|
||||||
|
for use, reproduction, or distribution of Your modifications, or
|
||||||
|
for any such Derivative Works as a whole, provided Your use,
|
||||||
|
reproduction, and distribution of the Work otherwise complies with
|
||||||
|
the conditions stated in this License.
|
||||||
|
|
||||||
|
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||||
|
any Contribution intentionally submitted for inclusion in the Work
|
||||||
|
by You to the Licensor shall be under the terms and conditions of
|
||||||
|
this License, without any additional terms or conditions.
|
||||||
|
Notwithstanding the above, nothing herein shall supersede or modify
|
||||||
|
the terms of any separate license agreement you may have executed
|
||||||
|
with Licensor regarding such Contributions.
|
||||||
|
|
||||||
|
6. Trademarks. This License does not grant permission to use the trade
|
||||||
|
names, trademarks, service marks, or product names of the Licensor,
|
||||||
|
except as required for reasonable and customary use in describing the
|
||||||
|
origin of the Work and reproducing the content of the NOTICE file.
|
||||||
|
|
||||||
|
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||||
|
agreed to in writing, Licensor provides the Work (and each
|
||||||
|
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||||
|
implied, including, without limitation, any warranties or conditions
|
||||||
|
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||||
|
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||||
|
appropriateness of using or redistributing the Work and assume any
|
||||||
|
risks associated with Your exercise of permissions under this License.
|
||||||
|
|
||||||
|
8. Limitation of Liability. In no event and under no legal theory,
|
||||||
|
whether in tort (including negligence), contract, or otherwise,
|
||||||
|
unless required by applicable law (such as deliberate and grossly
|
||||||
|
negligent acts) or agreed to in writing, shall any Contributor be
|
||||||
|
liable to You for damages, including any direct, indirect, special,
|
||||||
|
incidental, or consequential damages of any character arising as a
|
||||||
|
result of this License or out of the use or inability to use the
|
||||||
|
Work (including but not limited to damages for loss of goodwill,
|
||||||
|
work stoppage, computer failure or malfunction, or any and all
|
||||||
|
other commercial damages or losses), even if such Contributor
|
||||||
|
has been advised of the possibility of such damages.
|
||||||
|
|
||||||
|
9. Accepting Warranty or Additional Liability. While redistributing
|
||||||
|
the Work or Derivative Works thereof, You may choose to offer,
|
||||||
|
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||||
|
or other liability obligations and/or rights consistent with this
|
||||||
|
License. However, in accepting such obligations, You may act only
|
||||||
|
on Your own behalf and on Your sole responsibility, not on behalf
|
||||||
|
of any other Contributor, and only if You agree to indemnify,
|
||||||
|
defend, and hold each Contributor harmless for any liability
|
||||||
|
incurred by, or claims asserted against, such Contributor by reason
|
||||||
|
of your accepting any such warranty or additional liability.
|
||||||
|
|
||||||
|
END OF TERMS AND CONDITIONS
|
||||||
|
|
||||||
|
APPENDIX: How to apply the Apache License to your work.
|
||||||
|
|
||||||
|
To apply the Apache License to your work, attach the following
|
||||||
|
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||||
|
replaced with your own identifying information. (Don't include
|
||||||
|
the brackets!) The text should be enclosed in the appropriate
|
||||||
|
comment syntax for the file format. We also recommend that a
|
||||||
|
file or class name and description of purpose be included on the
|
||||||
|
same "printed page" as the copyright notice for easier
|
||||||
|
identification within third-party archives.
|
||||||
|
|
||||||
|
Copyright 2026 kingchenc and the Wickra contributors
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
+21
@@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2026 kingchenc and the Wickra contributors
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
<p align="center">
|
<p align="center">
|
||||||
<a href="https://wickra.org"><img src="https://raw.githubusercontent.com/wickra-lib/.github/main/profile/wickra-banner.webp?v=325" 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=339" alt="Wickra — streaming-first technical indicators" width="100%"></a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
[](https://github.com/wickra-lib/wickra/actions/workflows/ci.yml)
|
[](https://github.com/wickra-lib/wickra/actions/workflows/ci.yml)
|
||||||
@@ -9,7 +9,7 @@
|
|||||||
[](https://crates.io/crates/wickra)
|
[](https://crates.io/crates/wickra)
|
||||||
[](https://pypi.org/project/wickra/)
|
[](https://pypi.org/project/wickra/)
|
||||||
[](https://www.npmjs.com/package/wickra)
|
[](https://www.npmjs.com/package/wickra)
|
||||||
[](LICENSE)
|
[](#license)
|
||||||
[](https://scorecard.dev/viewer/?uri=github.com/wickra-lib/wickra)
|
[](https://scorecard.dev/viewer/?uri=github.com/wickra-lib/wickra)
|
||||||
[](https://github.com/wickra-lib/wickra/attestations)
|
[](https://github.com/wickra-lib/wickra/attestations)
|
||||||
[](https://docs.wickra.org)
|
[](https://docs.wickra.org)
|
||||||
@@ -47,7 +47,7 @@ Full documentation lives at **[docs.wickra.org](https://docs.wickra.org)**:
|
|||||||
[Node](https://docs.wickra.org/Quickstart-Node),
|
[Node](https://docs.wickra.org/Quickstart-Node),
|
||||||
[WASM](https://docs.wickra.org/Quickstart-WASM).
|
[WASM](https://docs.wickra.org/Quickstart-WASM).
|
||||||
- **Indicators** — a per-indicator deep dive (formula, parameters, warmup) for
|
- **Indicators** — a per-indicator deep dive (formula, parameters, warmup) for
|
||||||
every one of the 325 indicators; start at the
|
every one of the 339 indicators; start at the
|
||||||
[indicators overview](https://docs.wickra.org/Indicators-Overview).
|
[indicators overview](https://docs.wickra.org/Indicators-Overview).
|
||||||
- **Reference** — [warmup periods](https://docs.wickra.org/Warmup-Periods),
|
- **Reference** — [warmup periods](https://docs.wickra.org/Warmup-Periods),
|
||||||
[streaming vs batch](https://docs.wickra.org/Streaming-vs-Batch),
|
[streaming vs batch](https://docs.wickra.org/Streaming-vs-Batch),
|
||||||
@@ -135,7 +135,7 @@ python -m benchmarks.compare_libraries
|
|||||||
|
|
||||||
## Indicators
|
## Indicators
|
||||||
|
|
||||||
325 streaming-first indicators across twenty families. Every one passes the
|
339 streaming-first indicators across twenty families. Every one passes the
|
||||||
`batch == streaming` equivalence test, reference-value tests, and reset
|
`batch == streaming` equivalence test, reference-value tests, and reset
|
||||||
semantics tests. Each has a per-indicator deep dive (formula, parameters,
|
semantics tests. Each has a per-indicator deep dive (formula, parameters,
|
||||||
warmup) at [docs.wickra.org](https://docs.wickra.org/Indicators-Overview).
|
warmup) at [docs.wickra.org](https://docs.wickra.org/Indicators-Overview).
|
||||||
@@ -160,7 +160,7 @@ warmup) at [docs.wickra.org](https://docs.wickra.org/Indicators-Overview).
|
|||||||
| 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 |
|
| 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 |
|
| 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 |
|
| Market Profile | Value Area (POC / VAH / VAL), Volume Profile (histogram), TPO Profile, Initial Balance, Opening Range |
|
||||||
| Market Breadth | Advance/Decline Line |
|
| Market Breadth | Advance/Decline Line, Advance/Decline Ratio, Advance/Decline Volume Line, McClellan Oscillator, McClellan Summation Index, TRIN / Arms Index, Breadth Thrust, New Highs - New Lows, High-Low Index, Percent Above Moving Average, Up/Down Volume Ratio, Bullish Percent Index, Cumulative Volume Index, Absolute Breadth Index, TICK Index |
|
||||||
| Risk / Performance | Sharpe Ratio, Sortino Ratio, Calmar Ratio, Omega Ratio, Max Drawdown, Average Drawdown, Drawdown Duration, Pain Index, Value at Risk, Conditional Value at Risk (CVaR), Profit Factor, Gain/Loss Ratio, Recovery Factor, Kelly Criterion, Treynor Ratio, Information Ratio, Alpha (Jensen) |
|
| Risk / Performance | Sharpe Ratio, Sortino Ratio, Calmar Ratio, Omega Ratio, Max Drawdown, Average Drawdown, Drawdown Duration, Pain Index, Value at Risk, Conditional Value at Risk (CVaR), Profit Factor, Gain/Loss Ratio, Recovery Factor, Kelly Criterion, Treynor Ratio, Information Ratio, Alpha (Jensen) |
|
||||||
|
|
||||||
Every candlestick pattern emits a signed per-bar value — `+1.0` bullish,
|
Every candlestick pattern emits a signed per-bar value — `+1.0` bullish,
|
||||||
@@ -240,7 +240,7 @@ A Python live-trading example using the public `websockets` package lives at
|
|||||||
```
|
```
|
||||||
wickra/
|
wickra/
|
||||||
├── crates/
|
├── crates/
|
||||||
│ ├── wickra-core/ core engine + all 325 indicators
|
│ ├── wickra-core/ core engine + all 339 indicators
|
||||||
│ ├── wickra/ top-level facade crate (publishes on crates.io) + benches/
|
│ ├── wickra/ top-level facade crate (publishes on crates.io) + benches/
|
||||||
│ └── wickra-data/ CSV reader, tick aggregator, live exchange feeds
|
│ └── wickra-data/ CSV reader, tick aggregator, live exchange feeds
|
||||||
├── bindings/
|
├── bindings/
|
||||||
@@ -324,13 +324,20 @@ shape together before you invest the time.
|
|||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
Licensed under the **PolyForm Noncommercial License 1.0.0**. See [LICENSE](LICENSE).
|
Licensed under either of
|
||||||
|
|
||||||
In plain English: use it, fork it, modify it, redistribute it, file issues, send
|
- Apache License, Version 2.0 ([LICENSE-APACHE](LICENSE-APACHE) or
|
||||||
pull requests — all welcome. Personal projects, research, education, non-profits,
|
<http://www.apache.org/licenses/LICENSE-2.0>)
|
||||||
government, hobby trading bots: all fine. The one thing that's not allowed is
|
- MIT license ([LICENSE-MIT](LICENSE-MIT) or <http://opensource.org/licenses/MIT>)
|
||||||
commercial sale of the software or of services built around it. If you want to
|
|
||||||
use Wickra commercially, get in touch about a license.
|
at your option. Use it, fork it, modify it, redistribute it — commercially or
|
||||||
|
not — file issues, send pull requests; all welcome.
|
||||||
|
|
||||||
|
### Contribution
|
||||||
|
|
||||||
|
Unless you explicitly state otherwise, any contribution intentionally submitted
|
||||||
|
for inclusion in the work by you, as defined in the Apache-2.0 license, shall be
|
||||||
|
dual licensed as above, without any additional terms or conditions.
|
||||||
|
|
||||||
## Disclaimer
|
## Disclaimer
|
||||||
|
|
||||||
|
|||||||
+3
-3
@@ -2,13 +2,13 @@
|
|||||||
|
|
||||||
## Supported versions
|
## Supported versions
|
||||||
|
|
||||||
Wickra is pre-1.0. Security fixes are applied to the latest released `0.1.x`
|
Wickra is pre-1.0. Security fixes are applied to the latest released `0.5.x`
|
||||||
version only; please upgrade to the newest release before reporting an issue.
|
version only; please upgrade to the newest release before reporting an issue.
|
||||||
|
|
||||||
| Version | Supported |
|
| Version | Supported |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
| 0.1.x (latest) | :white_check_mark: |
|
| 0.5.x (latest) | :white_check_mark: |
|
||||||
| older 0.1.x | :x: |
|
| older 0.5.x | :x: |
|
||||||
|
|
||||||
## Reporting a vulnerability
|
## Reporting a vulnerability
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ edition.workspace = true
|
|||||||
# also emits `cargo::` directives that require >= 1.77 — that older floor is
|
# also emits `cargo::` directives that require >= 1.77 — that older floor is
|
||||||
# subsumed by the 1.88 requirement now.
|
# subsumed by the 1.88 requirement now.
|
||||||
rust-version = "1.88"
|
rust-version = "1.88"
|
||||||
license-file.workspace = true
|
license.workspace = true
|
||||||
repository.workspace = true
|
repository.workspace = true
|
||||||
homepage.workspace = true
|
homepage.workspace = true
|
||||||
readme.workspace = true
|
readme.workspace = true
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
[](https://github.com/wickra-lib/wickra/actions/workflows/ci.yml)
|
[](https://github.com/wickra-lib/wickra/actions/workflows/ci.yml)
|
||||||
[](https://codecov.io/gh/wickra-lib/wickra)
|
[](https://codecov.io/gh/wickra-lib/wickra)
|
||||||
[](https://www.npmjs.com/package/wickra)
|
[](https://www.npmjs.com/package/wickra)
|
||||||
[](https://github.com/wickra-lib/wickra/blob/main/LICENSE)
|
[](https://github.com/wickra-lib/wickra#license)
|
||||||
|
|
||||||
**Streaming-first technical indicators for Node.js. `npm install wickra` —
|
**Streaming-first technical indicators for Node.js. `npm install wickra` —
|
||||||
prebuilt native binary, no system dependencies.**
|
prebuilt native binary, no system dependencies.**
|
||||||
@@ -67,7 +67,5 @@ risk. The library is provided **as is**, without warranty of any kind.
|
|||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
Licensed under the **PolyForm Noncommercial License 1.0.0**. Personal projects,
|
Licensed under either of [Apache-2.0](https://github.com/wickra-lib/wickra/blob/main/LICENSE-APACHE)
|
||||||
research, education, non-profits, and hobby trading bots are all fine; the one
|
or [MIT](https://github.com/wickra-lib/wickra/blob/main/LICENSE-MIT) at your option.
|
||||||
thing not allowed is commercial sale of the software or of services built
|
|
||||||
around it. See [LICENSE](https://github.com/wickra-lib/wickra/blob/main/LICENSE).
|
|
||||||
|
|||||||
@@ -1284,6 +1284,112 @@ test('market breadth: AdvanceDecline rejects ragged universe', () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('market breadth: 14 indicators reference values + batch parity', () => {
|
||||||
|
const flags4 = [false, false, false, false];
|
||||||
|
|
||||||
|
// Advance/Decline Ratio: 3/1 = 3 ; 0 advancers -> 0.
|
||||||
|
const adr = new wickra.AdvanceDeclineRatio();
|
||||||
|
assert.equal(adr.update([1, 1, 1, -1], [10, 10, 10, 10], flags4, flags4), 3.0);
|
||||||
|
assert.equal(adr.update([-1, -1, -1, -1], [10, 10, 10, 10], flags4, flags4), 0.0);
|
||||||
|
assert.deepEqual(
|
||||||
|
Array.from(
|
||||||
|
new wickra.AdvanceDeclineRatio().batch(
|
||||||
|
[[1, 1, 1, -1], [-1, -1, -1, -1]],
|
||||||
|
[[10, 10, 10, 10], [10, 10, 10, 10]],
|
||||||
|
[flags4, flags4],
|
||||||
|
[flags4, flags4],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
[3.0, 0.0],
|
||||||
|
);
|
||||||
|
|
||||||
|
// AD Volume Line: cumulative net advancing volume.
|
||||||
|
const adv = new wickra.AdVolumeLine();
|
||||||
|
assert.equal(adv.update([1, -1], [150, 50], [false, false], [false, false]), 100.0);
|
||||||
|
assert.equal(adv.update([1, -1], [60, 60], [false, false], [false, false]), 100.0);
|
||||||
|
|
||||||
|
// McClellan Oscillator + Summation: seed 0, then -50.
|
||||||
|
const osc = new wickra.McClellanOscillator();
|
||||||
|
assert.ok(Math.abs(osc.update([1, 1, 1, -1], [10, 10, 10, 10], flags4, flags4)) < 1e-9);
|
||||||
|
assert.ok(Math.abs(osc.update([-1, -1, -1, 1], [10, 10, 10, 10], flags4, flags4) - -50.0) < 1e-9);
|
||||||
|
const msi = new wickra.McClellanSummationIndex();
|
||||||
|
assert.ok(Math.abs(msi.update([1, 1, 1, -1], [10, 10, 10, 10], flags4, flags4)) < 1e-9);
|
||||||
|
assert.ok(Math.abs(msi.update([-1, -1, -1, 1], [10, 10, 10, 10], flags4, flags4) - -50.0) < 1e-9);
|
||||||
|
|
||||||
|
// TRIN: balanced breadth -> 1.
|
||||||
|
assert.ok(
|
||||||
|
Math.abs(new wickra.Trin().update([1, 1, 1, -1], [50, 50, 50, 50], flags4, flags4) - 1.0) < 1e-9,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Breadth Thrust(2): warmup null, then SMA(2) of [0.8, 0.6] = 0.7.
|
||||||
|
const bt = new wickra.BreadthThrust(2);
|
||||||
|
const up10 = Array(10).fill(false);
|
||||||
|
assert.equal(bt.update([...Array(8).fill(1), -1, -1], Array(10).fill(10), up10, up10), null);
|
||||||
|
assert.ok(
|
||||||
|
Math.abs(bt.update([...Array(6).fill(1), -1, -1, -1, -1], Array(10).fill(10), up10, up10) - 0.7) < 1e-9,
|
||||||
|
);
|
||||||
|
|
||||||
|
// New Highs - New Lows: 2 - 1 = 1.
|
||||||
|
assert.equal(
|
||||||
|
new wickra.NewHighsNewLows().update([1, 1, -1], [10, 10, 10], [true, true, false], [false, false, true]),
|
||||||
|
1.0,
|
||||||
|
);
|
||||||
|
|
||||||
|
// High-Low Index(2): warmup null, then SMA(2) of [80, 60] = 70.
|
||||||
|
const hli = new wickra.HighLowIndex(2);
|
||||||
|
assert.equal(
|
||||||
|
hli.update(Array(10).fill(1), Array(10).fill(10), [...Array(8).fill(true), false, false], [...Array(8).fill(false), true, true]),
|
||||||
|
null,
|
||||||
|
);
|
||||||
|
assert.ok(
|
||||||
|
Math.abs(
|
||||||
|
hli.update(Array(10).fill(1), Array(10).fill(10), [...Array(6).fill(true), false, false, false, false], [...Array(6).fill(false), true, true, true, true]) - 70.0,
|
||||||
|
) < 1e-9,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Percent Above MA: 3/4 -> 75 (5-array update with aboveMa).
|
||||||
|
assert.equal(
|
||||||
|
new wickra.PercentAboveMa().update([1, 1, 1, -1], [10, 10, 10, 10], flags4, flags4, [true, true, true, false]),
|
||||||
|
75.0,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Up/Down Volume Ratio: 150/50 = 3.
|
||||||
|
assert.equal(
|
||||||
|
new wickra.UpDownVolumeRatio().update([1, -1], [150, 50], [false, false], [false, false]),
|
||||||
|
3.0,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Bullish Percent Index: 2/4 -> 50 (5-array update with onBuySignal).
|
||||||
|
assert.equal(
|
||||||
|
new wickra.BullishPercentIndex().update([1, 1, -1, -1], [10, 10, 10, 10], flags4, flags4, [true, true, false, false]),
|
||||||
|
50.0,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Cumulative Volume Index: (100/200) -> 0.5.
|
||||||
|
assert.ok(
|
||||||
|
Math.abs(new wickra.CumulativeVolumeIndex().update([1, -1], [150, 50], [false, false], [false, false]) - 0.5) < 1e-9,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Absolute Breadth Index: |2 - 3| = 1.
|
||||||
|
assert.equal(
|
||||||
|
new wickra.AbsoluteBreadthIndex().update([1, 1, -1, -1, -1], Array(5).fill(10), Array(5).fill(false), Array(5).fill(false)),
|
||||||
|
1.0,
|
||||||
|
);
|
||||||
|
|
||||||
|
// TICK Index: 2 - 3 = -1.
|
||||||
|
assert.equal(
|
||||||
|
new wickra.TickIndex().update([1, 1, -1, -1, -1], Array(5).fill(10), Array(5).fill(false), Array(5).fill(false)),
|
||||||
|
-1.0,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('market breadth: rejects ragged universe', () => {
|
||||||
|
assert.throws(() => new wickra.Trin().update([1, -1], [10], [false, false], [false, false]));
|
||||||
|
assert.throws(() =>
|
||||||
|
new wickra.PercentAboveMa().update([1, -1], [10, 10], [false, false], [false, false], [true]),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
test('OI / flow / liquidation indicators reference values', () => {
|
test('OI / flow / liquidation indicators reference values', () => {
|
||||||
// OI +10% while price flat -> divergence +0.1.
|
// OI +10% while price flat -> divergence +0.1.
|
||||||
const div = new wickra.OIPriceDivergence(1);
|
const div = new wickra.OIPriceDivergence(1);
|
||||||
|
|||||||
Vendored
+126
@@ -3253,6 +3253,132 @@ export declare class AdvanceDecline {
|
|||||||
isReady(): boolean
|
isReady(): boolean
|
||||||
warmupPeriod(): number
|
warmupPeriod(): number
|
||||||
}
|
}
|
||||||
|
export type AdvanceDeclineRatioNode = AdvanceDeclineRatio
|
||||||
|
export declare class AdvanceDeclineRatio {
|
||||||
|
constructor()
|
||||||
|
update(change: Array<number>, volume: Array<number>, newHigh: Array<boolean>, newLow: Array<boolean>): number | null
|
||||||
|
batch(change: Array<Array<number>>, volume: Array<Array<number>>, newHigh: Array<Array<boolean>>, newLow: Array<Array<boolean>>): Array<number>
|
||||||
|
reset(): void
|
||||||
|
isReady(): boolean
|
||||||
|
warmupPeriod(): number
|
||||||
|
}
|
||||||
|
export type AdVolumeLineNode = AdVolumeLine
|
||||||
|
export declare class AdVolumeLine {
|
||||||
|
constructor()
|
||||||
|
update(change: Array<number>, volume: Array<number>, newHigh: Array<boolean>, newLow: Array<boolean>): number | null
|
||||||
|
batch(change: Array<Array<number>>, volume: Array<Array<number>>, newHigh: Array<Array<boolean>>, newLow: Array<Array<boolean>>): Array<number>
|
||||||
|
reset(): void
|
||||||
|
isReady(): boolean
|
||||||
|
warmupPeriod(): number
|
||||||
|
}
|
||||||
|
export type McClellanOscillatorNode = McClellanOscillator
|
||||||
|
export declare class McClellanOscillator {
|
||||||
|
constructor()
|
||||||
|
update(change: Array<number>, volume: Array<number>, newHigh: Array<boolean>, newLow: Array<boolean>): number | null
|
||||||
|
batch(change: Array<Array<number>>, volume: Array<Array<number>>, newHigh: Array<Array<boolean>>, newLow: Array<Array<boolean>>): Array<number>
|
||||||
|
reset(): void
|
||||||
|
isReady(): boolean
|
||||||
|
warmupPeriod(): number
|
||||||
|
}
|
||||||
|
export type McClellanSummationIndexNode = McClellanSummationIndex
|
||||||
|
export declare class McClellanSummationIndex {
|
||||||
|
constructor()
|
||||||
|
update(change: Array<number>, volume: Array<number>, newHigh: Array<boolean>, newLow: Array<boolean>): number | null
|
||||||
|
batch(change: Array<Array<number>>, volume: Array<Array<number>>, newHigh: Array<Array<boolean>>, newLow: Array<Array<boolean>>): Array<number>
|
||||||
|
reset(): void
|
||||||
|
isReady(): boolean
|
||||||
|
warmupPeriod(): number
|
||||||
|
}
|
||||||
|
export type TrinNode = Trin
|
||||||
|
export declare class Trin {
|
||||||
|
constructor()
|
||||||
|
update(change: Array<number>, volume: Array<number>, newHigh: Array<boolean>, newLow: Array<boolean>): number | null
|
||||||
|
batch(change: Array<Array<number>>, volume: Array<Array<number>>, newHigh: Array<Array<boolean>>, newLow: Array<Array<boolean>>): Array<number>
|
||||||
|
reset(): void
|
||||||
|
isReady(): boolean
|
||||||
|
warmupPeriod(): number
|
||||||
|
}
|
||||||
|
export type BreadthThrustNode = BreadthThrust
|
||||||
|
export declare class BreadthThrust {
|
||||||
|
constructor(period: number)
|
||||||
|
update(change: Array<number>, volume: Array<number>, newHigh: Array<boolean>, newLow: Array<boolean>): number | null
|
||||||
|
batch(change: Array<Array<number>>, volume: Array<Array<number>>, newHigh: Array<Array<boolean>>, newLow: Array<Array<boolean>>): Array<number>
|
||||||
|
reset(): void
|
||||||
|
isReady(): boolean
|
||||||
|
warmupPeriod(): number
|
||||||
|
}
|
||||||
|
export type NewHighsNewLowsNode = NewHighsNewLows
|
||||||
|
export declare class NewHighsNewLows {
|
||||||
|
constructor()
|
||||||
|
update(change: Array<number>, volume: Array<number>, newHigh: Array<boolean>, newLow: Array<boolean>): number | null
|
||||||
|
batch(change: Array<Array<number>>, volume: Array<Array<number>>, newHigh: Array<Array<boolean>>, newLow: Array<Array<boolean>>): Array<number>
|
||||||
|
reset(): void
|
||||||
|
isReady(): boolean
|
||||||
|
warmupPeriod(): number
|
||||||
|
}
|
||||||
|
export type HighLowIndexNode = HighLowIndex
|
||||||
|
export declare class HighLowIndex {
|
||||||
|
constructor(period: number)
|
||||||
|
update(change: Array<number>, volume: Array<number>, newHigh: Array<boolean>, newLow: Array<boolean>): number | null
|
||||||
|
batch(change: Array<Array<number>>, volume: Array<Array<number>>, newHigh: Array<Array<boolean>>, newLow: Array<Array<boolean>>): Array<number>
|
||||||
|
reset(): void
|
||||||
|
isReady(): boolean
|
||||||
|
warmupPeriod(): number
|
||||||
|
}
|
||||||
|
export type PercentAboveMaNode = PercentAboveMa
|
||||||
|
export declare class PercentAboveMa {
|
||||||
|
constructor()
|
||||||
|
update(change: Array<number>, volume: Array<number>, newHigh: Array<boolean>, newLow: Array<boolean>, aboveMa: Array<boolean>): number | null
|
||||||
|
batch(change: Array<Array<number>>, volume: Array<Array<number>>, newHigh: Array<Array<boolean>>, newLow: Array<Array<boolean>>, aboveMa: Array<Array<boolean>>): Array<number>
|
||||||
|
reset(): void
|
||||||
|
isReady(): boolean
|
||||||
|
warmupPeriod(): number
|
||||||
|
}
|
||||||
|
export type UpDownVolumeRatioNode = UpDownVolumeRatio
|
||||||
|
export declare class UpDownVolumeRatio {
|
||||||
|
constructor()
|
||||||
|
update(change: Array<number>, volume: Array<number>, newHigh: Array<boolean>, newLow: Array<boolean>): number | null
|
||||||
|
batch(change: Array<Array<number>>, volume: Array<Array<number>>, newHigh: Array<Array<boolean>>, newLow: Array<Array<boolean>>): Array<number>
|
||||||
|
reset(): void
|
||||||
|
isReady(): boolean
|
||||||
|
warmupPeriod(): number
|
||||||
|
}
|
||||||
|
export type BullishPercentIndexNode = BullishPercentIndex
|
||||||
|
export declare class BullishPercentIndex {
|
||||||
|
constructor()
|
||||||
|
update(change: Array<number>, volume: Array<number>, newHigh: Array<boolean>, newLow: Array<boolean>, onBuySignal: Array<boolean>): number | null
|
||||||
|
batch(change: Array<Array<number>>, volume: Array<Array<number>>, newHigh: Array<Array<boolean>>, newLow: Array<Array<boolean>>, onBuySignal: Array<Array<boolean>>): Array<number>
|
||||||
|
reset(): void
|
||||||
|
isReady(): boolean
|
||||||
|
warmupPeriod(): number
|
||||||
|
}
|
||||||
|
export type CumulativeVolumeIndexNode = CumulativeVolumeIndex
|
||||||
|
export declare class CumulativeVolumeIndex {
|
||||||
|
constructor()
|
||||||
|
update(change: Array<number>, volume: Array<number>, newHigh: Array<boolean>, newLow: Array<boolean>): number | null
|
||||||
|
batch(change: Array<Array<number>>, volume: Array<Array<number>>, newHigh: Array<Array<boolean>>, newLow: Array<Array<boolean>>): Array<number>
|
||||||
|
reset(): void
|
||||||
|
isReady(): boolean
|
||||||
|
warmupPeriod(): number
|
||||||
|
}
|
||||||
|
export type AbsoluteBreadthIndexNode = AbsoluteBreadthIndex
|
||||||
|
export declare class AbsoluteBreadthIndex {
|
||||||
|
constructor()
|
||||||
|
update(change: Array<number>, volume: Array<number>, newHigh: Array<boolean>, newLow: Array<boolean>): number | null
|
||||||
|
batch(change: Array<Array<number>>, volume: Array<Array<number>>, newHigh: Array<Array<boolean>>, newLow: Array<Array<boolean>>): Array<number>
|
||||||
|
reset(): void
|
||||||
|
isReady(): boolean
|
||||||
|
warmupPeriod(): number
|
||||||
|
}
|
||||||
|
export type TickIndexNode = TickIndex
|
||||||
|
export declare class TickIndex {
|
||||||
|
constructor()
|
||||||
|
update(change: Array<number>, volume: Array<number>, newHigh: Array<boolean>, newLow: Array<boolean>): number | null
|
||||||
|
batch(change: Array<Array<number>>, volume: Array<Array<number>>, newHigh: Array<Array<boolean>>, newLow: Array<Array<boolean>>): Array<number>
|
||||||
|
reset(): void
|
||||||
|
isReady(): boolean
|
||||||
|
warmupPeriod(): number
|
||||||
|
}
|
||||||
export type SharpeRatioNode = SharpeRatio
|
export type SharpeRatioNode = SharpeRatio
|
||||||
export declare class SharpeRatio {
|
export declare class SharpeRatio {
|
||||||
constructor(period: number, riskFree: number)
|
constructor(period: number, riskFree: number)
|
||||||
|
|||||||
+15
-1
@@ -310,7 +310,7 @@ if (!nativeBinding) {
|
|||||||
throw new Error(`Failed to load native binding`)
|
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, SharpeRatio, SortinoRatio, CalmarRatio, OmegaRatio, MaxDrawdown, AverageDrawdown, DrawdownDuration, PainIndex, ValueAtRisk, ConditionalValueAtRisk, ProfitFactor, GainLossRatio, RecoveryFactor, KellyCriterion, TreynorRatio, InformationRatio, RenkoBars, KagiBars, PointAndFigureBars, Alpha } = 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, 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 } = nativeBinding
|
||||||
|
|
||||||
module.exports.version = version
|
module.exports.version = version
|
||||||
module.exports.SMA = SMA
|
module.exports.SMA = SMA
|
||||||
@@ -618,6 +618,20 @@ module.exports.LiquidationFeatures = LiquidationFeatures
|
|||||||
module.exports.TermStructureBasis = TermStructureBasis
|
module.exports.TermStructureBasis = TermStructureBasis
|
||||||
module.exports.CalendarSpread = CalendarSpread
|
module.exports.CalendarSpread = CalendarSpread
|
||||||
module.exports.AdvanceDecline = AdvanceDecline
|
module.exports.AdvanceDecline = AdvanceDecline
|
||||||
|
module.exports.AdvanceDeclineRatio = AdvanceDeclineRatio
|
||||||
|
module.exports.AdVolumeLine = AdVolumeLine
|
||||||
|
module.exports.McClellanOscillator = McClellanOscillator
|
||||||
|
module.exports.McClellanSummationIndex = McClellanSummationIndex
|
||||||
|
module.exports.Trin = Trin
|
||||||
|
module.exports.BreadthThrust = BreadthThrust
|
||||||
|
module.exports.NewHighsNewLows = NewHighsNewLows
|
||||||
|
module.exports.HighLowIndex = HighLowIndex
|
||||||
|
module.exports.PercentAboveMa = PercentAboveMa
|
||||||
|
module.exports.UpDownVolumeRatio = UpDownVolumeRatio
|
||||||
|
module.exports.BullishPercentIndex = BullishPercentIndex
|
||||||
|
module.exports.CumulativeVolumeIndex = CumulativeVolumeIndex
|
||||||
|
module.exports.AbsoluteBreadthIndex = AbsoluteBreadthIndex
|
||||||
|
module.exports.TickIndex = TickIndex
|
||||||
module.exports.SharpeRatio = SharpeRatio
|
module.exports.SharpeRatio = SharpeRatio
|
||||||
module.exports.SortinoRatio = SortinoRatio
|
module.exports.SortinoRatio = SortinoRatio
|
||||||
module.exports.CalmarRatio = CalmarRatio
|
module.exports.CalmarRatio = CalmarRatio
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "wickra-darwin-arm64",
|
"name": "wickra-darwin-arm64",
|
||||||
"version": "0.4.7",
|
"version": "0.5.0",
|
||||||
"description": "Native binding for wickra (macOS Apple Silicon). Installed automatically as an optional dependency of wickra on matching platforms.",
|
"description": "Native binding for wickra (macOS Apple Silicon). Installed automatically as an optional dependency of wickra on matching platforms.",
|
||||||
"main": "wickra.darwin-arm64.node",
|
"main": "wickra.darwin-arm64.node",
|
||||||
"files": [
|
"files": [
|
||||||
"wickra.darwin-arm64.node"
|
"wickra.darwin-arm64.node"
|
||||||
],
|
],
|
||||||
"license": "LicenseRef-Wickra-Noncommercial-1.0.0",
|
"license": "MIT OR Apache-2.0",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 18"
|
"node": ">= 18"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "wickra-darwin-x64",
|
"name": "wickra-darwin-x64",
|
||||||
"version": "0.4.7",
|
"version": "0.5.0",
|
||||||
"description": "Native binding for wickra (macOS Intel). Installed automatically as an optional dependency of wickra on matching platforms.",
|
"description": "Native binding for wickra (macOS Intel). Installed automatically as an optional dependency of wickra on matching platforms.",
|
||||||
"main": "wickra.darwin-x64.node",
|
"main": "wickra.darwin-x64.node",
|
||||||
"files": [
|
"files": [
|
||||||
"wickra.darwin-x64.node"
|
"wickra.darwin-x64.node"
|
||||||
],
|
],
|
||||||
"license": "LicenseRef-Wickra-Noncommercial-1.0.0",
|
"license": "MIT OR Apache-2.0",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 18"
|
"node": ">= 18"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "wickra-linux-arm64-gnu",
|
"name": "wickra-linux-arm64-gnu",
|
||||||
"version": "0.4.7",
|
"version": "0.5.0",
|
||||||
"description": "Native binding for wickra (linux arm64 GNU). Installed automatically as an optional dependency of wickra on matching platforms.",
|
"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",
|
"main": "wickra.linux-arm64-gnu.node",
|
||||||
"files": [
|
"files": [
|
||||||
"wickra.linux-arm64-gnu.node"
|
"wickra.linux-arm64-gnu.node"
|
||||||
],
|
],
|
||||||
"license": "LicenseRef-Wickra-Noncommercial-1.0.0",
|
"license": "MIT OR Apache-2.0",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 18"
|
"node": ">= 18"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "wickra-linux-x64-gnu",
|
"name": "wickra-linux-x64-gnu",
|
||||||
"version": "0.4.7",
|
"version": "0.5.0",
|
||||||
"description": "Native binding for wickra (linux x64 GNU). Installed automatically as an optional dependency of wickra on matching platforms.",
|
"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",
|
"main": "wickra.linux-x64-gnu.node",
|
||||||
"files": [
|
"files": [
|
||||||
"wickra.linux-x64-gnu.node"
|
"wickra.linux-x64-gnu.node"
|
||||||
],
|
],
|
||||||
"license": "LicenseRef-Wickra-Noncommercial-1.0.0",
|
"license": "MIT OR Apache-2.0",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 18"
|
"node": ">= 18"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "wickra-win32-arm64-msvc",
|
"name": "wickra-win32-arm64-msvc",
|
||||||
"version": "0.4.7",
|
"version": "0.5.0",
|
||||||
"description": "Native binding for wickra (Windows arm64 MSVC). Installed automatically as an optional dependency of wickra on matching platforms.",
|
"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",
|
"main": "wickra.win32-arm64-msvc.node",
|
||||||
"files": [
|
"files": [
|
||||||
"wickra.win32-arm64-msvc.node"
|
"wickra.win32-arm64-msvc.node"
|
||||||
],
|
],
|
||||||
"license": "LicenseRef-Wickra-Noncommercial-1.0.0",
|
"license": "MIT OR Apache-2.0",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 18"
|
"node": ">= 18"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "wickra-win32-x64-msvc",
|
"name": "wickra-win32-x64-msvc",
|
||||||
"version": "0.4.7",
|
"version": "0.5.0",
|
||||||
"description": "Native binding for wickra (Windows x64 MSVC). Installed automatically as an optional dependency of wickra on matching platforms.",
|
"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",
|
"main": "wickra.win32-x64-msvc.node",
|
||||||
"files": [
|
"files": [
|
||||||
"wickra.win32-x64-msvc.node"
|
"wickra.win32-x64-msvc.node"
|
||||||
],
|
],
|
||||||
"license": "LicenseRef-Wickra-Noncommercial-1.0.0",
|
"license": "MIT OR Apache-2.0",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 18"
|
"node": ">= 18"
|
||||||
},
|
},
|
||||||
|
|||||||
Generated
+27
-27
@@ -1,13 +1,13 @@
|
|||||||
{
|
{
|
||||||
"name": "wickra",
|
"name": "wickra",
|
||||||
"version": "0.4.7",
|
"version": "0.5.0",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "wickra",
|
"name": "wickra",
|
||||||
"version": "0.4.7",
|
"version": "0.5.0",
|
||||||
"license": "PolyForm-Noncommercial-1.0.0",
|
"license": "MIT OR Apache-2.0",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@napi-rs/cli": "^2.18.0"
|
"@napi-rs/cli": "^2.18.0"
|
||||||
},
|
},
|
||||||
@@ -15,12 +15,12 @@
|
|||||||
"node": ">= 18"
|
"node": ">= 18"
|
||||||
},
|
},
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
"wickra-darwin-arm64": "0.4.7",
|
"wickra-darwin-arm64": "0.5.0",
|
||||||
"wickra-darwin-x64": "0.4.7",
|
"wickra-darwin-x64": "0.5.0",
|
||||||
"wickra-linux-arm64-gnu": "0.4.7",
|
"wickra-linux-arm64-gnu": "0.5.0",
|
||||||
"wickra-linux-x64-gnu": "0.4.7",
|
"wickra-linux-x64-gnu": "0.5.0",
|
||||||
"wickra-win32-arm64-msvc": "0.4.7",
|
"wickra-win32-arm64-msvc": "0.5.0",
|
||||||
"wickra-win32-x64-msvc": "0.4.7"
|
"wickra-win32-x64-msvc": "0.5.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@napi-rs/cli": {
|
"node_modules/@napi-rs/cli": {
|
||||||
@@ -41,13 +41,13 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/wickra-darwin-arm64": {
|
"node_modules/wickra-darwin-arm64": {
|
||||||
"version": "0.4.7",
|
"version": "0.5.0",
|
||||||
"resolved": "https://registry.npmjs.org/wickra-darwin-arm64/-/wickra-darwin-arm64-0.4.7.tgz",
|
"resolved": "https://registry.npmjs.org/wickra-darwin-arm64/-/wickra-darwin-arm64-0.5.0.tgz",
|
||||||
"integrity": "sha512-4eZiBR/yGUdr4nzhEUFy2i69XgNx64iI2ax/LPamsThgylC0KpHOZKK19QzJ2d9KbK4C8nMjME5FLuR+4GNEwQ==",
|
"integrity": "sha512-4eZiBR/yGUdr4nzhEUFy2i69XgNx64iI2ax/LPamsThgylC0KpHOZKK19QzJ2d9KbK4C8nMjME5FLuR+4GNEwQ==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"license": "PolyForm-Noncommercial-1.0.0",
|
"license": "MIT OR Apache-2.0",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
"darwin"
|
"darwin"
|
||||||
@@ -57,13 +57,13 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/wickra-darwin-x64": {
|
"node_modules/wickra-darwin-x64": {
|
||||||
"version": "0.4.7",
|
"version": "0.5.0",
|
||||||
"resolved": "https://registry.npmjs.org/wickra-darwin-x64/-/wickra-darwin-x64-0.4.7.tgz",
|
"resolved": "https://registry.npmjs.org/wickra-darwin-x64/-/wickra-darwin-x64-0.5.0.tgz",
|
||||||
"integrity": "sha512-6hf8zI3QPjTFp4zCpmgUwDvNtu6jHqNUHKD5e55POo0CgA52HkpyxSPtVm8TGTIZDI7kPjlbOdBM8CJ76mmXwA==",
|
"integrity": "sha512-6hf8zI3QPjTFp4zCpmgUwDvNtu6jHqNUHKD5e55POo0CgA52HkpyxSPtVm8TGTIZDI7kPjlbOdBM8CJ76mmXwA==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"license": "PolyForm-Noncommercial-1.0.0",
|
"license": "MIT OR Apache-2.0",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
"darwin"
|
"darwin"
|
||||||
@@ -73,13 +73,13 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/wickra-linux-arm64-gnu": {
|
"node_modules/wickra-linux-arm64-gnu": {
|
||||||
"version": "0.4.7",
|
"version": "0.5.0",
|
||||||
"resolved": "https://registry.npmjs.org/wickra-linux-arm64-gnu/-/wickra-linux-arm64-gnu-0.4.7.tgz",
|
"resolved": "https://registry.npmjs.org/wickra-linux-arm64-gnu/-/wickra-linux-arm64-gnu-0.5.0.tgz",
|
||||||
"integrity": "sha512-kSe6y0xBMSiqdPLXNjwop5WZdHtvdBNKSEBCwZ4hFq33p4apW25/wrlzv9/oDuyD4kuPabJEhCCnFOplh58CUg==",
|
"integrity": "sha512-kSe6y0xBMSiqdPLXNjwop5WZdHtvdBNKSEBCwZ4hFq33p4apW25/wrlzv9/oDuyD4kuPabJEhCCnFOplh58CUg==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"license": "PolyForm-Noncommercial-1.0.0",
|
"license": "MIT OR Apache-2.0",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
"linux"
|
"linux"
|
||||||
@@ -89,13 +89,13 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/wickra-linux-x64-gnu": {
|
"node_modules/wickra-linux-x64-gnu": {
|
||||||
"version": "0.4.7",
|
"version": "0.5.0",
|
||||||
"resolved": "https://registry.npmjs.org/wickra-linux-x64-gnu/-/wickra-linux-x64-gnu-0.4.7.tgz",
|
"resolved": "https://registry.npmjs.org/wickra-linux-x64-gnu/-/wickra-linux-x64-gnu-0.5.0.tgz",
|
||||||
"integrity": "sha512-tWBWS4qz7hxM4xnpFb59bhf6TaLwXq0Z3jEa/2l7r8PiHA94g8r8S53NRMiT+4yiL5hSWe/nUiC/YXdRrhEZ4g==",
|
"integrity": "sha512-tWBWS4qz7hxM4xnpFb59bhf6TaLwXq0Z3jEa/2l7r8PiHA94g8r8S53NRMiT+4yiL5hSWe/nUiC/YXdRrhEZ4g==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"license": "PolyForm-Noncommercial-1.0.0",
|
"license": "MIT OR Apache-2.0",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
"linux"
|
"linux"
|
||||||
@@ -105,13 +105,13 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/wickra-win32-arm64-msvc": {
|
"node_modules/wickra-win32-arm64-msvc": {
|
||||||
"version": "0.4.7",
|
"version": "0.5.0",
|
||||||
"resolved": "https://registry.npmjs.org/wickra-win32-arm64-msvc/-/wickra-win32-arm64-msvc-0.4.7.tgz",
|
"resolved": "https://registry.npmjs.org/wickra-win32-arm64-msvc/-/wickra-win32-arm64-msvc-0.5.0.tgz",
|
||||||
"integrity": "sha512-EXIckHxAtF75PUGDKRzXyqMe9ldP0JjSdu68WFN6iJfp+McYrGu6h40TEJlQ/oUEIoPqiZB/xhVyo/el5Lg7zw==",
|
"integrity": "sha512-EXIckHxAtF75PUGDKRzXyqMe9ldP0JjSdu68WFN6iJfp+McYrGu6h40TEJlQ/oUEIoPqiZB/xhVyo/el5Lg7zw==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"license": "PolyForm-Noncommercial-1.0.0",
|
"license": "MIT OR Apache-2.0",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
"win32"
|
"win32"
|
||||||
@@ -121,13 +121,13 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/wickra-win32-x64-msvc": {
|
"node_modules/wickra-win32-x64-msvc": {
|
||||||
"version": "0.4.7",
|
"version": "0.5.0",
|
||||||
"resolved": "https://registry.npmjs.org/wickra-win32-x64-msvc/-/wickra-win32-x64-msvc-0.4.7.tgz",
|
"resolved": "https://registry.npmjs.org/wickra-win32-x64-msvc/-/wickra-win32-x64-msvc-0.5.0.tgz",
|
||||||
"integrity": "sha512-Yfsqq1Xwp6hdxMyLze411vNdo7BDwI6+lPSe7A9XdqyPecNDbtKwYLpsal2r8EHbNzqM+R8XnuRtUaEQS5VlUQ==",
|
"integrity": "sha512-Yfsqq1Xwp6hdxMyLze411vNdo7BDwI6+lPSe7A9XdqyPecNDbtKwYLpsal2r8EHbNzqM+R8XnuRtUaEQS5VlUQ==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"license": "PolyForm-Noncommercial-1.0.0",
|
"license": "MIT OR Apache-2.0",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
"win32"
|
"win32"
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
{
|
{
|
||||||
"name": "wickra",
|
"name": "wickra",
|
||||||
"version": "0.4.7",
|
"version": "0.5.0",
|
||||||
"description": "Streaming-first technical indicators: incremental, fast, install-free. Node bindings powered by Rust.",
|
"description": "Streaming-first technical indicators: incremental, fast, install-free. Node bindings powered by Rust.",
|
||||||
"author": "kingchenc <support@wickra.org>",
|
"author": "kingchenc <support@wickra.org>",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"types": "index.d.ts",
|
"types": "index.d.ts",
|
||||||
"license": "LicenseRef-Wickra-Noncommercial-1.0.0",
|
"license": "MIT OR Apache-2.0",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"trading",
|
"trading",
|
||||||
"indicators",
|
"indicators",
|
||||||
@@ -47,12 +47,12 @@
|
|||||||
"node": ">= 18"
|
"node": ">= 18"
|
||||||
},
|
},
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
"wickra-linux-x64-gnu": "0.4.7",
|
"wickra-linux-x64-gnu": "0.5.0",
|
||||||
"wickra-linux-arm64-gnu": "0.4.7",
|
"wickra-linux-arm64-gnu": "0.5.0",
|
||||||
"wickra-darwin-x64": "0.4.7",
|
"wickra-darwin-x64": "0.5.0",
|
||||||
"wickra-darwin-arm64": "0.4.7",
|
"wickra-darwin-arm64": "0.5.0",
|
||||||
"wickra-win32-x64-msvc": "0.4.7",
|
"wickra-win32-x64-msvc": "0.5.0",
|
||||||
"wickra-win32-arm64-msvc": "0.4.7"
|
"wickra-win32-arm64-msvc": "0.5.0"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "napi build --platform --release",
|
"build": "napi build --platform --release",
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -5,7 +5,7 @@ version.workspace = true
|
|||||||
authors.workspace = true
|
authors.workspace = true
|
||||||
edition.workspace = true
|
edition.workspace = true
|
||||||
rust-version.workspace = true
|
rust-version.workspace = true
|
||||||
license-file.workspace = true
|
license.workspace = true
|
||||||
repository.workspace = true
|
repository.workspace = true
|
||||||
homepage.workspace = true
|
homepage.workspace = true
|
||||||
readme.workspace = true
|
readme.workspace = true
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
[](https://github.com/wickra-lib/wickra/actions/workflows/ci.yml)
|
[](https://github.com/wickra-lib/wickra/actions/workflows/ci.yml)
|
||||||
[](https://codecov.io/gh/wickra-lib/wickra)
|
[](https://codecov.io/gh/wickra-lib/wickra)
|
||||||
[](https://pypi.org/project/wickra/)
|
[](https://pypi.org/project/wickra/)
|
||||||
[](https://github.com/wickra-lib/wickra/blob/main/LICENSE)
|
[](https://github.com/wickra-lib/wickra#license)
|
||||||
|
|
||||||
**Streaming-first technical indicators for Python. `pip install wickra` — no
|
**Streaming-first technical indicators for Python. `pip install wickra` — no
|
||||||
system dependencies, no C build tooling.**
|
system dependencies, no C build tooling.**
|
||||||
@@ -66,7 +66,5 @@ risk. The library is provided **as is**, without warranty of any kind.
|
|||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
Licensed under the **PolyForm Noncommercial License 1.0.0**. Personal projects,
|
Licensed under either of [Apache-2.0](https://github.com/wickra-lib/wickra/blob/main/LICENSE-APACHE)
|
||||||
research, education, non-profits, and hobby trading bots are all fine; the one
|
or [MIT](https://github.com/wickra-lib/wickra/blob/main/LICENSE-MIT) at your option.
|
||||||
thing not allowed is commercial sale of the software or of services built
|
|
||||||
around it. See [LICENSE](https://github.com/wickra-lib/wickra/blob/main/LICENSE).
|
|
||||||
|
|||||||
@@ -4,17 +4,16 @@ build-backend = "maturin"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "wickra"
|
name = "wickra"
|
||||||
version = "0.4.7"
|
version = "0.5.0"
|
||||||
description = "Streaming-first technical indicators: incremental, fast, install-free."
|
description = "Streaming-first technical indicators: incremental, fast, install-free."
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
license = { text = "PolyForm-Noncommercial-1.0.0 with additional personal-account permissions; see LICENSE" }
|
license = "MIT OR Apache-2.0"
|
||||||
authors = [{ name = "kingchenc", email = "support@wickra.org" }]
|
authors = [{ name = "kingchenc", email = "support@wickra.org" }]
|
||||||
requires-python = ">=3.9"
|
requires-python = ">=3.9"
|
||||||
keywords = ["finance", "trading", "indicators", "technical-analysis", "ta-lib"]
|
keywords = ["finance", "trading", "indicators", "technical-analysis", "ta-lib"]
|
||||||
classifiers = [
|
classifiers = [
|
||||||
"Development Status :: 4 - Beta",
|
"Development Status :: 4 - Beta",
|
||||||
"Intended Audience :: Financial and Insurance Industry",
|
"Intended Audience :: Financial and Insurance Industry",
|
||||||
"License :: Free for non-commercial use",
|
|
||||||
"Programming Language :: Python :: 3",
|
"Programming Language :: Python :: 3",
|
||||||
"Programming Language :: Python :: 3 :: Only",
|
"Programming Language :: Python :: 3 :: Only",
|
||||||
"Programming Language :: Python :: 3.9",
|
"Programming Language :: Python :: 3.9",
|
||||||
|
|||||||
@@ -352,6 +352,20 @@ from ._wickra import (
|
|||||||
TermStructureBasis,
|
TermStructureBasis,
|
||||||
CalendarSpread,
|
CalendarSpread,
|
||||||
# Market Breadth
|
# Market Breadth
|
||||||
|
TickIndex,
|
||||||
|
AbsoluteBreadthIndex,
|
||||||
|
CumulativeVolumeIndex,
|
||||||
|
BullishPercentIndex,
|
||||||
|
UpDownVolumeRatio,
|
||||||
|
PercentAboveMa,
|
||||||
|
HighLowIndex,
|
||||||
|
NewHighsNewLows,
|
||||||
|
BreadthThrust,
|
||||||
|
Trin,
|
||||||
|
McClellanSummationIndex,
|
||||||
|
McClellanOscillator,
|
||||||
|
AdVolumeLine,
|
||||||
|
AdvanceDeclineRatio,
|
||||||
AdvanceDecline,
|
AdvanceDecline,
|
||||||
# Risk / Performance
|
# Risk / Performance
|
||||||
SharpeRatio,
|
SharpeRatio,
|
||||||
@@ -702,6 +716,20 @@ __all__ = [
|
|||||||
"TermStructureBasis",
|
"TermStructureBasis",
|
||||||
"CalendarSpread",
|
"CalendarSpread",
|
||||||
# Market Breadth
|
# Market Breadth
|
||||||
|
"TickIndex",
|
||||||
|
"AbsoluteBreadthIndex",
|
||||||
|
"CumulativeVolumeIndex",
|
||||||
|
"BullishPercentIndex",
|
||||||
|
"UpDownVolumeRatio",
|
||||||
|
"PercentAboveMa",
|
||||||
|
"HighLowIndex",
|
||||||
|
"NewHighsNewLows",
|
||||||
|
"BreadthThrust",
|
||||||
|
"Trin",
|
||||||
|
"McClellanSummationIndex",
|
||||||
|
"McClellanOscillator",
|
||||||
|
"AdVolumeLine",
|
||||||
|
"AdvanceDeclineRatio",
|
||||||
"AdvanceDecline",
|
"AdvanceDecline",
|
||||||
# Risk / Performance
|
# Risk / Performance
|
||||||
"SharpeRatio",
|
"SharpeRatio",
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -2783,6 +2783,190 @@ def test_advance_decline_rejects_ragged_universe():
|
|||||||
ad.update([1.0, -1.0], [10.0], [False, False], [False, False])
|
ad.update([1.0, -1.0], [10.0], [False, False], [False, False])
|
||||||
|
|
||||||
|
|
||||||
|
def _breadth_streaming_equals_batch(indicator, change, volume, new_high, new_low):
|
||||||
|
"""Assert a 4-array breadth indicator's batch matches its streaming output."""
|
||||||
|
batch = indicator().batch(change, volume, new_high, new_low)
|
||||||
|
streamer = indicator()
|
||||||
|
streamed = np.array(
|
||||||
|
[
|
||||||
|
streamer.update(change[i], volume[i], new_high[i], new_low[i])
|
||||||
|
for i in range(len(change))
|
||||||
|
],
|
||||||
|
dtype=np.float64,
|
||||||
|
)
|
||||||
|
assert batch.shape == (len(change),)
|
||||||
|
assert _eq_nan(batch, streamed)
|
||||||
|
return batch
|
||||||
|
|
||||||
|
|
||||||
|
def test_advance_decline_ratio_breadth():
|
||||||
|
change = [[1.0, 1.0, 1.0, -1.0], [1.0, 0.0, 0.0, 0.0], [-1.0, -1.0, -1.0, -1.0]]
|
||||||
|
volume = [[10.0] * 4 for _ in range(3)]
|
||||||
|
flags = [[False] * 4 for _ in range(3)]
|
||||||
|
batch = _breadth_streaming_equals_batch(ta.AdvanceDeclineRatio, change, volume, flags, flags)
|
||||||
|
# 3/1 = 3 ; 1/max(0,1) = 1 ; 0/3 = 0.
|
||||||
|
assert list(batch) == [3.0, 1.0, 0.0]
|
||||||
|
|
||||||
|
|
||||||
|
def test_ad_volume_line_breadth():
|
||||||
|
change = [[1.0, -1.0], [1.0, -1.0], [1.0, 0.0]]
|
||||||
|
volume = [[150.0, 50.0], [60.0, 60.0], [30.0, 0.0]]
|
||||||
|
flags = [[False] * 2 for _ in range(3)]
|
||||||
|
batch = _breadth_streaming_equals_batch(ta.AdVolumeLine, change, volume, flags, flags)
|
||||||
|
# net +100 -> 100 ; net 0 -> 100 ; net +30 -> 130.
|
||||||
|
assert list(batch) == [100.0, 100.0, 130.0]
|
||||||
|
|
||||||
|
|
||||||
|
def test_mcclellan_oscillator_breadth():
|
||||||
|
change = [[1.0, 1.0, 1.0, -1.0], [-1.0, -1.0, -1.0, 1.0], [1.0, 1.0, -1.0, -1.0]]
|
||||||
|
volume = [[10.0] * 4 for _ in range(3)]
|
||||||
|
flags = [[False] * 4 for _ in range(3)]
|
||||||
|
batch = _breadth_streaming_equals_batch(ta.McClellanOscillator, change, volume, flags, flags)
|
||||||
|
# seed 0 ; -50 ; -67.5.
|
||||||
|
assert abs(batch[0]) < 1e-9
|
||||||
|
assert abs(batch[1] - (-50.0)) < 1e-9
|
||||||
|
assert abs(batch[2] - (-67.5)) < 1e-9
|
||||||
|
|
||||||
|
|
||||||
|
def test_mcclellan_summation_index_breadth():
|
||||||
|
change = [[1.0, 1.0, 1.0, -1.0], [-1.0, -1.0, -1.0, 1.0], [1.0, 1.0, -1.0, -1.0]]
|
||||||
|
volume = [[10.0] * 4 for _ in range(3)]
|
||||||
|
flags = [[False] * 4 for _ in range(3)]
|
||||||
|
batch = _breadth_streaming_equals_batch(ta.McClellanSummationIndex, change, volume, flags, flags)
|
||||||
|
# 0 ; -50 ; -117.5.
|
||||||
|
assert abs(batch[0]) < 1e-9
|
||||||
|
assert abs(batch[1] - (-50.0)) < 1e-9
|
||||||
|
assert abs(batch[2] - (-117.5)) < 1e-9
|
||||||
|
|
||||||
|
|
||||||
|
def test_trin_breadth():
|
||||||
|
change = [[1.0, 1.0, 1.0, -1.0], [1.0, 1.0, -1.0, -1.0]]
|
||||||
|
volume = [[50.0, 50.0, 50.0, 50.0], [10.0, 10.0, 40.0, 40.0]]
|
||||||
|
flags = [[False] * 4 for _ in range(2)]
|
||||||
|
batch = _breadth_streaming_equals_batch(ta.Trin, change, volume, flags, flags)
|
||||||
|
# (3/1)/(150/50) = 1 ; (2/2)/(20/80) = 4.
|
||||||
|
assert abs(batch[0] - 1.0) < 1e-9
|
||||||
|
assert abs(batch[1] - 4.0) < 1e-9
|
||||||
|
|
||||||
|
|
||||||
|
def test_breadth_thrust_breadth():
|
||||||
|
change = [[1.0] * 8 + [-1.0] * 2, [1.0] * 6 + [-1.0] * 4]
|
||||||
|
volume = [[10.0] * 10 for _ in range(2)]
|
||||||
|
flags = [[False] * 10 for _ in range(2)]
|
||||||
|
batch = ta.BreadthThrust(2).batch(change, volume, flags, flags)
|
||||||
|
streamer = ta.BreadthThrust(2)
|
||||||
|
streamed = np.array(
|
||||||
|
[streamer.update(change[i], volume[i], flags[i], flags[i]) for i in range(2)],
|
||||||
|
dtype=np.float64,
|
||||||
|
)
|
||||||
|
assert _eq_nan(batch, streamed)
|
||||||
|
# 0.8 (warmup -> NaN) ; SMA(2) of [0.8, 0.6] = 0.7.
|
||||||
|
assert math.isnan(batch[0])
|
||||||
|
assert abs(batch[1] - 0.7) < 1e-9
|
||||||
|
|
||||||
|
|
||||||
|
def test_new_highs_new_lows_breadth():
|
||||||
|
change = [[1.0, 1.0, -1.0], [1.0, -1.0, -1.0]]
|
||||||
|
volume = [[10.0] * 3 for _ in range(2)]
|
||||||
|
new_high = [[True, True, False], [True, False, False]]
|
||||||
|
new_low = [[False, False, True], [False, True, True]]
|
||||||
|
batch = _breadth_streaming_equals_batch(ta.NewHighsNewLows, change, volume, new_high, new_low)
|
||||||
|
# 2 - 1 = 1 ; 1 - 2 = -1.
|
||||||
|
assert list(batch) == [1.0, -1.0]
|
||||||
|
|
||||||
|
|
||||||
|
def test_high_low_index_breadth():
|
||||||
|
change = [[1.0] * 10, [1.0] * 10]
|
||||||
|
volume = [[10.0] * 10 for _ in range(2)]
|
||||||
|
new_high = [[True] * 8 + [False] * 2, [True] * 6 + [False] * 4]
|
||||||
|
new_low = [[False] * 8 + [True] * 2, [False] * 6 + [True] * 4]
|
||||||
|
batch = ta.HighLowIndex(2).batch(change, volume, new_high, new_low)
|
||||||
|
streamer = ta.HighLowIndex(2)
|
||||||
|
streamed = np.array(
|
||||||
|
[streamer.update(change[i], volume[i], new_high[i], new_low[i]) for i in range(2)],
|
||||||
|
dtype=np.float64,
|
||||||
|
)
|
||||||
|
assert _eq_nan(batch, streamed)
|
||||||
|
# 80% (warmup) ; SMA(2) of [80, 60] = 70.
|
||||||
|
assert math.isnan(batch[0])
|
||||||
|
assert abs(batch[1] - 70.0) < 1e-9
|
||||||
|
|
||||||
|
|
||||||
|
def test_percent_above_ma_breadth():
|
||||||
|
change = [[1.0, 1.0, 1.0, -1.0], [1.0, 1.0, -1.0, -1.0]]
|
||||||
|
volume = [[10.0] * 4 for _ in range(2)]
|
||||||
|
flags = [[False] * 4 for _ in range(2)]
|
||||||
|
above_ma = [[True, True, True, False], [True, False, False, False]]
|
||||||
|
batch = ta.PercentAboveMa().batch(change, volume, flags, flags, above_ma)
|
||||||
|
streamer = ta.PercentAboveMa()
|
||||||
|
streamed = np.array(
|
||||||
|
[streamer.update(change[i], volume[i], flags[i], flags[i], above_ma[i]) for i in range(2)],
|
||||||
|
dtype=np.float64,
|
||||||
|
)
|
||||||
|
assert _eq_nan(batch, streamed)
|
||||||
|
# 3/4 -> 75 ; 1/4 -> 25.
|
||||||
|
assert list(batch) == [75.0, 25.0]
|
||||||
|
|
||||||
|
|
||||||
|
def test_up_down_volume_ratio_breadth():
|
||||||
|
change = [[1.0, -1.0], [1.0, 0.0]]
|
||||||
|
volume = [[150.0, 50.0], [100.0, 0.0]]
|
||||||
|
flags = [[False] * 2 for _ in range(2)]
|
||||||
|
batch = _breadth_streaming_equals_batch(ta.UpDownVolumeRatio, change, volume, flags, flags)
|
||||||
|
# 150/50 = 3 ; 100/max(0,1) = 100.
|
||||||
|
assert list(batch) == [3.0, 100.0]
|
||||||
|
|
||||||
|
|
||||||
|
def test_bullish_percent_index_breadth():
|
||||||
|
change = [[1.0, 1.0, -1.0, -1.0], [1.0, 1.0, 1.0, 1.0]]
|
||||||
|
volume = [[10.0] * 4 for _ in range(2)]
|
||||||
|
flags = [[False] * 4 for _ in range(2)]
|
||||||
|
on_buy = [[True, True, False, False], [True, True, True, True]]
|
||||||
|
batch = ta.BullishPercentIndex().batch(change, volume, flags, flags, on_buy)
|
||||||
|
streamer = ta.BullishPercentIndex()
|
||||||
|
streamed = np.array(
|
||||||
|
[streamer.update(change[i], volume[i], flags[i], flags[i], on_buy[i]) for i in range(2)],
|
||||||
|
dtype=np.float64,
|
||||||
|
)
|
||||||
|
assert _eq_nan(batch, streamed)
|
||||||
|
# 2/4 -> 50 ; 4/4 -> 100.
|
||||||
|
assert list(batch) == [50.0, 100.0]
|
||||||
|
|
||||||
|
|
||||||
|
def test_cumulative_volume_index_breadth():
|
||||||
|
change = [[1.0, -1.0], [1.0, -1.0], [0.0]]
|
||||||
|
volume = [[150.0, 50.0], [60.0, 60.0], [0.0]]
|
||||||
|
new_high = [[False, False], [False, False], [False]]
|
||||||
|
new_low = [[False, False], [False, False], [False]]
|
||||||
|
batch = ta.CumulativeVolumeIndex().batch(change, volume, new_high, new_low)
|
||||||
|
streamer = ta.CumulativeVolumeIndex()
|
||||||
|
streamed = np.array(
|
||||||
|
[streamer.update(change[i], volume[i], new_high[i], new_low[i]) for i in range(3)],
|
||||||
|
dtype=np.float64,
|
||||||
|
)
|
||||||
|
assert _eq_nan(batch, streamed)
|
||||||
|
# (100/200) -> 0.5 ; net 0 -> 0.5 ; zero-volume tick -> 0.5.
|
||||||
|
assert list(batch) == [0.5, 0.5, 0.5]
|
||||||
|
|
||||||
|
|
||||||
|
def test_absolute_breadth_index_breadth():
|
||||||
|
change = [[1.0, 1.0, -1.0, -1.0, -1.0], [1.0, 1.0, 1.0, -1.0, -1.0]]
|
||||||
|
volume = [[10.0] * 5 for _ in range(2)]
|
||||||
|
flags = [[False] * 5 for _ in range(2)]
|
||||||
|
batch = _breadth_streaming_equals_batch(ta.AbsoluteBreadthIndex, change, volume, flags, flags)
|
||||||
|
# |2 - 3| = 1 ; |3 - 2| = 1.
|
||||||
|
assert list(batch) == [1.0, 1.0]
|
||||||
|
|
||||||
|
|
||||||
|
def test_tick_index_breadth():
|
||||||
|
change = [[1.0, 1.0, -1.0, -1.0, -1.0], [1.0, 1.0, 1.0, -1.0, -1.0]]
|
||||||
|
volume = [[10.0] * 5 for _ in range(2)]
|
||||||
|
flags = [[False] * 5 for _ in range(2)]
|
||||||
|
batch = _breadth_streaming_equals_batch(ta.TickIndex, change, volume, flags, flags)
|
||||||
|
# 2 - 3 = -1 ; 3 - 2 = 1.
|
||||||
|
assert list(batch) == [-1.0, 1.0]
|
||||||
|
|
||||||
|
|
||||||
def test_funding_basis_streaming_equals_batch():
|
def test_funding_basis_streaming_equals_batch():
|
||||||
n = 40
|
n = 40
|
||||||
index = np.array([100.0 + 0.5 * math.sin(i * 0.2) for i in range(n)], dtype=np.float64)
|
index = np.array([100.0 + 0.5 * math.sin(i * 0.2) for i in range(n)], dtype=np.float64)
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ version.workspace = true
|
|||||||
authors.workspace = true
|
authors.workspace = true
|
||||||
edition.workspace = true
|
edition.workspace = true
|
||||||
rust-version.workspace = true
|
rust-version.workspace = true
|
||||||
license-file.workspace = true
|
license.workspace = true
|
||||||
repository.workspace = true
|
repository.workspace = true
|
||||||
homepage.workspace = true
|
homepage.workspace = true
|
||||||
readme.workspace = true
|
readme.workspace = true
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
[](https://github.com/wickra-lib/wickra/actions/workflows/ci.yml)
|
[](https://github.com/wickra-lib/wickra/actions/workflows/ci.yml)
|
||||||
[](https://codecov.io/gh/wickra-lib/wickra)
|
[](https://codecov.io/gh/wickra-lib/wickra)
|
||||||
[](https://www.npmjs.com/package/wickra-wasm)
|
[](https://www.npmjs.com/package/wickra-wasm)
|
||||||
[](https://github.com/wickra-lib/wickra/blob/main/LICENSE)
|
[](https://github.com/wickra-lib/wickra#license)
|
||||||
|
|
||||||
**Streaming-first technical indicators in the browser. `npm install
|
**Streaming-first technical indicators in the browser. `npm install
|
||||||
wickra-wasm` — pure WebAssembly, runs anywhere a modern JS engine does.**
|
wickra-wasm` — pure WebAssembly, runs anywhere a modern JS engine does.**
|
||||||
@@ -66,7 +66,5 @@ risk. The library is provided **as is**, without warranty of any kind.
|
|||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
Licensed under the **PolyForm Noncommercial License 1.0.0**. Personal projects,
|
Licensed under either of [Apache-2.0](https://github.com/wickra-lib/wickra/blob/main/LICENSE-APACHE)
|
||||||
research, education, non-profits, and hobby trading bots are all fine; the one
|
or [MIT](https://github.com/wickra-lib/wickra/blob/main/LICENSE-MIT) at your option.
|
||||||
thing not allowed is commercial sale of the software or of services built
|
|
||||||
around it. See [LICENSE](https://github.com/wickra-lib/wickra/blob/main/LICENSE).
|
|
||||||
|
|||||||
@@ -8437,6 +8437,664 @@ impl WasmAdvanceDecline {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn build_cross_section_above_ma(
|
||||||
|
change: &[f64],
|
||||||
|
volume: &[f64],
|
||||||
|
new_high: &[f64],
|
||||||
|
new_low: &[f64],
|
||||||
|
above_ma: &[f64],
|
||||||
|
) -> Result<wc::CrossSection, JsError> {
|
||||||
|
if change.len() != volume.len()
|
||||||
|
|| change.len() != new_high.len()
|
||||||
|
|| change.len() != new_low.len()
|
||||||
|
|| change.len() != above_ma.len()
|
||||||
|
{
|
||||||
|
return Err(JsError::new(
|
||||||
|
"change, volume, newHigh, newLow and aboveMa must be equal length",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let members = (0..change.len())
|
||||||
|
.map(|i| {
|
||||||
|
wc::Member::with_signals(
|
||||||
|
change[i],
|
||||||
|
volume[i],
|
||||||
|
new_high[i] != 0.0,
|
||||||
|
new_low[i] != 0.0,
|
||||||
|
above_ma[i] != 0.0,
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
wc::CrossSection::new(members, 0).map_err(map_err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_cross_section_buy(
|
||||||
|
change: &[f64],
|
||||||
|
volume: &[f64],
|
||||||
|
new_high: &[f64],
|
||||||
|
new_low: &[f64],
|
||||||
|
on_buy_signal: &[f64],
|
||||||
|
) -> Result<wc::CrossSection, JsError> {
|
||||||
|
if change.len() != volume.len()
|
||||||
|
|| change.len() != new_high.len()
|
||||||
|
|| change.len() != new_low.len()
|
||||||
|
|| change.len() != on_buy_signal.len()
|
||||||
|
{
|
||||||
|
return Err(JsError::new(
|
||||||
|
"change, volume, newHigh, newLow and onBuySignal must be equal length",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let members = (0..change.len())
|
||||||
|
.map(|i| {
|
||||||
|
wc::Member::with_signals(
|
||||||
|
change[i],
|
||||||
|
volume[i],
|
||||||
|
new_high[i] != 0.0,
|
||||||
|
new_low[i] != 0.0,
|
||||||
|
false,
|
||||||
|
on_buy_signal[i] != 0.0,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
wc::CrossSection::new(members, 0).map_err(map_err)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[wasm_bindgen(js_name = AdvanceDeclineRatio)]
|
||||||
|
pub struct WasmAdvanceDeclineRatio {
|
||||||
|
inner: wc::AdvanceDeclineRatio,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for WasmAdvanceDeclineRatio {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[wasm_bindgen(js_class = AdvanceDeclineRatio)]
|
||||||
|
impl WasmAdvanceDeclineRatio {
|
||||||
|
#[wasm_bindgen(constructor)]
|
||||||
|
pub fn new() -> WasmAdvanceDeclineRatio {
|
||||||
|
Self {
|
||||||
|
inner: wc::AdvanceDeclineRatio::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn update(
|
||||||
|
&mut self,
|
||||||
|
change: Vec<f64>,
|
||||||
|
volume: Vec<f64>,
|
||||||
|
new_high: Vec<f64>,
|
||||||
|
new_low: Vec<f64>,
|
||||||
|
) -> Result<Option<f64>, JsError> {
|
||||||
|
Ok(self
|
||||||
|
.inner
|
||||||
|
.update(build_cross_section(&change, &volume, &new_high, &new_low)?))
|
||||||
|
}
|
||||||
|
pub fn reset(&mut self) {
|
||||||
|
self.inner.reset();
|
||||||
|
}
|
||||||
|
#[wasm_bindgen(js_name = isReady)]
|
||||||
|
pub fn is_ready(&self) -> bool {
|
||||||
|
self.inner.is_ready()
|
||||||
|
}
|
||||||
|
#[wasm_bindgen(js_name = warmupPeriod)]
|
||||||
|
pub fn warmup_period(&self) -> usize {
|
||||||
|
self.inner.warmup_period()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[wasm_bindgen(js_name = AdVolumeLine)]
|
||||||
|
pub struct WasmAdVolumeLine {
|
||||||
|
inner: wc::AdVolumeLine,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for WasmAdVolumeLine {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[wasm_bindgen(js_class = AdVolumeLine)]
|
||||||
|
impl WasmAdVolumeLine {
|
||||||
|
#[wasm_bindgen(constructor)]
|
||||||
|
pub fn new() -> WasmAdVolumeLine {
|
||||||
|
Self {
|
||||||
|
inner: wc::AdVolumeLine::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn update(
|
||||||
|
&mut self,
|
||||||
|
change: Vec<f64>,
|
||||||
|
volume: Vec<f64>,
|
||||||
|
new_high: Vec<f64>,
|
||||||
|
new_low: Vec<f64>,
|
||||||
|
) -> Result<Option<f64>, JsError> {
|
||||||
|
Ok(self
|
||||||
|
.inner
|
||||||
|
.update(build_cross_section(&change, &volume, &new_high, &new_low)?))
|
||||||
|
}
|
||||||
|
pub fn reset(&mut self) {
|
||||||
|
self.inner.reset();
|
||||||
|
}
|
||||||
|
#[wasm_bindgen(js_name = isReady)]
|
||||||
|
pub fn is_ready(&self) -> bool {
|
||||||
|
self.inner.is_ready()
|
||||||
|
}
|
||||||
|
#[wasm_bindgen(js_name = warmupPeriod)]
|
||||||
|
pub fn warmup_period(&self) -> usize {
|
||||||
|
self.inner.warmup_period()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[wasm_bindgen(js_name = McClellanOscillator)]
|
||||||
|
pub struct WasmMcClellanOscillator {
|
||||||
|
inner: wc::McClellanOscillator,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for WasmMcClellanOscillator {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[wasm_bindgen(js_class = McClellanOscillator)]
|
||||||
|
impl WasmMcClellanOscillator {
|
||||||
|
#[wasm_bindgen(constructor)]
|
||||||
|
pub fn new() -> WasmMcClellanOscillator {
|
||||||
|
Self {
|
||||||
|
inner: wc::McClellanOscillator::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn update(
|
||||||
|
&mut self,
|
||||||
|
change: Vec<f64>,
|
||||||
|
volume: Vec<f64>,
|
||||||
|
new_high: Vec<f64>,
|
||||||
|
new_low: Vec<f64>,
|
||||||
|
) -> Result<Option<f64>, JsError> {
|
||||||
|
Ok(self
|
||||||
|
.inner
|
||||||
|
.update(build_cross_section(&change, &volume, &new_high, &new_low)?))
|
||||||
|
}
|
||||||
|
pub fn reset(&mut self) {
|
||||||
|
self.inner.reset();
|
||||||
|
}
|
||||||
|
#[wasm_bindgen(js_name = isReady)]
|
||||||
|
pub fn is_ready(&self) -> bool {
|
||||||
|
self.inner.is_ready()
|
||||||
|
}
|
||||||
|
#[wasm_bindgen(js_name = warmupPeriod)]
|
||||||
|
pub fn warmup_period(&self) -> usize {
|
||||||
|
self.inner.warmup_period()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[wasm_bindgen(js_name = McClellanSummationIndex)]
|
||||||
|
pub struct WasmMcClellanSummationIndex {
|
||||||
|
inner: wc::McClellanSummationIndex,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for WasmMcClellanSummationIndex {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[wasm_bindgen(js_class = McClellanSummationIndex)]
|
||||||
|
impl WasmMcClellanSummationIndex {
|
||||||
|
#[wasm_bindgen(constructor)]
|
||||||
|
pub fn new() -> WasmMcClellanSummationIndex {
|
||||||
|
Self {
|
||||||
|
inner: wc::McClellanSummationIndex::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn update(
|
||||||
|
&mut self,
|
||||||
|
change: Vec<f64>,
|
||||||
|
volume: Vec<f64>,
|
||||||
|
new_high: Vec<f64>,
|
||||||
|
new_low: Vec<f64>,
|
||||||
|
) -> Result<Option<f64>, JsError> {
|
||||||
|
Ok(self
|
||||||
|
.inner
|
||||||
|
.update(build_cross_section(&change, &volume, &new_high, &new_low)?))
|
||||||
|
}
|
||||||
|
pub fn reset(&mut self) {
|
||||||
|
self.inner.reset();
|
||||||
|
}
|
||||||
|
#[wasm_bindgen(js_name = isReady)]
|
||||||
|
pub fn is_ready(&self) -> bool {
|
||||||
|
self.inner.is_ready()
|
||||||
|
}
|
||||||
|
#[wasm_bindgen(js_name = warmupPeriod)]
|
||||||
|
pub fn warmup_period(&self) -> usize {
|
||||||
|
self.inner.warmup_period()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[wasm_bindgen(js_name = Trin)]
|
||||||
|
pub struct WasmTrin {
|
||||||
|
inner: wc::Trin,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for WasmTrin {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[wasm_bindgen(js_class = Trin)]
|
||||||
|
impl WasmTrin {
|
||||||
|
#[wasm_bindgen(constructor)]
|
||||||
|
pub fn new() -> WasmTrin {
|
||||||
|
Self {
|
||||||
|
inner: wc::Trin::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn update(
|
||||||
|
&mut self,
|
||||||
|
change: Vec<f64>,
|
||||||
|
volume: Vec<f64>,
|
||||||
|
new_high: Vec<f64>,
|
||||||
|
new_low: Vec<f64>,
|
||||||
|
) -> Result<Option<f64>, JsError> {
|
||||||
|
Ok(self
|
||||||
|
.inner
|
||||||
|
.update(build_cross_section(&change, &volume, &new_high, &new_low)?))
|
||||||
|
}
|
||||||
|
pub fn reset(&mut self) {
|
||||||
|
self.inner.reset();
|
||||||
|
}
|
||||||
|
#[wasm_bindgen(js_name = isReady)]
|
||||||
|
pub fn is_ready(&self) -> bool {
|
||||||
|
self.inner.is_ready()
|
||||||
|
}
|
||||||
|
#[wasm_bindgen(js_name = warmupPeriod)]
|
||||||
|
pub fn warmup_period(&self) -> usize {
|
||||||
|
self.inner.warmup_period()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[wasm_bindgen(js_name = BreadthThrust)]
|
||||||
|
pub struct WasmBreadthThrust {
|
||||||
|
inner: wc::BreadthThrust,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[wasm_bindgen(js_class = BreadthThrust)]
|
||||||
|
impl WasmBreadthThrust {
|
||||||
|
#[wasm_bindgen(constructor)]
|
||||||
|
pub fn new(period: usize) -> Result<WasmBreadthThrust, JsError> {
|
||||||
|
Ok(WasmBreadthThrust {
|
||||||
|
inner: wc::BreadthThrust::new(period).map_err(map_err)?,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
pub fn update(
|
||||||
|
&mut self,
|
||||||
|
change: Vec<f64>,
|
||||||
|
volume: Vec<f64>,
|
||||||
|
new_high: Vec<f64>,
|
||||||
|
new_low: Vec<f64>,
|
||||||
|
) -> Result<Option<f64>, JsError> {
|
||||||
|
Ok(self
|
||||||
|
.inner
|
||||||
|
.update(build_cross_section(&change, &volume, &new_high, &new_low)?))
|
||||||
|
}
|
||||||
|
pub fn reset(&mut self) {
|
||||||
|
self.inner.reset();
|
||||||
|
}
|
||||||
|
#[wasm_bindgen(js_name = isReady)]
|
||||||
|
pub fn is_ready(&self) -> bool {
|
||||||
|
self.inner.is_ready()
|
||||||
|
}
|
||||||
|
#[wasm_bindgen(js_name = warmupPeriod)]
|
||||||
|
pub fn warmup_period(&self) -> usize {
|
||||||
|
self.inner.warmup_period()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[wasm_bindgen(js_name = NewHighsNewLows)]
|
||||||
|
pub struct WasmNewHighsNewLows {
|
||||||
|
inner: wc::NewHighsNewLows,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for WasmNewHighsNewLows {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[wasm_bindgen(js_class = NewHighsNewLows)]
|
||||||
|
impl WasmNewHighsNewLows {
|
||||||
|
#[wasm_bindgen(constructor)]
|
||||||
|
pub fn new() -> WasmNewHighsNewLows {
|
||||||
|
Self {
|
||||||
|
inner: wc::NewHighsNewLows::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn update(
|
||||||
|
&mut self,
|
||||||
|
change: Vec<f64>,
|
||||||
|
volume: Vec<f64>,
|
||||||
|
new_high: Vec<f64>,
|
||||||
|
new_low: Vec<f64>,
|
||||||
|
) -> Result<Option<f64>, JsError> {
|
||||||
|
Ok(self
|
||||||
|
.inner
|
||||||
|
.update(build_cross_section(&change, &volume, &new_high, &new_low)?))
|
||||||
|
}
|
||||||
|
pub fn reset(&mut self) {
|
||||||
|
self.inner.reset();
|
||||||
|
}
|
||||||
|
#[wasm_bindgen(js_name = isReady)]
|
||||||
|
pub fn is_ready(&self) -> bool {
|
||||||
|
self.inner.is_ready()
|
||||||
|
}
|
||||||
|
#[wasm_bindgen(js_name = warmupPeriod)]
|
||||||
|
pub fn warmup_period(&self) -> usize {
|
||||||
|
self.inner.warmup_period()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[wasm_bindgen(js_name = HighLowIndex)]
|
||||||
|
pub struct WasmHighLowIndex {
|
||||||
|
inner: wc::HighLowIndex,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[wasm_bindgen(js_class = HighLowIndex)]
|
||||||
|
impl WasmHighLowIndex {
|
||||||
|
#[wasm_bindgen(constructor)]
|
||||||
|
pub fn new(period: usize) -> Result<WasmHighLowIndex, JsError> {
|
||||||
|
Ok(WasmHighLowIndex {
|
||||||
|
inner: wc::HighLowIndex::new(period).map_err(map_err)?,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
pub fn update(
|
||||||
|
&mut self,
|
||||||
|
change: Vec<f64>,
|
||||||
|
volume: Vec<f64>,
|
||||||
|
new_high: Vec<f64>,
|
||||||
|
new_low: Vec<f64>,
|
||||||
|
) -> Result<Option<f64>, JsError> {
|
||||||
|
Ok(self
|
||||||
|
.inner
|
||||||
|
.update(build_cross_section(&change, &volume, &new_high, &new_low)?))
|
||||||
|
}
|
||||||
|
pub fn reset(&mut self) {
|
||||||
|
self.inner.reset();
|
||||||
|
}
|
||||||
|
#[wasm_bindgen(js_name = isReady)]
|
||||||
|
pub fn is_ready(&self) -> bool {
|
||||||
|
self.inner.is_ready()
|
||||||
|
}
|
||||||
|
#[wasm_bindgen(js_name = warmupPeriod)]
|
||||||
|
pub fn warmup_period(&self) -> usize {
|
||||||
|
self.inner.warmup_period()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[wasm_bindgen(js_name = PercentAboveMa)]
|
||||||
|
pub struct WasmPercentAboveMa {
|
||||||
|
inner: wc::PercentAboveMa,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for WasmPercentAboveMa {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[wasm_bindgen(js_class = PercentAboveMa)]
|
||||||
|
impl WasmPercentAboveMa {
|
||||||
|
#[wasm_bindgen(constructor)]
|
||||||
|
pub fn new() -> WasmPercentAboveMa {
|
||||||
|
Self {
|
||||||
|
inner: wc::PercentAboveMa::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn update(
|
||||||
|
&mut self,
|
||||||
|
change: Vec<f64>,
|
||||||
|
volume: Vec<f64>,
|
||||||
|
new_high: Vec<f64>,
|
||||||
|
new_low: Vec<f64>,
|
||||||
|
above_ma: Vec<f64>,
|
||||||
|
) -> Result<Option<f64>, JsError> {
|
||||||
|
Ok(self.inner.update(build_cross_section_above_ma(
|
||||||
|
&change, &volume, &new_high, &new_low, &above_ma,
|
||||||
|
)?))
|
||||||
|
}
|
||||||
|
pub fn reset(&mut self) {
|
||||||
|
self.inner.reset();
|
||||||
|
}
|
||||||
|
#[wasm_bindgen(js_name = isReady)]
|
||||||
|
pub fn is_ready(&self) -> bool {
|
||||||
|
self.inner.is_ready()
|
||||||
|
}
|
||||||
|
#[wasm_bindgen(js_name = warmupPeriod)]
|
||||||
|
pub fn warmup_period(&self) -> usize {
|
||||||
|
self.inner.warmup_period()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[wasm_bindgen(js_name = UpDownVolumeRatio)]
|
||||||
|
pub struct WasmUpDownVolumeRatio {
|
||||||
|
inner: wc::UpDownVolumeRatio,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for WasmUpDownVolumeRatio {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[wasm_bindgen(js_class = UpDownVolumeRatio)]
|
||||||
|
impl WasmUpDownVolumeRatio {
|
||||||
|
#[wasm_bindgen(constructor)]
|
||||||
|
pub fn new() -> WasmUpDownVolumeRatio {
|
||||||
|
Self {
|
||||||
|
inner: wc::UpDownVolumeRatio::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn update(
|
||||||
|
&mut self,
|
||||||
|
change: Vec<f64>,
|
||||||
|
volume: Vec<f64>,
|
||||||
|
new_high: Vec<f64>,
|
||||||
|
new_low: Vec<f64>,
|
||||||
|
) -> Result<Option<f64>, JsError> {
|
||||||
|
Ok(self
|
||||||
|
.inner
|
||||||
|
.update(build_cross_section(&change, &volume, &new_high, &new_low)?))
|
||||||
|
}
|
||||||
|
pub fn reset(&mut self) {
|
||||||
|
self.inner.reset();
|
||||||
|
}
|
||||||
|
#[wasm_bindgen(js_name = isReady)]
|
||||||
|
pub fn is_ready(&self) -> bool {
|
||||||
|
self.inner.is_ready()
|
||||||
|
}
|
||||||
|
#[wasm_bindgen(js_name = warmupPeriod)]
|
||||||
|
pub fn warmup_period(&self) -> usize {
|
||||||
|
self.inner.warmup_period()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[wasm_bindgen(js_name = BullishPercentIndex)]
|
||||||
|
pub struct WasmBullishPercentIndex {
|
||||||
|
inner: wc::BullishPercentIndex,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for WasmBullishPercentIndex {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[wasm_bindgen(js_class = BullishPercentIndex)]
|
||||||
|
impl WasmBullishPercentIndex {
|
||||||
|
#[wasm_bindgen(constructor)]
|
||||||
|
pub fn new() -> WasmBullishPercentIndex {
|
||||||
|
Self {
|
||||||
|
inner: wc::BullishPercentIndex::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn update(
|
||||||
|
&mut self,
|
||||||
|
change: Vec<f64>,
|
||||||
|
volume: Vec<f64>,
|
||||||
|
new_high: Vec<f64>,
|
||||||
|
new_low: Vec<f64>,
|
||||||
|
on_buy_signal: Vec<f64>,
|
||||||
|
) -> Result<Option<f64>, JsError> {
|
||||||
|
Ok(self.inner.update(build_cross_section_buy(
|
||||||
|
&change,
|
||||||
|
&volume,
|
||||||
|
&new_high,
|
||||||
|
&new_low,
|
||||||
|
&on_buy_signal,
|
||||||
|
)?))
|
||||||
|
}
|
||||||
|
pub fn reset(&mut self) {
|
||||||
|
self.inner.reset();
|
||||||
|
}
|
||||||
|
#[wasm_bindgen(js_name = isReady)]
|
||||||
|
pub fn is_ready(&self) -> bool {
|
||||||
|
self.inner.is_ready()
|
||||||
|
}
|
||||||
|
#[wasm_bindgen(js_name = warmupPeriod)]
|
||||||
|
pub fn warmup_period(&self) -> usize {
|
||||||
|
self.inner.warmup_period()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[wasm_bindgen(js_name = CumulativeVolumeIndex)]
|
||||||
|
pub struct WasmCumulativeVolumeIndex {
|
||||||
|
inner: wc::CumulativeVolumeIndex,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for WasmCumulativeVolumeIndex {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[wasm_bindgen(js_class = CumulativeVolumeIndex)]
|
||||||
|
impl WasmCumulativeVolumeIndex {
|
||||||
|
#[wasm_bindgen(constructor)]
|
||||||
|
pub fn new() -> WasmCumulativeVolumeIndex {
|
||||||
|
Self {
|
||||||
|
inner: wc::CumulativeVolumeIndex::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn update(
|
||||||
|
&mut self,
|
||||||
|
change: Vec<f64>,
|
||||||
|
volume: Vec<f64>,
|
||||||
|
new_high: Vec<f64>,
|
||||||
|
new_low: Vec<f64>,
|
||||||
|
) -> Result<Option<f64>, JsError> {
|
||||||
|
Ok(self
|
||||||
|
.inner
|
||||||
|
.update(build_cross_section(&change, &volume, &new_high, &new_low)?))
|
||||||
|
}
|
||||||
|
pub fn reset(&mut self) {
|
||||||
|
self.inner.reset();
|
||||||
|
}
|
||||||
|
#[wasm_bindgen(js_name = isReady)]
|
||||||
|
pub fn is_ready(&self) -> bool {
|
||||||
|
self.inner.is_ready()
|
||||||
|
}
|
||||||
|
#[wasm_bindgen(js_name = warmupPeriod)]
|
||||||
|
pub fn warmup_period(&self) -> usize {
|
||||||
|
self.inner.warmup_period()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[wasm_bindgen(js_name = AbsoluteBreadthIndex)]
|
||||||
|
pub struct WasmAbsoluteBreadthIndex {
|
||||||
|
inner: wc::AbsoluteBreadthIndex,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for WasmAbsoluteBreadthIndex {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[wasm_bindgen(js_class = AbsoluteBreadthIndex)]
|
||||||
|
impl WasmAbsoluteBreadthIndex {
|
||||||
|
#[wasm_bindgen(constructor)]
|
||||||
|
pub fn new() -> WasmAbsoluteBreadthIndex {
|
||||||
|
Self {
|
||||||
|
inner: wc::AbsoluteBreadthIndex::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn update(
|
||||||
|
&mut self,
|
||||||
|
change: Vec<f64>,
|
||||||
|
volume: Vec<f64>,
|
||||||
|
new_high: Vec<f64>,
|
||||||
|
new_low: Vec<f64>,
|
||||||
|
) -> Result<Option<f64>, JsError> {
|
||||||
|
Ok(self
|
||||||
|
.inner
|
||||||
|
.update(build_cross_section(&change, &volume, &new_high, &new_low)?))
|
||||||
|
}
|
||||||
|
pub fn reset(&mut self) {
|
||||||
|
self.inner.reset();
|
||||||
|
}
|
||||||
|
#[wasm_bindgen(js_name = isReady)]
|
||||||
|
pub fn is_ready(&self) -> bool {
|
||||||
|
self.inner.is_ready()
|
||||||
|
}
|
||||||
|
#[wasm_bindgen(js_name = warmupPeriod)]
|
||||||
|
pub fn warmup_period(&self) -> usize {
|
||||||
|
self.inner.warmup_period()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[wasm_bindgen(js_name = TickIndex)]
|
||||||
|
pub struct WasmTickIndex {
|
||||||
|
inner: wc::TickIndex,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for WasmTickIndex {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[wasm_bindgen(js_class = TickIndex)]
|
||||||
|
impl WasmTickIndex {
|
||||||
|
#[wasm_bindgen(constructor)]
|
||||||
|
pub fn new() -> WasmTickIndex {
|
||||||
|
Self {
|
||||||
|
inner: wc::TickIndex::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn update(
|
||||||
|
&mut self,
|
||||||
|
change: Vec<f64>,
|
||||||
|
volume: Vec<f64>,
|
||||||
|
new_high: Vec<f64>,
|
||||||
|
new_low: Vec<f64>,
|
||||||
|
) -> Result<Option<f64>, JsError> {
|
||||||
|
Ok(self
|
||||||
|
.inner
|
||||||
|
.update(build_cross_section(&change, &volume, &new_high, &new_low)?))
|
||||||
|
}
|
||||||
|
pub fn reset(&mut self) {
|
||||||
|
self.inner.reset();
|
||||||
|
}
|
||||||
|
#[wasm_bindgen(js_name = isReady)]
|
||||||
|
pub fn is_ready(&self) -> bool {
|
||||||
|
self.inner.is_ready()
|
||||||
|
}
|
||||||
|
#[wasm_bindgen(js_name = warmupPeriod)]
|
||||||
|
pub fn warmup_period(&self) -> usize {
|
||||||
|
self.inner.warmup_period()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
# Proper nouns that appear in indicator documentation. They are real names,
|
# Proper nouns that appear in indicator documentation. They are real names,
|
||||||
# not code identifiers, so `clippy::doc_markdown` must not demand backticks.
|
# not code identifiers, so `clippy::doc_markdown` must not demand backticks.
|
||||||
# `..` keeps clippy's built-in default identifier list in addition to these.
|
# `..` keeps clippy's built-in default identifier list in addition to these.
|
||||||
doc-valid-idents = ["LeBeau", ".."]
|
doc-valid-idents = ["LeBeau", "McClellan", ".."]
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ version.workspace = true
|
|||||||
authors.workspace = true
|
authors.workspace = true
|
||||||
edition.workspace = true
|
edition.workspace = true
|
||||||
rust-version.workspace = true
|
rust-version.workspace = true
|
||||||
license-file.workspace = true
|
license.workspace = true
|
||||||
repository.workspace = true
|
repository.workspace = true
|
||||||
homepage.workspace = true
|
homepage.workspace = true
|
||||||
readme.workspace = true
|
readme.workspace = true
|
||||||
|
|||||||
@@ -9,9 +9,10 @@
|
|||||||
//!
|
//!
|
||||||
//! Each [`Member`] precomputes the per-symbol signals the breadth indicators
|
//! Each [`Member`] precomputes the per-symbol signals the breadth indicators
|
||||||
//! need — a signed price `change` (whose sign classifies the symbol as
|
//! need — a signed price `change` (whose sign classifies the symbol as
|
||||||
//! advancing, declining or unchanged), the period `volume`, and the
|
//! advancing, declining or unchanged), the period `volume`, the
|
||||||
//! `new_high` / `new_low` extreme flags — so the indicators stay stateless per
|
//! `new_high` / `new_low` extreme flags, and the `above_ma` / `on_buy_signal`
|
||||||
//! tick and never have to track per-symbol history.
|
//! state flags — so the indicators stay stateless per tick and never have to
|
||||||
|
//! track per-symbol history.
|
||||||
//!
|
//!
|
||||||
//! [`DerivativesTick`]: crate::DerivativesTick
|
//! [`DerivativesTick`]: crate::DerivativesTick
|
||||||
//! [`OrderBook`]: crate::OrderBook
|
//! [`OrderBook`]: crate::OrderBook
|
||||||
@@ -28,9 +29,16 @@ use crate::error::{Error, Result};
|
|||||||
/// - `volume` is finite and non-negative.
|
/// - `volume` is finite and non-negative.
|
||||||
///
|
///
|
||||||
/// `new_high` / `new_low` are caller-supplied flags marking whether the symbol
|
/// `new_high` / `new_low` are caller-supplied flags marking whether the symbol
|
||||||
/// printed a new period extreme; they carry no numeric invariant.
|
/// printed a new period extreme; `above_ma` / `on_buy_signal` are caller-supplied
|
||||||
|
/// per-symbol state signals (whether the symbol trades above its reference moving
|
||||||
|
/// average, and whether it is on a point-and-figure buy signal). None of the four
|
||||||
|
/// flags carries a numeric invariant.
|
||||||
#[non_exhaustive]
|
#[non_exhaustive]
|
||||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||||
|
#[allow(
|
||||||
|
clippy::struct_excessive_bools,
|
||||||
|
reason = "the four flags are independent per-symbol breadth signals, not a state machine"
|
||||||
|
)]
|
||||||
pub struct Member {
|
pub struct Member {
|
||||||
/// Price change versus the previous close. Sign classifies the symbol:
|
/// Price change versus the previous close. Sign classifies the symbol:
|
||||||
/// positive is advancing, negative is declining, zero is unchanged.
|
/// positive is advancing, negative is declining, zero is unchanged.
|
||||||
@@ -41,10 +49,17 @@ pub struct Member {
|
|||||||
pub new_high: bool,
|
pub new_high: bool,
|
||||||
/// Whether the symbol printed a new period low.
|
/// Whether the symbol printed a new period low.
|
||||||
pub new_low: bool,
|
pub new_low: bool,
|
||||||
|
/// Whether the symbol is trading above its reference moving average
|
||||||
|
/// (consumed by the `% Above Moving Average` breadth indicator).
|
||||||
|
pub above_ma: bool,
|
||||||
|
/// Whether the symbol is on a point-and-figure buy signal
|
||||||
|
/// (consumed by the `Bullish Percent Index` breadth indicator).
|
||||||
|
pub on_buy_signal: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Member {
|
impl Member {
|
||||||
/// Assemble a cross-section member.
|
/// Assemble a cross-section member from its core signals, leaving the
|
||||||
|
/// extended per-symbol state flags (`above_ma`, `on_buy_signal`) cleared.
|
||||||
///
|
///
|
||||||
/// The field invariants documented on [`Member`] are validated centrally by
|
/// The field invariants documented on [`Member`] are validated centrally by
|
||||||
/// [`CrossSection::new`] when the member is placed into a tick; this
|
/// [`CrossSection::new`] when the member is placed into a tick; this
|
||||||
@@ -57,6 +72,37 @@ impl Member {
|
|||||||
volume,
|
volume,
|
||||||
new_high,
|
new_high,
|
||||||
new_low,
|
new_low,
|
||||||
|
above_ma: false,
|
||||||
|
on_buy_signal: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Assemble a cross-section member including the extended per-symbol state
|
||||||
|
/// signals `above_ma` and `on_buy_signal`.
|
||||||
|
///
|
||||||
|
/// Use this constructor for the breadth indicators that read per-symbol
|
||||||
|
/// state (`% Above Moving Average`, `Bullish Percent Index`); [`new`](Member::new)
|
||||||
|
/// is the shorthand that leaves both flags `false`.
|
||||||
|
#[must_use]
|
||||||
|
#[allow(
|
||||||
|
clippy::fn_params_excessive_bools,
|
||||||
|
reason = "mirrors the four independent per-symbol flag fields of Member"
|
||||||
|
)]
|
||||||
|
pub const fn with_signals(
|
||||||
|
change: f64,
|
||||||
|
volume: f64,
|
||||||
|
new_high: bool,
|
||||||
|
new_low: bool,
|
||||||
|
above_ma: bool,
|
||||||
|
on_buy_signal: bool,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
change,
|
||||||
|
volume,
|
||||||
|
new_high,
|
||||||
|
new_low,
|
||||||
|
above_ma,
|
||||||
|
on_buy_signal,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -126,6 +172,56 @@ impl CrossSection {
|
|||||||
pub fn decliners(&self) -> usize {
|
pub fn decliners(&self) -> usize {
|
||||||
self.members.iter().filter(|m| m.change < 0.0).count()
|
self.members.iter().filter(|m| m.change < 0.0).count()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Total volume traded by advancing symbols (those with positive `change`).
|
||||||
|
#[must_use]
|
||||||
|
pub fn advancing_volume(&self) -> f64 {
|
||||||
|
self.members
|
||||||
|
.iter()
|
||||||
|
.filter(|m| m.change > 0.0)
|
||||||
|
.map(|m| m.volume)
|
||||||
|
.sum()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Total volume traded by declining symbols (those with negative `change`).
|
||||||
|
#[must_use]
|
||||||
|
pub fn declining_volume(&self) -> f64 {
|
||||||
|
self.members
|
||||||
|
.iter()
|
||||||
|
.filter(|m| m.change < 0.0)
|
||||||
|
.map(|m| m.volume)
|
||||||
|
.sum()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Total volume traded across the whole universe.
|
||||||
|
#[must_use]
|
||||||
|
pub fn total_volume(&self) -> f64 {
|
||||||
|
self.members.iter().map(|m| m.volume).sum()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Number of symbols that printed a new period high.
|
||||||
|
#[must_use]
|
||||||
|
pub fn new_highs(&self) -> usize {
|
||||||
|
self.members.iter().filter(|m| m.new_high).count()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Number of symbols that printed a new period low.
|
||||||
|
#[must_use]
|
||||||
|
pub fn new_lows(&self) -> usize {
|
||||||
|
self.members.iter().filter(|m| m.new_low).count()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Number of symbols trading above their reference moving average.
|
||||||
|
#[must_use]
|
||||||
|
pub fn above_ma_count(&self) -> usize {
|
||||||
|
self.members.iter().filter(|m| m.above_ma).count()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Number of symbols on a point-and-figure buy signal.
|
||||||
|
#[must_use]
|
||||||
|
pub fn on_buy_signal_count(&self) -> usize {
|
||||||
|
self.members.iter().filter(|m| m.on_buy_signal).count()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
@@ -223,4 +319,69 @@ mod tests {
|
|||||||
assert_eq!(cs.advancers(), 0);
|
assert_eq!(cs.advancers(), 0);
|
||||||
assert_eq!(cs.decliners(), 0);
|
assert_eq!(cs.decliners(), 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn new_leaves_extended_flags_cleared() {
|
||||||
|
let m = Member::new(1.0, 10.0, true, false);
|
||||||
|
assert!(!m.above_ma);
|
||||||
|
assert!(!m.on_buy_signal);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn with_signals_assembles_all_fields() {
|
||||||
|
let m = Member::with_signals(2.0, 10.0, true, false, true, true);
|
||||||
|
assert_eq!(m.change, 2.0);
|
||||||
|
assert_eq!(m.volume, 10.0);
|
||||||
|
assert!(m.new_high);
|
||||||
|
assert!(!m.new_low);
|
||||||
|
assert!(m.above_ma);
|
||||||
|
assert!(m.on_buy_signal);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn volume_helpers_bucket_by_change_sign() {
|
||||||
|
let cs = CrossSection::new(
|
||||||
|
vec![
|
||||||
|
Member::new(1.5, 100.0, false, false), // advancing
|
||||||
|
Member::new(2.0, 40.0, false, false), // advancing
|
||||||
|
Member::new(-0.5, 50.0, false, false), // declining
|
||||||
|
Member::new(0.0, 7.0, false, false), // unchanged
|
||||||
|
],
|
||||||
|
0,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(cs.advancing_volume(), 140.0);
|
||||||
|
assert_eq!(cs.declining_volume(), 50.0);
|
||||||
|
assert_eq!(cs.total_volume(), 197.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn high_low_helpers_count_flags() {
|
||||||
|
let cs = CrossSection::new(
|
||||||
|
vec![
|
||||||
|
Member::new(1.0, 1.0, true, false),
|
||||||
|
Member::new(1.0, 1.0, true, false),
|
||||||
|
Member::new(-1.0, 1.0, false, true),
|
||||||
|
],
|
||||||
|
0,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(cs.new_highs(), 2);
|
||||||
|
assert_eq!(cs.new_lows(), 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn state_helpers_count_extended_flags() {
|
||||||
|
let cs = CrossSection::new(
|
||||||
|
vec![
|
||||||
|
Member::with_signals(1.0, 1.0, false, false, true, true),
|
||||||
|
Member::with_signals(1.0, 1.0, false, false, true, false),
|
||||||
|
Member::with_signals(-1.0, 1.0, false, false, false, true),
|
||||||
|
],
|
||||||
|
0,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(cs.above_ma_count(), 2);
|
||||||
|
assert_eq!(cs.on_buy_signal_count(), 2);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,144 @@
|
|||||||
|
//! Absolute Breadth Index — the magnitude of net advancing-minus-declining issues.
|
||||||
|
|
||||||
|
use crate::cross_section::CrossSection;
|
||||||
|
use crate::traits::Indicator;
|
||||||
|
|
||||||
|
/// Absolute Breadth Index (ABI) — the absolute value of net advancing issues,
|
||||||
|
/// `|advancers - decliners|`.
|
||||||
|
///
|
||||||
|
/// The ABI ignores the *direction* of breadth and measures only its *magnitude*:
|
||||||
|
/// a high reading means the universe moved decisively one way or the other (high
|
||||||
|
/// internal activity / volatility), while a low reading means advances and
|
||||||
|
/// declines were nearly balanced (a quiet, directionless market). It is sometimes
|
||||||
|
/// called a "market thermometer" because elevated readings often cluster around
|
||||||
|
/// turning points.
|
||||||
|
///
|
||||||
|
/// `Input = CrossSection`, `Output = f64`, `warmup_period == 1`.
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// use wickra_core::{AbsoluteBreadthIndex, CrossSection, Indicator, Member};
|
||||||
|
///
|
||||||
|
/// let mut abi = AbsoluteBreadthIndex::new();
|
||||||
|
/// // 2 advancers, 5 decliners -> |2 - 5| = 3.
|
||||||
|
/// let tick = CrossSection::new(
|
||||||
|
/// vec![
|
||||||
|
/// Member::new(1.0, 10.0, false, false),
|
||||||
|
/// Member::new(1.0, 10.0, false, false),
|
||||||
|
/// Member::new(-1.0, 10.0, false, false),
|
||||||
|
/// Member::new(-1.0, 10.0, false, false),
|
||||||
|
/// Member::new(-1.0, 10.0, false, false),
|
||||||
|
/// Member::new(-1.0, 10.0, false, false),
|
||||||
|
/// Member::new(-1.0, 10.0, false, false),
|
||||||
|
/// ],
|
||||||
|
/// 0,
|
||||||
|
/// )
|
||||||
|
/// .unwrap();
|
||||||
|
/// assert_eq!(abi.update(tick), Some(3.0));
|
||||||
|
/// ```
|
||||||
|
#[derive(Debug, Clone, Default)]
|
||||||
|
pub struct AbsoluteBreadthIndex {
|
||||||
|
has_emitted: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AbsoluteBreadthIndex {
|
||||||
|
/// Construct a new Absolute Breadth Index indicator.
|
||||||
|
#[must_use]
|
||||||
|
pub const fn new() -> Self {
|
||||||
|
Self { has_emitted: false }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Indicator for AbsoluteBreadthIndex {
|
||||||
|
type Input = CrossSection;
|
||||||
|
type Output = f64;
|
||||||
|
|
||||||
|
fn update(&mut self, section: CrossSection) -> Option<f64> {
|
||||||
|
let net = section.advancers() as f64 - section.decliners() as f64;
|
||||||
|
self.has_emitted = true;
|
||||||
|
Some(net.abs())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn reset(&mut self) {
|
||||||
|
self.has_emitted = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn warmup_period(&self) -> usize {
|
||||||
|
1
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_ready(&self) -> bool {
|
||||||
|
self.has_emitted
|
||||||
|
}
|
||||||
|
|
||||||
|
fn name(&self) -> &'static str {
|
||||||
|
"AbsoluteBreadthIndex"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::cross_section::Member;
|
||||||
|
use crate::traits::BatchExt;
|
||||||
|
|
||||||
|
fn section(up: usize, down: usize) -> CrossSection {
|
||||||
|
let mut members = Vec::new();
|
||||||
|
for _ in 0..up {
|
||||||
|
members.push(Member::new(1.0, 10.0, false, false));
|
||||||
|
}
|
||||||
|
for _ in 0..down {
|
||||||
|
members.push(Member::new(-1.0, 10.0, false, false));
|
||||||
|
}
|
||||||
|
members.push(Member::new(0.0, 10.0, false, false));
|
||||||
|
CrossSection::new(members, 0).unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn accessors_and_metadata() {
|
||||||
|
let abi = AbsoluteBreadthIndex::new();
|
||||||
|
assert_eq!(abi.name(), "AbsoluteBreadthIndex");
|
||||||
|
assert_eq!(abi.warmup_period(), 1);
|
||||||
|
assert!(!abi.is_ready());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn magnitude_ignores_direction() {
|
||||||
|
let mut abi = AbsoluteBreadthIndex::new();
|
||||||
|
assert_eq!(abi.update(section(2, 5)), Some(3.0));
|
||||||
|
// Same magnitude with the direction reversed.
|
||||||
|
let mut abi2 = AbsoluteBreadthIndex::new();
|
||||||
|
assert_eq!(abi2.update(section(5, 2)), Some(3.0));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn balanced_universe_yields_zero() {
|
||||||
|
let mut abi = AbsoluteBreadthIndex::new();
|
||||||
|
assert_eq!(abi.update(section(3, 3)), Some(0.0));
|
||||||
|
assert!(abi.is_ready());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn reset_clears_state() {
|
||||||
|
let mut abi = AbsoluteBreadthIndex::new();
|
||||||
|
abi.update(section(2, 5));
|
||||||
|
assert!(abi.is_ready());
|
||||||
|
abi.reset();
|
||||||
|
assert!(!abi.is_ready());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn batch_equals_streaming() {
|
||||||
|
let sections = vec![section(2, 5), section(5, 2), section(3, 3)];
|
||||||
|
let mut a = AbsoluteBreadthIndex::new();
|
||||||
|
let mut b = AbsoluteBreadthIndex::new();
|
||||||
|
assert_eq!(
|
||||||
|
a.batch(§ions),
|
||||||
|
sections
|
||||||
|
.iter()
|
||||||
|
.map(|s| b.update(s.clone()))
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,157 @@
|
|||||||
|
//! Advance/Decline Volume Line — cumulative net advancing-minus-declining volume.
|
||||||
|
|
||||||
|
use crate::cross_section::CrossSection;
|
||||||
|
use crate::traits::Indicator;
|
||||||
|
|
||||||
|
/// Advance/Decline Volume Line (AD Volume Line) — the running cumulative sum of
|
||||||
|
/// net advancing volume across a universe.
|
||||||
|
///
|
||||||
|
/// On each [`CrossSection`] tick the net is `advancing volume - declining volume`,
|
||||||
|
/// where advancing volume is the total volume of symbols with a positive change
|
||||||
|
/// and declining volume the total volume of symbols with a negative change. The
|
||||||
|
/// line accumulates this net over time, so a rising line means volume is flowing
|
||||||
|
/// into advancing issues (healthy participation) while a falling line warns that
|
||||||
|
/// declining issues are carrying the volume — the volume-weighted analogue of the
|
||||||
|
/// plain Advance/Decline Line.
|
||||||
|
///
|
||||||
|
/// `Input = CrossSection`, `Output = f64`, `warmup_period == 1` (defined from the
|
||||||
|
/// first tick).
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// use wickra_core::{AdVolumeLine, CrossSection, Indicator, Member};
|
||||||
|
///
|
||||||
|
/// let mut adv = AdVolumeLine::new();
|
||||||
|
/// // advancing volume 150, declining volume 50 -> net +100.
|
||||||
|
/// let tick = CrossSection::new(
|
||||||
|
/// vec![
|
||||||
|
/// Member::new(1.0, 150.0, false, false),
|
||||||
|
/// Member::new(-1.0, 50.0, false, false),
|
||||||
|
/// ],
|
||||||
|
/// 0,
|
||||||
|
/// )
|
||||||
|
/// .unwrap();
|
||||||
|
/// assert_eq!(adv.update(tick), Some(100.0));
|
||||||
|
/// ```
|
||||||
|
#[derive(Debug, Clone, Default)]
|
||||||
|
pub struct AdVolumeLine {
|
||||||
|
line: f64,
|
||||||
|
has_emitted: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AdVolumeLine {
|
||||||
|
/// Construct a new Advance/Decline Volume Line indicator.
|
||||||
|
#[must_use]
|
||||||
|
pub const fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
line: 0.0,
|
||||||
|
has_emitted: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Indicator for AdVolumeLine {
|
||||||
|
type Input = CrossSection;
|
||||||
|
type Output = f64;
|
||||||
|
|
||||||
|
fn update(&mut self, section: CrossSection) -> Option<f64> {
|
||||||
|
let net = section.advancing_volume() - section.declining_volume();
|
||||||
|
self.line += net;
|
||||||
|
self.has_emitted = true;
|
||||||
|
Some(self.line)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn reset(&mut self) {
|
||||||
|
self.line = 0.0;
|
||||||
|
self.has_emitted = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn warmup_period(&self) -> usize {
|
||||||
|
1
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_ready(&self) -> bool {
|
||||||
|
self.has_emitted
|
||||||
|
}
|
||||||
|
|
||||||
|
fn name(&self) -> &'static str {
|
||||||
|
"AdVolumeLine"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::cross_section::Member;
|
||||||
|
use crate::traits::BatchExt;
|
||||||
|
|
||||||
|
fn tick(items: &[(f64, f64)]) -> CrossSection {
|
||||||
|
CrossSection::new(
|
||||||
|
items
|
||||||
|
.iter()
|
||||||
|
.map(|&(change, volume)| Member::new(change, volume, false, false))
|
||||||
|
.collect(),
|
||||||
|
0,
|
||||||
|
)
|
||||||
|
.unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn accessors_and_metadata() {
|
||||||
|
let adv = AdVolumeLine::new();
|
||||||
|
assert_eq!(adv.name(), "AdVolumeLine");
|
||||||
|
assert_eq!(adv.warmup_period(), 1);
|
||||||
|
assert!(!adv.is_ready());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn first_tick_emits_net_volume() {
|
||||||
|
let mut adv = AdVolumeLine::new();
|
||||||
|
assert_eq!(adv.update(tick(&[(1.0, 150.0), (-1.0, 50.0)])), Some(100.0));
|
||||||
|
assert!(adv.is_ready());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn line_accumulates_across_ticks() {
|
||||||
|
let mut adv = AdVolumeLine::new();
|
||||||
|
assert_eq!(adv.update(tick(&[(1.0, 150.0), (-1.0, 50.0)])), Some(100.0));
|
||||||
|
assert_eq!(adv.update(tick(&[(1.0, 60.0), (-1.0, 60.0)])), Some(100.0));
|
||||||
|
assert_eq!(adv.update(tick(&[(1.0, 30.0)])), Some(130.0));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn unchanged_volume_is_ignored() {
|
||||||
|
let mut adv = AdVolumeLine::new();
|
||||||
|
// Unchanged symbols (zero change) contribute to neither bucket.
|
||||||
|
assert_eq!(adv.update(tick(&[(0.0, 1000.0), (1.0, 10.0)])), Some(10.0));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn reset_clears_state() {
|
||||||
|
let mut adv = AdVolumeLine::new();
|
||||||
|
adv.update(tick(&[(1.0, 100.0)]));
|
||||||
|
assert!(adv.is_ready());
|
||||||
|
adv.reset();
|
||||||
|
assert!(!adv.is_ready());
|
||||||
|
assert_eq!(adv.update(tick(&[(1.0, 20.0)])), Some(20.0));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn batch_equals_streaming() {
|
||||||
|
let sections = vec![
|
||||||
|
tick(&[(1.0, 150.0), (-1.0, 50.0)]),
|
||||||
|
tick(&[(1.0, 60.0), (-1.0, 60.0)]),
|
||||||
|
tick(&[(1.0, 30.0)]),
|
||||||
|
];
|
||||||
|
let mut a = AdVolumeLine::new();
|
||||||
|
let mut b = AdVolumeLine::new();
|
||||||
|
assert_eq!(
|
||||||
|
a.batch(§ions),
|
||||||
|
sections
|
||||||
|
.iter()
|
||||||
|
.map(|s| b.update(s.clone()))
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,151 @@
|
|||||||
|
//! Advance/Decline Ratio — advancing issues divided by declining issues.
|
||||||
|
|
||||||
|
use crate::cross_section::CrossSection;
|
||||||
|
use crate::traits::Indicator;
|
||||||
|
|
||||||
|
/// Advance/Decline Ratio (ADR) — the number of advancing symbols divided by the
|
||||||
|
/// number of declining symbols across a universe.
|
||||||
|
///
|
||||||
|
/// On each [`CrossSection`] tick the ratio is `advancers / decliners`: a reading
|
||||||
|
/// above one means advancing issues outnumber declining ones (broad strength),
|
||||||
|
/// while a reading below one signals broad weakness. Because it is a ratio rather
|
||||||
|
/// than a difference, the ADR is comparable across universes of different sizes.
|
||||||
|
///
|
||||||
|
/// When a tick has no declining symbols the denominator is floored to one, so the
|
||||||
|
/// ratio degrades gracefully to the advancer count instead of dividing by zero.
|
||||||
|
///
|
||||||
|
/// `Input = CrossSection`, `Output = f64`. The ratio is defined from the first
|
||||||
|
/// tick, so `warmup_period == 1` and the indicator is ready after one update.
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// use wickra_core::{AdvanceDeclineRatio, CrossSection, Indicator, Member};
|
||||||
|
///
|
||||||
|
/// let mut adr = AdvanceDeclineRatio::new();
|
||||||
|
/// // 3 advancers, 1 decliner -> ratio 3.0.
|
||||||
|
/// let tick = CrossSection::new(
|
||||||
|
/// vec![
|
||||||
|
/// Member::new(1.0, 10.0, false, false),
|
||||||
|
/// Member::new(0.5, 10.0, false, false),
|
||||||
|
/// Member::new(2.0, 10.0, false, false),
|
||||||
|
/// Member::new(-1.0, 10.0, false, false),
|
||||||
|
/// ],
|
||||||
|
/// 0,
|
||||||
|
/// )
|
||||||
|
/// .unwrap();
|
||||||
|
/// assert_eq!(adr.update(tick), Some(3.0));
|
||||||
|
/// ```
|
||||||
|
#[derive(Debug, Clone, Default)]
|
||||||
|
pub struct AdvanceDeclineRatio {
|
||||||
|
has_emitted: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AdvanceDeclineRatio {
|
||||||
|
/// Construct a new Advance/Decline Ratio indicator.
|
||||||
|
#[must_use]
|
||||||
|
pub const fn new() -> Self {
|
||||||
|
Self { has_emitted: false }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Indicator for AdvanceDeclineRatio {
|
||||||
|
type Input = CrossSection;
|
||||||
|
type Output = f64;
|
||||||
|
|
||||||
|
fn update(&mut self, section: CrossSection) -> Option<f64> {
|
||||||
|
let advancers = section.advancers() as f64;
|
||||||
|
let decliners = section.decliners().max(1) as f64;
|
||||||
|
self.has_emitted = true;
|
||||||
|
Some(advancers / decliners)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn reset(&mut self) {
|
||||||
|
self.has_emitted = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn warmup_period(&self) -> usize {
|
||||||
|
1
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_ready(&self) -> bool {
|
||||||
|
self.has_emitted
|
||||||
|
}
|
||||||
|
|
||||||
|
fn name(&self) -> &'static str {
|
||||||
|
"AdvanceDeclineRatio"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::cross_section::Member;
|
||||||
|
use crate::traits::BatchExt;
|
||||||
|
|
||||||
|
fn section(up: usize, down: usize) -> CrossSection {
|
||||||
|
let mut members = Vec::new();
|
||||||
|
for _ in 0..up {
|
||||||
|
members.push(Member::new(1.0, 10.0, false, false));
|
||||||
|
}
|
||||||
|
for _ in 0..down {
|
||||||
|
members.push(Member::new(-1.0, 10.0, false, false));
|
||||||
|
}
|
||||||
|
// A non-empty unchanged member guarantees a valid universe when both
|
||||||
|
// counts are zero.
|
||||||
|
members.push(Member::new(0.0, 10.0, false, false));
|
||||||
|
CrossSection::new(members, 0).unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn accessors_and_metadata() {
|
||||||
|
let adr = AdvanceDeclineRatio::new();
|
||||||
|
assert_eq!(adr.name(), "AdvanceDeclineRatio");
|
||||||
|
assert_eq!(adr.warmup_period(), 1);
|
||||||
|
assert!(!adr.is_ready());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn first_tick_emits_ratio() {
|
||||||
|
let mut adr = AdvanceDeclineRatio::new();
|
||||||
|
assert_eq!(adr.update(section(3, 1)), Some(3.0));
|
||||||
|
assert!(adr.is_ready());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn zero_decliners_floors_denominator() {
|
||||||
|
let mut adr = AdvanceDeclineRatio::new();
|
||||||
|
// 4 advancers, 0 decliners -> 4 / max(0, 1) = 4.0.
|
||||||
|
assert_eq!(adr.update(section(4, 0)), Some(4.0));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn no_advancers_yields_zero() {
|
||||||
|
let mut adr = AdvanceDeclineRatio::new();
|
||||||
|
assert_eq!(adr.update(section(0, 5)), Some(0.0));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn reset_clears_state() {
|
||||||
|
let mut adr = AdvanceDeclineRatio::new();
|
||||||
|
adr.update(section(3, 1));
|
||||||
|
assert!(adr.is_ready());
|
||||||
|
adr.reset();
|
||||||
|
assert!(!adr.is_ready());
|
||||||
|
assert_eq!(adr.update(section(2, 1)), Some(2.0));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn batch_equals_streaming() {
|
||||||
|
let sections = vec![section(3, 1), section(4, 0), section(0, 5), section(2, 2)];
|
||||||
|
let mut a = AdvanceDeclineRatio::new();
|
||||||
|
let mut b = AdvanceDeclineRatio::new();
|
||||||
|
assert_eq!(
|
||||||
|
a.batch(§ions),
|
||||||
|
sections
|
||||||
|
.iter()
|
||||||
|
.map(|s| b.update(s.clone()))
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,165 @@
|
|||||||
|
//! Breadth Thrust (Zweig) — a moving average of the advancing-issues share.
|
||||||
|
|
||||||
|
use crate::cross_section::CrossSection;
|
||||||
|
use crate::error::Result;
|
||||||
|
use crate::traits::Indicator;
|
||||||
|
use crate::Sma;
|
||||||
|
|
||||||
|
/// Breadth Thrust (Zweig) — a simple moving average of the advancing-issues
|
||||||
|
/// share, `advancers / (advancers + decliners)`.
|
||||||
|
///
|
||||||
|
/// Martin Zweig's breadth thrust smooths the fraction of participating issues
|
||||||
|
/// that are advancing over a short window (the classic period is 10). A "thrust"
|
||||||
|
/// fires when this average climbs from below ~0.40 (oversold, washed-out breadth)
|
||||||
|
/// to above ~0.615 within about ten sessions — historically a rare, reliable
|
||||||
|
/// signal that a powerful new advance has begun with broad participation.
|
||||||
|
///
|
||||||
|
/// Each tick's share floors the participating count to one, so a tick with no
|
||||||
|
/// advancing or declining issues contributes a defined `0.0` instead of dividing
|
||||||
|
/// by zero. The reading is `None` until `period` ticks have been seen.
|
||||||
|
///
|
||||||
|
/// `Input = CrossSection`, `Output = f64` (a share in `0..=1`),
|
||||||
|
/// `warmup_period == period`.
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// use wickra_core::{BreadthThrust, CrossSection, Indicator, Member};
|
||||||
|
///
|
||||||
|
/// let mut bt = BreadthThrust::new(2).unwrap();
|
||||||
|
/// let up = CrossSection::new(vec![Member::new(1.0, 1.0, false, false)], 0).unwrap();
|
||||||
|
/// assert_eq!(bt.update(up.clone()), None); // warming up
|
||||||
|
/// assert_eq!(bt.update(up), Some(1.0)); // both ticks 100% advancing
|
||||||
|
/// ```
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct BreadthThrust {
|
||||||
|
sma: Sma,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BreadthThrust {
|
||||||
|
/// Construct a new Breadth Thrust over the given window length.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// Returns [`Error::PeriodZero`](crate::Error::PeriodZero) if `period == 0`.
|
||||||
|
pub fn new(period: usize) -> Result<Self> {
|
||||||
|
Ok(Self {
|
||||||
|
sma: Sma::new(period)?,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Configured window length.
|
||||||
|
#[must_use]
|
||||||
|
pub const fn period(&self) -> usize {
|
||||||
|
self.sma.period()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Indicator for BreadthThrust {
|
||||||
|
type Input = CrossSection;
|
||||||
|
type Output = f64;
|
||||||
|
|
||||||
|
fn update(&mut self, section: CrossSection) -> Option<f64> {
|
||||||
|
let advancers = section.advancers();
|
||||||
|
let decliners = section.decliners();
|
||||||
|
let participating = (advancers + decliners).max(1) as f64;
|
||||||
|
let share = advancers as f64 / participating;
|
||||||
|
self.sma.update(share)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn reset(&mut self) {
|
||||||
|
self.sma.reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn warmup_period(&self) -> usize {
|
||||||
|
self.sma.period()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_ready(&self) -> bool {
|
||||||
|
self.sma.value().is_some()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn name(&self) -> &'static str {
|
||||||
|
"BreadthThrust"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::cross_section::Member;
|
||||||
|
use crate::error::Error;
|
||||||
|
use crate::traits::BatchExt;
|
||||||
|
|
||||||
|
fn section(up: usize, down: usize) -> CrossSection {
|
||||||
|
let mut members = Vec::new();
|
||||||
|
for _ in 0..up {
|
||||||
|
members.push(Member::new(1.0, 10.0, false, false));
|
||||||
|
}
|
||||||
|
for _ in 0..down {
|
||||||
|
members.push(Member::new(-1.0, 10.0, false, false));
|
||||||
|
}
|
||||||
|
members.push(Member::new(0.0, 10.0, false, false));
|
||||||
|
CrossSection::new(members, 0).unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn accessors_and_metadata() {
|
||||||
|
let bt = BreadthThrust::new(10).unwrap();
|
||||||
|
assert_eq!(bt.name(), "BreadthThrust");
|
||||||
|
assert_eq!(bt.warmup_period(), 10);
|
||||||
|
assert_eq!(bt.period(), 10);
|
||||||
|
assert!(!bt.is_ready());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn rejects_zero_period() {
|
||||||
|
assert!(matches!(BreadthThrust::new(0), Err(Error::PeriodZero)));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn averages_the_advancing_share() {
|
||||||
|
let mut bt = BreadthThrust::new(2).unwrap();
|
||||||
|
// share = 8 / 10 = 0.8 ; window not full yet.
|
||||||
|
assert_eq!(bt.update(section(8, 2)), None);
|
||||||
|
// share = 6 / 10 = 0.6 ; SMA(2) = (0.8 + 0.6) / 2 = 0.7.
|
||||||
|
let value = bt.update(section(6, 4)).unwrap();
|
||||||
|
assert!((value - 0.7).abs() < 1e-9);
|
||||||
|
assert!(bt.is_ready());
|
||||||
|
// share = 5 / 10 = 0.5 ; SMA(2) = (0.6 + 0.5) / 2 = 0.55.
|
||||||
|
let value = bt.update(section(5, 5)).unwrap();
|
||||||
|
assert!((value - 0.55).abs() < 1e-9);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn empty_participation_floors_to_zero_share() {
|
||||||
|
let mut bt = BreadthThrust::new(1).unwrap();
|
||||||
|
// No advancers or decliners -> 0 / max(0, 1) = 0.0.
|
||||||
|
assert_eq!(bt.update(section(0, 0)), Some(0.0));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn reset_clears_state() {
|
||||||
|
let mut bt = BreadthThrust::new(2).unwrap();
|
||||||
|
bt.update(section(8, 2));
|
||||||
|
bt.update(section(6, 4));
|
||||||
|
assert!(bt.is_ready());
|
||||||
|
bt.reset();
|
||||||
|
assert!(!bt.is_ready());
|
||||||
|
assert_eq!(bt.update(section(8, 2)), None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn batch_equals_streaming() {
|
||||||
|
let sections = vec![section(8, 2), section(6, 4), section(5, 5), section(0, 0)];
|
||||||
|
let mut a = BreadthThrust::new(2).unwrap();
|
||||||
|
let mut b = BreadthThrust::new(2).unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
a.batch(§ions),
|
||||||
|
sections
|
||||||
|
.iter()
|
||||||
|
.map(|s| b.update(s.clone()))
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,147 @@
|
|||||||
|
//! Bullish Percent Index — share of a universe on a point-and-figure buy signal.
|
||||||
|
|
||||||
|
use crate::cross_section::CrossSection;
|
||||||
|
use crate::traits::Indicator;
|
||||||
|
|
||||||
|
/// Bullish Percent Index (BPI) — the percentage of symbols in a universe that are
|
||||||
|
/// currently on a point-and-figure buy signal.
|
||||||
|
///
|
||||||
|
/// On each [`CrossSection`] tick the value is `100 * on_buy_signal_count /
|
||||||
|
/// universe size`, read from the per-symbol `on_buy_signal` flag (the caller
|
||||||
|
/// evaluates each symbol's point-and-figure chart when it builds the tick). It is
|
||||||
|
/// a bounded `0..=100` gauge of how many issues are in a confirmed uptrend.
|
||||||
|
/// Readings above 70 are considered overbought (broad strength, but a crowded
|
||||||
|
/// market) and below 30 oversold; reversals from those zones are classic BPI
|
||||||
|
/// buy/sell triggers.
|
||||||
|
///
|
||||||
|
/// `Input = CrossSection`, `Output = f64` (a percentage in `0..=100`),
|
||||||
|
/// `warmup_period == 1`. The universe is non-empty by construction, so the share
|
||||||
|
/// is always defined.
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// use wickra_core::{BullishPercentIndex, CrossSection, Indicator, Member};
|
||||||
|
///
|
||||||
|
/// let mut bpi = BullishPercentIndex::new();
|
||||||
|
/// // 2 of 4 symbols on a buy signal -> 50%.
|
||||||
|
/// let tick = CrossSection::new(
|
||||||
|
/// vec![
|
||||||
|
/// Member::with_signals(1.0, 10.0, false, false, false, true),
|
||||||
|
/// Member::with_signals(1.0, 10.0, false, false, false, true),
|
||||||
|
/// Member::with_signals(-1.0, 10.0, false, false, false, false),
|
||||||
|
/// Member::with_signals(-1.0, 10.0, false, false, false, false),
|
||||||
|
/// ],
|
||||||
|
/// 0,
|
||||||
|
/// )
|
||||||
|
/// .unwrap();
|
||||||
|
/// assert_eq!(bpi.update(tick), Some(50.0));
|
||||||
|
/// ```
|
||||||
|
#[derive(Debug, Clone, Default)]
|
||||||
|
pub struct BullishPercentIndex {
|
||||||
|
has_emitted: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BullishPercentIndex {
|
||||||
|
/// Construct a new Bullish Percent Index indicator.
|
||||||
|
#[must_use]
|
||||||
|
pub const fn new() -> Self {
|
||||||
|
Self { has_emitted: false }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Indicator for BullishPercentIndex {
|
||||||
|
type Input = CrossSection;
|
||||||
|
type Output = f64;
|
||||||
|
|
||||||
|
fn update(&mut self, section: CrossSection) -> Option<f64> {
|
||||||
|
let bullish = section.on_buy_signal_count() as f64;
|
||||||
|
let total = section.members.len() as f64;
|
||||||
|
self.has_emitted = true;
|
||||||
|
Some(100.0 * bullish / total)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn reset(&mut self) {
|
||||||
|
self.has_emitted = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn warmup_period(&self) -> usize {
|
||||||
|
1
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_ready(&self) -> bool {
|
||||||
|
self.has_emitted
|
||||||
|
}
|
||||||
|
|
||||||
|
fn name(&self) -> &'static str {
|
||||||
|
"BullishPercentIndex"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::cross_section::Member;
|
||||||
|
use crate::traits::BatchExt;
|
||||||
|
|
||||||
|
fn tick(bullish: usize, bearish: usize) -> CrossSection {
|
||||||
|
let mut members = Vec::new();
|
||||||
|
for _ in 0..bullish {
|
||||||
|
members.push(Member::with_signals(1.0, 10.0, false, false, false, true));
|
||||||
|
}
|
||||||
|
for _ in 0..bearish {
|
||||||
|
members.push(Member::with_signals(-1.0, 10.0, false, false, false, false));
|
||||||
|
}
|
||||||
|
CrossSection::new(members, 0).unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn accessors_and_metadata() {
|
||||||
|
let bpi = BullishPercentIndex::new();
|
||||||
|
assert_eq!(bpi.name(), "BullishPercentIndex");
|
||||||
|
assert_eq!(bpi.warmup_period(), 1);
|
||||||
|
assert!(!bpi.is_ready());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn first_tick_emits_percentage() {
|
||||||
|
let mut bpi = BullishPercentIndex::new();
|
||||||
|
assert_eq!(bpi.update(tick(2, 2)), Some(50.0));
|
||||||
|
assert!(bpi.is_ready());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn all_bullish_is_one_hundred() {
|
||||||
|
let mut bpi = BullishPercentIndex::new();
|
||||||
|
assert_eq!(bpi.update(tick(5, 0)), Some(100.0));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn none_bullish_is_zero() {
|
||||||
|
let mut bpi = BullishPercentIndex::new();
|
||||||
|
assert_eq!(bpi.update(tick(0, 4)), Some(0.0));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn reset_clears_state() {
|
||||||
|
let mut bpi = BullishPercentIndex::new();
|
||||||
|
bpi.update(tick(2, 2));
|
||||||
|
assert!(bpi.is_ready());
|
||||||
|
bpi.reset();
|
||||||
|
assert!(!bpi.is_ready());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn batch_equals_streaming() {
|
||||||
|
let sections = vec![tick(2, 2), tick(5, 0), tick(0, 4)];
|
||||||
|
let mut a = BullishPercentIndex::new();
|
||||||
|
let mut b = BullishPercentIndex::new();
|
||||||
|
assert_eq!(
|
||||||
|
a.batch(§ions),
|
||||||
|
sections
|
||||||
|
.iter()
|
||||||
|
.map(|s| b.update(s.clone()))
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,163 @@
|
|||||||
|
//! Cumulative Volume Index — running total of volume-normalised net advancing volume.
|
||||||
|
|
||||||
|
use crate::cross_section::CrossSection;
|
||||||
|
use crate::traits::Indicator;
|
||||||
|
|
||||||
|
/// Cumulative Volume Index (CVI) — the running total of *volume-normalised* net
|
||||||
|
/// advancing volume across a universe.
|
||||||
|
///
|
||||||
|
/// On each [`CrossSection`] tick the increment is `(advancing volume - declining
|
||||||
|
/// volume) / total volume`: the share of the tick's total volume that flowed,
|
||||||
|
/// net, into advancing issues. The index accumulates this share over time. Where
|
||||||
|
/// the raw [`AdVolumeLine`](crate::AdVolumeLine) sums *absolute* net volume — and
|
||||||
|
/// so drifts with secular growth in trading activity — the CVI normalises each
|
||||||
|
/// tick by its own total volume, so a one-share-net day in a thin market counts
|
||||||
|
/// the same as in a heavy one. This keeps the index comparable across regimes of
|
||||||
|
/// very different volume.
|
||||||
|
///
|
||||||
|
/// When a tick has zero total volume the net is necessarily zero too, so the
|
||||||
|
/// increment is zero and the index is unchanged (the divisor is floored to the
|
||||||
|
/// smallest positive `f64` purely to keep the division defined).
|
||||||
|
///
|
||||||
|
/// `Input = CrossSection`, `Output = f64`, `warmup_period == 1`.
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// use wickra_core::{CrossSection, CumulativeVolumeIndex, Indicator, Member};
|
||||||
|
///
|
||||||
|
/// let mut cvi = CumulativeVolumeIndex::new();
|
||||||
|
/// // adv vol 150, dec vol 50, total 200 -> (150 - 50) / 200 = 0.5.
|
||||||
|
/// let tick = CrossSection::new(
|
||||||
|
/// vec![
|
||||||
|
/// Member::new(1.0, 150.0, false, false),
|
||||||
|
/// Member::new(-1.0, 50.0, false, false),
|
||||||
|
/// ],
|
||||||
|
/// 0,
|
||||||
|
/// )
|
||||||
|
/// .unwrap();
|
||||||
|
/// assert_eq!(cvi.update(tick), Some(0.5));
|
||||||
|
/// ```
|
||||||
|
#[derive(Debug, Clone, Default)]
|
||||||
|
pub struct CumulativeVolumeIndex {
|
||||||
|
index: f64,
|
||||||
|
has_emitted: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CumulativeVolumeIndex {
|
||||||
|
/// Construct a new Cumulative Volume Index indicator.
|
||||||
|
#[must_use]
|
||||||
|
pub const fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
index: 0.0,
|
||||||
|
has_emitted: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Indicator for CumulativeVolumeIndex {
|
||||||
|
type Input = CrossSection;
|
||||||
|
type Output = f64;
|
||||||
|
|
||||||
|
fn update(&mut self, section: CrossSection) -> Option<f64> {
|
||||||
|
let net = section.advancing_volume() - section.declining_volume();
|
||||||
|
let total = section.total_volume().max(f64::MIN_POSITIVE);
|
||||||
|
self.index += net / total;
|
||||||
|
self.has_emitted = true;
|
||||||
|
Some(self.index)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn reset(&mut self) {
|
||||||
|
self.index = 0.0;
|
||||||
|
self.has_emitted = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn warmup_period(&self) -> usize {
|
||||||
|
1
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_ready(&self) -> bool {
|
||||||
|
self.has_emitted
|
||||||
|
}
|
||||||
|
|
||||||
|
fn name(&self) -> &'static str {
|
||||||
|
"CumulativeVolumeIndex"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::cross_section::Member;
|
||||||
|
use crate::traits::BatchExt;
|
||||||
|
|
||||||
|
fn tick(items: &[(f64, f64)]) -> CrossSection {
|
||||||
|
CrossSection::new(
|
||||||
|
items
|
||||||
|
.iter()
|
||||||
|
.map(|&(change, volume)| Member::new(change, volume, false, false))
|
||||||
|
.collect(),
|
||||||
|
0,
|
||||||
|
)
|
||||||
|
.unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn accessors_and_metadata() {
|
||||||
|
let cvi = CumulativeVolumeIndex::new();
|
||||||
|
assert_eq!(cvi.name(), "CumulativeVolumeIndex");
|
||||||
|
assert_eq!(cvi.warmup_period(), 1);
|
||||||
|
assert!(!cvi.is_ready());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn first_tick_emits_normalised_net() {
|
||||||
|
let mut cvi = CumulativeVolumeIndex::new();
|
||||||
|
assert_eq!(cvi.update(tick(&[(1.0, 150.0), (-1.0, 50.0)])), Some(0.5));
|
||||||
|
assert!(cvi.is_ready());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn index_accumulates_normalised_shares() {
|
||||||
|
let mut cvi = CumulativeVolumeIndex::new();
|
||||||
|
assert_eq!(cvi.update(tick(&[(1.0, 150.0), (-1.0, 50.0)])), Some(0.5));
|
||||||
|
// adv 60, dec 60, total 120 -> net 0 -> index unchanged.
|
||||||
|
assert_eq!(cvi.update(tick(&[(1.0, 60.0), (-1.0, 60.0)])), Some(0.5));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn zero_total_volume_leaves_index_unchanged() {
|
||||||
|
let mut cvi = CumulativeVolumeIndex::new();
|
||||||
|
cvi.update(tick(&[(1.0, 150.0), (-1.0, 50.0)]));
|
||||||
|
// A tick with no volume at all: net 0 / floored divisor -> 0 increment.
|
||||||
|
assert_eq!(cvi.update(tick(&[(0.0, 0.0)])), Some(0.5));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn reset_clears_state() {
|
||||||
|
let mut cvi = CumulativeVolumeIndex::new();
|
||||||
|
cvi.update(tick(&[(1.0, 150.0), (-1.0, 50.0)]));
|
||||||
|
assert!(cvi.is_ready());
|
||||||
|
cvi.reset();
|
||||||
|
assert!(!cvi.is_ready());
|
||||||
|
assert_eq!(cvi.update(tick(&[(1.0, 100.0)])), Some(1.0));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn batch_equals_streaming() {
|
||||||
|
let sections = vec![
|
||||||
|
tick(&[(1.0, 150.0), (-1.0, 50.0)]),
|
||||||
|
tick(&[(1.0, 60.0), (-1.0, 60.0)]),
|
||||||
|
tick(&[(0.0, 0.0)]),
|
||||||
|
];
|
||||||
|
let mut a = CumulativeVolumeIndex::new();
|
||||||
|
let mut b = CumulativeVolumeIndex::new();
|
||||||
|
assert_eq!(
|
||||||
|
a.batch(§ions),
|
||||||
|
sections
|
||||||
|
.iter()
|
||||||
|
.map(|s| b.update(s.clone()))
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,162 @@
|
|||||||
|
//! High-Low Index — a moving average of the record-high percentage.
|
||||||
|
|
||||||
|
use crate::cross_section::CrossSection;
|
||||||
|
use crate::error::Result;
|
||||||
|
use crate::traits::Indicator;
|
||||||
|
use crate::Sma;
|
||||||
|
|
||||||
|
/// High-Low Index — a simple moving average of the *record high percent*,
|
||||||
|
/// `100 * new_highs / (new_highs + new_lows)`.
|
||||||
|
///
|
||||||
|
/// The record high percent is the share of new-extreme issues that are new
|
||||||
|
/// *highs* rather than new *lows*; smoothing it over a window (the classic period
|
||||||
|
/// is 10) gives the High-Low Index. Readings above 50 mean new highs dominate
|
||||||
|
/// (a healthy, broadening trend), readings below 50 mean new lows dominate. The
|
||||||
|
/// 30 and 70 lines are watched as oversold / overbought breadth thresholds.
|
||||||
|
///
|
||||||
|
/// Each tick floors the new-extreme count to one, so a tick with no new highs or
|
||||||
|
/// lows contributes a defined `0.0` instead of dividing by zero. The reading is
|
||||||
|
/// `None` until `period` ticks have been seen.
|
||||||
|
///
|
||||||
|
/// `Input = CrossSection`, `Output = f64` (a percentage in `0..=100`),
|
||||||
|
/// `warmup_period == period`.
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// use wickra_core::{CrossSection, HighLowIndex, Indicator, Member};
|
||||||
|
///
|
||||||
|
/// let mut hli = HighLowIndex::new(2).unwrap();
|
||||||
|
/// let highs = CrossSection::new(vec![Member::new(1.0, 1.0, true, false)], 0).unwrap();
|
||||||
|
/// assert_eq!(hli.update(highs.clone()), None); // warming up
|
||||||
|
/// assert_eq!(hli.update(highs), Some(100.0)); // all new highs
|
||||||
|
/// ```
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct HighLowIndex {
|
||||||
|
sma: Sma,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl HighLowIndex {
|
||||||
|
/// Construct a new High-Low Index over the given window length.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// Returns [`Error::PeriodZero`](crate::Error::PeriodZero) if `period == 0`.
|
||||||
|
pub fn new(period: usize) -> Result<Self> {
|
||||||
|
Ok(Self {
|
||||||
|
sma: Sma::new(period)?,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Configured window length.
|
||||||
|
#[must_use]
|
||||||
|
pub const fn period(&self) -> usize {
|
||||||
|
self.sma.period()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Indicator for HighLowIndex {
|
||||||
|
type Input = CrossSection;
|
||||||
|
type Output = f64;
|
||||||
|
|
||||||
|
fn update(&mut self, section: CrossSection) -> Option<f64> {
|
||||||
|
let new_highs = section.new_highs();
|
||||||
|
let new_lows = section.new_lows();
|
||||||
|
let extremes = (new_highs + new_lows).max(1) as f64;
|
||||||
|
let record_high_percent = 100.0 * new_highs as f64 / extremes;
|
||||||
|
self.sma.update(record_high_percent)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn reset(&mut self) {
|
||||||
|
self.sma.reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn warmup_period(&self) -> usize {
|
||||||
|
self.sma.period()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_ready(&self) -> bool {
|
||||||
|
self.sma.value().is_some()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn name(&self) -> &'static str {
|
||||||
|
"HighLowIndex"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::cross_section::Member;
|
||||||
|
use crate::error::Error;
|
||||||
|
use crate::traits::BatchExt;
|
||||||
|
|
||||||
|
fn flags(highs: usize, lows: usize) -> CrossSection {
|
||||||
|
let mut members = Vec::new();
|
||||||
|
for _ in 0..highs {
|
||||||
|
members.push(Member::new(1.0, 10.0, true, false));
|
||||||
|
}
|
||||||
|
for _ in 0..lows {
|
||||||
|
members.push(Member::new(-1.0, 10.0, false, true));
|
||||||
|
}
|
||||||
|
members.push(Member::new(0.0, 10.0, false, false));
|
||||||
|
CrossSection::new(members, 0).unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn accessors_and_metadata() {
|
||||||
|
let hli = HighLowIndex::new(10).unwrap();
|
||||||
|
assert_eq!(hli.name(), "HighLowIndex");
|
||||||
|
assert_eq!(hli.warmup_period(), 10);
|
||||||
|
assert_eq!(hli.period(), 10);
|
||||||
|
assert!(!hli.is_ready());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn rejects_zero_period() {
|
||||||
|
assert!(matches!(HighLowIndex::new(0), Err(Error::PeriodZero)));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn averages_the_record_high_percent() {
|
||||||
|
let mut hli = HighLowIndex::new(2).unwrap();
|
||||||
|
// 8 highs / 10 extremes -> 80% ; window not full.
|
||||||
|
assert_eq!(hli.update(flags(8, 2)), None);
|
||||||
|
// 6 highs / 10 extremes -> 60% ; SMA(2) = (80 + 60) / 2 = 70.
|
||||||
|
let value = hli.update(flags(6, 4)).unwrap();
|
||||||
|
assert!((value - 70.0).abs() < 1e-9);
|
||||||
|
assert!(hli.is_ready());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn no_extremes_floors_to_zero_percent() {
|
||||||
|
let mut hli = HighLowIndex::new(1).unwrap();
|
||||||
|
// No new highs or lows -> 0 / max(0, 1) -> 0%.
|
||||||
|
assert_eq!(hli.update(flags(0, 0)), Some(0.0));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn reset_clears_state() {
|
||||||
|
let mut hli = HighLowIndex::new(2).unwrap();
|
||||||
|
hli.update(flags(8, 2));
|
||||||
|
hli.update(flags(6, 4));
|
||||||
|
assert!(hli.is_ready());
|
||||||
|
hli.reset();
|
||||||
|
assert!(!hli.is_ready());
|
||||||
|
assert_eq!(hli.update(flags(8, 2)), None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn batch_equals_streaming() {
|
||||||
|
let sections = vec![flags(8, 2), flags(6, 4), flags(3, 7), flags(0, 0)];
|
||||||
|
let mut a = HighLowIndex::new(2).unwrap();
|
||||||
|
let mut b = HighLowIndex::new(2).unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
a.batch(§ions),
|
||||||
|
sections
|
||||||
|
.iter()
|
||||||
|
.map(|s| b.update(s.clone()))
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,204 @@
|
|||||||
|
//! McClellan Oscillator — the spread between a fast and slow EMA of breadth.
|
||||||
|
|
||||||
|
use crate::cross_section::CrossSection;
|
||||||
|
use crate::traits::Indicator;
|
||||||
|
|
||||||
|
/// Fast EMA smoothing constant — the classic McClellan 19-period weight
|
||||||
|
/// `2 / (19 + 1)`.
|
||||||
|
const ALPHA_FAST: f64 = 0.1;
|
||||||
|
/// Slow EMA smoothing constant — the classic McClellan 39-period weight
|
||||||
|
/// `2 / (39 + 1)`.
|
||||||
|
const ALPHA_SLOW: f64 = 0.05;
|
||||||
|
/// Scale applied to the ratio-adjusted net advances so readings land on the
|
||||||
|
/// familiar McClellan amplitude.
|
||||||
|
const RANA_SCALE: f64 = 1000.0;
|
||||||
|
|
||||||
|
/// McClellan Oscillator — the difference between a 19-period and a 39-period
|
||||||
|
/// exponential moving average of *ratio-adjusted net advances*.
|
||||||
|
///
|
||||||
|
/// Each tick's breadth is reduced to ratio-adjusted net advances (RANA),
|
||||||
|
/// `(advancers - decliners) / (advancers + decliners) * 1000`. Dividing by the
|
||||||
|
/// number of participating issues makes the reading independent of universe size,
|
||||||
|
/// so the oscillator stays comparable as the universe grows or shrinks. The
|
||||||
|
/// oscillator is then the fast EMA minus the slow EMA of that series, using the
|
||||||
|
/// classic McClellan smoothing constants `0.10` (19-period) and `0.05`
|
||||||
|
/// (39-period). Both EMAs are seeded from the first tick's RANA, so the
|
||||||
|
/// oscillator is defined from the first update (`warmup_period == 1`); it starts
|
||||||
|
/// at `0.0` and crosses zero as breadth momentum shifts.
|
||||||
|
///
|
||||||
|
/// A tick with no advancing or declining issues yields a RANA of `0.0` (the
|
||||||
|
/// participating count is floored to one).
|
||||||
|
///
|
||||||
|
/// `Input = CrossSection`, `Output = f64`.
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// use wickra_core::{CrossSection, Indicator, McClellanOscillator, Member};
|
||||||
|
///
|
||||||
|
/// let mut osc = McClellanOscillator::new();
|
||||||
|
/// let tick = CrossSection::new(
|
||||||
|
/// vec![
|
||||||
|
/// Member::new(1.0, 10.0, false, false),
|
||||||
|
/// Member::new(1.0, 10.0, false, false),
|
||||||
|
/// Member::new(1.0, 10.0, false, false),
|
||||||
|
/// Member::new(-1.0, 10.0, false, false),
|
||||||
|
/// ],
|
||||||
|
/// 0,
|
||||||
|
/// )
|
||||||
|
/// .unwrap();
|
||||||
|
/// // First tick seeds both EMAs to the same value -> oscillator 0.
|
||||||
|
/// assert_eq!(osc.update(tick), Some(0.0));
|
||||||
|
/// ```
|
||||||
|
#[derive(Debug, Clone, Default)]
|
||||||
|
pub struct McClellanOscillator {
|
||||||
|
ema_fast: f64,
|
||||||
|
ema_slow: f64,
|
||||||
|
seeded: bool,
|
||||||
|
has_emitted: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl McClellanOscillator {
|
||||||
|
/// Construct a new McClellan Oscillator with the classic 19/39 smoothing.
|
||||||
|
#[must_use]
|
||||||
|
pub const fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
ema_fast: 0.0,
|
||||||
|
ema_slow: 0.0,
|
||||||
|
seeded: false,
|
||||||
|
has_emitted: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Feed a cross-section tick and return the oscillator value, which is defined
|
||||||
|
/// on every tick. Shared with [`McClellanSummationIndex`] so the summation
|
||||||
|
/// index can accumulate the oscillator without an `Option` round-trip.
|
||||||
|
///
|
||||||
|
/// [`McClellanSummationIndex`]: crate::McClellanSummationIndex
|
||||||
|
pub(crate) fn step(&mut self, section: &CrossSection) -> f64 {
|
||||||
|
let advancers = section.advancers();
|
||||||
|
let decliners = section.decliners();
|
||||||
|
let net = advancers as f64 - decliners as f64;
|
||||||
|
let participating = (advancers + decliners).max(1) as f64;
|
||||||
|
let rana = net / participating * RANA_SCALE;
|
||||||
|
if self.seeded {
|
||||||
|
self.ema_fast += ALPHA_FAST * (rana - self.ema_fast);
|
||||||
|
self.ema_slow += ALPHA_SLOW * (rana - self.ema_slow);
|
||||||
|
} else {
|
||||||
|
self.ema_fast = rana;
|
||||||
|
self.ema_slow = rana;
|
||||||
|
self.seeded = true;
|
||||||
|
}
|
||||||
|
self.has_emitted = true;
|
||||||
|
self.ema_fast - self.ema_slow
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Indicator for McClellanOscillator {
|
||||||
|
type Input = CrossSection;
|
||||||
|
type Output = f64;
|
||||||
|
|
||||||
|
fn update(&mut self, section: CrossSection) -> Option<f64> {
|
||||||
|
Some(self.step(§ion))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn reset(&mut self) {
|
||||||
|
self.ema_fast = 0.0;
|
||||||
|
self.ema_slow = 0.0;
|
||||||
|
self.seeded = false;
|
||||||
|
self.has_emitted = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn warmup_period(&self) -> usize {
|
||||||
|
1
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_ready(&self) -> bool {
|
||||||
|
self.has_emitted
|
||||||
|
}
|
||||||
|
|
||||||
|
fn name(&self) -> &'static str {
|
||||||
|
"McClellanOscillator"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::cross_section::Member;
|
||||||
|
use crate::traits::BatchExt;
|
||||||
|
|
||||||
|
fn section(up: usize, down: usize) -> CrossSection {
|
||||||
|
let mut members = Vec::new();
|
||||||
|
for _ in 0..up {
|
||||||
|
members.push(Member::new(1.0, 10.0, false, false));
|
||||||
|
}
|
||||||
|
for _ in 0..down {
|
||||||
|
members.push(Member::new(-1.0, 10.0, false, false));
|
||||||
|
}
|
||||||
|
members.push(Member::new(0.0, 10.0, false, false));
|
||||||
|
CrossSection::new(members, 0).unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn accessors_and_metadata() {
|
||||||
|
let osc = McClellanOscillator::new();
|
||||||
|
assert_eq!(osc.name(), "McClellanOscillator");
|
||||||
|
assert_eq!(osc.warmup_period(), 1);
|
||||||
|
assert!(!osc.is_ready());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn seeds_to_zero_on_first_tick() {
|
||||||
|
let mut osc = McClellanOscillator::new();
|
||||||
|
// RANA = (3 - 1) / 4 * 1000 = 500 ; both EMAs seed to 500 -> spread 0.
|
||||||
|
assert_eq!(osc.update(section(3, 1)), Some(0.0));
|
||||||
|
assert!(osc.is_ready());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn tracks_breadth_momentum_after_seeding() {
|
||||||
|
let mut osc = McClellanOscillator::new();
|
||||||
|
osc.update(section(3, 1)); // seed at RANA 500
|
||||||
|
// RANA = (1 - 3) / 4 * 1000 = -500.
|
||||||
|
// fast = 500 + 0.1 * (-1000) = 400 ; slow = 500 + 0.05 * (-1000) = 450.
|
||||||
|
let value = osc.update(section(1, 3)).unwrap();
|
||||||
|
assert!((value - (-50.0)).abs() < 1e-9);
|
||||||
|
// RANA = 0. fast = 400 + 0.1 * (-400) = 360 ; slow = 450 + 0.05 * (-450) = 427.5.
|
||||||
|
let value = osc.update(section(2, 2)).unwrap();
|
||||||
|
assert!((value - (-67.5)).abs() < 1e-9);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn empty_participation_yields_zero_rana() {
|
||||||
|
let mut osc = McClellanOscillator::new();
|
||||||
|
// No advancers or decliners -> RANA 0 ; seeds both EMAs to 0 -> spread 0.
|
||||||
|
assert_eq!(osc.update(section(0, 0)), Some(0.0));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn reset_clears_state() {
|
||||||
|
let mut osc = McClellanOscillator::new();
|
||||||
|
osc.update(section(3, 1));
|
||||||
|
osc.update(section(1, 3));
|
||||||
|
assert!(osc.is_ready());
|
||||||
|
osc.reset();
|
||||||
|
assert!(!osc.is_ready());
|
||||||
|
// After reset the next tick re-seeds to spread 0.
|
||||||
|
assert_eq!(osc.update(section(1, 3)), Some(0.0));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn batch_equals_streaming() {
|
||||||
|
let sections = vec![section(3, 1), section(1, 3), section(2, 2), section(0, 0)];
|
||||||
|
let mut a = McClellanOscillator::new();
|
||||||
|
let mut b = McClellanOscillator::new();
|
||||||
|
assert_eq!(
|
||||||
|
a.batch(§ions),
|
||||||
|
sections
|
||||||
|
.iter()
|
||||||
|
.map(|s| b.update(s.clone()))
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,160 @@
|
|||||||
|
//! McClellan Summation Index — the running total of the McClellan Oscillator.
|
||||||
|
|
||||||
|
use crate::cross_section::CrossSection;
|
||||||
|
use crate::indicators::mcclellan_oscillator::McClellanOscillator;
|
||||||
|
use crate::traits::Indicator;
|
||||||
|
|
||||||
|
/// McClellan Summation Index — the running cumulative sum of the
|
||||||
|
/// [`McClellanOscillator`].
|
||||||
|
///
|
||||||
|
/// Where the oscillator measures the *momentum* of breadth, the summation index
|
||||||
|
/// integrates it into a longer-term breadth trend: it rises while the oscillator
|
||||||
|
/// is positive and falls while it is negative, so it behaves like a slow,
|
||||||
|
/// smoothed advance/decline line. Sustained readings far above or below zero mark
|
||||||
|
/// strong bull or bear breadth regimes, and crosses of the zero line are read as
|
||||||
|
/// major trend changes.
|
||||||
|
///
|
||||||
|
/// The index embeds a [`McClellanOscillator`] and adds its value on every tick.
|
||||||
|
/// Because the oscillator seeds to `0.0` on the first tick, the summation index
|
||||||
|
/// also starts at `0.0` and is defined from the first update
|
||||||
|
/// (`warmup_period == 1`).
|
||||||
|
///
|
||||||
|
/// `Input = CrossSection`, `Output = f64`.
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// use wickra_core::{CrossSection, Indicator, McClellanSummationIndex, Member};
|
||||||
|
///
|
||||||
|
/// let mut msi = McClellanSummationIndex::new();
|
||||||
|
/// let tick = CrossSection::new(
|
||||||
|
/// vec![
|
||||||
|
/// Member::new(1.0, 10.0, false, false),
|
||||||
|
/// Member::new(-1.0, 10.0, false, false),
|
||||||
|
/// ],
|
||||||
|
/// 0,
|
||||||
|
/// )
|
||||||
|
/// .unwrap();
|
||||||
|
/// // First tick: oscillator seeds to 0, so the summation index is 0.
|
||||||
|
/// assert_eq!(msi.update(tick), Some(0.0));
|
||||||
|
/// ```
|
||||||
|
#[derive(Debug, Clone, Default)]
|
||||||
|
pub struct McClellanSummationIndex {
|
||||||
|
oscillator: McClellanOscillator,
|
||||||
|
sum: f64,
|
||||||
|
has_emitted: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl McClellanSummationIndex {
|
||||||
|
/// Construct a new McClellan Summation Index.
|
||||||
|
#[must_use]
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
oscillator: McClellanOscillator::new(),
|
||||||
|
sum: 0.0,
|
||||||
|
has_emitted: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Indicator for McClellanSummationIndex {
|
||||||
|
type Input = CrossSection;
|
||||||
|
type Output = f64;
|
||||||
|
|
||||||
|
fn update(&mut self, section: CrossSection) -> Option<f64> {
|
||||||
|
let oscillator = self.oscillator.step(§ion);
|
||||||
|
self.sum += oscillator;
|
||||||
|
self.has_emitted = true;
|
||||||
|
Some(self.sum)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn reset(&mut self) {
|
||||||
|
self.oscillator.reset();
|
||||||
|
self.sum = 0.0;
|
||||||
|
self.has_emitted = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn warmup_period(&self) -> usize {
|
||||||
|
1
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_ready(&self) -> bool {
|
||||||
|
self.has_emitted
|
||||||
|
}
|
||||||
|
|
||||||
|
fn name(&self) -> &'static str {
|
||||||
|
"McClellanSummationIndex"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::cross_section::Member;
|
||||||
|
use crate::traits::BatchExt;
|
||||||
|
|
||||||
|
fn section(up: usize, down: usize) -> CrossSection {
|
||||||
|
let mut members = Vec::new();
|
||||||
|
for _ in 0..up {
|
||||||
|
members.push(Member::new(1.0, 10.0, false, false));
|
||||||
|
}
|
||||||
|
for _ in 0..down {
|
||||||
|
members.push(Member::new(-1.0, 10.0, false, false));
|
||||||
|
}
|
||||||
|
members.push(Member::new(0.0, 10.0, false, false));
|
||||||
|
CrossSection::new(members, 0).unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn accessors_and_metadata() {
|
||||||
|
let msi = McClellanSummationIndex::new();
|
||||||
|
assert_eq!(msi.name(), "McClellanSummationIndex");
|
||||||
|
assert_eq!(msi.warmup_period(), 1);
|
||||||
|
assert!(!msi.is_ready());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn first_tick_starts_at_zero() {
|
||||||
|
let mut msi = McClellanSummationIndex::new();
|
||||||
|
assert_eq!(msi.update(section(3, 1)), Some(0.0));
|
||||||
|
assert!(msi.is_ready());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn accumulates_the_oscillator() {
|
||||||
|
let mut msi = McClellanSummationIndex::new();
|
||||||
|
assert_eq!(msi.update(section(3, 1)), Some(0.0)); // osc 0 -> sum 0
|
||||||
|
// osc -50 -> sum -50.
|
||||||
|
let value = msi.update(section(1, 3)).unwrap();
|
||||||
|
assert!((value - (-50.0)).abs() < 1e-9);
|
||||||
|
// osc -67.5 -> sum -117.5.
|
||||||
|
let value = msi.update(section(2, 2)).unwrap();
|
||||||
|
assert!((value - (-117.5)).abs() < 1e-9);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn reset_clears_state() {
|
||||||
|
let mut msi = McClellanSummationIndex::new();
|
||||||
|
msi.update(section(3, 1));
|
||||||
|
msi.update(section(1, 3));
|
||||||
|
assert!(msi.is_ready());
|
||||||
|
msi.reset();
|
||||||
|
assert!(!msi.is_ready());
|
||||||
|
// Oscillator re-seeds, so the summation index restarts at 0.
|
||||||
|
assert_eq!(msi.update(section(1, 3)), Some(0.0));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn batch_equals_streaming() {
|
||||||
|
let sections = vec![section(3, 1), section(1, 3), section(2, 2)];
|
||||||
|
let mut a = McClellanSummationIndex::new();
|
||||||
|
let mut b = McClellanSummationIndex::new();
|
||||||
|
assert_eq!(
|
||||||
|
a.batch(§ions),
|
||||||
|
sections
|
||||||
|
.iter()
|
||||||
|
.map(|s| b.update(s.clone()))
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,13 +5,16 @@
|
|||||||
//! from the crate root for convenience.
|
//! from the crate root for convenience.
|
||||||
|
|
||||||
mod abandoned_baby;
|
mod abandoned_baby;
|
||||||
|
mod absolute_breadth_index;
|
||||||
mod acceleration_bands;
|
mod acceleration_bands;
|
||||||
mod accelerator_oscillator;
|
mod accelerator_oscillator;
|
||||||
mod ad_oscillator;
|
mod ad_oscillator;
|
||||||
|
mod ad_volume_line;
|
||||||
mod adaptive_cycle;
|
mod adaptive_cycle;
|
||||||
mod adl;
|
mod adl;
|
||||||
mod advance_block;
|
mod advance_block;
|
||||||
mod advance_decline;
|
mod advance_decline;
|
||||||
|
mod advance_decline_ratio;
|
||||||
mod adx;
|
mod adx;
|
||||||
mod adxr;
|
mod adxr;
|
||||||
mod alligator;
|
mod alligator;
|
||||||
@@ -36,7 +39,9 @@ mod beta;
|
|||||||
mod beta_neutral_spread;
|
mod beta_neutral_spread;
|
||||||
mod bollinger;
|
mod bollinger;
|
||||||
mod bollinger_bandwidth;
|
mod bollinger_bandwidth;
|
||||||
|
mod breadth_thrust;
|
||||||
mod breakaway;
|
mod breakaway;
|
||||||
|
mod bullish_percent_index;
|
||||||
mod calendar_spread;
|
mod calendar_spread;
|
||||||
mod calmar_ratio;
|
mod calmar_ratio;
|
||||||
mod camarilla_pivots;
|
mod camarilla_pivots;
|
||||||
@@ -59,6 +64,7 @@ mod conditional_value_at_risk;
|
|||||||
mod connors_rsi;
|
mod connors_rsi;
|
||||||
mod coppock;
|
mod coppock;
|
||||||
mod counterattack;
|
mod counterattack;
|
||||||
|
mod cumulative_volume_index;
|
||||||
mod cvd;
|
mod cvd;
|
||||||
mod cybernetic_cycle;
|
mod cybernetic_cycle;
|
||||||
mod decycler;
|
mod decycler;
|
||||||
@@ -109,6 +115,7 @@ mod hammer;
|
|||||||
mod hanging_man;
|
mod hanging_man;
|
||||||
mod harami;
|
mod harami;
|
||||||
mod heikin_ashi;
|
mod heikin_ashi;
|
||||||
|
mod high_low_index;
|
||||||
mod high_wave;
|
mod high_wave;
|
||||||
mod hikkake;
|
mod hikkake;
|
||||||
mod hikkake_modified;
|
mod hikkake_modified;
|
||||||
@@ -166,6 +173,8 @@ mod mass_index;
|
|||||||
mod mat_hold;
|
mod mat_hold;
|
||||||
mod matching_low;
|
mod matching_low;
|
||||||
mod max_drawdown;
|
mod max_drawdown;
|
||||||
|
mod mcclellan_oscillator;
|
||||||
|
mod mcclellan_summation_index;
|
||||||
mod mcginley_dynamic;
|
mod mcginley_dynamic;
|
||||||
mod median_absolute_deviation;
|
mod median_absolute_deviation;
|
||||||
mod median_price;
|
mod median_price;
|
||||||
@@ -179,6 +188,7 @@ mod mom;
|
|||||||
mod morning_doji_star;
|
mod morning_doji_star;
|
||||||
mod morning_evening_star;
|
mod morning_evening_star;
|
||||||
mod natr;
|
mod natr;
|
||||||
|
mod new_highs_new_lows;
|
||||||
mod nvi;
|
mod nvi;
|
||||||
mod ob_imbalance_full;
|
mod ob_imbalance_full;
|
||||||
mod ob_imbalance_top1;
|
mod ob_imbalance_top1;
|
||||||
@@ -197,6 +207,7 @@ mod pair_spread_zscore;
|
|||||||
mod pairwise_beta;
|
mod pairwise_beta;
|
||||||
mod parkinson;
|
mod parkinson;
|
||||||
mod pearson_correlation;
|
mod pearson_correlation;
|
||||||
|
mod percent_above_ma;
|
||||||
mod percent_b;
|
mod percent_b;
|
||||||
mod percentage_trailing_stop;
|
mod percentage_trailing_stop;
|
||||||
mod pgo;
|
mod pgo;
|
||||||
@@ -282,11 +293,13 @@ mod three_outside;
|
|||||||
mod three_soldiers_or_crows;
|
mod three_soldiers_or_crows;
|
||||||
mod three_stars_in_south;
|
mod three_stars_in_south;
|
||||||
mod thrusting;
|
mod thrusting;
|
||||||
|
mod tick_index;
|
||||||
mod tii;
|
mod tii;
|
||||||
mod tpo_profile;
|
mod tpo_profile;
|
||||||
mod trade_imbalance;
|
mod trade_imbalance;
|
||||||
mod treynor_ratio;
|
mod treynor_ratio;
|
||||||
mod trima;
|
mod trima;
|
||||||
|
mod trin;
|
||||||
mod trix;
|
mod trix;
|
||||||
mod true_range;
|
mod true_range;
|
||||||
mod tsf;
|
mod tsf;
|
||||||
@@ -299,6 +312,7 @@ mod typical_price;
|
|||||||
mod ulcer_index;
|
mod ulcer_index;
|
||||||
mod ultimate_oscillator;
|
mod ultimate_oscillator;
|
||||||
mod unique_three_river;
|
mod unique_three_river;
|
||||||
|
mod up_down_volume_ratio;
|
||||||
mod upside_gap_three_methods;
|
mod upside_gap_three_methods;
|
||||||
mod upside_gap_two_crows;
|
mod upside_gap_two_crows;
|
||||||
mod value_area;
|
mod value_area;
|
||||||
@@ -330,13 +344,16 @@ mod zig_zag;
|
|||||||
mod zlema;
|
mod zlema;
|
||||||
|
|
||||||
pub use abandoned_baby::AbandonedBaby;
|
pub use abandoned_baby::AbandonedBaby;
|
||||||
|
pub use absolute_breadth_index::AbsoluteBreadthIndex;
|
||||||
pub use acceleration_bands::{AccelerationBands, AccelerationBandsOutput};
|
pub use acceleration_bands::{AccelerationBands, AccelerationBandsOutput};
|
||||||
pub use accelerator_oscillator::AcceleratorOscillator;
|
pub use accelerator_oscillator::AcceleratorOscillator;
|
||||||
pub use ad_oscillator::AdOscillator;
|
pub use ad_oscillator::AdOscillator;
|
||||||
|
pub use ad_volume_line::AdVolumeLine;
|
||||||
pub use adaptive_cycle::AdaptiveCycle;
|
pub use adaptive_cycle::AdaptiveCycle;
|
||||||
pub use adl::Adl;
|
pub use adl::Adl;
|
||||||
pub use advance_block::AdvanceBlock;
|
pub use advance_block::AdvanceBlock;
|
||||||
pub use advance_decline::AdvanceDecline;
|
pub use advance_decline::AdvanceDecline;
|
||||||
|
pub use advance_decline_ratio::AdvanceDeclineRatio;
|
||||||
pub use adx::{Adx, AdxOutput};
|
pub use adx::{Adx, AdxOutput};
|
||||||
pub use adxr::Adxr;
|
pub use adxr::Adxr;
|
||||||
pub use alligator::{Alligator, AlligatorOutput};
|
pub use alligator::{Alligator, AlligatorOutput};
|
||||||
@@ -361,7 +378,9 @@ pub use beta::Beta;
|
|||||||
pub use beta_neutral_spread::BetaNeutralSpread;
|
pub use beta_neutral_spread::BetaNeutralSpread;
|
||||||
pub use bollinger::{BollingerBands, BollingerOutput};
|
pub use bollinger::{BollingerBands, BollingerOutput};
|
||||||
pub use bollinger_bandwidth::BollingerBandwidth;
|
pub use bollinger_bandwidth::BollingerBandwidth;
|
||||||
|
pub use breadth_thrust::BreadthThrust;
|
||||||
pub use breakaway::Breakaway;
|
pub use breakaway::Breakaway;
|
||||||
|
pub use bullish_percent_index::BullishPercentIndex;
|
||||||
pub use calendar_spread::CalendarSpread;
|
pub use calendar_spread::CalendarSpread;
|
||||||
pub use calmar_ratio::CalmarRatio;
|
pub use calmar_ratio::CalmarRatio;
|
||||||
pub use camarilla_pivots::{Camarilla, CamarillaPivotsOutput};
|
pub use camarilla_pivots::{Camarilla, CamarillaPivotsOutput};
|
||||||
@@ -384,6 +403,7 @@ pub use conditional_value_at_risk::ConditionalValueAtRisk;
|
|||||||
pub use connors_rsi::ConnorsRsi;
|
pub use connors_rsi::ConnorsRsi;
|
||||||
pub use coppock::Coppock;
|
pub use coppock::Coppock;
|
||||||
pub use counterattack::Counterattack;
|
pub use counterattack::Counterattack;
|
||||||
|
pub use cumulative_volume_index::CumulativeVolumeIndex;
|
||||||
pub use cvd::CumulativeVolumeDelta;
|
pub use cvd::CumulativeVolumeDelta;
|
||||||
pub use cybernetic_cycle::CyberneticCycle;
|
pub use cybernetic_cycle::CyberneticCycle;
|
||||||
pub use decycler::Decycler;
|
pub use decycler::Decycler;
|
||||||
@@ -434,6 +454,7 @@ pub use hammer::Hammer;
|
|||||||
pub use hanging_man::HangingMan;
|
pub use hanging_man::HangingMan;
|
||||||
pub use harami::Harami;
|
pub use harami::Harami;
|
||||||
pub use heikin_ashi::{HeikinAshi, HeikinAshiOutput};
|
pub use heikin_ashi::{HeikinAshi, HeikinAshiOutput};
|
||||||
|
pub use high_low_index::HighLowIndex;
|
||||||
pub use high_wave::HighWave;
|
pub use high_wave::HighWave;
|
||||||
pub use hikkake::Hikkake;
|
pub use hikkake::Hikkake;
|
||||||
pub use hikkake_modified::HikkakeModified;
|
pub use hikkake_modified::HikkakeModified;
|
||||||
@@ -491,6 +512,8 @@ pub use mass_index::MassIndex;
|
|||||||
pub use mat_hold::MatHold;
|
pub use mat_hold::MatHold;
|
||||||
pub use matching_low::MatchingLow;
|
pub use matching_low::MatchingLow;
|
||||||
pub use max_drawdown::MaxDrawdown;
|
pub use max_drawdown::MaxDrawdown;
|
||||||
|
pub use mcclellan_oscillator::McClellanOscillator;
|
||||||
|
pub use mcclellan_summation_index::McClellanSummationIndex;
|
||||||
pub use mcginley_dynamic::McGinleyDynamic;
|
pub use mcginley_dynamic::McGinleyDynamic;
|
||||||
pub use median_absolute_deviation::MedianAbsoluteDeviation;
|
pub use median_absolute_deviation::MedianAbsoluteDeviation;
|
||||||
pub use median_price::MedianPrice;
|
pub use median_price::MedianPrice;
|
||||||
@@ -504,6 +527,7 @@ pub use mom::Mom;
|
|||||||
pub use morning_doji_star::MorningDojiStar;
|
pub use morning_doji_star::MorningDojiStar;
|
||||||
pub use morning_evening_star::MorningEveningStar;
|
pub use morning_evening_star::MorningEveningStar;
|
||||||
pub use natr::Natr;
|
pub use natr::Natr;
|
||||||
|
pub use new_highs_new_lows::NewHighsNewLows;
|
||||||
pub use nvi::Nvi;
|
pub use nvi::Nvi;
|
||||||
pub use ob_imbalance_full::OrderBookImbalanceFull;
|
pub use ob_imbalance_full::OrderBookImbalanceFull;
|
||||||
pub use ob_imbalance_top1::OrderBookImbalanceTop1;
|
pub use ob_imbalance_top1::OrderBookImbalanceTop1;
|
||||||
@@ -522,6 +546,7 @@ pub use pair_spread_zscore::PairSpreadZScore;
|
|||||||
pub use pairwise_beta::PairwiseBeta;
|
pub use pairwise_beta::PairwiseBeta;
|
||||||
pub use parkinson::ParkinsonVolatility;
|
pub use parkinson::ParkinsonVolatility;
|
||||||
pub use pearson_correlation::PearsonCorrelation;
|
pub use pearson_correlation::PearsonCorrelation;
|
||||||
|
pub use percent_above_ma::PercentAboveMa;
|
||||||
pub use percent_b::PercentB;
|
pub use percent_b::PercentB;
|
||||||
pub use percentage_trailing_stop::PercentageTrailingStop;
|
pub use percentage_trailing_stop::PercentageTrailingStop;
|
||||||
pub use pgo::Pgo;
|
pub use pgo::Pgo;
|
||||||
@@ -607,11 +632,13 @@ pub use three_outside::ThreeOutside;
|
|||||||
pub use three_soldiers_or_crows::ThreeSoldiersOrCrows;
|
pub use three_soldiers_or_crows::ThreeSoldiersOrCrows;
|
||||||
pub use three_stars_in_south::ThreeStarsInSouth;
|
pub use three_stars_in_south::ThreeStarsInSouth;
|
||||||
pub use thrusting::Thrusting;
|
pub use thrusting::Thrusting;
|
||||||
|
pub use tick_index::TickIndex;
|
||||||
pub use tii::Tii;
|
pub use tii::Tii;
|
||||||
pub use tpo_profile::{TpoProfile, TpoProfileOutput};
|
pub use tpo_profile::{TpoProfile, TpoProfileOutput};
|
||||||
pub use trade_imbalance::TradeImbalance;
|
pub use trade_imbalance::TradeImbalance;
|
||||||
pub use treynor_ratio::TreynorRatio;
|
pub use treynor_ratio::TreynorRatio;
|
||||||
pub use trima::Trima;
|
pub use trima::Trima;
|
||||||
|
pub use trin::Trin;
|
||||||
pub use trix::Trix;
|
pub use trix::Trix;
|
||||||
pub use true_range::TrueRange;
|
pub use true_range::TrueRange;
|
||||||
pub use tsf::Tsf;
|
pub use tsf::Tsf;
|
||||||
@@ -624,6 +651,7 @@ pub use typical_price::TypicalPrice;
|
|||||||
pub use ulcer_index::UlcerIndex;
|
pub use ulcer_index::UlcerIndex;
|
||||||
pub use ultimate_oscillator::UltimateOscillator;
|
pub use ultimate_oscillator::UltimateOscillator;
|
||||||
pub use unique_three_river::UniqueThreeRiver;
|
pub use unique_three_river::UniqueThreeRiver;
|
||||||
|
pub use up_down_volume_ratio::UpDownVolumeRatio;
|
||||||
pub use upside_gap_three_methods::UpsideGapThreeMethods;
|
pub use upside_gap_three_methods::UpsideGapThreeMethods;
|
||||||
pub use upside_gap_two_crows::UpsideGapTwoCrows;
|
pub use upside_gap_two_crows::UpsideGapTwoCrows;
|
||||||
pub use value_area::{ValueArea, ValueAreaOutput};
|
pub use value_area::{ValueArea, ValueAreaOutput};
|
||||||
@@ -1070,7 +1098,26 @@ pub const FAMILIES: &[(&str, &[&str])] = &[
|
|||||||
"Alt-Chart Bars",
|
"Alt-Chart Bars",
|
||||||
&["RenkoBars", "KagiBars", "PointAndFigureBars"],
|
&["RenkoBars", "KagiBars", "PointAndFigureBars"],
|
||||||
),
|
),
|
||||||
("Market Breadth", &["AdvanceDecline"]),
|
(
|
||||||
|
"Market Breadth",
|
||||||
|
&[
|
||||||
|
"AdvanceDecline",
|
||||||
|
"AdvanceDeclineRatio",
|
||||||
|
"AdVolumeLine",
|
||||||
|
"McClellanOscillator",
|
||||||
|
"McClellanSummationIndex",
|
||||||
|
"Trin",
|
||||||
|
"BreadthThrust",
|
||||||
|
"NewHighsNewLows",
|
||||||
|
"HighLowIndex",
|
||||||
|
"PercentAboveMa",
|
||||||
|
"UpDownVolumeRatio",
|
||||||
|
"BullishPercentIndex",
|
||||||
|
"CumulativeVolumeIndex",
|
||||||
|
"AbsoluteBreadthIndex",
|
||||||
|
"TickIndex",
|
||||||
|
],
|
||||||
|
),
|
||||||
];
|
];
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
@@ -1099,6 +1146,6 @@ mod family_tests {
|
|||||||
// the actual indicator count is the early-warning signal that an
|
// the actual indicator count is the early-warning signal that an
|
||||||
// indicator was added without being assigned a family.
|
// indicator was added without being assigned a family.
|
||||||
let total: usize = FAMILIES.iter().map(|(_, ns)| ns.len()).sum();
|
let total: usize = FAMILIES.iter().map(|(_, ns)| ns.len()).sum();
|
||||||
assert_eq!(total, 325, "FAMILIES total drifted from indicator count");
|
assert_eq!(total, 339, "FAMILIES total drifted from indicator count");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,142 @@
|
|||||||
|
//! New Highs − New Lows — net count of fresh period extremes across a universe.
|
||||||
|
|
||||||
|
use crate::cross_section::CrossSection;
|
||||||
|
use crate::traits::Indicator;
|
||||||
|
|
||||||
|
/// New Highs − New Lows — the number of symbols printing a new period high minus
|
||||||
|
/// the number printing a new period low across a universe.
|
||||||
|
///
|
||||||
|
/// On each [`CrossSection`] tick the value is `new_highs - new_lows`, read from the
|
||||||
|
/// per-symbol `new_high` / `new_low` flags. A persistently positive reading means
|
||||||
|
/// fresh leadership is broad (many names making new highs); a negative reading
|
||||||
|
/// during an index advance is a classic breadth divergence warning that the rally
|
||||||
|
/// is narrowing.
|
||||||
|
///
|
||||||
|
/// `Input = CrossSection`, `Output = f64`, `warmup_period == 1`.
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// use wickra_core::{CrossSection, Indicator, Member, NewHighsNewLows};
|
||||||
|
///
|
||||||
|
/// let mut nhnl = NewHighsNewLows::new();
|
||||||
|
/// // 2 new highs, 1 new low -> net +1.
|
||||||
|
/// let tick = CrossSection::new(
|
||||||
|
/// vec![
|
||||||
|
/// Member::new(1.0, 10.0, true, false),
|
||||||
|
/// Member::new(1.0, 10.0, true, false),
|
||||||
|
/// Member::new(-1.0, 10.0, false, true),
|
||||||
|
/// ],
|
||||||
|
/// 0,
|
||||||
|
/// )
|
||||||
|
/// .unwrap();
|
||||||
|
/// assert_eq!(nhnl.update(tick), Some(1.0));
|
||||||
|
/// ```
|
||||||
|
#[derive(Debug, Clone, Default)]
|
||||||
|
pub struct NewHighsNewLows {
|
||||||
|
has_emitted: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl NewHighsNewLows {
|
||||||
|
/// Construct a new New Highs − New Lows indicator.
|
||||||
|
#[must_use]
|
||||||
|
pub const fn new() -> Self {
|
||||||
|
Self { has_emitted: false }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Indicator for NewHighsNewLows {
|
||||||
|
type Input = CrossSection;
|
||||||
|
type Output = f64;
|
||||||
|
|
||||||
|
fn update(&mut self, section: CrossSection) -> Option<f64> {
|
||||||
|
let net = section.new_highs() as f64 - section.new_lows() as f64;
|
||||||
|
self.has_emitted = true;
|
||||||
|
Some(net)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn reset(&mut self) {
|
||||||
|
self.has_emitted = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn warmup_period(&self) -> usize {
|
||||||
|
1
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_ready(&self) -> bool {
|
||||||
|
self.has_emitted
|
||||||
|
}
|
||||||
|
|
||||||
|
fn name(&self) -> &'static str {
|
||||||
|
"NewHighsNewLows"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::cross_section::Member;
|
||||||
|
use crate::traits::BatchExt;
|
||||||
|
|
||||||
|
fn flags(highs: usize, lows: usize) -> CrossSection {
|
||||||
|
let mut members = Vec::new();
|
||||||
|
for _ in 0..highs {
|
||||||
|
members.push(Member::new(1.0, 10.0, true, false));
|
||||||
|
}
|
||||||
|
for _ in 0..lows {
|
||||||
|
members.push(Member::new(-1.0, 10.0, false, true));
|
||||||
|
}
|
||||||
|
members.push(Member::new(0.0, 10.0, false, false));
|
||||||
|
CrossSection::new(members, 0).unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn accessors_and_metadata() {
|
||||||
|
let nhnl = NewHighsNewLows::new();
|
||||||
|
assert_eq!(nhnl.name(), "NewHighsNewLows");
|
||||||
|
assert_eq!(nhnl.warmup_period(), 1);
|
||||||
|
assert!(!nhnl.is_ready());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn first_tick_emits_net_extremes() {
|
||||||
|
let mut nhnl = NewHighsNewLows::new();
|
||||||
|
assert_eq!(nhnl.update(flags(5, 2)), Some(3.0));
|
||||||
|
assert!(nhnl.is_ready());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn more_lows_than_highs_is_negative() {
|
||||||
|
let mut nhnl = NewHighsNewLows::new();
|
||||||
|
assert_eq!(nhnl.update(flags(1, 4)), Some(-3.0));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn no_extremes_yields_zero() {
|
||||||
|
let mut nhnl = NewHighsNewLows::new();
|
||||||
|
assert_eq!(nhnl.update(flags(0, 0)), Some(0.0));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn reset_clears_state() {
|
||||||
|
let mut nhnl = NewHighsNewLows::new();
|
||||||
|
nhnl.update(flags(3, 1));
|
||||||
|
assert!(nhnl.is_ready());
|
||||||
|
nhnl.reset();
|
||||||
|
assert!(!nhnl.is_ready());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn batch_equals_streaming() {
|
||||||
|
let sections = vec![flags(5, 2), flags(1, 4), flags(0, 0)];
|
||||||
|
let mut a = NewHighsNewLows::new();
|
||||||
|
let mut b = NewHighsNewLows::new();
|
||||||
|
assert_eq!(
|
||||||
|
a.batch(§ions),
|
||||||
|
sections
|
||||||
|
.iter()
|
||||||
|
.map(|s| b.update(s.clone()))
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,146 @@
|
|||||||
|
//! Percent Above Moving Average — share of a universe trading above its MA.
|
||||||
|
|
||||||
|
use crate::cross_section::CrossSection;
|
||||||
|
use crate::traits::Indicator;
|
||||||
|
|
||||||
|
/// Percent Above Moving Average — the percentage of symbols in a universe that
|
||||||
|
/// are trading above their reference moving average.
|
||||||
|
///
|
||||||
|
/// On each [`CrossSection`] tick the value is `100 * above_ma_count / universe
|
||||||
|
/// size`, read from the per-symbol `above_ma` flag (the caller decides which MA —
|
||||||
|
/// 50-day, 200-day — when it builds the tick). It is a bounded `0..=100` breadth
|
||||||
|
/// gauge: readings near 100 mean almost the whole universe is in an uptrend
|
||||||
|
/// (broad participation, but also a potential overbought extreme), readings near
|
||||||
|
/// zero mark washouts. Crosses of the 50 line are read as bull/bear regime flips.
|
||||||
|
///
|
||||||
|
/// `Input = CrossSection`, `Output = f64` (a percentage in `0..=100`),
|
||||||
|
/// `warmup_period == 1`. The universe is non-empty by construction, so the share
|
||||||
|
/// is always defined.
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// use wickra_core::{CrossSection, Indicator, Member, PercentAboveMa};
|
||||||
|
///
|
||||||
|
/// let mut pct = PercentAboveMa::new();
|
||||||
|
/// // 3 of 4 symbols above their MA -> 75%.
|
||||||
|
/// let tick = CrossSection::new(
|
||||||
|
/// vec![
|
||||||
|
/// Member::with_signals(1.0, 10.0, false, false, true, false),
|
||||||
|
/// Member::with_signals(1.0, 10.0, false, false, true, false),
|
||||||
|
/// Member::with_signals(-1.0, 10.0, false, false, true, false),
|
||||||
|
/// Member::with_signals(-1.0, 10.0, false, false, false, false),
|
||||||
|
/// ],
|
||||||
|
/// 0,
|
||||||
|
/// )
|
||||||
|
/// .unwrap();
|
||||||
|
/// assert_eq!(pct.update(tick), Some(75.0));
|
||||||
|
/// ```
|
||||||
|
#[derive(Debug, Clone, Default)]
|
||||||
|
pub struct PercentAboveMa {
|
||||||
|
has_emitted: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PercentAboveMa {
|
||||||
|
/// Construct a new Percent Above Moving Average indicator.
|
||||||
|
#[must_use]
|
||||||
|
pub const fn new() -> Self {
|
||||||
|
Self { has_emitted: false }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Indicator for PercentAboveMa {
|
||||||
|
type Input = CrossSection;
|
||||||
|
type Output = f64;
|
||||||
|
|
||||||
|
fn update(&mut self, section: CrossSection) -> Option<f64> {
|
||||||
|
let above = section.above_ma_count() as f64;
|
||||||
|
let total = section.members.len() as f64;
|
||||||
|
self.has_emitted = true;
|
||||||
|
Some(100.0 * above / total)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn reset(&mut self) {
|
||||||
|
self.has_emitted = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn warmup_period(&self) -> usize {
|
||||||
|
1
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_ready(&self) -> bool {
|
||||||
|
self.has_emitted
|
||||||
|
}
|
||||||
|
|
||||||
|
fn name(&self) -> &'static str {
|
||||||
|
"PercentAboveMa"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::cross_section::Member;
|
||||||
|
use crate::traits::BatchExt;
|
||||||
|
|
||||||
|
fn tick(above: usize, below: usize) -> CrossSection {
|
||||||
|
let mut members = Vec::new();
|
||||||
|
for _ in 0..above {
|
||||||
|
members.push(Member::with_signals(1.0, 10.0, false, false, true, false));
|
||||||
|
}
|
||||||
|
for _ in 0..below {
|
||||||
|
members.push(Member::with_signals(-1.0, 10.0, false, false, false, false));
|
||||||
|
}
|
||||||
|
CrossSection::new(members, 0).unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn accessors_and_metadata() {
|
||||||
|
let pct = PercentAboveMa::new();
|
||||||
|
assert_eq!(pct.name(), "PercentAboveMa");
|
||||||
|
assert_eq!(pct.warmup_period(), 1);
|
||||||
|
assert!(!pct.is_ready());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn first_tick_emits_percentage() {
|
||||||
|
let mut pct = PercentAboveMa::new();
|
||||||
|
assert_eq!(pct.update(tick(3, 1)), Some(75.0));
|
||||||
|
assert!(pct.is_ready());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn all_above_is_one_hundred() {
|
||||||
|
let mut pct = PercentAboveMa::new();
|
||||||
|
assert_eq!(pct.update(tick(4, 0)), Some(100.0));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn none_above_is_zero() {
|
||||||
|
let mut pct = PercentAboveMa::new();
|
||||||
|
assert_eq!(pct.update(tick(0, 5)), Some(0.0));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn reset_clears_state() {
|
||||||
|
let mut pct = PercentAboveMa::new();
|
||||||
|
pct.update(tick(3, 1));
|
||||||
|
assert!(pct.is_ready());
|
||||||
|
pct.reset();
|
||||||
|
assert!(!pct.is_ready());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn batch_equals_streaming() {
|
||||||
|
let sections = vec![tick(3, 1), tick(4, 0), tick(0, 5)];
|
||||||
|
let mut a = PercentAboveMa::new();
|
||||||
|
let mut b = PercentAboveMa::new();
|
||||||
|
assert_eq!(
|
||||||
|
a.batch(§ions),
|
||||||
|
sections
|
||||||
|
.iter()
|
||||||
|
.map(|s| b.update(s.clone()))
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,148 @@
|
|||||||
|
//! TICK Index — instantaneous net advancing-minus-declining issues.
|
||||||
|
|
||||||
|
use crate::cross_section::CrossSection;
|
||||||
|
use crate::traits::Indicator;
|
||||||
|
|
||||||
|
/// TICK Index — the instantaneous net of advancing minus declining issues across
|
||||||
|
/// a universe, `advancers - decliners`.
|
||||||
|
///
|
||||||
|
/// Unlike the cumulative [`AdvanceDecline`](crate::AdvanceDecline) line, the TICK
|
||||||
|
/// is *not* accumulated: each tick reports the breadth of that snapshot alone. It
|
||||||
|
/// oscillates around zero — strongly positive readings mean a broad surge of
|
||||||
|
/// upticks (often an intraday overbought extreme), strongly negative readings a
|
||||||
|
/// broad flush. Traders fade extremes and watch the zero line for intraday bias.
|
||||||
|
///
|
||||||
|
/// `Input = CrossSection`, `Output = f64`, `warmup_period == 1`.
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// use wickra_core::{CrossSection, Indicator, Member, TickIndex};
|
||||||
|
///
|
||||||
|
/// let mut tick = TickIndex::new();
|
||||||
|
/// // 2 advancers, 5 decliners -> net -3.
|
||||||
|
/// let snapshot = CrossSection::new(
|
||||||
|
/// vec![
|
||||||
|
/// Member::new(1.0, 10.0, false, false),
|
||||||
|
/// Member::new(1.0, 10.0, false, false),
|
||||||
|
/// Member::new(-1.0, 10.0, false, false),
|
||||||
|
/// Member::new(-1.0, 10.0, false, false),
|
||||||
|
/// Member::new(-1.0, 10.0, false, false),
|
||||||
|
/// Member::new(-1.0, 10.0, false, false),
|
||||||
|
/// Member::new(-1.0, 10.0, false, false),
|
||||||
|
/// ],
|
||||||
|
/// 0,
|
||||||
|
/// )
|
||||||
|
/// .unwrap();
|
||||||
|
/// assert_eq!(tick.update(snapshot), Some(-3.0));
|
||||||
|
/// ```
|
||||||
|
#[derive(Debug, Clone, Default)]
|
||||||
|
pub struct TickIndex {
|
||||||
|
has_emitted: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TickIndex {
|
||||||
|
/// Construct a new TICK Index indicator.
|
||||||
|
#[must_use]
|
||||||
|
pub const fn new() -> Self {
|
||||||
|
Self { has_emitted: false }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Indicator for TickIndex {
|
||||||
|
type Input = CrossSection;
|
||||||
|
type Output = f64;
|
||||||
|
|
||||||
|
fn update(&mut self, section: CrossSection) -> Option<f64> {
|
||||||
|
let net = section.advancers() as f64 - section.decliners() as f64;
|
||||||
|
self.has_emitted = true;
|
||||||
|
Some(net)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn reset(&mut self) {
|
||||||
|
self.has_emitted = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn warmup_period(&self) -> usize {
|
||||||
|
1
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_ready(&self) -> bool {
|
||||||
|
self.has_emitted
|
||||||
|
}
|
||||||
|
|
||||||
|
fn name(&self) -> &'static str {
|
||||||
|
"TickIndex"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::cross_section::Member;
|
||||||
|
use crate::traits::BatchExt;
|
||||||
|
|
||||||
|
fn section(up: usize, down: usize) -> CrossSection {
|
||||||
|
let mut members = Vec::new();
|
||||||
|
for _ in 0..up {
|
||||||
|
members.push(Member::new(1.0, 10.0, false, false));
|
||||||
|
}
|
||||||
|
for _ in 0..down {
|
||||||
|
members.push(Member::new(-1.0, 10.0, false, false));
|
||||||
|
}
|
||||||
|
members.push(Member::new(0.0, 10.0, false, false));
|
||||||
|
CrossSection::new(members, 0).unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn accessors_and_metadata() {
|
||||||
|
let tick = TickIndex::new();
|
||||||
|
assert_eq!(tick.name(), "TickIndex");
|
||||||
|
assert_eq!(tick.warmup_period(), 1);
|
||||||
|
assert!(!tick.is_ready());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn positive_when_advancers_lead() {
|
||||||
|
let mut tick = TickIndex::new();
|
||||||
|
assert_eq!(tick.update(section(5, 2)), Some(3.0));
|
||||||
|
assert!(tick.is_ready());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn negative_when_decliners_lead() {
|
||||||
|
let mut tick = TickIndex::new();
|
||||||
|
assert_eq!(tick.update(section(2, 5)), Some(-3.0));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn does_not_accumulate() {
|
||||||
|
let mut tick = TickIndex::new();
|
||||||
|
// Each tick is independent — the second reading does not carry the first.
|
||||||
|
assert_eq!(tick.update(section(3, 0)), Some(3.0));
|
||||||
|
assert_eq!(tick.update(section(0, 1)), Some(-1.0));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn reset_clears_state() {
|
||||||
|
let mut tick = TickIndex::new();
|
||||||
|
tick.update(section(3, 0));
|
||||||
|
assert!(tick.is_ready());
|
||||||
|
tick.reset();
|
||||||
|
assert!(!tick.is_ready());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn batch_equals_streaming() {
|
||||||
|
let sections = vec![section(5, 2), section(2, 5), section(3, 0), section(0, 1)];
|
||||||
|
let mut a = TickIndex::new();
|
||||||
|
let mut b = TickIndex::new();
|
||||||
|
assert_eq!(
|
||||||
|
a.batch(§ions),
|
||||||
|
sections
|
||||||
|
.iter()
|
||||||
|
.map(|s| b.update(s.clone()))
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,171 @@
|
|||||||
|
//! TRIN / Arms Index — the advance-decline ratio over the up-down volume ratio.
|
||||||
|
|
||||||
|
use crate::cross_section::CrossSection;
|
||||||
|
use crate::traits::Indicator;
|
||||||
|
|
||||||
|
/// TRIN (Arms Index) — `(advancers / decliners) / (advancing volume / declining
|
||||||
|
/// volume)`.
|
||||||
|
///
|
||||||
|
/// The TRIN compares the breadth of a move in *issues* to the breadth of the move
|
||||||
|
/// in *volume*. A value near `1.0` means advancing issues and advancing volume are
|
||||||
|
/// in balance; a value below `1.0` is bullish (volume is concentrated in advancing
|
||||||
|
/// issues relative to their count); a value above `1.0` is bearish (declining
|
||||||
|
/// issues are absorbing disproportionate volume).
|
||||||
|
///
|
||||||
|
/// To stay finite on degenerate ticks the decliner count is floored to one and
|
||||||
|
/// both volume sums are floored to `1.0`, so a tick with no declining issues or no
|
||||||
|
/// volume on one side still yields a defined reading instead of a division by
|
||||||
|
/// zero.
|
||||||
|
///
|
||||||
|
/// `Input = CrossSection`, `Output = f64`, `warmup_period == 1`.
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// use wickra_core::{CrossSection, Indicator, Member, Trin};
|
||||||
|
///
|
||||||
|
/// let mut trin = Trin::new();
|
||||||
|
/// // 3 advancers / 1 decliner = 3; adv vol 150 / dec vol 50 = 3; TRIN = 1.0.
|
||||||
|
/// let tick = CrossSection::new(
|
||||||
|
/// vec![
|
||||||
|
/// Member::new(1.0, 50.0, false, false),
|
||||||
|
/// Member::new(1.0, 50.0, false, false),
|
||||||
|
/// Member::new(1.0, 50.0, false, false),
|
||||||
|
/// Member::new(-1.0, 50.0, false, false),
|
||||||
|
/// ],
|
||||||
|
/// 0,
|
||||||
|
/// )
|
||||||
|
/// .unwrap();
|
||||||
|
/// assert_eq!(trin.update(tick), Some(1.0));
|
||||||
|
/// ```
|
||||||
|
#[derive(Debug, Clone, Default)]
|
||||||
|
pub struct Trin {
|
||||||
|
has_emitted: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Trin {
|
||||||
|
/// Construct a new TRIN / Arms Index indicator.
|
||||||
|
#[must_use]
|
||||||
|
pub const fn new() -> Self {
|
||||||
|
Self { has_emitted: false }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Indicator for Trin {
|
||||||
|
type Input = CrossSection;
|
||||||
|
type Output = f64;
|
||||||
|
|
||||||
|
fn update(&mut self, section: CrossSection) -> Option<f64> {
|
||||||
|
let advancers = section.advancers() as f64;
|
||||||
|
let decliners = section.decliners().max(1) as f64;
|
||||||
|
let advancing_volume = section.advancing_volume().max(1.0);
|
||||||
|
let declining_volume = section.declining_volume().max(1.0);
|
||||||
|
let ad_ratio = advancers / decliners;
|
||||||
|
let volume_ratio = advancing_volume / declining_volume;
|
||||||
|
self.has_emitted = true;
|
||||||
|
Some(ad_ratio / volume_ratio)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn reset(&mut self) {
|
||||||
|
self.has_emitted = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn warmup_period(&self) -> usize {
|
||||||
|
1
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_ready(&self) -> bool {
|
||||||
|
self.has_emitted
|
||||||
|
}
|
||||||
|
|
||||||
|
fn name(&self) -> &'static str {
|
||||||
|
"Trin"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::cross_section::Member;
|
||||||
|
use crate::traits::BatchExt;
|
||||||
|
|
||||||
|
fn tick(items: &[(f64, f64)]) -> CrossSection {
|
||||||
|
CrossSection::new(
|
||||||
|
items
|
||||||
|
.iter()
|
||||||
|
.map(|&(change, volume)| Member::new(change, volume, false, false))
|
||||||
|
.collect(),
|
||||||
|
0,
|
||||||
|
)
|
||||||
|
.unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn accessors_and_metadata() {
|
||||||
|
let trin = Trin::new();
|
||||||
|
assert_eq!(trin.name(), "Trin");
|
||||||
|
assert_eq!(trin.warmup_period(), 1);
|
||||||
|
assert!(!trin.is_ready());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn balanced_breadth_yields_one() {
|
||||||
|
let mut trin = Trin::new();
|
||||||
|
let value = trin
|
||||||
|
.update(tick(&[(1.0, 50.0), (1.0, 50.0), (1.0, 50.0), (-1.0, 50.0)]))
|
||||||
|
.unwrap();
|
||||||
|
assert!((value - 1.0).abs() < 1e-9);
|
||||||
|
assert!(trin.is_ready());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn zero_decliners_and_volume_are_floored() {
|
||||||
|
let mut trin = Trin::new();
|
||||||
|
// 2 advancers, 0 decliners, adv vol 100, dec vol 0.
|
||||||
|
// ad_ratio = 2 / max(0,1) = 2; volume_ratio = 100 / max(0,1) = 100; TRIN = 0.02.
|
||||||
|
let value = trin.update(tick(&[(1.0, 50.0), (1.0, 50.0)])).unwrap();
|
||||||
|
assert!((value - 0.02).abs() < 1e-9);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn heavy_declining_volume_pushes_above_one() {
|
||||||
|
let mut trin = Trin::new();
|
||||||
|
// 2 adv / 2 dec = 1; adv vol 20 / dec vol 80 = 0.25; TRIN = 4.0.
|
||||||
|
let value = trin
|
||||||
|
.update(tick(&[
|
||||||
|
(1.0, 10.0),
|
||||||
|
(1.0, 10.0),
|
||||||
|
(-1.0, 40.0),
|
||||||
|
(-1.0, 40.0),
|
||||||
|
]))
|
||||||
|
.unwrap();
|
||||||
|
assert!((value - 4.0).abs() < 1e-9);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn reset_clears_state() {
|
||||||
|
let mut trin = Trin::new();
|
||||||
|
trin.update(tick(&[(1.0, 10.0), (-1.0, 10.0)]));
|
||||||
|
assert!(trin.is_ready());
|
||||||
|
trin.reset();
|
||||||
|
assert!(!trin.is_ready());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn batch_equals_streaming() {
|
||||||
|
let sections = vec![
|
||||||
|
tick(&[(1.0, 50.0), (1.0, 50.0), (1.0, 50.0), (-1.0, 50.0)]),
|
||||||
|
tick(&[(1.0, 50.0), (1.0, 50.0)]),
|
||||||
|
tick(&[(1.0, 10.0), (1.0, 10.0), (-1.0, 40.0), (-1.0, 40.0)]),
|
||||||
|
];
|
||||||
|
let mut a = Trin::new();
|
||||||
|
let mut b = Trin::new();
|
||||||
|
assert_eq!(
|
||||||
|
a.batch(§ions),
|
||||||
|
sections
|
||||||
|
.iter()
|
||||||
|
.map(|s| b.update(s.clone()))
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,143 @@
|
|||||||
|
//! Up/Down Volume Ratio — advancing volume divided by declining volume.
|
||||||
|
|
||||||
|
use crate::cross_section::CrossSection;
|
||||||
|
use crate::traits::Indicator;
|
||||||
|
|
||||||
|
/// Up/Down Volume Ratio — total advancing volume divided by total declining
|
||||||
|
/// volume across a universe.
|
||||||
|
///
|
||||||
|
/// On each [`CrossSection`] tick the ratio is `advancing volume / declining
|
||||||
|
/// volume`. A reading above one means more volume is trading in advancing issues
|
||||||
|
/// than declining ones (accumulation); a reading below one means distribution.
|
||||||
|
/// Sustained extremes are used to flag breadth thrusts and washout bottoms.
|
||||||
|
///
|
||||||
|
/// When a tick has no declining volume the denominator is floored to `1.0`, so the
|
||||||
|
/// ratio stays finite (it degrades to the advancing-volume total) instead of
|
||||||
|
/// dividing by zero.
|
||||||
|
///
|
||||||
|
/// `Input = CrossSection`, `Output = f64`, `warmup_period == 1`.
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// use wickra_core::{CrossSection, Indicator, Member, UpDownVolumeRatio};
|
||||||
|
///
|
||||||
|
/// let mut udv = UpDownVolumeRatio::new();
|
||||||
|
/// // advancing volume 150, declining volume 50 -> ratio 3.0.
|
||||||
|
/// let tick = CrossSection::new(
|
||||||
|
/// vec![
|
||||||
|
/// Member::new(1.0, 150.0, false, false),
|
||||||
|
/// Member::new(-1.0, 50.0, false, false),
|
||||||
|
/// ],
|
||||||
|
/// 0,
|
||||||
|
/// )
|
||||||
|
/// .unwrap();
|
||||||
|
/// assert_eq!(udv.update(tick), Some(3.0));
|
||||||
|
/// ```
|
||||||
|
#[derive(Debug, Clone, Default)]
|
||||||
|
pub struct UpDownVolumeRatio {
|
||||||
|
has_emitted: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl UpDownVolumeRatio {
|
||||||
|
/// Construct a new Up/Down Volume Ratio indicator.
|
||||||
|
#[must_use]
|
||||||
|
pub const fn new() -> Self {
|
||||||
|
Self { has_emitted: false }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Indicator for UpDownVolumeRatio {
|
||||||
|
type Input = CrossSection;
|
||||||
|
type Output = f64;
|
||||||
|
|
||||||
|
fn update(&mut self, section: CrossSection) -> Option<f64> {
|
||||||
|
let advancing_volume = section.advancing_volume();
|
||||||
|
let declining_volume = section.declining_volume().max(1.0);
|
||||||
|
self.has_emitted = true;
|
||||||
|
Some(advancing_volume / declining_volume)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn reset(&mut self) {
|
||||||
|
self.has_emitted = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn warmup_period(&self) -> usize {
|
||||||
|
1
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_ready(&self) -> bool {
|
||||||
|
self.has_emitted
|
||||||
|
}
|
||||||
|
|
||||||
|
fn name(&self) -> &'static str {
|
||||||
|
"UpDownVolumeRatio"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::cross_section::Member;
|
||||||
|
use crate::traits::BatchExt;
|
||||||
|
|
||||||
|
fn tick(items: &[(f64, f64)]) -> CrossSection {
|
||||||
|
CrossSection::new(
|
||||||
|
items
|
||||||
|
.iter()
|
||||||
|
.map(|&(change, volume)| Member::new(change, volume, false, false))
|
||||||
|
.collect(),
|
||||||
|
0,
|
||||||
|
)
|
||||||
|
.unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn accessors_and_metadata() {
|
||||||
|
let udv = UpDownVolumeRatio::new();
|
||||||
|
assert_eq!(udv.name(), "UpDownVolumeRatio");
|
||||||
|
assert_eq!(udv.warmup_period(), 1);
|
||||||
|
assert!(!udv.is_ready());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn first_tick_emits_ratio() {
|
||||||
|
let mut udv = UpDownVolumeRatio::new();
|
||||||
|
assert_eq!(udv.update(tick(&[(1.0, 150.0), (-1.0, 50.0)])), Some(3.0));
|
||||||
|
assert!(udv.is_ready());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn zero_declining_volume_floors_denominator() {
|
||||||
|
let mut udv = UpDownVolumeRatio::new();
|
||||||
|
// advancing volume 100, declining volume 0 -> 100 / max(0, 1) = 100.0.
|
||||||
|
assert_eq!(udv.update(tick(&[(1.0, 100.0)])), Some(100.0));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn reset_clears_state() {
|
||||||
|
let mut udv = UpDownVolumeRatio::new();
|
||||||
|
udv.update(tick(&[(1.0, 10.0), (-1.0, 10.0)]));
|
||||||
|
assert!(udv.is_ready());
|
||||||
|
udv.reset();
|
||||||
|
assert!(!udv.is_ready());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn batch_equals_streaming() {
|
||||||
|
let sections = vec![
|
||||||
|
tick(&[(1.0, 150.0), (-1.0, 50.0)]),
|
||||||
|
tick(&[(1.0, 100.0)]),
|
||||||
|
tick(&[(1.0, 20.0), (-1.0, 80.0)]),
|
||||||
|
];
|
||||||
|
let mut a = UpDownVolumeRatio::new();
|
||||||
|
let mut b = UpDownVolumeRatio::new();
|
||||||
|
assert_eq!(
|
||||||
|
a.batch(§ions),
|
||||||
|
sections
|
||||||
|
.iter()
|
||||||
|
.map(|s| b.update(s.clone()))
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -55,20 +55,21 @@ pub use cross_section::{CrossSection, Member};
|
|||||||
pub use derivatives::DerivativesTick;
|
pub use derivatives::DerivativesTick;
|
||||||
pub use error::{Error, Result};
|
pub use error::{Error, Result};
|
||||||
pub use indicators::{
|
pub use indicators::{
|
||||||
AbandonedBaby, AccelerationBands, AccelerationBandsOutput, AcceleratorOscillator, AdOscillator,
|
AbandonedBaby, AbsoluteBreadthIndex, AccelerationBands, AccelerationBandsOutput,
|
||||||
AdaptiveCycle, Adl, AdvanceBlock, AdvanceDecline, Adx, AdxOutput, Adxr, Alligator,
|
AcceleratorOscillator, AdOscillator, AdVolumeLine, AdaptiveCycle, Adl, AdvanceBlock,
|
||||||
AlligatorOutput, Alma, Alpha, AnchoredRsi, AnchoredVwap, Apo, Aroon, AroonOscillator,
|
AdvanceDecline, AdvanceDeclineRatio, Adx, AdxOutput, Adxr, Alligator, AlligatorOutput, Alma,
|
||||||
AroonOutput, Atr, AtrBands, AtrBandsOutput, AtrTrailingStop, Autocorrelation, AverageDrawdown,
|
Alpha, AnchoredRsi, AnchoredVwap, Apo, Aroon, AroonOscillator, AroonOutput, Atr, AtrBands,
|
||||||
AvgPrice, AwesomeOscillator, AwesomeOscillatorHistogram, BalanceOfPower, BeltHold, Beta,
|
AtrBandsOutput, AtrTrailingStop, Autocorrelation, AverageDrawdown, AvgPrice, AwesomeOscillator,
|
||||||
BetaNeutralSpread, BollingerBands, BollingerBandwidth, BollingerOutput, Breakaway,
|
AwesomeOscillatorHistogram, BalanceOfPower, BeltHold, Beta, BetaNeutralSpread, BollingerBands,
|
||||||
|
BollingerBandwidth, BollingerOutput, BreadthThrust, Breakaway, BullishPercentIndex,
|
||||||
CalendarSpread, CalmarRatio, Camarilla, CamarillaPivotsOutput, Cci, CenterOfGravity, Cfo,
|
CalendarSpread, CalmarRatio, Camarilla, CamarillaPivotsOutput, Cci, CenterOfGravity, Cfo,
|
||||||
ChaikinMoneyFlow, ChaikinOscillator, ChaikinVolatility, ChandeKrollStop, ChandeKrollStopOutput,
|
ChaikinMoneyFlow, ChaikinOscillator, ChaikinVolatility, ChandeKrollStop, ChandeKrollStopOutput,
|
||||||
ChandelierExit, ChandelierExitOutput, ChoppinessIndex, ClassicPivots, ClassicPivotsOutput,
|
ChandelierExit, ChandelierExitOutput, ChoppinessIndex, ClassicPivots, ClassicPivotsOutput,
|
||||||
ClosingMarubozu, Cmo, CoefficientOfVariation, Cointegration, CointegrationOutput,
|
ClosingMarubozu, Cmo, CoefficientOfVariation, Cointegration, CointegrationOutput,
|
||||||
ConcealingBabySwallow, ConditionalValueAtRisk, ConnorsRsi, Coppock, Counterattack,
|
ConcealingBabySwallow, ConditionalValueAtRisk, ConnorsRsi, Coppock, Counterattack,
|
||||||
CumulativeVolumeDelta, CyberneticCycle, Decycler, DecyclerOscillator, Dema, DemandIndex,
|
CumulativeVolumeDelta, CumulativeVolumeIndex, CyberneticCycle, Decycler, DecyclerOscillator,
|
||||||
DemarkPivots, DemarkPivotsOutput, DepthSlope, DetrendedStdDev, DistanceSsd, Doji, DojiStar,
|
Dema, DemandIndex, DemarkPivots, DemarkPivotsOutput, DepthSlope, DetrendedStdDev, DistanceSsd,
|
||||||
Donchian, DonchianOutput, DonchianStop, DonchianStopOutput, DoubleBollinger,
|
Doji, DojiStar, Donchian, DonchianOutput, DonchianStop, DonchianStopOutput, DoubleBollinger,
|
||||||
DoubleBollingerOutput, DownsideGapThreeMethods, Dpo, DragonflyDoji, DrawdownDuration, Dx,
|
DoubleBollingerOutput, DownsideGapThreeMethods, Dpo, DragonflyDoji, DrawdownDuration, Dx,
|
||||||
EaseOfMovement, EffectiveSpread, EhlersStochastic, ElderImpulse, Ema,
|
EaseOfMovement, EffectiveSpread, EhlersStochastic, ElderImpulse, Ema,
|
||||||
EmpiricalModeDecomposition, Engulfing, EveningDojiStar, Evwma, FallingThreeMethods, Fama,
|
EmpiricalModeDecomposition, Engulfing, EveningDojiStar, Evwma, FallingThreeMethods, Fama,
|
||||||
@@ -76,47 +77,48 @@ pub use indicators::{
|
|||||||
ForceIndex, FractalChaosBands, FractalChaosBandsOutput, Frama, FundingBasis, FundingRate,
|
ForceIndex, FractalChaosBands, FractalChaosBandsOutput, Frama, FundingBasis, FundingRate,
|
||||||
FundingRateMean, FundingRateZScore, GainLossRatio, GapSideBySideWhite, GarmanKlassVolatility,
|
FundingRateMean, FundingRateZScore, GainLossRatio, GapSideBySideWhite, GarmanKlassVolatility,
|
||||||
GrangerCausality, GravestoneDoji, Hammer, HangingMan, Harami, HeikinAshi, HeikinAshiOutput,
|
GrangerCausality, GravestoneDoji, Hammer, HangingMan, Harami, HeikinAshi, HeikinAshiOutput,
|
||||||
HiLoActivator, HighWave, Hikkake, HikkakeModified, HilbertDominantCycle, HistoricalVolatility,
|
HiLoActivator, HighLowIndex, HighWave, Hikkake, HikkakeModified, HilbertDominantCycle,
|
||||||
Hma, HomingPigeon, HtDcPhase, HtPhasor, HtPhasorOutput, HtTrendMode, HurstChannel,
|
HistoricalVolatility, Hma, HomingPigeon, HtDcPhase, HtPhasor, HtPhasorOutput, HtTrendMode,
|
||||||
HurstChannelOutput, HurstExponent, Ichimoku, IchimokuOutput, IdenticalThreeCrows, InNeck,
|
HurstChannel, HurstChannelOutput, HurstExponent, Ichimoku, IchimokuOutput, IdenticalThreeCrows,
|
||||||
Inertia, InformationRatio, InitialBalance, InitialBalanceOutput, InstantaneousTrendline,
|
InNeck, Inertia, InformationRatio, InitialBalance, InitialBalanceOutput,
|
||||||
InverseFisherTransform, InvertedHammer, Jma, KagiBars, KalmanHedgeRatio,
|
InstantaneousTrendline, InverseFisherTransform, InvertedHammer, Jma, KagiBars,
|
||||||
KalmanHedgeRatioOutput, Kama, KellyCriterion, Keltner, KeltnerOutput, Kicking, KickingByLength,
|
KalmanHedgeRatio, KalmanHedgeRatioOutput, Kama, KellyCriterion, Keltner, KeltnerOutput,
|
||||||
Kst, KstOutput, Kurtosis, Kvo, KylesLambda, LadderBottom, LaguerreRsi, LeadLagCrossCorrelation,
|
Kicking, KickingByLength, Kst, KstOutput, Kurtosis, Kvo, KylesLambda, LadderBottom,
|
||||||
LeadLagCrossCorrelationOutput, LinRegAngle, LinRegChannel, LinRegChannelOutput,
|
LaguerreRsi, LeadLagCrossCorrelation, LeadLagCrossCorrelationOutput, LinRegAngle,
|
||||||
LinRegIntercept, LinRegSlope, LinearRegression, LiquidationFeatures, LiquidationFeaturesOutput,
|
LinRegChannel, LinRegChannelOutput, LinRegIntercept, LinRegSlope, LinearRegression,
|
||||||
LongLeggedDoji, LongLine, LongShortRatio, MaEnvelope, MaEnvelopeOutput, MacdExt, MacdFix,
|
LiquidationFeatures, LiquidationFeaturesOutput, LongLeggedDoji, LongLine, LongShortRatio,
|
||||||
MacdIndicator, MacdOutput, Mama, MamaOutput, MarketFacilitationIndex, Marubozu, MassIndex,
|
MaEnvelope, MaEnvelopeOutput, MacdExt, MacdFix, MacdIndicator, MacdOutput, Mama, MamaOutput,
|
||||||
MatHold, MatchingLow, MaxDrawdown, McGinleyDynamic, MedianAbsoluteDeviation, MedianPrice, Mfi,
|
MarketFacilitationIndex, Marubozu, MassIndex, MatHold, MatchingLow, MaxDrawdown,
|
||||||
Microprice, MidPoint, MidPrice, MinusDi, MinusDm, Mom, MorningDojiStar, MorningEveningStar,
|
McClellanOscillator, McClellanSummationIndex, McGinleyDynamic, MedianAbsoluteDeviation,
|
||||||
Natr, Nvi, OIPriceDivergence, OIWeighted, Obv, OmegaRatio, OnNeck, OpenInterestDelta,
|
MedianPrice, Mfi, Microprice, MidPoint, MidPrice, MinusDi, MinusDm, Mom, MorningDojiStar,
|
||||||
OpeningMarubozu, OpeningRange, OpeningRangeOutput, OrderBookImbalanceFull,
|
MorningEveningStar, Natr, NewHighsNewLows, Nvi, OIPriceDivergence, OIWeighted, Obv, OmegaRatio,
|
||||||
OrderBookImbalanceTop1, OrderBookImbalanceTopN, OuHalfLife, PainIndex, PairSpreadZScore,
|
OnNeck, OpenInterestDelta, OpeningMarubozu, OpeningRange, OpeningRangeOutput,
|
||||||
PairwiseBeta, ParkinsonVolatility, PearsonCorrelation, PercentB, PercentageTrailingStop, Pgo,
|
OrderBookImbalanceFull, OrderBookImbalanceTop1, OrderBookImbalanceTopN, OuHalfLife, PainIndex,
|
||||||
PiercingDarkCloud, PlusDi, PlusDm, Pmo, PointAndFigureBars, Ppo, ProfitFactor, Psar, Pvi,
|
PairSpreadZScore, PairwiseBeta, ParkinsonVolatility, PearsonCorrelation, PercentAboveMa,
|
||||||
QuotedSpread, RSquared, RealizedSpread, RecoveryFactor, RelativeStrengthAB,
|
PercentB, PercentageTrailingStop, Pgo, PiercingDarkCloud, PlusDi, PlusDm, Pmo,
|
||||||
RelativeStrengthOutput, RenkoBars, RenkoTrailingStop, RickshawMan, RisingThreeMethods, Roc,
|
PointAndFigureBars, Ppo, ProfitFactor, Psar, Pvi, QuotedSpread, RSquared, RealizedSpread,
|
||||||
Rocp, Rocr, Rocr100, RogersSatchellVolatility, RollingCorrelation, RollingCovariance,
|
RecoveryFactor, RelativeStrengthAB, RelativeStrengthOutput, RenkoBars, RenkoTrailingStop,
|
||||||
RollingVwap, RoofingFilter, Rsi, Rvi, RviVolatility, Rwi, RwiOutput, SarExt, SeparatingLines,
|
RickshawMan, RisingThreeMethods, Roc, Rocp, Rocr, Rocr100, RogersSatchellVolatility,
|
||||||
SharpeRatio, ShootingStar, ShortLine, SignedVolume, SineWave, Skewness, Sma, Smi, Smma,
|
RollingCorrelation, RollingCovariance, RollingVwap, RoofingFilter, Rsi, Rvi, RviVolatility,
|
||||||
SortinoRatio, SpearmanCorrelation, SpinningTop, SpreadBollingerBands,
|
Rwi, RwiOutput, SarExt, SeparatingLines, SharpeRatio, ShootingStar, ShortLine, SignedVolume,
|
||||||
SpreadBollingerBandsOutput, SpreadHurst, StalledPattern, StandardError, StandardErrorBands,
|
SineWave, Skewness, Sma, Smi, Smma, SortinoRatio, SpearmanCorrelation, SpinningTop,
|
||||||
StandardErrorBandsOutput, StarcBands, StarcBandsOutput, Stc, StdDev, StepTrailingStop,
|
SpreadBollingerBands, SpreadBollingerBandsOutput, SpreadHurst, StalledPattern, StandardError,
|
||||||
StickSandwich, StochRsi, Stochastic, StochasticOutput, SuperSmoother, SuperTrend,
|
StandardErrorBands, StandardErrorBandsOutput, StarcBands, StarcBandsOutput, Stc, StdDev,
|
||||||
SuperTrendOutput, TakerBuySellRatio, Takuri, TasukiGap, TdCombo, TdCountdown, TdDeMarker,
|
StepTrailingStop, StickSandwich, StochRsi, Stochastic, StochasticOutput, SuperSmoother,
|
||||||
TdDifferential, TdLines, TdLinesOutput, TdOpen, TdPressure, TdRangeProjection,
|
SuperTrend, SuperTrendOutput, TakerBuySellRatio, Takuri, TasukiGap, TdCombo, TdCountdown,
|
||||||
|
TdDeMarker, TdDifferential, TdLines, TdLinesOutput, TdOpen, TdPressure, TdRangeProjection,
|
||||||
TdRangeProjectionOutput, TdRei, TdRiskLevel, TdRiskLevelOutput, TdSequential,
|
TdRangeProjectionOutput, TdRei, TdRiskLevel, TdRiskLevelOutput, TdSequential,
|
||||||
TdSequentialOutput, TdSetup, Tema, TermStructureBasis, ThreeInside, ThreeLineStrike,
|
TdSequentialOutput, TdSetup, Tema, TermStructureBasis, ThreeInside, ThreeLineStrike,
|
||||||
ThreeOutside, ThreeSoldiersOrCrows, ThreeStarsInSouth, Thrusting, Tii, TpoProfile,
|
ThreeOutside, ThreeSoldiersOrCrows, ThreeStarsInSouth, Thrusting, TickIndex, Tii, TpoProfile,
|
||||||
TpoProfileOutput, TradeImbalance, TreynorRatio, Trima, Trix, TrueRange, Tsf, Tsi, Tsv,
|
TpoProfileOutput, TradeImbalance, TreynorRatio, Trima, Trin, Trix, TrueRange, Tsf, Tsi, Tsv,
|
||||||
TtmSqueeze, TtmSqueezeOutput, Tweezer, TwoCrows, TypicalPrice, UlcerIndex, UltimateOscillator,
|
TtmSqueeze, TtmSqueezeOutput, Tweezer, TwoCrows, TypicalPrice, UlcerIndex, UltimateOscillator,
|
||||||
UniqueThreeRiver, UpsideGapThreeMethods, UpsideGapTwoCrows, ValueArea, ValueAreaOutput,
|
UniqueThreeRiver, UpDownVolumeRatio, UpsideGapThreeMethods, UpsideGapTwoCrows, ValueArea,
|
||||||
ValueAtRisk, Variance, VarianceRatio, VerticalHorizontalFilter, Vidya, VoltyStop,
|
ValueAreaOutput, ValueAtRisk, Variance, VarianceRatio, VerticalHorizontalFilter, Vidya,
|
||||||
VolumeOscillator, VolumePriceTrend, VolumeProfile, VolumeProfileOutput, Vortex, VortexOutput,
|
VoltyStop, VolumeOscillator, VolumePriceTrend, VolumeProfile, VolumeProfileOutput, Vortex,
|
||||||
Vwap, VwapStdDevBands, VwapStdDevBandsOutput, Vwma, Vzo, WaveTrend, WaveTrendOutput,
|
VortexOutput, Vwap, VwapStdDevBands, VwapStdDevBandsOutput, Vwma, Vzo, WaveTrend,
|
||||||
WeightedClose, WilliamsFractals, WilliamsFractalsOutput, WilliamsR, Wma, WoodiePivots,
|
WaveTrendOutput, WeightedClose, WilliamsFractals, WilliamsFractalsOutput, WilliamsR, Wma,
|
||||||
WoodiePivotsOutput, YangZhangVolatility, YoyoExit, ZScore, ZeroLagMacd, ZeroLagMacdOutput,
|
WoodiePivots, WoodiePivotsOutput, YangZhangVolatility, YoyoExit, ZScore, ZeroLagMacd,
|
||||||
ZigZag, ZigZagOutput, Zlema, FAMILIES, T3,
|
ZeroLagMacdOutput, ZigZag, ZigZagOutput, Zlema, FAMILIES, T3,
|
||||||
};
|
};
|
||||||
// `FootprintLevel` is a row element of `FootprintOutput`, re-exported on its own
|
// `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
|
// line so the indicator-count tooling (which scans the braced block above and
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ version.workspace = true
|
|||||||
authors.workspace = true
|
authors.workspace = true
|
||||||
edition.workspace = true
|
edition.workspace = true
|
||||||
rust-version.workspace = true
|
rust-version.workspace = true
|
||||||
license-file.workspace = true
|
license.workspace = true
|
||||||
repository.workspace = true
|
repository.workspace = true
|
||||||
homepage.workspace = true
|
homepage.workspace = true
|
||||||
readme.workspace = true
|
readme.workspace = true
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ version.workspace = true
|
|||||||
authors.workspace = true
|
authors.workspace = true
|
||||||
edition.workspace = true
|
edition.workspace = true
|
||||||
rust-version.workspace = true
|
rust-version.workspace = true
|
||||||
license-file.workspace = true
|
license.workspace = true
|
||||||
repository.workspace = true
|
repository.workspace = true
|
||||||
homepage.workspace = true
|
homepage.workspace = true
|
||||||
readme.workspace = true
|
readme.workspace = true
|
||||||
|
|||||||
@@ -21,10 +21,9 @@ allow-wildcard-paths = true
|
|||||||
[licenses]
|
[licenses]
|
||||||
version = 2
|
version = 2
|
||||||
# Licenses permitted for every crate in the dependency graph. Wickra itself is
|
# Licenses permitted for every crate in the dependency graph. Wickra itself is
|
||||||
# PolyForm-Noncommercial-1.0.0; the rest are the permissive licenses its
|
# dual-licensed MIT OR Apache-2.0; the rest are the permissive licenses its
|
||||||
# dependency tree actually uses.
|
# dependency tree actually uses.
|
||||||
allow = [
|
allow = [
|
||||||
"PolyForm-Noncommercial-1.0.0",
|
|
||||||
"MIT",
|
"MIT",
|
||||||
"Apache-2.0",
|
"Apache-2.0",
|
||||||
"BSD-2-Clause",
|
"BSD-2-Clause",
|
||||||
|
|||||||
+1
-1
@@ -8,7 +8,7 @@ That includes:
|
|||||||
[Python](https://docs.wickra.org/Quickstart-Python),
|
[Python](https://docs.wickra.org/Quickstart-Python),
|
||||||
[Node](https://docs.wickra.org/Quickstart-Node), and
|
[Node](https://docs.wickra.org/Quickstart-Node), and
|
||||||
[WASM](https://docs.wickra.org/Quickstart-WASM).
|
[WASM](https://docs.wickra.org/Quickstart-WASM).
|
||||||
- A per-indicator deep dive for every one of the **325 indicators** across
|
- A per-indicator deep dive for every one of the **339 indicators** across
|
||||||
the sixteen families (Moving Averages, Momentum Oscillators, Trend &
|
the sixteen families (Moving Averages, Momentum Oscillators, Trend &
|
||||||
Directional, Price Oscillators, Volatility & Bands, Bands & Channels,
|
Directional, Price Oscillators, Volatility & Bands, Bands & Channels,
|
||||||
Trailing Stops, Volume, Price Statistics, Ehlers / Cycle DSP, Pivots &
|
Trailing Stops, Volume, Price Statistics, Ehlers / Cycle DSP, Pivots &
|
||||||
|
|||||||
Generated
+8
-8
@@ -17,8 +17,8 @@
|
|||||||
},
|
},
|
||||||
"../../bindings/node": {
|
"../../bindings/node": {
|
||||||
"name": "wickra",
|
"name": "wickra",
|
||||||
"version": "0.4.7",
|
"version": "0.5.0",
|
||||||
"license": "PolyForm-Noncommercial-1.0.0",
|
"license": "MIT OR Apache-2.0",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@napi-rs/cli": "^2.18.0"
|
"@napi-rs/cli": "^2.18.0"
|
||||||
},
|
},
|
||||||
@@ -26,12 +26,12 @@
|
|||||||
"node": ">= 18"
|
"node": ">= 18"
|
||||||
},
|
},
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
"wickra-darwin-arm64": "0.4.7",
|
"wickra-darwin-arm64": "0.5.0",
|
||||||
"wickra-darwin-x64": "0.4.7",
|
"wickra-darwin-x64": "0.5.0",
|
||||||
"wickra-linux-arm64-gnu": "0.4.7",
|
"wickra-linux-arm64-gnu": "0.5.0",
|
||||||
"wickra-linux-x64-gnu": "0.4.7",
|
"wickra-linux-x64-gnu": "0.5.0",
|
||||||
"wickra-win32-arm64-msvc": "0.4.7",
|
"wickra-win32-arm64-msvc": "0.5.0",
|
||||||
"wickra-win32-x64-msvc": "0.4.7"
|
"wickra-win32-x64-msvc": "0.5.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/wickra": {
|
"node_modules/wickra": {
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ description = "Runnable Rust examples for the Wickra technical-analysis library.
|
|||||||
authors.workspace = true
|
authors.workspace = true
|
||||||
edition.workspace = true
|
edition.workspace = true
|
||||||
rust-version.workspace = true
|
rust-version.workspace = true
|
||||||
license-file.workspace = true
|
license.workspace = true
|
||||||
repository.workspace = true
|
repository.workspace = true
|
||||||
homepage.workspace = true
|
homepage.workspace = true
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
//! streaming or batched.
|
//! streaming or batched.
|
||||||
|
|
||||||
use libfuzzer_sys::fuzz_target;
|
use libfuzzer_sys::fuzz_target;
|
||||||
use wickra_core::{AdvanceDecline, BatchExt, CrossSection, Indicator, Member};
|
use wickra_core::{AbsoluteBreadthIndex, AdVolumeLine, AdvanceDecline, AdvanceDeclineRatio, BatchExt, BreadthThrust, BullishPercentIndex, CrossSection, CumulativeVolumeIndex, HighLowIndex, Indicator, McClellanOscillator, McClellanSummationIndex, Member, NewHighsNewLows, PercentAboveMa, TickIndex, Trin, UpDownVolumeRatio};
|
||||||
|
|
||||||
#[inline(never)]
|
#[inline(never)]
|
||||||
fn drive<I>(make: impl Fn() -> I, sections: &[CrossSection])
|
fn drive<I>(make: impl Fn() -> I, sections: &[CrossSection])
|
||||||
@@ -44,4 +44,18 @@ fuzz_target!(|data: &[u8]| {
|
|||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
drive(AdvanceDecline::new, §ions);
|
drive(AdvanceDecline::new, §ions);
|
||||||
|
drive(AdvanceDeclineRatio::new, §ions);
|
||||||
|
drive(AdVolumeLine::new, §ions);
|
||||||
|
drive(McClellanOscillator::new, §ions);
|
||||||
|
drive(McClellanSummationIndex::new, §ions);
|
||||||
|
drive(Trin::new, §ions);
|
||||||
|
drive(|| BreadthThrust::new(10).unwrap(), §ions);
|
||||||
|
drive(NewHighsNewLows::new, §ions);
|
||||||
|
drive(|| HighLowIndex::new(10).unwrap(), §ions);
|
||||||
|
drive(PercentAboveMa::new, §ions);
|
||||||
|
drive(UpDownVolumeRatio::new, §ions);
|
||||||
|
drive(BullishPercentIndex::new, §ions);
|
||||||
|
drive(CumulativeVolumeIndex::new, §ions);
|
||||||
|
drive(AbsoluteBreadthIndex::new, §ions);
|
||||||
|
drive(TickIndex::new, §ions);
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user