Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4d602df8a3 | |||
| 3ab2d6ec2d | |||
| 5e96d41916 |
+19
-1
@@ -7,6 +7,23 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [0.5.1] - 2026-06-03
|
||||
|
||||
### Added — Seasonality & Session family (12 indicators)
|
||||
|
||||
- **Volume-by-Time Profile** — mean traded volume bucketed by intraday time (`VOLUME_BY_TIME_PROFILE`).
|
||||
- **Intraday Volatility Profile** — return standard deviation bucketed by intraday time (`INTRADAY_VOLATILITY_PROFILE`).
|
||||
- **Day-of-Week Profile** — mean bar return bucketed by weekday (`DAY_OF_WEEK_PROFILE`).
|
||||
- **Time-of-Day Return Profile** — mean bar return bucketed by intraday time (`TIME_OF_DAY_RETURN_PROFILE`).
|
||||
- **Seasonal Z-Score** — z-score of the current return versus the same hour-of-day history (`SEASONAL_Z_SCORE`).
|
||||
- **Turn-of-Month** — mean daily return inside the turn-of-month window (`TURN_OF_MONTH`).
|
||||
- **Overnight/Intraday Return** — decomposition of session return into overnight and intraday legs (`OVERNIGHT_INTRADAY_RETURN`).
|
||||
- **Overnight Gap** — close-to-open return across the session boundary (`OVERNIGHT_GAP`).
|
||||
- **Average Daily Range** — mean high-low range of the last N completed sessions (`AVERAGE_DAILY_RANGE`).
|
||||
- **Session Range** — per-session (Asia/EU/US) high-low range (`SESSION_RANGE`).
|
||||
- **Session High/Low** — running high and low of the current session (`SESSION_HIGH_LOW`).
|
||||
- **Session VWAP** — session-anchored volume-weighted average price (`SESSION_VWAP`).
|
||||
|
||||
## [0.5.0] - 2026-06-03
|
||||
|
||||
### Added
|
||||
@@ -1168,7 +1185,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
optional Binance live feed.
|
||||
- Bindings for Python, Node.js, and WebAssembly.
|
||||
|
||||
[Unreleased]: https://github.com/wickra-lib/wickra/compare/v0.5.0...HEAD
|
||||
[Unreleased]: https://github.com/wickra-lib/wickra/compare/v0.5.1...HEAD
|
||||
[0.5.1]: https://github.com/wickra-lib/wickra/compare/v0.5.0...v0.5.1
|
||||
[0.5.0]: https://github.com/wickra-lib/wickra/compare/v0.4.7...v0.5.0
|
||||
[0.4.7]: https://github.com/wickra-lib/wickra/compare/v0.4.6...v0.4.7
|
||||
[0.4.6]: https://github.com/wickra-lib/wickra/compare/v0.4.5...v0.4.6
|
||||
|
||||
Generated
+6
-6
@@ -1867,7 +1867,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wickra"
|
||||
version = "0.5.0"
|
||||
version = "0.5.1"
|
||||
dependencies = [
|
||||
"approx",
|
||||
"criterion",
|
||||
@@ -1878,7 +1878,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wickra-core"
|
||||
version = "0.5.0"
|
||||
version = "0.5.1"
|
||||
dependencies = [
|
||||
"approx",
|
||||
"proptest",
|
||||
@@ -1888,7 +1888,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wickra-data"
|
||||
version = "0.5.0"
|
||||
version = "0.5.1"
|
||||
dependencies = [
|
||||
"approx",
|
||||
"csv",
|
||||
@@ -1915,7 +1915,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wickra-node"
|
||||
version = "0.5.0"
|
||||
version = "0.5.1"
|
||||
dependencies = [
|
||||
"napi",
|
||||
"napi-build",
|
||||
@@ -1925,7 +1925,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wickra-python"
|
||||
version = "0.5.0"
|
||||
version = "0.5.1"
|
||||
dependencies = [
|
||||
"numpy",
|
||||
"pyo3",
|
||||
@@ -1934,7 +1934,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wickra-wasm"
|
||||
version = "0.5.0"
|
||||
version = "0.5.1"
|
||||
dependencies = [
|
||||
"console_error_panic_hook",
|
||||
"js-sys",
|
||||
|
||||
+2
-2
@@ -12,7 +12,7 @@ members = [
|
||||
exclude = ["fuzz"]
|
||||
|
||||
[workspace.package]
|
||||
version = "0.5.0"
|
||||
version = "0.5.1"
|
||||
authors = ["kingchenc <support@wickra.org>"]
|
||||
edition = "2021"
|
||||
rust-version = "1.86"
|
||||
@@ -24,7 +24,7 @@ keywords = ["finance", "trading", "indicators", "technical-analysis", "ta"]
|
||||
categories = ["finance", "mathematics", "science"]
|
||||
|
||||
[workspace.dependencies]
|
||||
wickra-core = { path = "crates/wickra-core", version = "0.5.0" }
|
||||
wickra-core = { path = "crates/wickra-core", version = "0.5.1" }
|
||||
|
||||
thiserror = "2"
|
||||
rayon = "1.10"
|
||||
|
||||
@@ -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.
|
||||
@@ -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">
|
||||
<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>
|
||||
<a href="https://wickra.org"><img src="https://raw.githubusercontent.com/wickra-lib/.github/main/profile/wickra-banner.webp?v=351" alt="Wickra — streaming-first technical indicators" width="100%"></a>
|
||||
</p>
|
||||
|
||||
[](https://github.com/wickra-lib/wickra/actions/workflows/ci.yml)
|
||||
@@ -47,7 +47,7 @@ Full documentation lives at **[docs.wickra.org](https://docs.wickra.org)**:
|
||||
[Node](https://docs.wickra.org/Quickstart-Node),
|
||||
[WASM](https://docs.wickra.org/Quickstart-WASM).
|
||||
- **Indicators** — a per-indicator deep dive (formula, parameters, warmup) for
|
||||
every one of the 339 indicators; start at the
|
||||
every one of the 351 indicators; start at the
|
||||
[indicators overview](https://docs.wickra.org/Indicators-Overview).
|
||||
- **Reference** — [warmup periods](https://docs.wickra.org/Warmup-Periods),
|
||||
[streaming vs batch](https://docs.wickra.org/Streaming-vs-Batch),
|
||||
@@ -135,7 +135,7 @@ python -m benchmarks.compare_libraries
|
||||
|
||||
## Indicators
|
||||
|
||||
339 streaming-first indicators across twenty families. Every one passes the
|
||||
351 streaming-first indicators across twenty-one families. Every one passes the
|
||||
`batch == streaming` equivalence test, reference-value tests, and reset
|
||||
semantics tests. Each has a per-indicator deep dive (formula, parameters,
|
||||
warmup) at [docs.wickra.org](https://docs.wickra.org/Indicators-Overview).
|
||||
@@ -162,6 +162,7 @@ warmup) at [docs.wickra.org](https://docs.wickra.org/Indicators-Overview).
|
||||
| Market Profile | Value Area (POC / VAH / VAL), Volume Profile (histogram), TPO Profile, Initial Balance, Opening Range |
|
||||
| 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) |
|
||||
| Seasonality & Session | Session VWAP, Session High/Low, Session Range, Average Daily Range, Overnight Gap, Overnight/Intraday Return, Turn-of-Month, Seasonal Z-Score, Time-of-Day Return Profile, Day-of-Week Profile, Intraday Volatility Profile, Volume-by-Time Profile |
|
||||
|
||||
Every candlestick pattern emits a signed per-bar value — `+1.0` bullish,
|
||||
`−1.0` bearish, `0.0` none — so the family drops straight into a feature matrix
|
||||
@@ -240,7 +241,7 @@ A Python live-trading example using the public `websockets` package lives at
|
||||
```
|
||||
wickra/
|
||||
├── crates/
|
||||
│ ├── wickra-core/ core engine + all 339 indicators
|
||||
│ ├── wickra-core/ core engine + all 351 indicators
|
||||
│ ├── wickra/ top-level facade crate (publishes on crates.io) + benches/
|
||||
│ └── wickra-data/ CSV reader, tick aggregator, live exchange feeds
|
||||
├── bindings/
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
// Streaming-vs-batch equivalence and reference values for the Seasonality &
|
||||
// Session family. These indicators consume the full candle (open, high, low,
|
||||
// close, volume, timestamp), so they have a dedicated suite.
|
||||
|
||||
const test = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const wickra = require('..');
|
||||
|
||||
const HOUR = 3_600_000;
|
||||
const N = 240;
|
||||
const close = Array.from({ length: N }, (_, i) => 100 + Math.sin(i * 0.3) * 5 + Math.cos(i * 0.1) * 3);
|
||||
const open = close.map((c, i) => c + Math.sin(i * 0.5) * 0.5);
|
||||
const high = close.map((c, i) => Math.max(open[i], c) + 1);
|
||||
const low = close.map((c, i) => Math.min(open[i], c) - 1);
|
||||
const volume = Array.from({ length: N }, (_, i) => 1000 + (i % 24) * 50);
|
||||
const ts = Array.from({ length: N }, (_, i) => i * HOUR);
|
||||
|
||||
function eq(a, b) {
|
||||
if (Number.isNaN(a)) return Number.isNaN(b);
|
||||
return Math.abs(a - b) < 1e-9;
|
||||
}
|
||||
|
||||
function streamScalar(ind, i) {
|
||||
const v = ind.update(open[i], high[i], low[i], close[i], volume[i], ts[i]);
|
||||
return v === null || v === undefined ? NaN : v;
|
||||
}
|
||||
|
||||
function checkScalar(name, make) {
|
||||
test(`${name} streaming equals batch`, () => {
|
||||
const a = make();
|
||||
const b = make();
|
||||
const batch = b.batch(open, high, low, close, volume, ts);
|
||||
for (let i = 0; i < N; i += 1) {
|
||||
assert.ok(eq(streamScalar(a, i), batch[i]), `${name} row ${i}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function checkMatrix(name, make, k, pick) {
|
||||
test(`${name} streaming equals batch`, () => {
|
||||
const a = make();
|
||||
const b = make();
|
||||
const batch = b.batch(open, high, low, close, volume, ts);
|
||||
for (let i = 0; i < N; i += 1) {
|
||||
const out = a.update(open[i], high[i], low[i], close[i], volume[i], ts[i]);
|
||||
for (let j = 0; j < k; j += 1) {
|
||||
const s = out === null || out === undefined ? NaN : pick(out, j);
|
||||
assert.ok(eq(s, batch[i * k + j]), `${name} row ${i} col ${j}`);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
checkScalar('SessionVwap', () => new wickra.SessionVwap(0));
|
||||
checkScalar('OvernightGap', () => new wickra.OvernightGap(0));
|
||||
checkScalar('SeasonalZScore', () => new wickra.SeasonalZScore(0));
|
||||
checkScalar('AverageDailyRange', () => new wickra.AverageDailyRange(3, 0));
|
||||
checkScalar('TurnOfMonth', () => new wickra.TurnOfMonth(3, 1, 0));
|
||||
|
||||
checkMatrix('SessionHighLow', () => new wickra.SessionHighLow(0), 2, (o, j) => (j === 0 ? o.high : o.low));
|
||||
checkMatrix('SessionRange', () => new wickra.SessionRange(0), 3, (o, j) => [o.asia, o.eu, o.us][j]);
|
||||
checkMatrix(
|
||||
'OvernightIntradayReturn',
|
||||
() => new wickra.OvernightIntradayReturn(0),
|
||||
2,
|
||||
(o, j) => (j === 0 ? o.overnight : o.intraday),
|
||||
);
|
||||
checkMatrix('TimeOfDayReturnProfile', () => new wickra.TimeOfDayReturnProfile(24, 0), 24, (o, j) => o[j]);
|
||||
checkMatrix('IntradayVolatilityProfile', () => new wickra.IntradayVolatilityProfile(12, 0), 12, (o, j) => o[j]);
|
||||
checkMatrix('VolumeByTimeProfile', () => new wickra.VolumeByTimeProfile(24, 0), 24, (o, j) => o[j]);
|
||||
checkMatrix('DayOfWeekProfile', () => new wickra.DayOfWeekProfile(0), 7, (o, j) => o[j]);
|
||||
|
||||
test('SessionVwap reference value', () => {
|
||||
const vwap = new wickra.SessionVwap(0);
|
||||
assert.ok(eq(vwap.update(100, 100, 100, 100, 10, 0), 100));
|
||||
assert.ok(eq(vwap.update(110, 110, 110, 110, 30, HOUR), 107.5));
|
||||
assert.ok(eq(vwap.update(200, 200, 200, 200, 5, 24 * HOUR), 200));
|
||||
});
|
||||
|
||||
test('OvernightGap reference value', () => {
|
||||
const gap = new wickra.OvernightGap(0);
|
||||
assert.equal(gap.update(99, 101, 98, 100, 1, 0), null);
|
||||
assert.ok(eq(gap.update(105, 106, 104, 105.5, 1, 24 * HOUR), 0.05));
|
||||
});
|
||||
|
||||
test('SessionHighLow reference object', () => {
|
||||
const shl = new wickra.SessionHighLow(0);
|
||||
shl.update(100, 105, 99, 101, 1, 0);
|
||||
const out = shl.update(101, 108, 100, 107, 1, HOUR);
|
||||
assert.ok(eq(out.high, 108));
|
||||
assert.ok(eq(out.low, 99));
|
||||
});
|
||||
|
||||
test('AverageDailyRange rejects zero period', () => {
|
||||
assert.throws(() => new wickra.AverageDailyRange(0, 0));
|
||||
});
|
||||
Vendored
+131
@@ -349,6 +349,19 @@ export interface PnfColumnValue {
|
||||
high: number
|
||||
low: number
|
||||
}
|
||||
export interface SessionHighLowValue {
|
||||
high: number
|
||||
low: number
|
||||
}
|
||||
export interface SessionRangeValue {
|
||||
asia: number
|
||||
eu: number
|
||||
us: number
|
||||
}
|
||||
export interface OvernightIntradayReturnValue {
|
||||
overnight: number
|
||||
intraday: number
|
||||
}
|
||||
export type SmaNode = SMA
|
||||
export declare class SMA {
|
||||
constructor(period: number)
|
||||
@@ -3557,3 +3570,121 @@ export declare class Alpha {
|
||||
isReady(): boolean
|
||||
warmupPeriod(): number
|
||||
}
|
||||
export type SessionVwapNode = SessionVwap
|
||||
export declare class SessionVwap {
|
||||
constructor(utcOffsetMinutes: number)
|
||||
update(open: number, high: number, low: number, close: number, volume: number, timestamp: number): number | null
|
||||
batch(open: Array<number>, high: Array<number>, low: Array<number>, close: Array<number>, volume: Array<number>, timestamp: Array<number>): Array<number>
|
||||
reset(): void
|
||||
isReady(): boolean
|
||||
warmupPeriod(): number
|
||||
utcOffsetMinutes(): number
|
||||
}
|
||||
export type OvernightGapNode = OvernightGap
|
||||
export declare class OvernightGap {
|
||||
constructor(utcOffsetMinutes: number)
|
||||
update(open: number, high: number, low: number, close: number, volume: number, timestamp: number): number | null
|
||||
batch(open: Array<number>, high: Array<number>, low: Array<number>, close: Array<number>, volume: Array<number>, timestamp: Array<number>): Array<number>
|
||||
reset(): void
|
||||
isReady(): boolean
|
||||
warmupPeriod(): number
|
||||
utcOffsetMinutes(): number
|
||||
}
|
||||
export type SeasonalZScoreNode = SeasonalZScore
|
||||
export declare class SeasonalZScore {
|
||||
constructor(utcOffsetMinutes: number)
|
||||
update(open: number, high: number, low: number, close: number, volume: number, timestamp: number): number | null
|
||||
batch(open: Array<number>, high: Array<number>, low: Array<number>, close: Array<number>, volume: Array<number>, timestamp: Array<number>): Array<number>
|
||||
reset(): void
|
||||
isReady(): boolean
|
||||
warmupPeriod(): number
|
||||
utcOffsetMinutes(): number
|
||||
}
|
||||
export type TimeOfDayReturnProfileNode = TimeOfDayReturnProfile
|
||||
export declare class TimeOfDayReturnProfile {
|
||||
constructor(buckets: number, utcOffsetMinutes: number)
|
||||
update(open: number, high: number, low: number, close: number, volume: number, timestamp: number): Array<number> | null
|
||||
batch(open: Array<number>, high: Array<number>, low: Array<number>, close: Array<number>, volume: Array<number>, timestamp: Array<number>): Array<number>
|
||||
reset(): void
|
||||
isReady(): boolean
|
||||
warmupPeriod(): number
|
||||
buckets(): number
|
||||
utcOffsetMinutes(): number
|
||||
}
|
||||
export type IntradayVolatilityProfileNode = IntradayVolatilityProfile
|
||||
export declare class IntradayVolatilityProfile {
|
||||
constructor(buckets: number, utcOffsetMinutes: number)
|
||||
update(open: number, high: number, low: number, close: number, volume: number, timestamp: number): Array<number> | null
|
||||
batch(open: Array<number>, high: Array<number>, low: Array<number>, close: Array<number>, volume: Array<number>, timestamp: Array<number>): Array<number>
|
||||
reset(): void
|
||||
isReady(): boolean
|
||||
warmupPeriod(): number
|
||||
buckets(): number
|
||||
utcOffsetMinutes(): number
|
||||
}
|
||||
export type VolumeByTimeProfileNode = VolumeByTimeProfile
|
||||
export declare class VolumeByTimeProfile {
|
||||
constructor(buckets: number, utcOffsetMinutes: number)
|
||||
update(open: number, high: number, low: number, close: number, volume: number, timestamp: number): Array<number> | null
|
||||
batch(open: Array<number>, high: Array<number>, low: Array<number>, close: Array<number>, volume: Array<number>, timestamp: Array<number>): Array<number>
|
||||
reset(): void
|
||||
isReady(): boolean
|
||||
warmupPeriod(): number
|
||||
buckets(): number
|
||||
utcOffsetMinutes(): number
|
||||
}
|
||||
export type DayOfWeekProfileNode = DayOfWeekProfile
|
||||
export declare class DayOfWeekProfile {
|
||||
constructor(utcOffsetMinutes: number)
|
||||
update(open: number, high: number, low: number, close: number, volume: number, timestamp: number): Array<number> | null
|
||||
batch(open: Array<number>, high: Array<number>, low: Array<number>, close: Array<number>, volume: Array<number>, timestamp: Array<number>): Array<number>
|
||||
reset(): void
|
||||
isReady(): boolean
|
||||
warmupPeriod(): number
|
||||
utcOffsetMinutes(): number
|
||||
}
|
||||
export type AverageDailyRangeNode = AverageDailyRange
|
||||
export declare class AverageDailyRange {
|
||||
constructor(period: number, utcOffsetMinutes: number)
|
||||
update(open: number, high: number, low: number, close: number, volume: number, timestamp: number): number | null
|
||||
batch(open: Array<number>, high: Array<number>, low: Array<number>, close: Array<number>, volume: Array<number>, timestamp: Array<number>): Array<number>
|
||||
reset(): void
|
||||
isReady(): boolean
|
||||
warmupPeriod(): number
|
||||
}
|
||||
export type TurnOfMonthNode = TurnOfMonth
|
||||
export declare class TurnOfMonth {
|
||||
constructor(nFirst: number, nLast: number, utcOffsetMinutes: number)
|
||||
update(open: number, high: number, low: number, close: number, volume: number, timestamp: number): number | null
|
||||
batch(open: Array<number>, high: Array<number>, low: Array<number>, close: Array<number>, volume: Array<number>, timestamp: Array<number>): Array<number>
|
||||
reset(): void
|
||||
isReady(): boolean
|
||||
warmupPeriod(): number
|
||||
}
|
||||
export type SessionHighLowNode = SessionHighLow
|
||||
export declare class SessionHighLow {
|
||||
constructor(utcOffsetMinutes: number)
|
||||
update(open: number, high: number, low: number, close: number, volume: number, timestamp: number): SessionHighLowValue | null
|
||||
batch(open: Array<number>, high: Array<number>, low: Array<number>, close: Array<number>, volume: Array<number>, timestamp: Array<number>): Array<number>
|
||||
reset(): void
|
||||
isReady(): boolean
|
||||
warmupPeriod(): number
|
||||
}
|
||||
export type SessionRangeNode = SessionRange
|
||||
export declare class SessionRange {
|
||||
constructor(utcOffsetMinutes: number)
|
||||
update(open: number, high: number, low: number, close: number, volume: number, timestamp: number): SessionRangeValue | null
|
||||
batch(open: Array<number>, high: Array<number>, low: Array<number>, close: Array<number>, volume: Array<number>, timestamp: Array<number>): Array<number>
|
||||
reset(): void
|
||||
isReady(): boolean
|
||||
warmupPeriod(): number
|
||||
}
|
||||
export type OvernightIntradayReturnNode = OvernightIntradayReturn
|
||||
export declare class OvernightIntradayReturn {
|
||||
constructor(utcOffsetMinutes: number)
|
||||
update(open: number, high: number, low: number, close: number, volume: number, timestamp: number): OvernightIntradayReturnValue | null
|
||||
batch(open: Array<number>, high: Array<number>, low: Array<number>, close: Array<number>, volume: Array<number>, timestamp: Array<number>): Array<number>
|
||||
reset(): void
|
||||
isReady(): boolean
|
||||
warmupPeriod(): number
|
||||
}
|
||||
|
||||
+13
-1
@@ -310,7 +310,7 @@ if (!nativeBinding) {
|
||||
throw new Error(`Failed to load native binding`)
|
||||
}
|
||||
|
||||
const { version, SMA, EMA, WMA, RSI, DEMA, TEMA, HMA, ROC, TRIX, SMMA, TRIMA, ZLEMA, MOM, CMO, DPO, StdDev, UlcerIndex, VerticalHorizontalFilter, ZScore, McGinleyDynamic, FRAMA, SuperSmoother, FisherTransform, Decycler, CenterOfGravity, CyberneticCycle, InstantaneousTrendline, EhlersStochastic, RVIVolatility, Variance, CoefficientOfVariation, Skewness, Kurtosis, StandardError, DetrendedStdDev, RSquared, MedianAbsoluteDeviation, MIDPOINT, ROCP, ROCR, ROCR100, LINEARREG_INTERCEPT, TSF, Autocorrelation, HurstExponent, PearsonCorrelation, Beta, PairwiseBeta, SpearmanCorrelation, RollingCorrelation, RollingCovariance, OuHalfLife, SpreadHurst, DistanceSsd, BetaNeutralSpread, PairSpreadZScore, LeadLagCrossCorrelation, Cointegration, RelativeStrengthAB, VarianceRatio, GrangerCausality, KalmanHedgeRatio, SpreadBollingerBands, MACD, MACDFIX, MACDEXT, BollingerBands, ATR, PLUS_DM, MINUS_DM, PLUS_DI, MINUS_DI, DX, MIDPRICE, AVGPRICE, SAREXT, HT_PHASOR, Stochastic, OBV, ADX, ADXR, CCI, WilliamsR, MFI, PSAR, Keltner, Donchian, VWAP, RollingVWAP, AwesomeOscillator, Aroon, Inertia, ConnorsRSI, LaguerreRSI, SMI, KST, PGO, RVI, AwesomeOscillatorHistogram, STC, ElderImpulse, ZeroLagMACD, CFO, APO, KAMA, EVWMA, Alligator, JMA, VIDYA, ALMA, T3, TSI, PMO, TII, ADL, VolumePriceTrend, ChaikinMoneyFlow, ChaikinOscillator, ForceIndex, NVI, PVI, VolumeOscillator, KVO, WilliamsAD, AnchoredRSI, AnchoredVWAP, DemandIndex, TSV, VZO, MarketFacilitationIndex, EaseOfMovement, SuperTrend, ChandelierExit, ChandeKrollStop, AtrTrailingStop, HiLoActivator, VoltyStop, YoyoExit, DonchianStop, PercentageTrailingStop, StepTrailingStop, RenkoTrailingStop, TypicalPrice, MedianPrice, WeightedClose, LinearRegression, LinRegSlope, AcceleratorOscillator, BalanceOfPower, ChoppinessIndex, TrueRange, ChaikinVolatility, YangZhangVolatility, RogersSatchellVolatility, GarmanKlassVolatility, ParkinsonVolatility, LinRegAngle, BollingerBandwidth, PercentB, NATR, HistoricalVolatility, AroonOscillator, WaveTrend, RWI, Vortex, MassIndex, StochRSI, UltimateOscillator, PPO, Coppock, VWMA, MaEnvelope, AccelerationBands, StarcBands, AtrBands, HurstChannel, LinRegChannel, StandardErrorBands, DoubleBollinger, TtmSqueeze, FractalChaosBands, VwapStdDevBands, ClassicPivots, FibonacciPivots, Camarilla, WoodiePivots, DemarkPivots, WilliamsFractals, ZigZag, TDSetup, TDSequential, TDDeMarker, TDREI, TDPressure, TDCombo, TDCountdown, TDLines, TDRangeProjection, TDDifferential, TDOpen, TDRiskLevel, InverseFisherTransform, DecyclerOscillator, RoofingFilter, EmpiricalModeDecomposition, HT_DCPHASE, HT_TRENDMODE, HilbertDominantCycle, AdaptiveCycle, SineWave, MAMA, FAMA, Ichimoku, HeikinAshi, ValueArea, VolumeProfile, TpoProfile, InitialBalance, OpeningRange, Doji, Hammer, InvertedHammer, HangingMan, ShootingStar, Engulfing, Harami, MorningEveningStar, ThreeSoldiersOrCrows, PiercingDarkCloud, Marubozu, Tweezer, SpinningTop, ThreeInside, ThreeOutside, TwoCrows, UpsideGapTwoCrows, IdenticalThreeCrows, ThreeLineStrike, ThreeStarsInSouth, AbandonedBaby, AdvanceBlock, BeltHold, Breakaway, Counterattack, DojiStar, DragonflyDoji, GravestoneDoji, LongLeggedDoji, RickshawMan, EveningDojiStar, MorningDojiStar, GapSideBySideWhite, HighWave, Hikkake, HikkakeModified, HomingPigeon, OnNeck, InNeck, Thrusting, SeparatingLines, Kicking, KickingByLength, LadderBottom, MatHold, MatchingLow, LongLine, ShortLine, RisingThreeMethods, FallingThreeMethods, UpsideGapThreeMethods, DownsideGapThreeMethods, StalledPattern, StickSandwich, Takuri, ClosingMarubozu, OpeningMarubozu, TasukiGap, UniqueThreeRiver, ConcealingBabySwallow, OrderBookImbalanceTop1, OrderBookImbalanceFull, Microprice, QuotedSpread, DepthSlope, OrderBookImbalanceTopN, SignedVolume, CumulativeVolumeDelta, TradeImbalance, EffectiveSpread, RealizedSpread, KylesLambda, Footprint, FundingRate, FundingRateMean, FundingRateZScore, FundingBasis, OpenInterestDelta, OIPriceDivergence, OIWeighted, LongShortRatio, TakerBuySellRatio, LiquidationFeatures, TermStructureBasis, CalendarSpread, AdvanceDecline, AdvanceDeclineRatio, AdVolumeLine, McClellanOscillator, McClellanSummationIndex, Trin, BreadthThrust, NewHighsNewLows, HighLowIndex, PercentAboveMa, UpDownVolumeRatio, BullishPercentIndex, CumulativeVolumeIndex, AbsoluteBreadthIndex, TickIndex, SharpeRatio, SortinoRatio, CalmarRatio, OmegaRatio, MaxDrawdown, AverageDrawdown, DrawdownDuration, PainIndex, ValueAtRisk, ConditionalValueAtRisk, ProfitFactor, GainLossRatio, RecoveryFactor, KellyCriterion, TreynorRatio, InformationRatio, RenkoBars, KagiBars, PointAndFigureBars, Alpha } = 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, SessionVwap, OvernightGap, SeasonalZScore, TimeOfDayReturnProfile, IntradayVolatilityProfile, VolumeByTimeProfile, DayOfWeekProfile, AverageDailyRange, TurnOfMonth, SessionHighLow, SessionRange, OvernightIntradayReturn } = nativeBinding
|
||||
|
||||
module.exports.version = version
|
||||
module.exports.SMA = SMA
|
||||
@@ -652,3 +652,15 @@ module.exports.RenkoBars = RenkoBars
|
||||
module.exports.KagiBars = KagiBars
|
||||
module.exports.PointAndFigureBars = PointAndFigureBars
|
||||
module.exports.Alpha = Alpha
|
||||
module.exports.SessionVwap = SessionVwap
|
||||
module.exports.OvernightGap = OvernightGap
|
||||
module.exports.SeasonalZScore = SeasonalZScore
|
||||
module.exports.TimeOfDayReturnProfile = TimeOfDayReturnProfile
|
||||
module.exports.IntradayVolatilityProfile = IntradayVolatilityProfile
|
||||
module.exports.VolumeByTimeProfile = VolumeByTimeProfile
|
||||
module.exports.DayOfWeekProfile = DayOfWeekProfile
|
||||
module.exports.AverageDailyRange = AverageDailyRange
|
||||
module.exports.TurnOfMonth = TurnOfMonth
|
||||
module.exports.SessionHighLow = SessionHighLow
|
||||
module.exports.SessionRange = SessionRange
|
||||
module.exports.OvernightIntradayReturn = OvernightIntradayReturn
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "wickra-darwin-arm64",
|
||||
"version": "0.5.0",
|
||||
"version": "0.5.1",
|
||||
"description": "Native binding for wickra (macOS Apple Silicon). Installed automatically as an optional dependency of wickra on matching platforms.",
|
||||
"main": "wickra.darwin-arm64.node",
|
||||
"files": [
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "wickra-darwin-x64",
|
||||
"version": "0.5.0",
|
||||
"version": "0.5.1",
|
||||
"description": "Native binding for wickra (macOS Intel). Installed automatically as an optional dependency of wickra on matching platforms.",
|
||||
"main": "wickra.darwin-x64.node",
|
||||
"files": [
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "wickra-linux-arm64-gnu",
|
||||
"version": "0.5.0",
|
||||
"version": "0.5.1",
|
||||
"description": "Native binding for wickra (linux arm64 GNU). Installed automatically as an optional dependency of wickra on matching platforms.",
|
||||
"main": "wickra.linux-arm64-gnu.node",
|
||||
"files": [
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "wickra-linux-x64-gnu",
|
||||
"version": "0.5.0",
|
||||
"version": "0.5.1",
|
||||
"description": "Native binding for wickra (linux x64 GNU). Installed automatically as an optional dependency of wickra on matching platforms.",
|
||||
"main": "wickra.linux-x64-gnu.node",
|
||||
"files": [
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "wickra-win32-arm64-msvc",
|
||||
"version": "0.5.0",
|
||||
"version": "0.5.1",
|
||||
"description": "Native binding for wickra (Windows arm64 MSVC). Installed automatically as an optional dependency of wickra on matching platforms.",
|
||||
"main": "wickra.win32-arm64-msvc.node",
|
||||
"files": [
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "wickra-win32-x64-msvc",
|
||||
"version": "0.5.0",
|
||||
"version": "0.5.1",
|
||||
"description": "Native binding for wickra (Windows x64 MSVC). Installed automatically as an optional dependency of wickra on matching platforms.",
|
||||
"main": "wickra.win32-x64-msvc.node",
|
||||
"files": [
|
||||
|
||||
Generated
+20
-20
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "wickra",
|
||||
"version": "0.5.0",
|
||||
"version": "0.5.1",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "wickra",
|
||||
"version": "0.5.0",
|
||||
"version": "0.5.1",
|
||||
"license": "MIT OR Apache-2.0",
|
||||
"devDependencies": {
|
||||
"@napi-rs/cli": "^2.18.0"
|
||||
@@ -15,12 +15,12 @@
|
||||
"node": ">= 18"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"wickra-darwin-arm64": "0.5.0",
|
||||
"wickra-darwin-x64": "0.5.0",
|
||||
"wickra-linux-arm64-gnu": "0.5.0",
|
||||
"wickra-linux-x64-gnu": "0.5.0",
|
||||
"wickra-win32-arm64-msvc": "0.5.0",
|
||||
"wickra-win32-x64-msvc": "0.5.0"
|
||||
"wickra-darwin-arm64": "0.5.1",
|
||||
"wickra-darwin-x64": "0.5.1",
|
||||
"wickra-linux-arm64-gnu": "0.5.1",
|
||||
"wickra-linux-x64-gnu": "0.5.1",
|
||||
"wickra-win32-arm64-msvc": "0.5.1",
|
||||
"wickra-win32-x64-msvc": "0.5.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@napi-rs/cli": {
|
||||
@@ -41,8 +41,8 @@
|
||||
}
|
||||
},
|
||||
"node_modules/wickra-darwin-arm64": {
|
||||
"version": "0.5.0",
|
||||
"resolved": "https://registry.npmjs.org/wickra-darwin-arm64/-/wickra-darwin-arm64-0.5.0.tgz",
|
||||
"version": "0.5.1",
|
||||
"resolved": "https://registry.npmjs.org/wickra-darwin-arm64/-/wickra-darwin-arm64-0.5.1.tgz",
|
||||
"integrity": "sha512-4eZiBR/yGUdr4nzhEUFy2i69XgNx64iI2ax/LPamsThgylC0KpHOZKK19QzJ2d9KbK4C8nMjME5FLuR+4GNEwQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
@@ -57,8 +57,8 @@
|
||||
}
|
||||
},
|
||||
"node_modules/wickra-darwin-x64": {
|
||||
"version": "0.5.0",
|
||||
"resolved": "https://registry.npmjs.org/wickra-darwin-x64/-/wickra-darwin-x64-0.5.0.tgz",
|
||||
"version": "0.5.1",
|
||||
"resolved": "https://registry.npmjs.org/wickra-darwin-x64/-/wickra-darwin-x64-0.5.1.tgz",
|
||||
"integrity": "sha512-6hf8zI3QPjTFp4zCpmgUwDvNtu6jHqNUHKD5e55POo0CgA52HkpyxSPtVm8TGTIZDI7kPjlbOdBM8CJ76mmXwA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
@@ -73,8 +73,8 @@
|
||||
}
|
||||
},
|
||||
"node_modules/wickra-linux-arm64-gnu": {
|
||||
"version": "0.5.0",
|
||||
"resolved": "https://registry.npmjs.org/wickra-linux-arm64-gnu/-/wickra-linux-arm64-gnu-0.5.0.tgz",
|
||||
"version": "0.5.1",
|
||||
"resolved": "https://registry.npmjs.org/wickra-linux-arm64-gnu/-/wickra-linux-arm64-gnu-0.5.1.tgz",
|
||||
"integrity": "sha512-kSe6y0xBMSiqdPLXNjwop5WZdHtvdBNKSEBCwZ4hFq33p4apW25/wrlzv9/oDuyD4kuPabJEhCCnFOplh58CUg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
@@ -89,8 +89,8 @@
|
||||
}
|
||||
},
|
||||
"node_modules/wickra-linux-x64-gnu": {
|
||||
"version": "0.5.0",
|
||||
"resolved": "https://registry.npmjs.org/wickra-linux-x64-gnu/-/wickra-linux-x64-gnu-0.5.0.tgz",
|
||||
"version": "0.5.1",
|
||||
"resolved": "https://registry.npmjs.org/wickra-linux-x64-gnu/-/wickra-linux-x64-gnu-0.5.1.tgz",
|
||||
"integrity": "sha512-tWBWS4qz7hxM4xnpFb59bhf6TaLwXq0Z3jEa/2l7r8PiHA94g8r8S53NRMiT+4yiL5hSWe/nUiC/YXdRrhEZ4g==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
@@ -105,8 +105,8 @@
|
||||
}
|
||||
},
|
||||
"node_modules/wickra-win32-arm64-msvc": {
|
||||
"version": "0.5.0",
|
||||
"resolved": "https://registry.npmjs.org/wickra-win32-arm64-msvc/-/wickra-win32-arm64-msvc-0.5.0.tgz",
|
||||
"version": "0.5.1",
|
||||
"resolved": "https://registry.npmjs.org/wickra-win32-arm64-msvc/-/wickra-win32-arm64-msvc-0.5.1.tgz",
|
||||
"integrity": "sha512-EXIckHxAtF75PUGDKRzXyqMe9ldP0JjSdu68WFN6iJfp+McYrGu6h40TEJlQ/oUEIoPqiZB/xhVyo/el5Lg7zw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
@@ -121,8 +121,8 @@
|
||||
}
|
||||
},
|
||||
"node_modules/wickra-win32-x64-msvc": {
|
||||
"version": "0.5.0",
|
||||
"resolved": "https://registry.npmjs.org/wickra-win32-x64-msvc/-/wickra-win32-x64-msvc-0.5.0.tgz",
|
||||
"version": "0.5.1",
|
||||
"resolved": "https://registry.npmjs.org/wickra-win32-x64-msvc/-/wickra-win32-x64-msvc-0.5.1.tgz",
|
||||
"integrity": "sha512-Yfsqq1Xwp6hdxMyLze411vNdo7BDwI6+lPSe7A9XdqyPecNDbtKwYLpsal2r8EHbNzqM+R8XnuRtUaEQS5VlUQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "wickra",
|
||||
"version": "0.5.0",
|
||||
"version": "0.5.1",
|
||||
"description": "Streaming-first technical indicators: incremental, fast, install-free. Node bindings powered by Rust.",
|
||||
"author": "kingchenc <support@wickra.org>",
|
||||
"main": "index.js",
|
||||
@@ -47,12 +47,12 @@
|
||||
"node": ">= 18"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"wickra-linux-x64-gnu": "0.5.0",
|
||||
"wickra-linux-arm64-gnu": "0.5.0",
|
||||
"wickra-darwin-x64": "0.5.0",
|
||||
"wickra-darwin-arm64": "0.5.0",
|
||||
"wickra-win32-x64-msvc": "0.5.0",
|
||||
"wickra-win32-arm64-msvc": "0.5.0"
|
||||
"wickra-linux-x64-gnu": "0.5.1",
|
||||
"wickra-linux-arm64-gnu": "0.5.1",
|
||||
"wickra-darwin-x64": "0.5.1",
|
||||
"wickra-darwin-arm64": "0.5.1",
|
||||
"wickra-win32-x64-msvc": "0.5.1",
|
||||
"wickra-win32-arm64-msvc": "0.5.1"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "napi build --platform --release",
|
||||
|
||||
@@ -13398,3 +13398,615 @@ impl AlphaNode {
|
||||
self.inner.warmup_period() as u32
|
||||
}
|
||||
}
|
||||
|
||||
// ====================== Seasonality & Session (full-candle) ======================
|
||||
//
|
||||
// These read the wall-clock fields of `Candle::timestamp`, so the bindings take
|
||||
// the full candle (open, high, low, close, volume, timestamp) rather than the
|
||||
// high/low/close slice used by the candle indicators above.
|
||||
|
||||
fn season_candles(
|
||||
open: &[f64],
|
||||
high: &[f64],
|
||||
low: &[f64],
|
||||
close: &[f64],
|
||||
volume: &[f64],
|
||||
timestamp: &[i64],
|
||||
) -> napi::Result<Vec<wc::Candle>> {
|
||||
let n = open.len();
|
||||
if [
|
||||
high.len(),
|
||||
low.len(),
|
||||
close.len(),
|
||||
volume.len(),
|
||||
timestamp.len(),
|
||||
]
|
||||
.iter()
|
||||
.any(|&x| x != n)
|
||||
{
|
||||
return Err(NapiError::from_reason(
|
||||
"open, high, low, close, volume, timestamp must be equal length".to_string(),
|
||||
));
|
||||
}
|
||||
let mut out = Vec::with_capacity(n);
|
||||
for i in 0..n {
|
||||
out.push(
|
||||
wc::Candle::new(open[i], high[i], low[i], close[i], volume[i], timestamp[i])
|
||||
.map_err(map_err)?,
|
||||
);
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
macro_rules! node_seasonality_offset_scalar {
|
||||
($wrapper:ident, $node_name:literal, $rust_ty:ty) => {
|
||||
#[napi(js_name = $node_name)]
|
||||
pub struct $wrapper {
|
||||
inner: $rust_ty,
|
||||
}
|
||||
#[napi]
|
||||
impl $wrapper {
|
||||
#[napi(constructor)]
|
||||
pub fn new(utc_offset_minutes: i32) -> Self {
|
||||
Self {
|
||||
inner: <$rust_ty>::new(utc_offset_minutes),
|
||||
}
|
||||
}
|
||||
#[napi]
|
||||
pub fn update(
|
||||
&mut self,
|
||||
open: f64,
|
||||
high: f64,
|
||||
low: f64,
|
||||
close: f64,
|
||||
volume: f64,
|
||||
timestamp: i64,
|
||||
) -> napi::Result<Option<f64>> {
|
||||
Ok(self.inner.update(
|
||||
wc::Candle::new(open, high, low, close, volume, timestamp).map_err(map_err)?,
|
||||
))
|
||||
}
|
||||
#[napi]
|
||||
pub fn batch(
|
||||
&mut self,
|
||||
open: Vec<f64>,
|
||||
high: Vec<f64>,
|
||||
low: Vec<f64>,
|
||||
close: Vec<f64>,
|
||||
volume: Vec<f64>,
|
||||
timestamp: Vec<i64>,
|
||||
) -> napi::Result<Vec<f64>> {
|
||||
let candles = season_candles(&open, &high, &low, &close, &volume, ×tamp)?;
|
||||
Ok(candles
|
||||
.into_iter()
|
||||
.map(|c| self.inner.update(c).unwrap_or(f64::NAN))
|
||||
.collect())
|
||||
}
|
||||
#[napi]
|
||||
pub fn reset(&mut self) {
|
||||
self.inner.reset();
|
||||
}
|
||||
#[napi(js_name = "isReady")]
|
||||
pub fn is_ready(&self) -> bool {
|
||||
self.inner.is_ready()
|
||||
}
|
||||
#[napi(js_name = "warmupPeriod")]
|
||||
pub fn warmup_period(&self) -> u32 {
|
||||
self.inner.warmup_period() as u32
|
||||
}
|
||||
#[napi(js_name = "utcOffsetMinutes")]
|
||||
pub fn utc_offset_minutes(&self) -> i32 {
|
||||
self.inner.utc_offset_minutes()
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! node_seasonality_bucket_profile {
|
||||
($wrapper:ident, $node_name:literal, $rust_ty:ty) => {
|
||||
#[napi(js_name = $node_name)]
|
||||
pub struct $wrapper {
|
||||
inner: $rust_ty,
|
||||
}
|
||||
#[napi]
|
||||
impl $wrapper {
|
||||
#[napi(constructor)]
|
||||
pub fn new(buckets: u32, utc_offset_minutes: i32) -> napi::Result<Self> {
|
||||
Ok(Self {
|
||||
inner: <$rust_ty>::new(buckets as usize, utc_offset_minutes)
|
||||
.map_err(map_err)?,
|
||||
})
|
||||
}
|
||||
#[napi]
|
||||
pub fn update(
|
||||
&mut self,
|
||||
open: f64,
|
||||
high: f64,
|
||||
low: f64,
|
||||
close: f64,
|
||||
volume: f64,
|
||||
timestamp: i64,
|
||||
) -> napi::Result<Option<Vec<f64>>> {
|
||||
Ok(self
|
||||
.inner
|
||||
.update(
|
||||
wc::Candle::new(open, high, low, close, volume, timestamp)
|
||||
.map_err(map_err)?,
|
||||
)
|
||||
.map(|o| o.bins))
|
||||
}
|
||||
#[napi]
|
||||
pub fn batch(
|
||||
&mut self,
|
||||
open: Vec<f64>,
|
||||
high: Vec<f64>,
|
||||
low: Vec<f64>,
|
||||
close: Vec<f64>,
|
||||
volume: Vec<f64>,
|
||||
timestamp: Vec<i64>,
|
||||
) -> napi::Result<Vec<f64>> {
|
||||
let candles = season_candles(&open, &high, &low, &close, &volume, ×tamp)?;
|
||||
let k = self.inner.params().0;
|
||||
let n = candles.len();
|
||||
let mut out = vec![f64::NAN; n * k];
|
||||
for (i, c) in candles.into_iter().enumerate() {
|
||||
if let Some(o) = self.inner.update(c) {
|
||||
for (j, b) in o.bins.iter().enumerate() {
|
||||
out[i * k + j] = *b;
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
#[napi]
|
||||
pub fn reset(&mut self) {
|
||||
self.inner.reset();
|
||||
}
|
||||
#[napi(js_name = "isReady")]
|
||||
pub fn is_ready(&self) -> bool {
|
||||
self.inner.is_ready()
|
||||
}
|
||||
#[napi(js_name = "warmupPeriod")]
|
||||
pub fn warmup_period(&self) -> u32 {
|
||||
self.inner.warmup_period() as u32
|
||||
}
|
||||
#[napi(js_name = "buckets")]
|
||||
pub fn buckets(&self) -> u32 {
|
||||
self.inner.params().0 as u32
|
||||
}
|
||||
#[napi(js_name = "utcOffsetMinutes")]
|
||||
pub fn utc_offset_minutes(&self) -> i32 {
|
||||
self.inner.params().1
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! node_seasonality_offset_profile {
|
||||
($wrapper:ident, $node_name:literal, $rust_ty:ty, $k:expr) => {
|
||||
#[napi(js_name = $node_name)]
|
||||
pub struct $wrapper {
|
||||
inner: $rust_ty,
|
||||
}
|
||||
#[napi]
|
||||
impl $wrapper {
|
||||
#[napi(constructor)]
|
||||
pub fn new(utc_offset_minutes: i32) -> Self {
|
||||
Self {
|
||||
inner: <$rust_ty>::new(utc_offset_minutes),
|
||||
}
|
||||
}
|
||||
#[napi]
|
||||
pub fn update(
|
||||
&mut self,
|
||||
open: f64,
|
||||
high: f64,
|
||||
low: f64,
|
||||
close: f64,
|
||||
volume: f64,
|
||||
timestamp: i64,
|
||||
) -> napi::Result<Option<Vec<f64>>> {
|
||||
Ok(self
|
||||
.inner
|
||||
.update(
|
||||
wc::Candle::new(open, high, low, close, volume, timestamp)
|
||||
.map_err(map_err)?,
|
||||
)
|
||||
.map(|o| o.bins))
|
||||
}
|
||||
#[napi]
|
||||
pub fn batch(
|
||||
&mut self,
|
||||
open: Vec<f64>,
|
||||
high: Vec<f64>,
|
||||
low: Vec<f64>,
|
||||
close: Vec<f64>,
|
||||
volume: Vec<f64>,
|
||||
timestamp: Vec<i64>,
|
||||
) -> napi::Result<Vec<f64>> {
|
||||
let candles = season_candles(&open, &high, &low, &close, &volume, ×tamp)?;
|
||||
let k = $k;
|
||||
let n = candles.len();
|
||||
let mut out = vec![f64::NAN; n * k];
|
||||
for (i, c) in candles.into_iter().enumerate() {
|
||||
if let Some(o) = self.inner.update(c) {
|
||||
for (j, b) in o.bins.iter().enumerate() {
|
||||
out[i * k + j] = *b;
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
#[napi]
|
||||
pub fn reset(&mut self) {
|
||||
self.inner.reset();
|
||||
}
|
||||
#[napi(js_name = "isReady")]
|
||||
pub fn is_ready(&self) -> bool {
|
||||
self.inner.is_ready()
|
||||
}
|
||||
#[napi(js_name = "warmupPeriod")]
|
||||
pub fn warmup_period(&self) -> u32 {
|
||||
self.inner.warmup_period() as u32
|
||||
}
|
||||
#[napi(js_name = "utcOffsetMinutes")]
|
||||
pub fn utc_offset_minutes(&self) -> i32 {
|
||||
self.inner.utc_offset_minutes()
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
node_seasonality_offset_scalar!(SessionVwapNode, "SessionVwap", wc::SessionVwap);
|
||||
node_seasonality_offset_scalar!(OvernightGapNode, "OvernightGap", wc::OvernightGap);
|
||||
node_seasonality_offset_scalar!(SeasonalZScoreNode, "SeasonalZScore", wc::SeasonalZScore);
|
||||
node_seasonality_bucket_profile!(
|
||||
TimeOfDayReturnProfileNode,
|
||||
"TimeOfDayReturnProfile",
|
||||
wc::TimeOfDayReturnProfile
|
||||
);
|
||||
node_seasonality_bucket_profile!(
|
||||
IntradayVolatilityProfileNode,
|
||||
"IntradayVolatilityProfile",
|
||||
wc::IntradayVolatilityProfile
|
||||
);
|
||||
node_seasonality_bucket_profile!(
|
||||
VolumeByTimeProfileNode,
|
||||
"VolumeByTimeProfile",
|
||||
wc::VolumeByTimeProfile
|
||||
);
|
||||
node_seasonality_offset_profile!(
|
||||
DayOfWeekProfileNode,
|
||||
"DayOfWeekProfile",
|
||||
wc::DayOfWeekProfile,
|
||||
7
|
||||
);
|
||||
|
||||
#[napi(js_name = "AverageDailyRange")]
|
||||
pub struct AverageDailyRangeNode {
|
||||
inner: wc::AverageDailyRange,
|
||||
}
|
||||
#[napi]
|
||||
impl AverageDailyRangeNode {
|
||||
#[napi(constructor)]
|
||||
pub fn new(period: u32, utc_offset_minutes: i32) -> napi::Result<Self> {
|
||||
Ok(Self {
|
||||
inner: wc::AverageDailyRange::new(period as usize, utc_offset_minutes)
|
||||
.map_err(map_err)?,
|
||||
})
|
||||
}
|
||||
#[napi]
|
||||
pub fn update(
|
||||
&mut self,
|
||||
open: f64,
|
||||
high: f64,
|
||||
low: f64,
|
||||
close: f64,
|
||||
volume: f64,
|
||||
timestamp: i64,
|
||||
) -> napi::Result<Option<f64>> {
|
||||
Ok(self
|
||||
.inner
|
||||
.update(wc::Candle::new(open, high, low, close, volume, timestamp).map_err(map_err)?))
|
||||
}
|
||||
#[napi]
|
||||
pub fn batch(
|
||||
&mut self,
|
||||
open: Vec<f64>,
|
||||
high: Vec<f64>,
|
||||
low: Vec<f64>,
|
||||
close: Vec<f64>,
|
||||
volume: Vec<f64>,
|
||||
timestamp: Vec<i64>,
|
||||
) -> napi::Result<Vec<f64>> {
|
||||
let candles = season_candles(&open, &high, &low, &close, &volume, ×tamp)?;
|
||||
Ok(candles
|
||||
.into_iter()
|
||||
.map(|c| self.inner.update(c).unwrap_or(f64::NAN))
|
||||
.collect())
|
||||
}
|
||||
#[napi]
|
||||
pub fn reset(&mut self) {
|
||||
self.inner.reset();
|
||||
}
|
||||
#[napi(js_name = "isReady")]
|
||||
pub fn is_ready(&self) -> bool {
|
||||
self.inner.is_ready()
|
||||
}
|
||||
#[napi(js_name = "warmupPeriod")]
|
||||
pub fn warmup_period(&self) -> u32 {
|
||||
self.inner.warmup_period() as u32
|
||||
}
|
||||
}
|
||||
|
||||
#[napi(js_name = "TurnOfMonth")]
|
||||
pub struct TurnOfMonthNode {
|
||||
inner: wc::TurnOfMonth,
|
||||
}
|
||||
#[napi]
|
||||
impl TurnOfMonthNode {
|
||||
#[napi(constructor)]
|
||||
pub fn new(n_first: u32, n_last: u32, utc_offset_minutes: i32) -> napi::Result<Self> {
|
||||
Ok(Self {
|
||||
inner: wc::TurnOfMonth::new(n_first, n_last, utc_offset_minutes).map_err(map_err)?,
|
||||
})
|
||||
}
|
||||
#[napi]
|
||||
pub fn update(
|
||||
&mut self,
|
||||
open: f64,
|
||||
high: f64,
|
||||
low: f64,
|
||||
close: f64,
|
||||
volume: f64,
|
||||
timestamp: i64,
|
||||
) -> napi::Result<Option<f64>> {
|
||||
Ok(self
|
||||
.inner
|
||||
.update(wc::Candle::new(open, high, low, close, volume, timestamp).map_err(map_err)?))
|
||||
}
|
||||
#[napi]
|
||||
pub fn batch(
|
||||
&mut self,
|
||||
open: Vec<f64>,
|
||||
high: Vec<f64>,
|
||||
low: Vec<f64>,
|
||||
close: Vec<f64>,
|
||||
volume: Vec<f64>,
|
||||
timestamp: Vec<i64>,
|
||||
) -> napi::Result<Vec<f64>> {
|
||||
let candles = season_candles(&open, &high, &low, &close, &volume, ×tamp)?;
|
||||
Ok(candles
|
||||
.into_iter()
|
||||
.map(|c| self.inner.update(c).unwrap_or(f64::NAN))
|
||||
.collect())
|
||||
}
|
||||
#[napi]
|
||||
pub fn reset(&mut self) {
|
||||
self.inner.reset();
|
||||
}
|
||||
#[napi(js_name = "isReady")]
|
||||
pub fn is_ready(&self) -> bool {
|
||||
self.inner.is_ready()
|
||||
}
|
||||
#[napi(js_name = "warmupPeriod")]
|
||||
pub fn warmup_period(&self) -> u32 {
|
||||
self.inner.warmup_period() as u32
|
||||
}
|
||||
}
|
||||
|
||||
#[napi(object)]
|
||||
pub struct SessionHighLowValue {
|
||||
pub high: f64,
|
||||
pub low: f64,
|
||||
}
|
||||
|
||||
#[napi(js_name = "SessionHighLow")]
|
||||
pub struct SessionHighLowNode {
|
||||
inner: wc::SessionHighLow,
|
||||
}
|
||||
#[napi]
|
||||
impl SessionHighLowNode {
|
||||
#[napi(constructor)]
|
||||
pub fn new(utc_offset_minutes: i32) -> Self {
|
||||
Self {
|
||||
inner: wc::SessionHighLow::new(utc_offset_minutes),
|
||||
}
|
||||
}
|
||||
#[napi]
|
||||
pub fn update(
|
||||
&mut self,
|
||||
open: f64,
|
||||
high: f64,
|
||||
low: f64,
|
||||
close: f64,
|
||||
volume: f64,
|
||||
timestamp: i64,
|
||||
) -> napi::Result<Option<SessionHighLowValue>> {
|
||||
Ok(self
|
||||
.inner
|
||||
.update(wc::Candle::new(open, high, low, close, volume, timestamp).map_err(map_err)?)
|
||||
.map(|o| SessionHighLowValue {
|
||||
high: o.high,
|
||||
low: o.low,
|
||||
}))
|
||||
}
|
||||
#[napi]
|
||||
pub fn batch(
|
||||
&mut self,
|
||||
open: Vec<f64>,
|
||||
high: Vec<f64>,
|
||||
low: Vec<f64>,
|
||||
close: Vec<f64>,
|
||||
volume: Vec<f64>,
|
||||
timestamp: Vec<i64>,
|
||||
) -> napi::Result<Vec<f64>> {
|
||||
let candles = season_candles(&open, &high, &low, &close, &volume, ×tamp)?;
|
||||
let n = candles.len();
|
||||
let mut out = vec![f64::NAN; n * 2];
|
||||
for (i, c) in candles.into_iter().enumerate() {
|
||||
if let Some(o) = self.inner.update(c) {
|
||||
out[i * 2] = o.high;
|
||||
out[i * 2 + 1] = o.low;
|
||||
}
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
#[napi]
|
||||
pub fn reset(&mut self) {
|
||||
self.inner.reset();
|
||||
}
|
||||
#[napi(js_name = "isReady")]
|
||||
pub fn is_ready(&self) -> bool {
|
||||
self.inner.is_ready()
|
||||
}
|
||||
#[napi(js_name = "warmupPeriod")]
|
||||
pub fn warmup_period(&self) -> u32 {
|
||||
self.inner.warmup_period() as u32
|
||||
}
|
||||
}
|
||||
|
||||
#[napi(object)]
|
||||
pub struct SessionRangeValue {
|
||||
pub asia: f64,
|
||||
pub eu: f64,
|
||||
pub us: f64,
|
||||
}
|
||||
|
||||
#[napi(js_name = "SessionRange")]
|
||||
pub struct SessionRangeNode {
|
||||
inner: wc::SessionRange,
|
||||
}
|
||||
#[napi]
|
||||
impl SessionRangeNode {
|
||||
#[napi(constructor)]
|
||||
pub fn new(utc_offset_minutes: i32) -> Self {
|
||||
Self {
|
||||
inner: wc::SessionRange::new(utc_offset_minutes),
|
||||
}
|
||||
}
|
||||
#[napi]
|
||||
pub fn update(
|
||||
&mut self,
|
||||
open: f64,
|
||||
high: f64,
|
||||
low: f64,
|
||||
close: f64,
|
||||
volume: f64,
|
||||
timestamp: i64,
|
||||
) -> napi::Result<Option<SessionRangeValue>> {
|
||||
Ok(self
|
||||
.inner
|
||||
.update(wc::Candle::new(open, high, low, close, volume, timestamp).map_err(map_err)?)
|
||||
.map(|o| SessionRangeValue {
|
||||
asia: o.asia,
|
||||
eu: o.eu,
|
||||
us: o.us,
|
||||
}))
|
||||
}
|
||||
#[napi]
|
||||
pub fn batch(
|
||||
&mut self,
|
||||
open: Vec<f64>,
|
||||
high: Vec<f64>,
|
||||
low: Vec<f64>,
|
||||
close: Vec<f64>,
|
||||
volume: Vec<f64>,
|
||||
timestamp: Vec<i64>,
|
||||
) -> napi::Result<Vec<f64>> {
|
||||
let candles = season_candles(&open, &high, &low, &close, &volume, ×tamp)?;
|
||||
let n = candles.len();
|
||||
let mut out = vec![f64::NAN; n * 3];
|
||||
for (i, c) in candles.into_iter().enumerate() {
|
||||
if let Some(o) = self.inner.update(c) {
|
||||
out[i * 3] = o.asia;
|
||||
out[i * 3 + 1] = o.eu;
|
||||
out[i * 3 + 2] = o.us;
|
||||
}
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
#[napi]
|
||||
pub fn reset(&mut self) {
|
||||
self.inner.reset();
|
||||
}
|
||||
#[napi(js_name = "isReady")]
|
||||
pub fn is_ready(&self) -> bool {
|
||||
self.inner.is_ready()
|
||||
}
|
||||
#[napi(js_name = "warmupPeriod")]
|
||||
pub fn warmup_period(&self) -> u32 {
|
||||
self.inner.warmup_period() as u32
|
||||
}
|
||||
}
|
||||
|
||||
#[napi(object)]
|
||||
pub struct OvernightIntradayReturnValue {
|
||||
pub overnight: f64,
|
||||
pub intraday: f64,
|
||||
}
|
||||
|
||||
#[napi(js_name = "OvernightIntradayReturn")]
|
||||
pub struct OvernightIntradayReturnNode {
|
||||
inner: wc::OvernightIntradayReturn,
|
||||
}
|
||||
#[napi]
|
||||
impl OvernightIntradayReturnNode {
|
||||
#[napi(constructor)]
|
||||
pub fn new(utc_offset_minutes: i32) -> Self {
|
||||
Self {
|
||||
inner: wc::OvernightIntradayReturn::new(utc_offset_minutes),
|
||||
}
|
||||
}
|
||||
#[napi]
|
||||
pub fn update(
|
||||
&mut self,
|
||||
open: f64,
|
||||
high: f64,
|
||||
low: f64,
|
||||
close: f64,
|
||||
volume: f64,
|
||||
timestamp: i64,
|
||||
) -> napi::Result<Option<OvernightIntradayReturnValue>> {
|
||||
Ok(self
|
||||
.inner
|
||||
.update(wc::Candle::new(open, high, low, close, volume, timestamp).map_err(map_err)?)
|
||||
.map(|o| OvernightIntradayReturnValue {
|
||||
overnight: o.overnight,
|
||||
intraday: o.intraday,
|
||||
}))
|
||||
}
|
||||
#[napi]
|
||||
pub fn batch(
|
||||
&mut self,
|
||||
open: Vec<f64>,
|
||||
high: Vec<f64>,
|
||||
low: Vec<f64>,
|
||||
close: Vec<f64>,
|
||||
volume: Vec<f64>,
|
||||
timestamp: Vec<i64>,
|
||||
) -> napi::Result<Vec<f64>> {
|
||||
let candles = season_candles(&open, &high, &low, &close, &volume, ×tamp)?;
|
||||
let n = candles.len();
|
||||
let mut out = vec![f64::NAN; n * 2];
|
||||
for (i, c) in candles.into_iter().enumerate() {
|
||||
if let Some(o) = self.inner.update(c) {
|
||||
out[i * 2] = o.overnight;
|
||||
out[i * 2 + 1] = o.intraday;
|
||||
}
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
#[napi]
|
||||
pub fn reset(&mut self) {
|
||||
self.inner.reset();
|
||||
}
|
||||
#[napi(js_name = "isReady")]
|
||||
pub fn is_ready(&self) -> bool {
|
||||
self.inner.is_ready()
|
||||
}
|
||||
#[napi(js_name = "warmupPeriod")]
|
||||
pub fn warmup_period(&self) -> u32 {
|
||||
self.inner.warmup_period() as u32
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ build-backend = "maturin"
|
||||
|
||||
[project]
|
||||
name = "wickra"
|
||||
version = "0.5.0"
|
||||
version = "0.5.1"
|
||||
description = "Streaming-first technical indicators: incremental, fast, install-free."
|
||||
readme = "README.md"
|
||||
license = "MIT OR Apache-2.0"
|
||||
|
||||
@@ -385,6 +385,19 @@ from ._wickra import (
|
||||
TreynorRatio,
|
||||
InformationRatio,
|
||||
Alpha,
|
||||
# Seasonality & Session
|
||||
SessionVwap,
|
||||
SessionHighLow,
|
||||
SessionRange,
|
||||
AverageDailyRange,
|
||||
OvernightGap,
|
||||
OvernightIntradayReturn,
|
||||
TurnOfMonth,
|
||||
SeasonalZScore,
|
||||
TimeOfDayReturnProfile,
|
||||
DayOfWeekProfile,
|
||||
IntradayVolatilityProfile,
|
||||
VolumeByTimeProfile,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
@@ -749,4 +762,17 @@ __all__ = [
|
||||
"TreynorRatio",
|
||||
"InformationRatio",
|
||||
"Alpha",
|
||||
# Seasonality & Session
|
||||
"SessionVwap",
|
||||
"SessionHighLow",
|
||||
"SessionRange",
|
||||
"AverageDailyRange",
|
||||
"OvernightGap",
|
||||
"OvernightIntradayReturn",
|
||||
"TurnOfMonth",
|
||||
"SeasonalZScore",
|
||||
"TimeOfDayReturnProfile",
|
||||
"DayOfWeekProfile",
|
||||
"IntradayVolatilityProfile",
|
||||
"VolumeByTimeProfile",
|
||||
]
|
||||
|
||||
@@ -17243,6 +17243,593 @@ impl PyPointAndFigureBars {
|
||||
|
||||
// ============================== Module ==============================
|
||||
|
||||
// ====================== Seasonality & Session (full-candle) ======================
|
||||
//
|
||||
// These indicators read the wall-clock fields of `Candle::timestamp`, so the
|
||||
// bindings consume the FULL candle (open, high, low, close, volume, timestamp)
|
||||
// — unlike the high/low/close candle indicators above.
|
||||
|
||||
fn build_seasonality_candles<'py>(
|
||||
open: &PyReadonlyArray1<'py, f64>,
|
||||
high: &PyReadonlyArray1<'py, f64>,
|
||||
low: &PyReadonlyArray1<'py, f64>,
|
||||
close: &PyReadonlyArray1<'py, f64>,
|
||||
volume: &PyReadonlyArray1<'py, f64>,
|
||||
timestamp: &PyReadonlyArray1<'py, i64>,
|
||||
) -> PyResult<Vec<wc::Candle>> {
|
||||
let o = open
|
||||
.as_slice()
|
||||
.map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?;
|
||||
let h = high
|
||||
.as_slice()
|
||||
.map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?;
|
||||
let l = low
|
||||
.as_slice()
|
||||
.map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?;
|
||||
let c = close
|
||||
.as_slice()
|
||||
.map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?;
|
||||
let v = volume
|
||||
.as_slice()
|
||||
.map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?;
|
||||
let t = timestamp
|
||||
.as_slice()
|
||||
.map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?;
|
||||
let n = o.len();
|
||||
if [h.len(), l.len(), c.len(), v.len(), t.len()]
|
||||
.iter()
|
||||
.any(|&x| x != n)
|
||||
{
|
||||
return Err(PyValueError::new_err(
|
||||
"open, high, low, close, volume, timestamp must be equal length",
|
||||
));
|
||||
}
|
||||
let mut candles = Vec::with_capacity(n);
|
||||
for i in 0..n {
|
||||
candles.push(wc::Candle::new(o[i], h[i], l[i], c[i], v[i], t[i]).map_err(map_err)?);
|
||||
}
|
||||
Ok(candles)
|
||||
}
|
||||
|
||||
macro_rules! py_seasonality_offset_scalar {
|
||||
($pytype:ident, $name:literal, $rust:ident) => {
|
||||
#[pyclass(name = $name, module = "wickra._wickra", skip_from_py_object)]
|
||||
#[derive(Clone)]
|
||||
struct $pytype {
|
||||
inner: wc::$rust,
|
||||
}
|
||||
#[pymethods]
|
||||
impl $pytype {
|
||||
#[new]
|
||||
#[pyo3(signature = (utc_offset_minutes = 0))]
|
||||
fn new(utc_offset_minutes: i32) -> Self {
|
||||
Self {
|
||||
inner: wc::$rust::new(utc_offset_minutes),
|
||||
}
|
||||
}
|
||||
fn update(&mut self, candle: &Bound<'_, PyAny>) -> PyResult<Option<f64>> {
|
||||
Ok(self.inner.update(extract_candle(candle)?))
|
||||
}
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn batch<'py>(
|
||||
&mut self,
|
||||
py: Python<'py>,
|
||||
open: PyReadonlyArray1<'py, f64>,
|
||||
high: PyReadonlyArray1<'py, f64>,
|
||||
low: PyReadonlyArray1<'py, f64>,
|
||||
close: PyReadonlyArray1<'py, f64>,
|
||||
volume: PyReadonlyArray1<'py, f64>,
|
||||
timestamp: PyReadonlyArray1<'py, i64>,
|
||||
) -> PyResult<Bound<'py, PyArray1<f64>>> {
|
||||
let candles =
|
||||
build_seasonality_candles(&open, &high, &low, &close, &volume, ×tamp)?;
|
||||
let out: Vec<f64> = candles
|
||||
.into_iter()
|
||||
.map(|c| self.inner.update(c).unwrap_or(f64::NAN))
|
||||
.collect();
|
||||
Ok(out.into_pyarray(py))
|
||||
}
|
||||
#[getter]
|
||||
fn utc_offset_minutes(&self) -> i32 {
|
||||
self.inner.utc_offset_minutes()
|
||||
}
|
||||
fn reset(&mut self) {
|
||||
self.inner.reset();
|
||||
}
|
||||
fn is_ready(&self) -> bool {
|
||||
self.inner.is_ready()
|
||||
}
|
||||
fn warmup_period(&self) -> usize {
|
||||
self.inner.warmup_period()
|
||||
}
|
||||
fn __repr__(&self) -> String {
|
||||
format!(
|
||||
"{}(utc_offset_minutes={})",
|
||||
$name,
|
||||
self.inner.utc_offset_minutes()
|
||||
)
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! py_seasonality_bucket_profile {
|
||||
($pytype:ident, $name:literal, $rust:ident) => {
|
||||
#[pyclass(name = $name, module = "wickra._wickra", skip_from_py_object)]
|
||||
#[derive(Clone)]
|
||||
struct $pytype {
|
||||
inner: wc::$rust,
|
||||
}
|
||||
#[pymethods]
|
||||
impl $pytype {
|
||||
#[new]
|
||||
#[pyo3(signature = (buckets = 24, utc_offset_minutes = 0))]
|
||||
fn new(buckets: usize, utc_offset_minutes: i32) -> PyResult<Self> {
|
||||
Ok(Self {
|
||||
inner: wc::$rust::new(buckets, utc_offset_minutes).map_err(map_err)?,
|
||||
})
|
||||
}
|
||||
fn update<'py>(
|
||||
&mut self,
|
||||
py: Python<'py>,
|
||||
candle: &Bound<'_, PyAny>,
|
||||
) -> PyResult<Option<Bound<'py, PyArray1<f64>>>> {
|
||||
let c = extract_candle(candle)?;
|
||||
Ok(self.inner.update(c).map(|o| o.bins.into_pyarray(py)))
|
||||
}
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn batch<'py>(
|
||||
&mut self,
|
||||
py: Python<'py>,
|
||||
open: PyReadonlyArray1<'py, f64>,
|
||||
high: PyReadonlyArray1<'py, f64>,
|
||||
low: PyReadonlyArray1<'py, f64>,
|
||||
close: PyReadonlyArray1<'py, f64>,
|
||||
volume: PyReadonlyArray1<'py, f64>,
|
||||
timestamp: PyReadonlyArray1<'py, i64>,
|
||||
) -> PyResult<Bound<'py, PyArray2<f64>>> {
|
||||
let candles =
|
||||
build_seasonality_candles(&open, &high, &low, &close, &volume, ×tamp)?;
|
||||
let k = self.inner.params().0;
|
||||
let n = candles.len();
|
||||
let mut out = vec![f64::NAN; n * k];
|
||||
for (i, c) in candles.into_iter().enumerate() {
|
||||
if let Some(o) = self.inner.update(c) {
|
||||
for (j, b) in o.bins.iter().enumerate() {
|
||||
out[i * k + j] = *b;
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(numpy::ndarray::Array2::from_shape_vec((n, k), out)
|
||||
.expect("shape consistent")
|
||||
.into_pyarray(py))
|
||||
}
|
||||
#[getter]
|
||||
fn params(&self) -> (usize, i32) {
|
||||
self.inner.params()
|
||||
}
|
||||
fn reset(&mut self) {
|
||||
self.inner.reset();
|
||||
}
|
||||
fn is_ready(&self) -> bool {
|
||||
self.inner.is_ready()
|
||||
}
|
||||
fn warmup_period(&self) -> usize {
|
||||
self.inner.warmup_period()
|
||||
}
|
||||
fn __repr__(&self) -> String {
|
||||
let (buckets, offset) = self.inner.params();
|
||||
format!("{}(buckets={buckets}, utc_offset_minutes={offset})", $name)
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! py_seasonality_offset_profile {
|
||||
($pytype:ident, $name:literal, $rust:ident, $k:expr) => {
|
||||
#[pyclass(name = $name, module = "wickra._wickra", skip_from_py_object)]
|
||||
#[derive(Clone)]
|
||||
struct $pytype {
|
||||
inner: wc::$rust,
|
||||
}
|
||||
#[pymethods]
|
||||
impl $pytype {
|
||||
#[new]
|
||||
#[pyo3(signature = (utc_offset_minutes = 0))]
|
||||
fn new(utc_offset_minutes: i32) -> Self {
|
||||
Self {
|
||||
inner: wc::$rust::new(utc_offset_minutes),
|
||||
}
|
||||
}
|
||||
fn update<'py>(
|
||||
&mut self,
|
||||
py: Python<'py>,
|
||||
candle: &Bound<'_, PyAny>,
|
||||
) -> PyResult<Option<Bound<'py, PyArray1<f64>>>> {
|
||||
let c = extract_candle(candle)?;
|
||||
Ok(self.inner.update(c).map(|o| o.bins.into_pyarray(py)))
|
||||
}
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn batch<'py>(
|
||||
&mut self,
|
||||
py: Python<'py>,
|
||||
open: PyReadonlyArray1<'py, f64>,
|
||||
high: PyReadonlyArray1<'py, f64>,
|
||||
low: PyReadonlyArray1<'py, f64>,
|
||||
close: PyReadonlyArray1<'py, f64>,
|
||||
volume: PyReadonlyArray1<'py, f64>,
|
||||
timestamp: PyReadonlyArray1<'py, i64>,
|
||||
) -> PyResult<Bound<'py, PyArray2<f64>>> {
|
||||
let candles =
|
||||
build_seasonality_candles(&open, &high, &low, &close, &volume, ×tamp)?;
|
||||
let k = $k;
|
||||
let n = candles.len();
|
||||
let mut out = vec![f64::NAN; n * k];
|
||||
for (i, c) in candles.into_iter().enumerate() {
|
||||
if let Some(o) = self.inner.update(c) {
|
||||
for (j, b) in o.bins.iter().enumerate() {
|
||||
out[i * k + j] = *b;
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(numpy::ndarray::Array2::from_shape_vec((n, k), out)
|
||||
.expect("shape consistent")
|
||||
.into_pyarray(py))
|
||||
}
|
||||
#[getter]
|
||||
fn utc_offset_minutes(&self) -> i32 {
|
||||
self.inner.utc_offset_minutes()
|
||||
}
|
||||
fn reset(&mut self) {
|
||||
self.inner.reset();
|
||||
}
|
||||
fn is_ready(&self) -> bool {
|
||||
self.inner.is_ready()
|
||||
}
|
||||
fn warmup_period(&self) -> usize {
|
||||
self.inner.warmup_period()
|
||||
}
|
||||
fn __repr__(&self) -> String {
|
||||
format!(
|
||||
"{}(utc_offset_minutes={})",
|
||||
$name,
|
||||
self.inner.utc_offset_minutes()
|
||||
)
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
py_seasonality_offset_scalar!(PySessionVwap, "SessionVwap", SessionVwap);
|
||||
py_seasonality_offset_scalar!(PyOvernightGap, "OvernightGap", OvernightGap);
|
||||
py_seasonality_offset_scalar!(PySeasonalZScore, "SeasonalZScore", SeasonalZScore);
|
||||
py_seasonality_bucket_profile!(
|
||||
PyTimeOfDayReturnProfile,
|
||||
"TimeOfDayReturnProfile",
|
||||
TimeOfDayReturnProfile
|
||||
);
|
||||
py_seasonality_bucket_profile!(
|
||||
PyIntradayVolatilityProfile,
|
||||
"IntradayVolatilityProfile",
|
||||
IntradayVolatilityProfile
|
||||
);
|
||||
py_seasonality_bucket_profile!(
|
||||
PyVolumeByTimeProfile,
|
||||
"VolumeByTimeProfile",
|
||||
VolumeByTimeProfile
|
||||
);
|
||||
py_seasonality_offset_profile!(PyDayOfWeekProfile, "DayOfWeekProfile", DayOfWeekProfile, 7);
|
||||
|
||||
#[pyclass(
|
||||
name = "AverageDailyRange",
|
||||
module = "wickra._wickra",
|
||||
skip_from_py_object
|
||||
)]
|
||||
#[derive(Clone)]
|
||||
struct PyAverageDailyRange {
|
||||
inner: wc::AverageDailyRange,
|
||||
}
|
||||
#[pymethods]
|
||||
impl PyAverageDailyRange {
|
||||
#[new]
|
||||
#[pyo3(signature = (period = 14, utc_offset_minutes = 0))]
|
||||
fn new(period: usize, utc_offset_minutes: i32) -> PyResult<Self> {
|
||||
Ok(Self {
|
||||
inner: wc::AverageDailyRange::new(period, utc_offset_minutes).map_err(map_err)?,
|
||||
})
|
||||
}
|
||||
fn update(&mut self, candle: &Bound<'_, PyAny>) -> PyResult<Option<f64>> {
|
||||
Ok(self.inner.update(extract_candle(candle)?))
|
||||
}
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn batch<'py>(
|
||||
&mut self,
|
||||
py: Python<'py>,
|
||||
open: PyReadonlyArray1<'py, f64>,
|
||||
high: PyReadonlyArray1<'py, f64>,
|
||||
low: PyReadonlyArray1<'py, f64>,
|
||||
close: PyReadonlyArray1<'py, f64>,
|
||||
volume: PyReadonlyArray1<'py, f64>,
|
||||
timestamp: PyReadonlyArray1<'py, i64>,
|
||||
) -> PyResult<Bound<'py, PyArray1<f64>>> {
|
||||
let candles = build_seasonality_candles(&open, &high, &low, &close, &volume, ×tamp)?;
|
||||
let out: Vec<f64> = candles
|
||||
.into_iter()
|
||||
.map(|c| self.inner.update(c).unwrap_or(f64::NAN))
|
||||
.collect();
|
||||
Ok(out.into_pyarray(py))
|
||||
}
|
||||
#[getter]
|
||||
fn params(&self) -> (usize, i32) {
|
||||
self.inner.params()
|
||||
}
|
||||
fn reset(&mut self) {
|
||||
self.inner.reset();
|
||||
}
|
||||
fn is_ready(&self) -> bool {
|
||||
self.inner.is_ready()
|
||||
}
|
||||
fn warmup_period(&self) -> usize {
|
||||
self.inner.warmup_period()
|
||||
}
|
||||
fn __repr__(&self) -> String {
|
||||
let (period, offset) = self.inner.params();
|
||||
format!("AverageDailyRange(period={period}, utc_offset_minutes={offset})")
|
||||
}
|
||||
}
|
||||
|
||||
#[pyclass(name = "TurnOfMonth", module = "wickra._wickra", skip_from_py_object)]
|
||||
#[derive(Clone)]
|
||||
struct PyTurnOfMonth {
|
||||
inner: wc::TurnOfMonth,
|
||||
}
|
||||
#[pymethods]
|
||||
impl PyTurnOfMonth {
|
||||
#[new]
|
||||
#[pyo3(signature = (n_first = 3, n_last = 1, utc_offset_minutes = 0))]
|
||||
fn new(n_first: u32, n_last: u32, utc_offset_minutes: i32) -> PyResult<Self> {
|
||||
Ok(Self {
|
||||
inner: wc::TurnOfMonth::new(n_first, n_last, utc_offset_minutes).map_err(map_err)?,
|
||||
})
|
||||
}
|
||||
fn update(&mut self, candle: &Bound<'_, PyAny>) -> PyResult<Option<f64>> {
|
||||
Ok(self.inner.update(extract_candle(candle)?))
|
||||
}
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn batch<'py>(
|
||||
&mut self,
|
||||
py: Python<'py>,
|
||||
open: PyReadonlyArray1<'py, f64>,
|
||||
high: PyReadonlyArray1<'py, f64>,
|
||||
low: PyReadonlyArray1<'py, f64>,
|
||||
close: PyReadonlyArray1<'py, f64>,
|
||||
volume: PyReadonlyArray1<'py, f64>,
|
||||
timestamp: PyReadonlyArray1<'py, i64>,
|
||||
) -> PyResult<Bound<'py, PyArray1<f64>>> {
|
||||
let candles = build_seasonality_candles(&open, &high, &low, &close, &volume, ×tamp)?;
|
||||
let out: Vec<f64> = candles
|
||||
.into_iter()
|
||||
.map(|c| self.inner.update(c).unwrap_or(f64::NAN))
|
||||
.collect();
|
||||
Ok(out.into_pyarray(py))
|
||||
}
|
||||
#[getter]
|
||||
fn params(&self) -> (u32, u32, i32) {
|
||||
self.inner.params()
|
||||
}
|
||||
fn reset(&mut self) {
|
||||
self.inner.reset();
|
||||
}
|
||||
fn is_ready(&self) -> bool {
|
||||
self.inner.is_ready()
|
||||
}
|
||||
fn warmup_period(&self) -> usize {
|
||||
self.inner.warmup_period()
|
||||
}
|
||||
fn __repr__(&self) -> String {
|
||||
let (n_first, n_last, offset) = self.inner.params();
|
||||
format!("TurnOfMonth(n_first={n_first}, n_last={n_last}, utc_offset_minutes={offset})")
|
||||
}
|
||||
}
|
||||
|
||||
#[pyclass(
|
||||
name = "SessionHighLow",
|
||||
module = "wickra._wickra",
|
||||
skip_from_py_object
|
||||
)]
|
||||
#[derive(Clone)]
|
||||
struct PySessionHighLow {
|
||||
inner: wc::SessionHighLow,
|
||||
}
|
||||
#[pymethods]
|
||||
impl PySessionHighLow {
|
||||
#[new]
|
||||
#[pyo3(signature = (utc_offset_minutes = 0))]
|
||||
fn new(utc_offset_minutes: i32) -> Self {
|
||||
Self {
|
||||
inner: wc::SessionHighLow::new(utc_offset_minutes),
|
||||
}
|
||||
}
|
||||
fn update(&mut self, candle: &Bound<'_, PyAny>) -> PyResult<Option<(f64, f64)>> {
|
||||
let c = extract_candle(candle)?;
|
||||
Ok(self.inner.update(c).map(|o| (o.high, o.low)))
|
||||
}
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn batch<'py>(
|
||||
&mut self,
|
||||
py: Python<'py>,
|
||||
open: PyReadonlyArray1<'py, f64>,
|
||||
high: PyReadonlyArray1<'py, f64>,
|
||||
low: PyReadonlyArray1<'py, f64>,
|
||||
close: PyReadonlyArray1<'py, f64>,
|
||||
volume: PyReadonlyArray1<'py, f64>,
|
||||
timestamp: PyReadonlyArray1<'py, i64>,
|
||||
) -> PyResult<Bound<'py, PyArray2<f64>>> {
|
||||
let candles = build_seasonality_candles(&open, &high, &low, &close, &volume, ×tamp)?;
|
||||
let n = candles.len();
|
||||
let mut out = vec![f64::NAN; n * 2];
|
||||
for (i, c) in candles.into_iter().enumerate() {
|
||||
if let Some(o) = self.inner.update(c) {
|
||||
out[i * 2] = o.high;
|
||||
out[i * 2 + 1] = o.low;
|
||||
}
|
||||
}
|
||||
Ok(numpy::ndarray::Array2::from_shape_vec((n, 2), out)
|
||||
.expect("shape consistent")
|
||||
.into_pyarray(py))
|
||||
}
|
||||
#[getter]
|
||||
fn utc_offset_minutes(&self) -> i32 {
|
||||
self.inner.utc_offset_minutes()
|
||||
}
|
||||
fn reset(&mut self) {
|
||||
self.inner.reset();
|
||||
}
|
||||
fn is_ready(&self) -> bool {
|
||||
self.inner.is_ready()
|
||||
}
|
||||
fn warmup_period(&self) -> usize {
|
||||
self.inner.warmup_period()
|
||||
}
|
||||
fn __repr__(&self) -> String {
|
||||
format!(
|
||||
"SessionHighLow(utc_offset_minutes={})",
|
||||
self.inner.utc_offset_minutes()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[pyclass(name = "SessionRange", module = "wickra._wickra", skip_from_py_object)]
|
||||
#[derive(Clone)]
|
||||
struct PySessionRange {
|
||||
inner: wc::SessionRange,
|
||||
}
|
||||
#[pymethods]
|
||||
impl PySessionRange {
|
||||
#[new]
|
||||
#[pyo3(signature = (utc_offset_minutes = 0))]
|
||||
fn new(utc_offset_minutes: i32) -> Self {
|
||||
Self {
|
||||
inner: wc::SessionRange::new(utc_offset_minutes),
|
||||
}
|
||||
}
|
||||
fn update(&mut self, candle: &Bound<'_, PyAny>) -> PyResult<Option<(f64, f64, f64)>> {
|
||||
let c = extract_candle(candle)?;
|
||||
Ok(self.inner.update(c).map(|o| (o.asia, o.eu, o.us)))
|
||||
}
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn batch<'py>(
|
||||
&mut self,
|
||||
py: Python<'py>,
|
||||
open: PyReadonlyArray1<'py, f64>,
|
||||
high: PyReadonlyArray1<'py, f64>,
|
||||
low: PyReadonlyArray1<'py, f64>,
|
||||
close: PyReadonlyArray1<'py, f64>,
|
||||
volume: PyReadonlyArray1<'py, f64>,
|
||||
timestamp: PyReadonlyArray1<'py, i64>,
|
||||
) -> PyResult<Bound<'py, PyArray2<f64>>> {
|
||||
let candles = build_seasonality_candles(&open, &high, &low, &close, &volume, ×tamp)?;
|
||||
let n = candles.len();
|
||||
let mut out = vec![f64::NAN; n * 3];
|
||||
for (i, c) in candles.into_iter().enumerate() {
|
||||
if let Some(o) = self.inner.update(c) {
|
||||
out[i * 3] = o.asia;
|
||||
out[i * 3 + 1] = o.eu;
|
||||
out[i * 3 + 2] = o.us;
|
||||
}
|
||||
}
|
||||
Ok(numpy::ndarray::Array2::from_shape_vec((n, 3), out)
|
||||
.expect("shape consistent")
|
||||
.into_pyarray(py))
|
||||
}
|
||||
#[getter]
|
||||
fn utc_offset_minutes(&self) -> i32 {
|
||||
self.inner.utc_offset_minutes()
|
||||
}
|
||||
fn reset(&mut self) {
|
||||
self.inner.reset();
|
||||
}
|
||||
fn is_ready(&self) -> bool {
|
||||
self.inner.is_ready()
|
||||
}
|
||||
fn warmup_period(&self) -> usize {
|
||||
self.inner.warmup_period()
|
||||
}
|
||||
fn __repr__(&self) -> String {
|
||||
format!(
|
||||
"SessionRange(utc_offset_minutes={})",
|
||||
self.inner.utc_offset_minutes()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[pyclass(
|
||||
name = "OvernightIntradayReturn",
|
||||
module = "wickra._wickra",
|
||||
skip_from_py_object
|
||||
)]
|
||||
#[derive(Clone)]
|
||||
struct PyOvernightIntradayReturn {
|
||||
inner: wc::OvernightIntradayReturn,
|
||||
}
|
||||
#[pymethods]
|
||||
impl PyOvernightIntradayReturn {
|
||||
#[new]
|
||||
#[pyo3(signature = (utc_offset_minutes = 0))]
|
||||
fn new(utc_offset_minutes: i32) -> Self {
|
||||
Self {
|
||||
inner: wc::OvernightIntradayReturn::new(utc_offset_minutes),
|
||||
}
|
||||
}
|
||||
fn update(&mut self, candle: &Bound<'_, PyAny>) -> PyResult<Option<(f64, f64)>> {
|
||||
let c = extract_candle(candle)?;
|
||||
Ok(self.inner.update(c).map(|o| (o.overnight, o.intraday)))
|
||||
}
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn batch<'py>(
|
||||
&mut self,
|
||||
py: Python<'py>,
|
||||
open: PyReadonlyArray1<'py, f64>,
|
||||
high: PyReadonlyArray1<'py, f64>,
|
||||
low: PyReadonlyArray1<'py, f64>,
|
||||
close: PyReadonlyArray1<'py, f64>,
|
||||
volume: PyReadonlyArray1<'py, f64>,
|
||||
timestamp: PyReadonlyArray1<'py, i64>,
|
||||
) -> PyResult<Bound<'py, PyArray2<f64>>> {
|
||||
let candles = build_seasonality_candles(&open, &high, &low, &close, &volume, ×tamp)?;
|
||||
let n = candles.len();
|
||||
let mut out = vec![f64::NAN; n * 2];
|
||||
for (i, c) in candles.into_iter().enumerate() {
|
||||
if let Some(o) = self.inner.update(c) {
|
||||
out[i * 2] = o.overnight;
|
||||
out[i * 2 + 1] = o.intraday;
|
||||
}
|
||||
}
|
||||
Ok(numpy::ndarray::Array2::from_shape_vec((n, 2), out)
|
||||
.expect("shape consistent")
|
||||
.into_pyarray(py))
|
||||
}
|
||||
#[getter]
|
||||
fn utc_offset_minutes(&self) -> i32 {
|
||||
self.inner.utc_offset_minutes()
|
||||
}
|
||||
fn reset(&mut self) {
|
||||
self.inner.reset();
|
||||
}
|
||||
fn is_ready(&self) -> bool {
|
||||
self.inner.is_ready()
|
||||
}
|
||||
fn warmup_period(&self) -> usize {
|
||||
self.inner.warmup_period()
|
||||
}
|
||||
fn __repr__(&self) -> String {
|
||||
format!(
|
||||
"OvernightIntradayReturn(utc_offset_minutes={})",
|
||||
self.inner.utc_offset_minutes()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[pymodule]
|
||||
#[allow(clippy::too_many_lines)]
|
||||
fn _wickra(_py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> {
|
||||
@@ -17596,5 +18183,18 @@ fn _wickra(_py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> {
|
||||
m.add_class::<PyRocr100>()?;
|
||||
m.add_class::<PyLinRegIntercept>()?;
|
||||
m.add_class::<PyTsf>()?;
|
||||
// Family 16: Seasonality & Session.
|
||||
m.add_class::<PySessionVwap>()?;
|
||||
m.add_class::<PySessionHighLow>()?;
|
||||
m.add_class::<PySessionRange>()?;
|
||||
m.add_class::<PyAverageDailyRange>()?;
|
||||
m.add_class::<PyOvernightGap>()?;
|
||||
m.add_class::<PyOvernightIntradayReturn>()?;
|
||||
m.add_class::<PyTurnOfMonth>()?;
|
||||
m.add_class::<PySeasonalZScore>()?;
|
||||
m.add_class::<PyTimeOfDayReturnProfile>()?;
|
||||
m.add_class::<PyDayOfWeekProfile>()?;
|
||||
m.add_class::<PyIntradayVolatilityProfile>()?;
|
||||
m.add_class::<PyVolumeByTimeProfile>()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -0,0 +1,132 @@
|
||||
"""Streaming-vs-batch equivalence and reference values for the Seasonality &
|
||||
Session family.
|
||||
|
||||
These indicators read the full candle (including ``timestamp``), so they have a
|
||||
dedicated test rather than joining the timestamp-less parametrize harness in
|
||||
``test_new_indicators.py``.
|
||||
"""
|
||||
|
||||
import numpy as np
|
||||
import pytest
|
||||
|
||||
import wickra as ta
|
||||
|
||||
HOUR_MS = 3_600_000
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def candle_columns():
|
||||
"""240 hourly candles (10 days) with valid OHLCV and epoch-ms timestamps."""
|
||||
n = 240
|
||||
t = np.arange(n, dtype=np.float64)
|
||||
close = 100.0 + np.sin(t * 0.3) * 5.0 + np.cos(t * 0.1) * 3.0
|
||||
open_ = close + np.sin(t * 0.5) * 0.5
|
||||
high = np.maximum(open_, close) + 1.0
|
||||
low = np.minimum(open_, close) - 1.0
|
||||
volume = 1000.0 + (t % 24) * 50.0
|
||||
timestamp = (np.arange(n, dtype=np.int64)) * HOUR_MS
|
||||
return open_, high, low, close, volume, timestamp
|
||||
|
||||
|
||||
def _candles(cols):
|
||||
open_, high, low, close, volume, timestamp = cols
|
||||
return [
|
||||
(open_[i], high[i], low[i], close[i], volume[i], int(timestamp[i]))
|
||||
for i in range(len(close))
|
||||
]
|
||||
|
||||
|
||||
def _check_scalar(make, cols):
|
||||
candles = _candles(cols)
|
||||
a, b = make(), make()
|
||||
stream = np.array(
|
||||
[np.nan if (v := a.update(c)) is None else v for c in candles],
|
||||
dtype=np.float64,
|
||||
)
|
||||
batch = np.asarray(b.batch(*cols))
|
||||
np.testing.assert_allclose(stream, batch, equal_nan=True, rtol=1e-9, atol=1e-9)
|
||||
|
||||
|
||||
def _check_matrix(make, k, cols):
|
||||
candles = _candles(cols)
|
||||
a, b = make(), make()
|
||||
rows = []
|
||||
for c in candles:
|
||||
out = a.update(c)
|
||||
rows.append(np.full(k, np.nan) if out is None else np.asarray(out, dtype=float))
|
||||
stream = np.vstack(rows)
|
||||
batch = np.asarray(b.batch(*cols))
|
||||
assert batch.shape == (len(candles), k)
|
||||
np.testing.assert_allclose(stream, batch, equal_nan=True, rtol=1e-9, atol=1e-9)
|
||||
|
||||
|
||||
SCALAR = [
|
||||
lambda: ta.SessionVwap(0),
|
||||
lambda: ta.OvernightGap(0),
|
||||
lambda: ta.SeasonalZScore(0),
|
||||
lambda: ta.AverageDailyRange(3, 0),
|
||||
lambda: ta.TurnOfMonth(3, 1, 0),
|
||||
]
|
||||
|
||||
MATRIX = [
|
||||
(lambda: ta.SessionHighLow(0), 2),
|
||||
(lambda: ta.SessionRange(0), 3),
|
||||
(lambda: ta.OvernightIntradayReturn(0), 2),
|
||||
(lambda: ta.TimeOfDayReturnProfile(24, 0), 24),
|
||||
(lambda: ta.IntradayVolatilityProfile(12, 0), 12),
|
||||
(lambda: ta.VolumeByTimeProfile(24, 0), 24),
|
||||
(lambda: ta.DayOfWeekProfile(0), 7),
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("make", SCALAR)
|
||||
def test_scalar_streaming_equals_batch(make, candle_columns):
|
||||
_check_scalar(make, candle_columns)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("make,k", MATRIX)
|
||||
def test_matrix_streaming_equals_batch(make, k, candle_columns):
|
||||
_check_matrix(make, k, candle_columns)
|
||||
|
||||
|
||||
def test_session_vwap_reference():
|
||||
vwap = ta.SessionVwap(0)
|
||||
# typical = close for a flat candle; volume-weighted within the day.
|
||||
v1 = vwap.update((100.0, 100.0, 100.0, 100.0, 10.0, 0))
|
||||
assert v1 == pytest.approx(100.0)
|
||||
v2 = vwap.update((110.0, 110.0, 110.0, 110.0, 30.0, HOUR_MS))
|
||||
assert v2 == pytest.approx(107.5)
|
||||
# New day re-anchors.
|
||||
v3 = vwap.update((200.0, 200.0, 200.0, 200.0, 5.0, 24 * HOUR_MS))
|
||||
assert v3 == pytest.approx(200.0)
|
||||
|
||||
|
||||
def test_overnight_gap_reference():
|
||||
gap = ta.OvernightGap(0)
|
||||
assert gap.update((99.0, 101.0, 98.0, 100.0, 1.0, 0)) is None
|
||||
g = gap.update((105.0, 106.0, 104.0, 105.5, 1.0, 24 * HOUR_MS))
|
||||
assert g == pytest.approx(0.05)
|
||||
|
||||
|
||||
def test_session_high_low_reference():
|
||||
shl = ta.SessionHighLow(0)
|
||||
shl.update((100.0, 105.0, 99.0, 101.0, 1.0, 0))
|
||||
out = shl.update((101.0, 108.0, 100.0, 107.0, 1.0, HOUR_MS))
|
||||
assert out == (108.0, 99.0)
|
||||
|
||||
|
||||
def test_volume_by_time_profile_reference():
|
||||
prof = ta.VolumeByTimeProfile(24, 0)
|
||||
out = prof.update((100.0, 100.0, 100.0, 100.0, 500.0, HOUR_MS)) # 01:00 -> bucket 1
|
||||
assert out[1] == pytest.approx(500.0)
|
||||
assert out[0] == pytest.approx(0.0)
|
||||
|
||||
|
||||
def test_rejects_zero_buckets():
|
||||
with pytest.raises(ValueError):
|
||||
ta.TimeOfDayReturnProfile(0, 0)
|
||||
|
||||
|
||||
def test_average_daily_range_rejects_zero_period():
|
||||
with pytest.raises(ValueError):
|
||||
ta.AverageDailyRange(0, 0)
|
||||
@@ -10226,3 +10226,390 @@ impl WasmAlpha {
|
||||
self.inner.warmup_period()
|
||||
}
|
||||
}
|
||||
|
||||
// ====================== Seasonality & Session (full-candle) ======================
|
||||
//
|
||||
// These read the wall-clock fields of `Candle::timestamp`. JS passes `timestamp`
|
||||
// as a BigInt (epoch milliseconds). Following the multi-input precedent
|
||||
// (microstructure / derivatives), WASM exposes streaming `update` only — no
|
||||
// batch over ragged multi-arrays.
|
||||
|
||||
macro_rules! wasm_seasonality_offset_scalar {
|
||||
($wrapper:ident, $js:ident, $rust:ty) => {
|
||||
#[wasm_bindgen(js_name = $js)]
|
||||
pub struct $wrapper {
|
||||
inner: $rust,
|
||||
}
|
||||
#[wasm_bindgen(js_class = $js)]
|
||||
impl $wrapper {
|
||||
#[wasm_bindgen(constructor)]
|
||||
pub fn new(utc_offset_minutes: i32) -> $wrapper {
|
||||
Self {
|
||||
inner: <$rust>::new(utc_offset_minutes),
|
||||
}
|
||||
}
|
||||
pub fn update(
|
||||
&mut self,
|
||||
open: f64,
|
||||
high: f64,
|
||||
low: f64,
|
||||
close: f64,
|
||||
volume: f64,
|
||||
timestamp: i64,
|
||||
) -> Result<Option<f64>, JsError> {
|
||||
Ok(self.inner.update(
|
||||
wc::Candle::new(open, high, low, close, volume, timestamp).map_err(map_err)?,
|
||||
))
|
||||
}
|
||||
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 = utcOffsetMinutes)]
|
||||
pub fn utc_offset_minutes(&self) -> i32 {
|
||||
self.inner.utc_offset_minutes()
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! wasm_seasonality_bucket_profile {
|
||||
($wrapper:ident, $js:ident, $rust:ty) => {
|
||||
#[wasm_bindgen(js_name = $js)]
|
||||
pub struct $wrapper {
|
||||
inner: $rust,
|
||||
}
|
||||
#[wasm_bindgen(js_class = $js)]
|
||||
impl $wrapper {
|
||||
#[wasm_bindgen(constructor)]
|
||||
pub fn new(buckets: usize, utc_offset_minutes: i32) -> Result<$wrapper, JsError> {
|
||||
Ok(Self {
|
||||
inner: <$rust>::new(buckets, utc_offset_minutes).map_err(map_err)?,
|
||||
})
|
||||
}
|
||||
pub fn update(
|
||||
&mut self,
|
||||
open: f64,
|
||||
high: f64,
|
||||
low: f64,
|
||||
close: f64,
|
||||
volume: f64,
|
||||
timestamp: i64,
|
||||
) -> Result<JsValue, JsError> {
|
||||
let c =
|
||||
wc::Candle::new(open, high, low, close, volume, timestamp).map_err(map_err)?;
|
||||
Ok(match self.inner.update(c) {
|
||||
Some(o) => Float64Array::from(o.bins.as_slice()).into(),
|
||||
None => JsValue::NULL,
|
||||
})
|
||||
}
|
||||
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 = utcOffsetMinutes)]
|
||||
pub fn utc_offset_minutes(&self) -> i32 {
|
||||
self.inner.params().1
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! wasm_seasonality_offset_profile {
|
||||
($wrapper:ident, $js:ident, $rust:ty) => {
|
||||
#[wasm_bindgen(js_name = $js)]
|
||||
pub struct $wrapper {
|
||||
inner: $rust,
|
||||
}
|
||||
#[wasm_bindgen(js_class = $js)]
|
||||
impl $wrapper {
|
||||
#[wasm_bindgen(constructor)]
|
||||
pub fn new(utc_offset_minutes: i32) -> $wrapper {
|
||||
Self {
|
||||
inner: <$rust>::new(utc_offset_minutes),
|
||||
}
|
||||
}
|
||||
pub fn update(
|
||||
&mut self,
|
||||
open: f64,
|
||||
high: f64,
|
||||
low: f64,
|
||||
close: f64,
|
||||
volume: f64,
|
||||
timestamp: i64,
|
||||
) -> Result<JsValue, JsError> {
|
||||
let c =
|
||||
wc::Candle::new(open, high, low, close, volume, timestamp).map_err(map_err)?;
|
||||
Ok(match self.inner.update(c) {
|
||||
Some(o) => Float64Array::from(o.bins.as_slice()).into(),
|
||||
None => JsValue::NULL,
|
||||
})
|
||||
}
|
||||
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 = utcOffsetMinutes)]
|
||||
pub fn utc_offset_minutes(&self) -> i32 {
|
||||
self.inner.utc_offset_minutes()
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
wasm_seasonality_offset_scalar!(WasmSessionVwap, SessionVwap, wc::SessionVwap);
|
||||
wasm_seasonality_offset_scalar!(WasmOvernightGap, OvernightGap, wc::OvernightGap);
|
||||
wasm_seasonality_offset_scalar!(WasmSeasonalZScore, SeasonalZScore, wc::SeasonalZScore);
|
||||
wasm_seasonality_bucket_profile!(
|
||||
WasmTimeOfDayReturnProfile,
|
||||
TimeOfDayReturnProfile,
|
||||
wc::TimeOfDayReturnProfile
|
||||
);
|
||||
wasm_seasonality_bucket_profile!(
|
||||
WasmIntradayVolatilityProfile,
|
||||
IntradayVolatilityProfile,
|
||||
wc::IntradayVolatilityProfile
|
||||
);
|
||||
wasm_seasonality_bucket_profile!(
|
||||
WasmVolumeByTimeProfile,
|
||||
VolumeByTimeProfile,
|
||||
wc::VolumeByTimeProfile
|
||||
);
|
||||
wasm_seasonality_offset_profile!(WasmDayOfWeekProfile, DayOfWeekProfile, wc::DayOfWeekProfile);
|
||||
|
||||
#[wasm_bindgen(js_name = AverageDailyRange)]
|
||||
pub struct WasmAverageDailyRange {
|
||||
inner: wc::AverageDailyRange,
|
||||
}
|
||||
#[wasm_bindgen(js_class = AverageDailyRange)]
|
||||
impl WasmAverageDailyRange {
|
||||
#[wasm_bindgen(constructor)]
|
||||
pub fn new(period: usize, utc_offset_minutes: i32) -> Result<WasmAverageDailyRange, JsError> {
|
||||
Ok(Self {
|
||||
inner: wc::AverageDailyRange::new(period, utc_offset_minutes).map_err(map_err)?,
|
||||
})
|
||||
}
|
||||
pub fn update(
|
||||
&mut self,
|
||||
open: f64,
|
||||
high: f64,
|
||||
low: f64,
|
||||
close: f64,
|
||||
volume: f64,
|
||||
timestamp: i64,
|
||||
) -> Result<Option<f64>, JsError> {
|
||||
Ok(self
|
||||
.inner
|
||||
.update(wc::Candle::new(open, high, low, close, volume, timestamp).map_err(map_err)?))
|
||||
}
|
||||
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 = TurnOfMonth)]
|
||||
pub struct WasmTurnOfMonth {
|
||||
inner: wc::TurnOfMonth,
|
||||
}
|
||||
#[wasm_bindgen(js_class = TurnOfMonth)]
|
||||
impl WasmTurnOfMonth {
|
||||
#[wasm_bindgen(constructor)]
|
||||
pub fn new(
|
||||
n_first: u32,
|
||||
n_last: u32,
|
||||
utc_offset_minutes: i32,
|
||||
) -> Result<WasmTurnOfMonth, JsError> {
|
||||
Ok(Self {
|
||||
inner: wc::TurnOfMonth::new(n_first, n_last, utc_offset_minutes).map_err(map_err)?,
|
||||
})
|
||||
}
|
||||
pub fn update(
|
||||
&mut self,
|
||||
open: f64,
|
||||
high: f64,
|
||||
low: f64,
|
||||
close: f64,
|
||||
volume: f64,
|
||||
timestamp: i64,
|
||||
) -> Result<Option<f64>, JsError> {
|
||||
Ok(self
|
||||
.inner
|
||||
.update(wc::Candle::new(open, high, low, close, volume, timestamp).map_err(map_err)?))
|
||||
}
|
||||
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 = SessionHighLow)]
|
||||
pub struct WasmSessionHighLow {
|
||||
inner: wc::SessionHighLow,
|
||||
}
|
||||
#[wasm_bindgen(js_class = SessionHighLow)]
|
||||
impl WasmSessionHighLow {
|
||||
#[wasm_bindgen(constructor)]
|
||||
pub fn new(utc_offset_minutes: i32) -> WasmSessionHighLow {
|
||||
Self {
|
||||
inner: wc::SessionHighLow::new(utc_offset_minutes),
|
||||
}
|
||||
}
|
||||
pub fn update(
|
||||
&mut self,
|
||||
open: f64,
|
||||
high: f64,
|
||||
low: f64,
|
||||
close: f64,
|
||||
volume: f64,
|
||||
timestamp: i64,
|
||||
) -> Result<JsValue, JsError> {
|
||||
let c = wc::Candle::new(open, high, low, close, volume, timestamp).map_err(map_err)?;
|
||||
Ok(match self.inner.update(c) {
|
||||
Some(o) => {
|
||||
let obj = Object::new();
|
||||
Reflect::set(&obj, &"high".into(), &o.high.into()).ok();
|
||||
Reflect::set(&obj, &"low".into(), &o.low.into()).ok();
|
||||
obj.into()
|
||||
}
|
||||
None => JsValue::NULL,
|
||||
})
|
||||
}
|
||||
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 = SessionRange)]
|
||||
pub struct WasmSessionRange {
|
||||
inner: wc::SessionRange,
|
||||
}
|
||||
#[wasm_bindgen(js_class = SessionRange)]
|
||||
impl WasmSessionRange {
|
||||
#[wasm_bindgen(constructor)]
|
||||
pub fn new(utc_offset_minutes: i32) -> WasmSessionRange {
|
||||
Self {
|
||||
inner: wc::SessionRange::new(utc_offset_minutes),
|
||||
}
|
||||
}
|
||||
pub fn update(
|
||||
&mut self,
|
||||
open: f64,
|
||||
high: f64,
|
||||
low: f64,
|
||||
close: f64,
|
||||
volume: f64,
|
||||
timestamp: i64,
|
||||
) -> Result<JsValue, JsError> {
|
||||
let c = wc::Candle::new(open, high, low, close, volume, timestamp).map_err(map_err)?;
|
||||
Ok(match self.inner.update(c) {
|
||||
Some(o) => {
|
||||
let obj = Object::new();
|
||||
Reflect::set(&obj, &"asia".into(), &o.asia.into()).ok();
|
||||
Reflect::set(&obj, &"eu".into(), &o.eu.into()).ok();
|
||||
Reflect::set(&obj, &"us".into(), &o.us.into()).ok();
|
||||
obj.into()
|
||||
}
|
||||
None => JsValue::NULL,
|
||||
})
|
||||
}
|
||||
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 = OvernightIntradayReturn)]
|
||||
pub struct WasmOvernightIntradayReturn {
|
||||
inner: wc::OvernightIntradayReturn,
|
||||
}
|
||||
#[wasm_bindgen(js_class = OvernightIntradayReturn)]
|
||||
impl WasmOvernightIntradayReturn {
|
||||
#[wasm_bindgen(constructor)]
|
||||
pub fn new(utc_offset_minutes: i32) -> WasmOvernightIntradayReturn {
|
||||
Self {
|
||||
inner: wc::OvernightIntradayReturn::new(utc_offset_minutes),
|
||||
}
|
||||
}
|
||||
pub fn update(
|
||||
&mut self,
|
||||
open: f64,
|
||||
high: f64,
|
||||
low: f64,
|
||||
close: f64,
|
||||
volume: f64,
|
||||
timestamp: i64,
|
||||
) -> Result<JsValue, JsError> {
|
||||
let c = wc::Candle::new(open, high, low, close, volume, timestamp).map_err(map_err)?;
|
||||
Ok(match self.inner.update(c) {
|
||||
Some(o) => {
|
||||
let obj = Object::new();
|
||||
Reflect::set(&obj, &"overnight".into(), &o.overnight.into()).ok();
|
||||
Reflect::set(&obj, &"intraday".into(), &o.intraday.into()).ok();
|
||||
obj.into()
|
||||
}
|
||||
None => JsValue::NULL,
|
||||
})
|
||||
}
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,203 @@
|
||||
//! Pure calendar arithmetic for the timestamp-driven seasonality indicators.
|
||||
//!
|
||||
//! Every indicator in the *Seasonality & Session* family keys off the wall-clock
|
||||
//! fields of [`Candle::timestamp`](crate::Candle) (epoch milliseconds), shifted
|
||||
//! by a caller-supplied `utc_offset_minutes` so the buckets line up with the
|
||||
//! relevant exchange session rather than UTC. This module turns an epoch
|
||||
//! millisecond instant into its civil fields using Howard Hinnant's
|
||||
//! branch-light `civil_from_days` algorithm (the same one libc++ ships).
|
||||
//!
|
||||
//! All arithmetic is floor-based (`div_euclid`/`rem_euclid`) so instants before
|
||||
//! the Unix epoch decompose correctly without a dedicated negative-input branch.
|
||||
|
||||
/// Civil (wall-clock) decomposition of an epoch-millisecond instant.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub(crate) struct CivilTime {
|
||||
/// Proleptic Gregorian year (can be negative for instants before year 1).
|
||||
pub(crate) year: i64,
|
||||
/// Month of year, `1..=12`.
|
||||
pub(crate) month: u32,
|
||||
/// Day of month, `1..=31`.
|
||||
pub(crate) day: u32,
|
||||
/// Hour of day, `0..=23`.
|
||||
pub(crate) hour: u32,
|
||||
/// Minute of hour, `0..=59`.
|
||||
pub(crate) minute: u32,
|
||||
/// Day of week with Monday as `0` through Sunday as `6`.
|
||||
pub(crate) weekday: u32,
|
||||
}
|
||||
|
||||
impl CivilTime {
|
||||
/// Minute of day, `0..=1439`.
|
||||
pub(crate) const fn minute_of_day(&self) -> u32 {
|
||||
self.hour * 60 + self.minute
|
||||
}
|
||||
}
|
||||
|
||||
/// Decompose an epoch-millisecond instant into local civil fields.
|
||||
///
|
||||
/// `utc_offset_minutes` shifts the instant before decomposition: `0` yields
|
||||
/// UTC, `-300` U.S. Eastern standard time, `60` Central European time, etc.
|
||||
pub(crate) fn civil_from_timestamp(millis: i64, utc_offset_minutes: i32) -> CivilTime {
|
||||
let local_secs = millis.div_euclid(1000) + i64::from(utc_offset_minutes) * 60;
|
||||
let days = local_secs.div_euclid(86_400);
|
||||
let secs_of_day = local_secs.rem_euclid(86_400);
|
||||
let hour = (secs_of_day / 3600) as u32;
|
||||
let minute = ((secs_of_day % 3600) / 60) as u32;
|
||||
let (year, month, day) = civil_from_days(days);
|
||||
// 1970-01-01 was a Thursday; Monday-based weekday is `(z + 3) mod 7`.
|
||||
let weekday = (days + 3).rem_euclid(7) as u32;
|
||||
CivilTime {
|
||||
year,
|
||||
month,
|
||||
day,
|
||||
hour,
|
||||
minute,
|
||||
weekday,
|
||||
}
|
||||
}
|
||||
|
||||
/// Gregorian `(year, month, day)` for a day count `z` relative to 1970-01-01.
|
||||
///
|
||||
/// Howard Hinnant, "chrono-Compatible Low-Level Date Algorithms".
|
||||
fn civil_from_days(z: i64) -> (i64, u32, u32) {
|
||||
let z = z + 719_468;
|
||||
let era = if z >= 0 { z } else { z - 146_096 } / 146_097;
|
||||
let doe = z - era * 146_097; // [0, 146096]
|
||||
let yoe = (doe - doe / 1460 + doe / 36_524 - doe / 146_096) / 365; // [0, 399]
|
||||
let year = yoe + era * 400;
|
||||
let doy = doe - (365 * yoe + yoe / 4 - yoe / 100); // [0, 365]
|
||||
let mp = (5 * doy + 2) / 153; // [0, 11]
|
||||
let day = (doy - (153 * mp + 2) / 5 + 1) as u32; // [1, 31]
|
||||
let month = if mp < 10 { mp + 3 } else { mp - 9 } as u32; // [1, 12]
|
||||
(if month <= 2 { year + 1 } else { year }, month, day)
|
||||
}
|
||||
|
||||
/// Whether `year` is a Gregorian leap year.
|
||||
pub(crate) const fn is_leap(year: i64) -> bool {
|
||||
(year % 4 == 0 && year % 100 != 0) || year % 400 == 0
|
||||
}
|
||||
|
||||
/// Number of days in `month` (`1..=12`) of `year`.
|
||||
pub(crate) const fn days_in_month(year: i64, month: u32) -> u32 {
|
||||
match month {
|
||||
1 | 3 | 5 | 7 | 8 | 10 | 12 => 31,
|
||||
4 | 6 | 9 | 11 => 30,
|
||||
_ => {
|
||||
if is_leap(year) {
|
||||
29
|
||||
} else {
|
||||
28
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn epoch_zero_is_thursday_midnight() {
|
||||
let t = civil_from_timestamp(0, 0);
|
||||
assert_eq!(
|
||||
t,
|
||||
CivilTime {
|
||||
year: 1970,
|
||||
month: 1,
|
||||
day: 1,
|
||||
hour: 0,
|
||||
minute: 0,
|
||||
weekday: 3, // Thursday
|
||||
}
|
||||
);
|
||||
assert_eq!(t.minute_of_day(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn known_utc_instant_mid_year() {
|
||||
// 2021-06-15 13:45:00 UTC = 1623764700 s.
|
||||
let t = civil_from_timestamp(1_623_764_700_000, 0);
|
||||
assert_eq!(t.year, 2021);
|
||||
assert_eq!(t.month, 6);
|
||||
assert_eq!(t.day, 15);
|
||||
assert_eq!(t.hour, 13);
|
||||
assert_eq!(t.minute, 45);
|
||||
assert_eq!(t.weekday, 1); // Tuesday
|
||||
assert_eq!(t.minute_of_day(), 13 * 60 + 45);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn new_year_2021_is_friday() {
|
||||
// 2021-01-01 00:00:00 UTC = 1609459200 s — exercises the m<=2 year bump.
|
||||
let t = civil_from_timestamp(1_609_459_200_000, 0);
|
||||
assert_eq!((t.year, t.month, t.day), (2021, 1, 1));
|
||||
assert_eq!(t.weekday, 4); // Friday
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn positive_offset_rolls_to_next_day() {
|
||||
// 2021-01-01 23:30 UTC shifted +60 min -> 2021-01-02 00:30 local.
|
||||
let base = 1_609_459_200_000 + (23 * 3600 + 30 * 60) * 1000;
|
||||
let t = civil_from_timestamp(base, 60);
|
||||
assert_eq!((t.year, t.month, t.day), (2021, 1, 2));
|
||||
assert_eq!((t.hour, t.minute), (0, 30));
|
||||
assert_eq!(t.weekday, 5); // Saturday
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn negative_offset_rolls_to_previous_day() {
|
||||
// 2021-01-01 00:30 UTC shifted -60 min -> 2020-12-31 23:30 local.
|
||||
let base = 1_609_459_200_000 + 30 * 60 * 1000;
|
||||
let t = civil_from_timestamp(base, -60);
|
||||
assert_eq!((t.year, t.month, t.day), (2020, 12, 31));
|
||||
assert_eq!((t.hour, t.minute), (23, 30));
|
||||
assert_eq!(t.weekday, 3); // Thursday
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sub_epoch_millis_floor_correctly() {
|
||||
// -1 ms -> 1969-12-31 23:59:59.999, a Wednesday.
|
||||
let t = civil_from_timestamp(-1, 0);
|
||||
assert_eq!((t.year, t.month, t.day), (1969, 12, 31));
|
||||
assert_eq!((t.hour, t.minute), (23, 59));
|
||||
assert_eq!(t.weekday, 2); // Wednesday
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn far_negative_day_count_hits_pre_era_branch() {
|
||||
// A day count below -719468 drives `z + 719468` negative, exercising the
|
||||
// `z - 146096` era branch in civil_from_days (year < 1).
|
||||
let (year, month, day) = civil_from_days(-1_000_000);
|
||||
// -1_000_000 days before 1970-01-01 is 0768-02-04 BCE (proleptic
|
||||
// Gregorian, astronomical year numbering where year 0 exists).
|
||||
assert_eq!((year, month, day), (-768, 2, 4));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn leap_year_rules() {
|
||||
assert!(is_leap(2000));
|
||||
assert!(!is_leap(1900));
|
||||
assert!(is_leap(2024));
|
||||
assert!(!is_leap(2023));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn days_in_month_all_cases() {
|
||||
assert_eq!(days_in_month(2023, 1), 31);
|
||||
assert_eq!(days_in_month(2023, 4), 30);
|
||||
assert_eq!(days_in_month(2023, 2), 28);
|
||||
assert_eq!(days_in_month(2024, 2), 29);
|
||||
assert_eq!(days_in_month(2023, 12), 31);
|
||||
assert_eq!(days_in_month(2023, 11), 30);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn leap_day_decodes() {
|
||||
// 2024-02-29 12:00 UTC.
|
||||
let secs = 1_709_208_000; // 2024-02-29T12:00:00Z
|
||||
let t = civil_from_timestamp(secs * 1000, 0);
|
||||
assert_eq!((t.year, t.month, t.day), (2024, 2, 29));
|
||||
assert_eq!(t.hour, 12);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,231 @@
|
||||
//! Average Daily Range (ADR) — the mean high-minus-low range of the last `period`
|
||||
//! completed calendar-day sessions.
|
||||
|
||||
use std::collections::VecDeque;
|
||||
|
||||
use crate::calendar::civil_from_timestamp;
|
||||
use crate::error::{Error, Result};
|
||||
use crate::ohlcv::Candle;
|
||||
use crate::traits::Indicator;
|
||||
|
||||
/// Average Daily Range over the last `period` completed sessions.
|
||||
///
|
||||
/// The indicator tracks the running high / low of the current session (the
|
||||
/// wall-clock day of [`Candle::timestamp`](crate::Candle) shifted by
|
||||
/// `utc_offset_minutes`). When a new day begins, the just-finished session's
|
||||
/// range (`high - low`) joins a rolling window of the last `period` completed
|
||||
/// days, and the reported value is their mean. The current, still-forming day is
|
||||
/// excluded until it closes. No value is produced until the first session
|
||||
/// completes.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// use wickra_core::{Candle, Indicator, AverageDailyRange};
|
||||
///
|
||||
/// let hour = 3_600_000;
|
||||
/// let mut adr = AverageDailyRange::new(2, 0).unwrap();
|
||||
/// // Day 1 range 10 (high 110, low 100) — still forming, so None.
|
||||
/// assert!(adr.update(Candle::new(105.0, 110.0, 100.0, 108.0, 1.0, 0).unwrap()).is_none());
|
||||
/// // First bar of day 2 closes day 1: ADR = 10.
|
||||
/// let v = adr.update(Candle::new(108.0, 112.0, 106.0, 109.0, 1.0, 24 * hour).unwrap()).unwrap();
|
||||
/// assert!((v - 10.0).abs() < 1e-9);
|
||||
/// ```
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct AverageDailyRange {
|
||||
period: usize,
|
||||
utc_offset_minutes: i32,
|
||||
day_key: Option<(i64, u32, u32)>,
|
||||
cur_high: f64,
|
||||
cur_low: f64,
|
||||
completed: VecDeque<f64>,
|
||||
sum: f64,
|
||||
}
|
||||
|
||||
impl AverageDailyRange {
|
||||
/// Construct an ADR indicator over `period` completed days.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns [`Error::PeriodZero`] if `period == 0`.
|
||||
pub fn new(period: usize, utc_offset_minutes: i32) -> Result<Self> {
|
||||
if period == 0 {
|
||||
return Err(Error::PeriodZero);
|
||||
}
|
||||
Ok(Self {
|
||||
period,
|
||||
utc_offset_minutes,
|
||||
day_key: None,
|
||||
cur_high: f64::NEG_INFINITY,
|
||||
cur_low: f64::INFINITY,
|
||||
completed: VecDeque::with_capacity(period),
|
||||
sum: 0.0,
|
||||
})
|
||||
}
|
||||
|
||||
/// Configured `(period, utc_offset_minutes)`.
|
||||
pub const fn params(&self) -> (usize, i32) {
|
||||
(self.period, self.utc_offset_minutes)
|
||||
}
|
||||
|
||||
/// Most recent ADR if at least one session has completed.
|
||||
pub fn value(&self) -> Option<f64> {
|
||||
if self.completed.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(self.sum / self.completed.len() as f64)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Indicator for AverageDailyRange {
|
||||
type Input = Candle;
|
||||
type Output = f64;
|
||||
|
||||
fn update(&mut self, candle: Candle) -> Option<f64> {
|
||||
let civil = civil_from_timestamp(candle.timestamp, self.utc_offset_minutes);
|
||||
let key = (civil.year, civil.month, civil.day);
|
||||
match self.day_key {
|
||||
Some(prev) if prev == key => {
|
||||
if candle.high > self.cur_high {
|
||||
self.cur_high = candle.high;
|
||||
}
|
||||
if candle.low < self.cur_low {
|
||||
self.cur_low = candle.low;
|
||||
}
|
||||
}
|
||||
Some(_) => {
|
||||
let range = self.cur_high - self.cur_low;
|
||||
self.completed.push_back(range);
|
||||
self.sum += range;
|
||||
if self.completed.len() > self.period {
|
||||
self.sum -= self
|
||||
.completed
|
||||
.pop_front()
|
||||
.expect("len > period implies a front element");
|
||||
}
|
||||
self.day_key = Some(key);
|
||||
self.cur_high = candle.high;
|
||||
self.cur_low = candle.low;
|
||||
}
|
||||
None => {
|
||||
self.day_key = Some(key);
|
||||
self.cur_high = candle.high;
|
||||
self.cur_low = candle.low;
|
||||
}
|
||||
}
|
||||
self.value()
|
||||
}
|
||||
|
||||
fn reset(&mut self) {
|
||||
self.day_key = None;
|
||||
self.cur_high = f64::NEG_INFINITY;
|
||||
self.cur_low = f64::INFINITY;
|
||||
self.completed.clear();
|
||||
self.sum = 0.0;
|
||||
}
|
||||
|
||||
fn warmup_period(&self) -> usize {
|
||||
self.period
|
||||
}
|
||||
|
||||
fn is_ready(&self) -> bool {
|
||||
!self.completed.is_empty()
|
||||
}
|
||||
|
||||
fn name(&self) -> &'static str {
|
||||
"AverageDailyRange"
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::traits::BatchExt;
|
||||
use approx::assert_relative_eq;
|
||||
|
||||
const HOUR: i64 = 3_600_000;
|
||||
const DAY: i64 = 24 * HOUR;
|
||||
|
||||
fn c(high: f64, low: f64, ts: i64) -> Candle {
|
||||
let mid = f64::midpoint(high, low);
|
||||
Candle::new(mid, high, low, mid, 1.0, ts).unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_zero_period() {
|
||||
assert!(matches!(
|
||||
AverageDailyRange::new(0, 0),
|
||||
Err(Error::PeriodZero)
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn metadata_and_accessors() {
|
||||
let adr = AverageDailyRange::new(5, -60).unwrap();
|
||||
assert_eq!(adr.params(), (5, -60));
|
||||
assert_eq!(adr.name(), "AverageDailyRange");
|
||||
assert_eq!(adr.warmup_period(), 5);
|
||||
assert!(!adr.is_ready());
|
||||
assert!(adr.value().is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn averages_completed_day_ranges() {
|
||||
let mut adr = AverageDailyRange::new(3, 0).unwrap();
|
||||
// Day 1: range 10.
|
||||
assert!(adr.update(c(110.0, 100.0, 0)).is_none());
|
||||
assert!(adr.update(c(108.0, 104.0, HOUR)).is_none());
|
||||
// Day 2 opens -> day 1 (range 10) completes.
|
||||
let v = adr.update(c(120.0, 110.0, DAY)).unwrap();
|
||||
assert_relative_eq!(v, 10.0);
|
||||
assert!(adr.is_ready());
|
||||
// Day 3 opens -> day 2 (range 10) completes: mean of [10, 10] = 10.
|
||||
let v = adr.update(c(130.0, 100.0, 2 * DAY)).unwrap();
|
||||
assert_relative_eq!(v, 10.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rolls_off_oldest_day_beyond_period() {
|
||||
let mut adr = AverageDailyRange::new(2, 0).unwrap();
|
||||
adr.update(c(110.0, 100.0, 0)); // day 1 range 10
|
||||
let v = adr.update(c(125.0, 110.0, DAY)).unwrap(); // close day 1 -> [10]
|
||||
assert_relative_eq!(v, 10.0);
|
||||
// Close day 2 (range 125-110=15) -> window [10, 15], mean 12.5.
|
||||
let v = adr.update(c(130.0, 110.0, 2 * DAY)).unwrap();
|
||||
assert_relative_eq!(v, 12.5);
|
||||
// Close day 3 (range 130-110=20) -> window [15, 20], oldest (10) rolled off.
|
||||
let v = adr.update(c(140.0, 138.0, 3 * DAY)).unwrap();
|
||||
assert_relative_eq!(v, 17.5);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reset_clears_state() {
|
||||
let mut adr = AverageDailyRange::new(2, 0).unwrap();
|
||||
adr.update(c(110.0, 100.0, 0));
|
||||
adr.update(c(120.0, 110.0, DAY));
|
||||
adr.reset();
|
||||
assert!(!adr.is_ready());
|
||||
assert!(adr.value().is_none());
|
||||
assert!(adr.update(c(50.0, 40.0, 2 * DAY)).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn batch_equals_streaming() {
|
||||
let candles: Vec<Candle> = (0..60)
|
||||
.map(|i| {
|
||||
c(
|
||||
110.0 + f64::from(i % 5),
|
||||
100.0 - f64::from(i % 3),
|
||||
i64::from(i) * 6 * HOUR,
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
let mut a = AverageDailyRange::new(4, 0).unwrap();
|
||||
let mut b = AverageDailyRange::new(4, 0).unwrap();
|
||||
assert_eq!(
|
||||
a.batch(&candles),
|
||||
candles.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,202 @@
|
||||
//! Day-of-Week Profile — the mean bar return for each weekday.
|
||||
|
||||
use crate::calendar::civil_from_timestamp;
|
||||
use crate::ohlcv::Candle;
|
||||
use crate::traits::Indicator;
|
||||
|
||||
const DAYS: usize = 7;
|
||||
|
||||
/// Day-of-Week Profile output: the per-weekday mean return.
|
||||
///
|
||||
/// `bins[i]` is the mean simple return of all bars whose local weekday was `i`,
|
||||
/// with Monday as `0` through Sunday as `6`. Weekdays with no bars read `0.0`.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct DayOfWeekProfileOutput {
|
||||
/// Per-weekday mean return, Monday first. Always length 7.
|
||||
pub bins: Vec<f64>,
|
||||
}
|
||||
|
||||
/// Mean bar return bucketed by local weekday (Monday `0` .. Sunday `6`).
|
||||
///
|
||||
/// Each bar's simple return `close / previous_close - 1` is accumulated into the
|
||||
/// bucket of its local weekday (the wall-clock day of
|
||||
/// [`Candle::timestamp`](crate::Candle) shifted by `utc_offset_minutes`), and the
|
||||
/// profile reports the running mean per weekday. The first bar produces no output.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// use wickra_core::{Candle, Indicator, DayOfWeekProfile};
|
||||
///
|
||||
/// let day = 24 * 3_600_000;
|
||||
/// let mut prof = DayOfWeekProfile::new(0);
|
||||
/// // 1970-01-01 was a Thursday (weekday 3).
|
||||
/// assert!(prof.update(Candle::new(100.0, 100.0, 100.0, 100.0, 1.0, 0).unwrap()).is_none());
|
||||
/// let out = prof.update(Candle::new(101.0, 101.0, 101.0, 101.0, 1.0, day).unwrap()).unwrap();
|
||||
/// assert_eq!(out.bins.len(), 7);
|
||||
/// ```
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct DayOfWeekProfile {
|
||||
utc_offset_minutes: i32,
|
||||
prev_close: Option<f64>,
|
||||
sum: [f64; DAYS],
|
||||
count: [u64; DAYS],
|
||||
last: Option<DayOfWeekProfileOutput>,
|
||||
}
|
||||
|
||||
impl DayOfWeekProfile {
|
||||
/// Construct a Day-of-Week Profile with the given UTC offset (minutes).
|
||||
pub const fn new(utc_offset_minutes: i32) -> Self {
|
||||
Self {
|
||||
utc_offset_minutes,
|
||||
prev_close: None,
|
||||
sum: [0.0; DAYS],
|
||||
count: [0; DAYS],
|
||||
last: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Configured UTC offset in minutes.
|
||||
pub const fn utc_offset_minutes(&self) -> i32 {
|
||||
self.utc_offset_minutes
|
||||
}
|
||||
|
||||
/// Most recent profile if at least one return has been recorded.
|
||||
pub fn value(&self) -> Option<&DayOfWeekProfileOutput> {
|
||||
self.last.as_ref()
|
||||
}
|
||||
|
||||
fn snapshot(&self) -> DayOfWeekProfileOutput {
|
||||
let bins = self
|
||||
.sum
|
||||
.iter()
|
||||
.zip(&self.count)
|
||||
.map(|(total, n)| if *n > 0 { total / *n as f64 } else { 0.0 })
|
||||
.collect();
|
||||
DayOfWeekProfileOutput { bins }
|
||||
}
|
||||
}
|
||||
|
||||
impl Indicator for DayOfWeekProfile {
|
||||
type Input = Candle;
|
||||
type Output = DayOfWeekProfileOutput;
|
||||
|
||||
fn update(&mut self, candle: Candle) -> Option<DayOfWeekProfileOutput> {
|
||||
let civil = civil_from_timestamp(candle.timestamp, self.utc_offset_minutes);
|
||||
let result = if let Some(prev) = self.prev_close {
|
||||
let ret = if prev == 0.0 {
|
||||
0.0
|
||||
} else {
|
||||
candle.close / prev - 1.0
|
||||
};
|
||||
let day = civil.weekday as usize;
|
||||
self.sum[day] += ret;
|
||||
self.count[day] += 1;
|
||||
let out = self.snapshot();
|
||||
self.last = Some(out.clone());
|
||||
Some(out)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
self.prev_close = Some(candle.close);
|
||||
result
|
||||
}
|
||||
|
||||
fn reset(&mut self) {
|
||||
self.prev_close = None;
|
||||
self.sum = [0.0; DAYS];
|
||||
self.count = [0; DAYS];
|
||||
self.last = None;
|
||||
}
|
||||
|
||||
fn warmup_period(&self) -> usize {
|
||||
2
|
||||
}
|
||||
|
||||
fn is_ready(&self) -> bool {
|
||||
self.last.is_some()
|
||||
}
|
||||
|
||||
fn name(&self) -> &'static str {
|
||||
"DayOfWeekProfile"
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::traits::BatchExt;
|
||||
use approx::assert_relative_eq;
|
||||
|
||||
const DAY: i64 = 24 * 3_600_000;
|
||||
|
||||
fn c(close: f64, ts: i64) -> Candle {
|
||||
Candle::new(close, close, close, close, 1.0, ts).unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn metadata_and_accessors() {
|
||||
let prof = DayOfWeekProfile::new(60);
|
||||
assert_eq!(prof.utc_offset_minutes(), 60);
|
||||
assert_eq!(prof.name(), "DayOfWeekProfile");
|
||||
assert_eq!(prof.warmup_period(), 2);
|
||||
assert!(!prof.is_ready());
|
||||
assert!(prof.value().is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn buckets_by_weekday() {
|
||||
let mut prof = DayOfWeekProfile::new(0);
|
||||
// 1970-01-01 Thursday (3); 01-02 Friday (4).
|
||||
assert!(prof.update(c(100.0, 0)).is_none());
|
||||
let out = prof.update(c(101.0, DAY)).unwrap(); // Friday return +0.01
|
||||
assert_eq!(out.bins.len(), 7);
|
||||
assert_relative_eq!(out.bins[4], 0.01); // Friday
|
||||
assert_relative_eq!(out.bins[3], 0.0); // Thursday had no return
|
||||
assert!(prof.is_ready());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn averages_same_weekday_across_weeks() {
|
||||
let mut prof = DayOfWeekProfile::new(0);
|
||||
prof.update(c(100.0, 0)); // Thu
|
||||
prof.update(c(101.0, DAY)); // Fri +0.01
|
||||
// Jump to next Friday (7 days later from day 0 -> +7 days, weekday 4).
|
||||
prof.update(c(100.0, 7 * DAY)); // Thu+? actually day 7 -> weekday (7+3)%7=3 Thu
|
||||
let out = prof.update(c(103.0, 8 * DAY)).unwrap(); // day 8 -> Fri, return
|
||||
// Friday now has two samples; both positive.
|
||||
assert!(out.bins[4] > 0.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn zero_prev_close_uses_zero_return() {
|
||||
let mut prof = DayOfWeekProfile::new(0);
|
||||
prof.update(c(0.0, 0));
|
||||
let out = prof.update(c(5.0, DAY)).unwrap();
|
||||
assert_relative_eq!(out.bins[4], 0.0); // Friday, guarded return 0
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reset_clears_state() {
|
||||
let mut prof = DayOfWeekProfile::new(0);
|
||||
prof.update(c(100.0, 0));
|
||||
prof.update(c(101.0, DAY));
|
||||
prof.reset();
|
||||
assert!(!prof.is_ready());
|
||||
assert!(prof.value().is_none());
|
||||
assert!(prof.update(c(100.0, 2 * DAY)).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn batch_equals_streaming() {
|
||||
let candles: Vec<Candle> = (0..30)
|
||||
.map(|i| c(100.0 + f64::from(i % 5), i64::from(i) * DAY))
|
||||
.collect();
|
||||
let mut a = DayOfWeekProfile::new(0);
|
||||
let mut b = DayOfWeekProfile::new(0);
|
||||
assert_eq!(
|
||||
a.batch(&candles),
|
||||
candles.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,236 @@
|
||||
//! Intraday Volatility Profile — the return volatility in each intraday bucket.
|
||||
|
||||
use crate::calendar::civil_from_timestamp;
|
||||
use crate::error::{Error, Result};
|
||||
use crate::ohlcv::Candle;
|
||||
use crate::traits::Indicator;
|
||||
|
||||
/// Intraday Volatility Profile output: the per-bucket return standard deviation.
|
||||
///
|
||||
/// `bins[i]` is the sample standard deviation of the simple returns of all bars
|
||||
/// whose local time-of-day fell in bucket `i`. Buckets with fewer than two
|
||||
/// samples read `0.0`.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct IntradayVolatilityProfileOutput {
|
||||
/// Per-bucket return standard deviation, earliest bucket first.
|
||||
pub bins: Vec<f64>,
|
||||
}
|
||||
|
||||
/// Return volatility bucketed by local time of day.
|
||||
///
|
||||
/// The local day (the wall-clock day of [`Candle::timestamp`](crate::Candle)
|
||||
/// shifted by `utc_offset_minutes`) is split into `buckets` equal slices. Each
|
||||
/// bar's simple return `close / previous_close - 1` updates the per-bucket
|
||||
/// running variance (Welford), and the profile reports the per-bucket sample
|
||||
/// standard deviation. The first bar produces no output.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// use wickra_core::{Candle, Indicator, IntradayVolatilityProfile};
|
||||
///
|
||||
/// let hour = 3_600_000;
|
||||
/// let mut prof = IntradayVolatilityProfile::new(24, 0).unwrap();
|
||||
/// assert!(prof.update(Candle::new(100.0, 100.0, 100.0, 100.0, 1.0, 0).unwrap()).is_none());
|
||||
/// let out = prof.update(Candle::new(101.0, 101.0, 101.0, 101.0, 1.0, hour).unwrap()).unwrap();
|
||||
/// assert_eq!(out.bins.len(), 24);
|
||||
/// ```
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct IntradayVolatilityProfile {
|
||||
buckets: usize,
|
||||
utc_offset_minutes: i32,
|
||||
prev_close: Option<f64>,
|
||||
count: Vec<u64>,
|
||||
mean: Vec<f64>,
|
||||
m2: Vec<f64>,
|
||||
last: Option<IntradayVolatilityProfileOutput>,
|
||||
}
|
||||
|
||||
impl IntradayVolatilityProfile {
|
||||
/// Construct an Intraday Volatility Profile with `buckets` intraday slices.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns [`Error::PeriodZero`] if `buckets == 0`.
|
||||
pub fn new(buckets: usize, utc_offset_minutes: i32) -> Result<Self> {
|
||||
if buckets == 0 {
|
||||
return Err(Error::PeriodZero);
|
||||
}
|
||||
Ok(Self {
|
||||
buckets,
|
||||
utc_offset_minutes,
|
||||
prev_close: None,
|
||||
count: vec![0; buckets],
|
||||
mean: vec![0.0; buckets],
|
||||
m2: vec![0.0; buckets],
|
||||
last: None,
|
||||
})
|
||||
}
|
||||
|
||||
/// Configured `(buckets, utc_offset_minutes)`.
|
||||
pub const fn params(&self) -> (usize, i32) {
|
||||
(self.buckets, self.utc_offset_minutes)
|
||||
}
|
||||
|
||||
/// Most recent profile if at least one return has been recorded.
|
||||
pub fn value(&self) -> Option<&IntradayVolatilityProfileOutput> {
|
||||
self.last.as_ref()
|
||||
}
|
||||
|
||||
fn bucket_of(&self, minute_of_day: u32) -> usize {
|
||||
let raw = (minute_of_day as usize * self.buckets) / 1440;
|
||||
raw.min(self.buckets - 1)
|
||||
}
|
||||
|
||||
fn snapshot(&self) -> IntradayVolatilityProfileOutput {
|
||||
let bins = self
|
||||
.count
|
||||
.iter()
|
||||
.zip(&self.m2)
|
||||
.map(|(n, m2)| {
|
||||
if *n >= 2 {
|
||||
(m2 / (*n - 1) as f64).sqrt()
|
||||
} else {
|
||||
0.0
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
IntradayVolatilityProfileOutput { bins }
|
||||
}
|
||||
}
|
||||
|
||||
impl Indicator for IntradayVolatilityProfile {
|
||||
type Input = Candle;
|
||||
type Output = IntradayVolatilityProfileOutput;
|
||||
|
||||
fn update(&mut self, candle: Candle) -> Option<IntradayVolatilityProfileOutput> {
|
||||
let civil = civil_from_timestamp(candle.timestamp, self.utc_offset_minutes);
|
||||
let result = if let Some(prev) = self.prev_close {
|
||||
let ret = if prev == 0.0 {
|
||||
0.0
|
||||
} else {
|
||||
candle.close / prev - 1.0
|
||||
};
|
||||
let bucket = self.bucket_of(civil.minute_of_day());
|
||||
self.count[bucket] += 1;
|
||||
let delta = ret - self.mean[bucket];
|
||||
self.mean[bucket] += delta / self.count[bucket] as f64;
|
||||
let delta2 = ret - self.mean[bucket];
|
||||
self.m2[bucket] += delta * delta2;
|
||||
let out = self.snapshot();
|
||||
self.last = Some(out.clone());
|
||||
Some(out)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
self.prev_close = Some(candle.close);
|
||||
result
|
||||
}
|
||||
|
||||
fn reset(&mut self) {
|
||||
self.prev_close = None;
|
||||
self.count.iter_mut().for_each(|x| *x = 0);
|
||||
self.mean.iter_mut().for_each(|x| *x = 0.0);
|
||||
self.m2.iter_mut().for_each(|x| *x = 0.0);
|
||||
self.last = None;
|
||||
}
|
||||
|
||||
fn warmup_period(&self) -> usize {
|
||||
2
|
||||
}
|
||||
|
||||
fn is_ready(&self) -> bool {
|
||||
self.last.is_some()
|
||||
}
|
||||
|
||||
fn name(&self) -> &'static str {
|
||||
"IntradayVolatilityProfile"
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::traits::BatchExt;
|
||||
use approx::assert_relative_eq;
|
||||
|
||||
const HOUR: i64 = 3_600_000;
|
||||
const DAY: i64 = 24 * HOUR;
|
||||
|
||||
fn c(close: f64, ts: i64) -> Candle {
|
||||
Candle::new(close, close, close, close, 1.0, ts).unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_zero_buckets() {
|
||||
assert!(matches!(
|
||||
IntradayVolatilityProfile::new(0, 0),
|
||||
Err(Error::PeriodZero)
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn metadata_and_accessors() {
|
||||
let prof = IntradayVolatilityProfile::new(24, 90).unwrap();
|
||||
assert_eq!(prof.params(), (24, 90));
|
||||
assert_eq!(prof.name(), "IntradayVolatilityProfile");
|
||||
assert_eq!(prof.warmup_period(), 2);
|
||||
assert!(!prof.is_ready());
|
||||
assert!(prof.value().is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn single_sample_bucket_has_zero_vol() {
|
||||
let mut prof = IntradayVolatilityProfile::new(24, 0).unwrap();
|
||||
assert!(prof.update(c(100.0, 0)).is_none());
|
||||
let out = prof.update(c(101.0, HOUR)).unwrap();
|
||||
assert_eq!(out.bins.len(), 24);
|
||||
assert_relative_eq!(out.bins[1], 0.0); // only one sample in bucket 1
|
||||
assert!(prof.is_ready());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn std_matches_manual_two_samples() {
|
||||
let mut prof = IntradayVolatilityProfile::new(24, 0).unwrap();
|
||||
prof.update(c(100.0, 0)); // 00:00
|
||||
prof.update(c(101.0, HOUR)); // 01:00 r=0.01 into bucket 1
|
||||
// Next day 01:00, r2 = 0.03 into bucket 1.
|
||||
let out = prof.update(c(101.0 * 1.03, 25 * HOUR)).unwrap();
|
||||
// sample std of {0.01, 0.03} = sqrt(((.01-.02)^2+(.03-.02)^2)/1) = 0.01414..
|
||||
let mean = 0.02;
|
||||
let expected = (((0.01_f64 - mean).powi(2) + (0.03 - mean).powi(2)) / 1.0).sqrt();
|
||||
assert_relative_eq!(out.bins[1], expected, epsilon = 1e-9);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn zero_prev_close_uses_zero_return() {
|
||||
let mut prof = IntradayVolatilityProfile::new(4, 0).unwrap();
|
||||
prof.update(c(0.0, 0));
|
||||
let out = prof.update(c(5.0, HOUR)).unwrap();
|
||||
assert_relative_eq!(out.bins[0], 0.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reset_clears_state() {
|
||||
let mut prof = IntradayVolatilityProfile::new(24, 0).unwrap();
|
||||
prof.update(c(100.0, 0));
|
||||
prof.update(c(101.0, HOUR));
|
||||
prof.reset();
|
||||
assert!(!prof.is_ready());
|
||||
assert!(prof.value().is_none());
|
||||
assert!(prof.update(c(100.0, DAY)).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn batch_equals_streaming() {
|
||||
let candles: Vec<Candle> = (0..50)
|
||||
.map(|i| c(100.0 + f64::from(i % 6), i64::from(i) * HOUR))
|
||||
.collect();
|
||||
let mut a = IntradayVolatilityProfile::new(12, 0).unwrap();
|
||||
let mut b = IntradayVolatilityProfile::new(12, 0).unwrap();
|
||||
assert_eq!(
|
||||
a.batch(&candles),
|
||||
candles.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -29,6 +29,7 @@ mod atr;
|
||||
mod atr_bands;
|
||||
mod atr_trailing_stop;
|
||||
mod autocorrelation;
|
||||
mod average_daily_range;
|
||||
mod average_drawdown;
|
||||
mod avg_price;
|
||||
mod awesome_oscillator;
|
||||
@@ -67,6 +68,7 @@ mod counterattack;
|
||||
mod cumulative_volume_index;
|
||||
mod cvd;
|
||||
mod cybernetic_cycle;
|
||||
mod day_of_week_profile;
|
||||
mod decycler;
|
||||
mod decycler_oscillator;
|
||||
mod dema;
|
||||
@@ -136,6 +138,7 @@ mod inertia;
|
||||
mod information_ratio;
|
||||
mod initial_balance;
|
||||
mod instantaneous_trendline;
|
||||
mod intraday_volatility_profile;
|
||||
mod inverse_fisher_transform;
|
||||
mod inverted_hammer;
|
||||
mod jma;
|
||||
@@ -202,6 +205,8 @@ mod on_neck;
|
||||
mod opening_marubozu;
|
||||
mod opening_range;
|
||||
mod ou_half_life;
|
||||
mod overnight_gap;
|
||||
mod overnight_intraday_return;
|
||||
mod pain_index;
|
||||
mod pair_spread_zscore;
|
||||
mod pairwise_beta;
|
||||
@@ -242,7 +247,11 @@ mod rvi;
|
||||
mod rvi_volatility;
|
||||
mod rwi;
|
||||
mod sar_ext;
|
||||
mod seasonal_z_score;
|
||||
mod separating_lines;
|
||||
mod session_high_low;
|
||||
mod session_range;
|
||||
mod session_vwap;
|
||||
mod sharpe_ratio;
|
||||
mod shooting_star;
|
||||
mod short_line;
|
||||
@@ -295,6 +304,7 @@ mod three_stars_in_south;
|
||||
mod thrusting;
|
||||
mod tick_index;
|
||||
mod tii;
|
||||
mod time_of_day_return_profile;
|
||||
mod tpo_profile;
|
||||
mod trade_imbalance;
|
||||
mod treynor_ratio;
|
||||
@@ -306,6 +316,7 @@ mod tsf;
|
||||
mod tsi;
|
||||
mod tsv;
|
||||
mod ttm_squeeze;
|
||||
mod turn_of_month;
|
||||
mod tweezer;
|
||||
mod two_crows;
|
||||
mod typical_price;
|
||||
@@ -322,6 +333,7 @@ mod variance_ratio;
|
||||
mod vertical_horizontal_filter;
|
||||
mod vidya;
|
||||
mod volty_stop;
|
||||
mod volume_by_time_profile;
|
||||
mod volume_oscillator;
|
||||
mod volume_profile;
|
||||
mod vortex;
|
||||
@@ -368,6 +380,7 @@ pub use atr::Atr;
|
||||
pub use atr_bands::{AtrBands, AtrBandsOutput};
|
||||
pub use atr_trailing_stop::AtrTrailingStop;
|
||||
pub use autocorrelation::Autocorrelation;
|
||||
pub use average_daily_range::AverageDailyRange;
|
||||
pub use average_drawdown::AverageDrawdown;
|
||||
pub use avg_price::AvgPrice;
|
||||
pub use awesome_oscillator::AwesomeOscillator;
|
||||
@@ -406,6 +419,7 @@ pub use counterattack::Counterattack;
|
||||
pub use cumulative_volume_index::CumulativeVolumeIndex;
|
||||
pub use cvd::CumulativeVolumeDelta;
|
||||
pub use cybernetic_cycle::CyberneticCycle;
|
||||
pub use day_of_week_profile::{DayOfWeekProfile, DayOfWeekProfileOutput};
|
||||
pub use decycler::Decycler;
|
||||
pub use decycler_oscillator::DecyclerOscillator;
|
||||
pub use dema::Dema;
|
||||
@@ -475,6 +489,7 @@ pub use inertia::Inertia;
|
||||
pub use information_ratio::InformationRatio;
|
||||
pub use initial_balance::{InitialBalance, InitialBalanceOutput};
|
||||
pub use instantaneous_trendline::InstantaneousTrendline;
|
||||
pub use intraday_volatility_profile::{IntradayVolatilityProfile, IntradayVolatilityProfileOutput};
|
||||
pub use inverse_fisher_transform::InverseFisherTransform;
|
||||
pub use inverted_hammer::InvertedHammer;
|
||||
pub use jma::Jma;
|
||||
@@ -541,6 +556,8 @@ pub use on_neck::OnNeck;
|
||||
pub use opening_marubozu::OpeningMarubozu;
|
||||
pub use opening_range::{OpeningRange, OpeningRangeOutput};
|
||||
pub use ou_half_life::OuHalfLife;
|
||||
pub use overnight_gap::OvernightGap;
|
||||
pub use overnight_intraday_return::{OvernightIntradayReturn, OvernightIntradayReturnOutput};
|
||||
pub use pain_index::PainIndex;
|
||||
pub use pair_spread_zscore::PairSpreadZScore;
|
||||
pub use pairwise_beta::PairwiseBeta;
|
||||
@@ -581,7 +598,11 @@ pub use rvi::Rvi;
|
||||
pub use rvi_volatility::RviVolatility;
|
||||
pub use rwi::{Rwi, RwiOutput};
|
||||
pub use sar_ext::SarExt;
|
||||
pub use seasonal_z_score::SeasonalZScore;
|
||||
pub use separating_lines::SeparatingLines;
|
||||
pub use session_high_low::{SessionHighLow, SessionHighLowOutput};
|
||||
pub use session_range::{SessionRange, SessionRangeOutput};
|
||||
pub use session_vwap::SessionVwap;
|
||||
pub use sharpe_ratio::SharpeRatio;
|
||||
pub use shooting_star::ShootingStar;
|
||||
pub use short_line::ShortLine;
|
||||
@@ -634,6 +655,7 @@ pub use three_stars_in_south::ThreeStarsInSouth;
|
||||
pub use thrusting::Thrusting;
|
||||
pub use tick_index::TickIndex;
|
||||
pub use tii::Tii;
|
||||
pub use time_of_day_return_profile::{TimeOfDayReturnProfile, TimeOfDayReturnProfileOutput};
|
||||
pub use tpo_profile::{TpoProfile, TpoProfileOutput};
|
||||
pub use trade_imbalance::TradeImbalance;
|
||||
pub use treynor_ratio::TreynorRatio;
|
||||
@@ -645,6 +667,7 @@ pub use tsf::Tsf;
|
||||
pub use tsi::Tsi;
|
||||
pub use tsv::Tsv;
|
||||
pub use ttm_squeeze::{TtmSqueeze, TtmSqueezeOutput};
|
||||
pub use turn_of_month::TurnOfMonth;
|
||||
pub use tweezer::Tweezer;
|
||||
pub use two_crows::TwoCrows;
|
||||
pub use typical_price::TypicalPrice;
|
||||
@@ -661,6 +684,7 @@ pub use variance_ratio::VarianceRatio;
|
||||
pub use vertical_horizontal_filter::VerticalHorizontalFilter;
|
||||
pub use vidya::Vidya;
|
||||
pub use volty_stop::VoltyStop;
|
||||
pub use volume_by_time_profile::{VolumeByTimeProfile, VolumeByTimeProfileOutput};
|
||||
pub use volume_oscillator::VolumeOscillator;
|
||||
pub use volume_profile::{VolumeProfile, VolumeProfileOutput};
|
||||
pub use vortex::{Vortex, VortexOutput};
|
||||
@@ -1118,6 +1142,23 @@ pub const FAMILIES: &[(&str, &[&str])] = &[
|
||||
"TickIndex",
|
||||
],
|
||||
),
|
||||
(
|
||||
"Seasonality & Session",
|
||||
&[
|
||||
"SessionVwap",
|
||||
"SessionHighLow",
|
||||
"SessionRange",
|
||||
"AverageDailyRange",
|
||||
"OvernightGap",
|
||||
"OvernightIntradayReturn",
|
||||
"TurnOfMonth",
|
||||
"SeasonalZScore",
|
||||
"TimeOfDayReturnProfile",
|
||||
"DayOfWeekProfile",
|
||||
"IntradayVolatilityProfile",
|
||||
"VolumeByTimeProfile",
|
||||
],
|
||||
),
|
||||
];
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -1146,6 +1187,6 @@ mod family_tests {
|
||||
// the actual indicator count is the early-warning signal that an
|
||||
// indicator was added without being assigned a family.
|
||||
let total: usize = FAMILIES.iter().map(|(_, ns)| ns.len()).sum();
|
||||
assert_eq!(total, 339, "FAMILIES total drifted from indicator count");
|
||||
assert_eq!(total, 351, "FAMILIES total drifted from indicator count");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,191 @@
|
||||
//! Overnight Gap — the return from the previous session's close to the current
|
||||
//! session's open, detected automatically at each day boundary.
|
||||
|
||||
use crate::calendar::civil_from_timestamp;
|
||||
use crate::ohlcv::Candle;
|
||||
use crate::traits::Indicator;
|
||||
|
||||
/// Close-to-open overnight gap as a simple return.
|
||||
///
|
||||
/// At every local day boundary the indicator computes
|
||||
/// `open / previous_close - 1`, where `previous_close` is the close of the last
|
||||
/// bar of the prior session and `open` is the open of the first bar of the new
|
||||
/// session. The value holds for the rest of the session until the next boundary.
|
||||
/// The boundary is the wall-clock day of [`Candle::timestamp`](crate::Candle)
|
||||
/// shifted by `utc_offset_minutes`. The first session yields no gap (there is no
|
||||
/// prior close to compare against).
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// use wickra_core::{Candle, Indicator, OvernightGap};
|
||||
///
|
||||
/// let hour = 3_600_000;
|
||||
/// let mut gap = OvernightGap::new(0);
|
||||
/// // Day 1 closes at 100.
|
||||
/// assert!(gap.update(Candle::new(99.0, 101.0, 98.0, 100.0, 1.0, 0).unwrap()).is_none());
|
||||
/// // Day 2 opens at 105 -> gap = 105 / 100 - 1 = 0.05.
|
||||
/// let g = gap.update(Candle::new(105.0, 106.0, 104.0, 105.5, 1.0, 24 * hour).unwrap()).unwrap();
|
||||
/// assert!((g - 0.05).abs() < 1e-9);
|
||||
/// ```
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct OvernightGap {
|
||||
utc_offset_minutes: i32,
|
||||
day_key: Option<(i64, u32, u32)>,
|
||||
last_close: Option<f64>,
|
||||
gap: Option<f64>,
|
||||
}
|
||||
|
||||
impl OvernightGap {
|
||||
/// Construct an Overnight Gap indicator with the given UTC offset (minutes).
|
||||
pub const fn new(utc_offset_minutes: i32) -> Self {
|
||||
Self {
|
||||
utc_offset_minutes,
|
||||
day_key: None,
|
||||
last_close: None,
|
||||
gap: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Configured UTC offset in minutes.
|
||||
pub const fn utc_offset_minutes(&self) -> i32 {
|
||||
self.utc_offset_minutes
|
||||
}
|
||||
|
||||
/// Most recent overnight gap if at least one day boundary has been crossed.
|
||||
pub const fn value(&self) -> Option<f64> {
|
||||
self.gap
|
||||
}
|
||||
}
|
||||
|
||||
impl Indicator for OvernightGap {
|
||||
type Input = Candle;
|
||||
type Output = f64;
|
||||
|
||||
fn update(&mut self, candle: Candle) -> Option<f64> {
|
||||
let civil = civil_from_timestamp(candle.timestamp, self.utc_offset_minutes);
|
||||
let key = (civil.year, civil.month, civil.day);
|
||||
if self.day_key != Some(key) {
|
||||
if let Some(prev_close) = self.last_close {
|
||||
self.gap = Some(if prev_close == 0.0 {
|
||||
0.0
|
||||
} else {
|
||||
candle.open / prev_close - 1.0
|
||||
});
|
||||
}
|
||||
self.day_key = Some(key);
|
||||
}
|
||||
self.last_close = Some(candle.close);
|
||||
self.gap
|
||||
}
|
||||
|
||||
fn reset(&mut self) {
|
||||
self.day_key = None;
|
||||
self.last_close = None;
|
||||
self.gap = None;
|
||||
}
|
||||
|
||||
fn warmup_period(&self) -> usize {
|
||||
2
|
||||
}
|
||||
|
||||
fn is_ready(&self) -> bool {
|
||||
self.gap.is_some()
|
||||
}
|
||||
|
||||
fn name(&self) -> &'static str {
|
||||
"OvernightGap"
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::traits::BatchExt;
|
||||
use approx::assert_relative_eq;
|
||||
|
||||
const HOUR: i64 = 3_600_000;
|
||||
|
||||
fn c(open: f64, close: f64, ts: i64) -> Candle {
|
||||
let high = open.max(close);
|
||||
let low = open.min(close);
|
||||
Candle::new(open, high, low, close, 1.0, ts).unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn metadata_and_accessors() {
|
||||
let gap = OvernightGap::new(330);
|
||||
assert_eq!(gap.utc_offset_minutes(), 330);
|
||||
assert_eq!(gap.name(), "OvernightGap");
|
||||
assert_eq!(gap.warmup_period(), 2);
|
||||
assert!(!gap.is_ready());
|
||||
assert!(gap.value().is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn first_session_has_no_gap() {
|
||||
let mut gap = OvernightGap::new(0);
|
||||
assert!(gap.update(c(99.0, 100.0, 0)).is_none());
|
||||
// Same day, still no gap.
|
||||
assert!(gap.update(c(100.0, 101.0, HOUR)).is_none());
|
||||
assert!(!gap.is_ready());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn computes_gap_at_day_boundary() {
|
||||
let mut gap = OvernightGap::new(0);
|
||||
gap.update(c(99.0, 100.0, 0)); // day 1 closes 100
|
||||
let g = gap.update(c(105.0, 105.5, 24 * HOUR)).unwrap();
|
||||
assert_relative_eq!(g, 0.05);
|
||||
assert!(gap.is_ready());
|
||||
// Holds for the rest of the session.
|
||||
let same = gap.update(c(106.0, 107.0, 25 * HOUR)).unwrap();
|
||||
assert_relative_eq!(same, 0.05);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn negative_gap_down() {
|
||||
let mut gap = OvernightGap::new(0);
|
||||
gap.update(c(99.0, 100.0, 0));
|
||||
let g = gap.update(c(90.0, 91.0, 24 * HOUR)).unwrap();
|
||||
assert_relative_eq!(g, -0.1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn zero_prev_close_yields_zero_gap() {
|
||||
let mut gap = OvernightGap::new(0);
|
||||
gap.update(c(0.0, 0.0, 0)); // degenerate day 1 closing at 0
|
||||
let g = gap.update(c(5.0, 6.0, 24 * HOUR)).unwrap();
|
||||
assert_relative_eq!(g, 0.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reset_clears_state() {
|
||||
let mut gap = OvernightGap::new(0);
|
||||
gap.update(c(99.0, 100.0, 0));
|
||||
gap.update(c(105.0, 105.5, 24 * HOUR));
|
||||
gap.reset();
|
||||
assert!(!gap.is_ready());
|
||||
assert!(gap.value().is_none());
|
||||
assert!(gap.update(c(10.0, 11.0, 48 * HOUR)).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn batch_equals_streaming() {
|
||||
let candles: Vec<Candle> = (0..50)
|
||||
.map(|i| {
|
||||
c(
|
||||
100.0 + f64::from(i % 7),
|
||||
100.0 + f64::from(i % 5),
|
||||
i64::from(i) * 6 * HOUR,
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
let mut a = OvernightGap::new(0);
|
||||
let mut b = OvernightGap::new(0);
|
||||
assert_eq!(
|
||||
a.batch(&candles),
|
||||
candles.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,225 @@
|
||||
//! Overnight vs. Intraday Return — decomposes a session's total return into its
|
||||
//! overnight (close-to-open) and intraday (open-to-close) components.
|
||||
|
||||
use crate::calendar::civil_from_timestamp;
|
||||
use crate::ohlcv::Candle;
|
||||
use crate::traits::Indicator;
|
||||
|
||||
/// The two return components of the current session.
|
||||
///
|
||||
/// `overnight` is fixed at the session open (`open / previous_close - 1`);
|
||||
/// `intraday` updates with every bar (`close / open - 1`). Compounding the two —
|
||||
/// `(1 + overnight)(1 + intraday) - 1` — reconstructs the full previous-close to
|
||||
/// latest-close return.
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub struct OvernightIntradayReturnOutput {
|
||||
/// Close-to-open return carried into the session.
|
||||
pub overnight: f64,
|
||||
/// Open-to-latest-close return accumulated within the session.
|
||||
pub intraday: f64,
|
||||
}
|
||||
|
||||
/// Overnight / intraday return decomposition, re-anchored at each local day
|
||||
/// boundary of [`Candle::timestamp`](crate::Candle) shifted by
|
||||
/// `utc_offset_minutes`.
|
||||
///
|
||||
/// The first session yields no output (there is no prior close to anchor the
|
||||
/// overnight leg); from the second session onward every bar reports both
|
||||
/// components.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// use wickra_core::{Candle, Indicator, OvernightIntradayReturn};
|
||||
///
|
||||
/// let hour = 3_600_000;
|
||||
/// let mut oi = OvernightIntradayReturn::new(0);
|
||||
/// // Day 1 closes at 100.
|
||||
/// assert!(oi.update(Candle::new(99.0, 101.0, 98.0, 100.0, 1.0, 0).unwrap()).is_none());
|
||||
/// // Day 2 opens 110 (overnight +10%), closes 121 (intraday +10%).
|
||||
/// let v = oi.update(Candle::new(110.0, 122.0, 109.0, 121.0, 1.0, 24 * hour).unwrap()).unwrap();
|
||||
/// assert!((v.overnight - 0.10).abs() < 1e-9);
|
||||
/// assert!((v.intraday - 0.10).abs() < 1e-9);
|
||||
/// ```
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct OvernightIntradayReturn {
|
||||
utc_offset_minutes: i32,
|
||||
day_key: Option<(i64, u32, u32)>,
|
||||
last_close: Option<f64>,
|
||||
today_open: f64,
|
||||
overnight: Option<f64>,
|
||||
last: Option<OvernightIntradayReturnOutput>,
|
||||
}
|
||||
|
||||
impl OvernightIntradayReturn {
|
||||
/// Construct the indicator with the given UTC offset (minutes).
|
||||
pub const fn new(utc_offset_minutes: i32) -> Self {
|
||||
Self {
|
||||
utc_offset_minutes,
|
||||
day_key: None,
|
||||
last_close: None,
|
||||
today_open: 0.0,
|
||||
overnight: None,
|
||||
last: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Configured UTC offset in minutes.
|
||||
pub const fn utc_offset_minutes(&self) -> i32 {
|
||||
self.utc_offset_minutes
|
||||
}
|
||||
|
||||
/// Most recent decomposition if at least one day boundary has been crossed.
|
||||
pub const fn value(&self) -> Option<OvernightIntradayReturnOutput> {
|
||||
self.last
|
||||
}
|
||||
}
|
||||
|
||||
impl Indicator for OvernightIntradayReturn {
|
||||
type Input = Candle;
|
||||
type Output = OvernightIntradayReturnOutput;
|
||||
|
||||
fn update(&mut self, candle: Candle) -> Option<OvernightIntradayReturnOutput> {
|
||||
let civil = civil_from_timestamp(candle.timestamp, self.utc_offset_minutes);
|
||||
let key = (civil.year, civil.month, civil.day);
|
||||
if self.day_key != Some(key) {
|
||||
if let Some(prev_close) = self.last_close {
|
||||
self.overnight = Some(if prev_close == 0.0 {
|
||||
0.0
|
||||
} else {
|
||||
candle.open / prev_close - 1.0
|
||||
});
|
||||
}
|
||||
self.today_open = candle.open;
|
||||
self.day_key = Some(key);
|
||||
}
|
||||
self.last_close = Some(candle.close);
|
||||
let overnight = self.overnight?;
|
||||
let intraday = if self.today_open == 0.0 {
|
||||
0.0
|
||||
} else {
|
||||
candle.close / self.today_open - 1.0
|
||||
};
|
||||
let out = OvernightIntradayReturnOutput {
|
||||
overnight,
|
||||
intraday,
|
||||
};
|
||||
self.last = Some(out);
|
||||
Some(out)
|
||||
}
|
||||
|
||||
fn reset(&mut self) {
|
||||
self.day_key = None;
|
||||
self.last_close = None;
|
||||
self.today_open = 0.0;
|
||||
self.overnight = None;
|
||||
self.last = None;
|
||||
}
|
||||
|
||||
fn warmup_period(&self) -> usize {
|
||||
2
|
||||
}
|
||||
|
||||
fn is_ready(&self) -> bool {
|
||||
self.last.is_some()
|
||||
}
|
||||
|
||||
fn name(&self) -> &'static str {
|
||||
"OvernightIntradayReturn"
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::traits::BatchExt;
|
||||
use approx::assert_relative_eq;
|
||||
|
||||
const HOUR: i64 = 3_600_000;
|
||||
|
||||
fn c(open: f64, close: f64, ts: i64) -> Candle {
|
||||
let high = open.max(close) + 1.0;
|
||||
let low = open.min(close) - 1.0;
|
||||
Candle::new(open, high, low.max(0.0), close, 1.0, ts).unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn metadata_and_accessors() {
|
||||
let oi = OvernightIntradayReturn::new(-300);
|
||||
assert_eq!(oi.utc_offset_minutes(), -300);
|
||||
assert_eq!(oi.name(), "OvernightIntradayReturn");
|
||||
assert_eq!(oi.warmup_period(), 2);
|
||||
assert!(!oi.is_ready());
|
||||
assert!(oi.value().is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn first_session_yields_none() {
|
||||
let mut oi = OvernightIntradayReturn::new(0);
|
||||
assert!(oi.update(c(99.0, 100.0, 0)).is_none());
|
||||
assert!(oi.update(c(100.0, 102.0, HOUR)).is_none());
|
||||
assert!(!oi.is_ready());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn decomposes_overnight_and_intraday() {
|
||||
let mut oi = OvernightIntradayReturn::new(0);
|
||||
oi.update(c(99.0, 100.0, 0)); // day 1 close 100
|
||||
let v = oi.update(c(110.0, 121.0, 24 * HOUR)).unwrap();
|
||||
assert_relative_eq!(v.overnight, 0.10);
|
||||
assert_relative_eq!(v.intraday, 0.10);
|
||||
assert!(oi.is_ready());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn intraday_updates_through_the_session() {
|
||||
let mut oi = OvernightIntradayReturn::new(0);
|
||||
oi.update(c(99.0, 100.0, 0));
|
||||
oi.update(c(110.0, 110.0, 24 * HOUR)); // open 110, close 110 -> intraday 0
|
||||
let later = oi.update(c(111.0, 132.0, 25 * HOUR)).unwrap();
|
||||
assert_relative_eq!(later.overnight, 0.10); // fixed at open
|
||||
assert_relative_eq!(later.intraday, 0.20); // 132 / 110 - 1
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn zero_anchors_yield_zero_components() {
|
||||
let mut oi = OvernightIntradayReturn::new(0);
|
||||
oi.update(c(1.0, 0.0, 0)); // day 1 closes at 0
|
||||
// Day 2 opens at 0: overnight uses zero prev_close -> 0; intraday uses
|
||||
// zero today_open -> 0.
|
||||
let candle = Candle::new(0.0, 5.0, 0.0, 4.0, 1.0, 24 * HOUR).unwrap();
|
||||
let v = oi.update(candle).unwrap();
|
||||
assert_relative_eq!(v.overnight, 0.0);
|
||||
assert_relative_eq!(v.intraday, 0.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reset_clears_state() {
|
||||
let mut oi = OvernightIntradayReturn::new(0);
|
||||
oi.update(c(99.0, 100.0, 0));
|
||||
oi.update(c(110.0, 121.0, 24 * HOUR));
|
||||
oi.reset();
|
||||
assert!(!oi.is_ready());
|
||||
assert!(oi.value().is_none());
|
||||
assert!(oi.update(c(50.0, 55.0, 48 * HOUR)).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn batch_equals_streaming() {
|
||||
let candles: Vec<Candle> = (0..48)
|
||||
.map(|i| {
|
||||
c(
|
||||
100.0 + f64::from(i % 6),
|
||||
100.0 + f64::from(i % 4),
|
||||
i64::from(i) * 8 * HOUR,
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
let mut a = OvernightIntradayReturn::new(0);
|
||||
let mut b = OvernightIntradayReturn::new(0);
|
||||
assert_eq!(
|
||||
a.batch(&candles),
|
||||
candles.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,232 @@
|
||||
//! Seasonal Z-Score — how far the current bar's return sits from the historical
|
||||
//! mean return of bars in the *same hour of day*, in standard deviations.
|
||||
|
||||
use crate::calendar::civil_from_timestamp;
|
||||
use crate::ohlcv::Candle;
|
||||
use crate::traits::Indicator;
|
||||
|
||||
const HOURS: usize = 24;
|
||||
|
||||
/// Seasonal Z-Score keyed on hour of day.
|
||||
///
|
||||
/// For every bar the indicator forms the simple return `close / previous_close - 1`
|
||||
/// and compares it to the running mean and standard deviation of all prior
|
||||
/// returns that fell in the *same* local hour (the wall-clock hour of
|
||||
/// [`Candle::timestamp`](crate::Candle) shifted by `utc_offset_minutes`). The
|
||||
/// output is `(return - hour_mean) / hour_std`. A bucket needs at least two prior
|
||||
/// samples before it can emit; a bucket with zero historical variance reports
|
||||
/// `0.0`. The per-hour statistics use Welford's online algorithm.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// use wickra_core::{Candle, Indicator, SeasonalZScore};
|
||||
///
|
||||
/// let day = 24 * 3_600_000;
|
||||
/// let mut z = SeasonalZScore::new(0);
|
||||
/// // Same hour each day so they share a bucket; close grows then jumps.
|
||||
/// for (i, close) in [100.0, 101.0, 103.0].iter().enumerate() {
|
||||
/// z.update(Candle::new(*close, *close, *close, *close, 1.0, i as i64 * day).unwrap());
|
||||
/// }
|
||||
/// // Fourth same-hour sample has two priors in the bucket -> emits a z-score.
|
||||
/// let out = z.update(Candle::new(110.0, 110.0, 110.0, 110.0, 1.0, 3 * day).unwrap());
|
||||
/// assert!(out.is_some());
|
||||
/// ```
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct SeasonalZScore {
|
||||
utc_offset_minutes: i32,
|
||||
prev_close: Option<f64>,
|
||||
count: [u64; HOURS],
|
||||
mean: [f64; HOURS],
|
||||
m2: [f64; HOURS],
|
||||
last: Option<f64>,
|
||||
}
|
||||
|
||||
impl SeasonalZScore {
|
||||
/// Construct a Seasonal Z-Score indicator with the given UTC offset (minutes).
|
||||
pub const fn new(utc_offset_minutes: i32) -> Self {
|
||||
Self {
|
||||
utc_offset_minutes,
|
||||
prev_close: None,
|
||||
count: [0; HOURS],
|
||||
mean: [0.0; HOURS],
|
||||
m2: [0.0; HOURS],
|
||||
last: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Configured UTC offset in minutes.
|
||||
pub const fn utc_offset_minutes(&self) -> i32 {
|
||||
self.utc_offset_minutes
|
||||
}
|
||||
|
||||
/// Most recent z-score if a populated bucket has produced one.
|
||||
pub const fn value(&self) -> Option<f64> {
|
||||
self.last
|
||||
}
|
||||
|
||||
fn z_for(&self, hour: usize, ret: f64) -> Option<f64> {
|
||||
if self.count[hour] < 2 {
|
||||
return None;
|
||||
}
|
||||
let variance = self.m2[hour] / (self.count[hour] - 1) as f64;
|
||||
if variance > 0.0 {
|
||||
Some((ret - self.mean[hour]) / variance.sqrt())
|
||||
} else {
|
||||
Some(0.0)
|
||||
}
|
||||
}
|
||||
|
||||
fn accumulate(&mut self, hour: usize, ret: f64) {
|
||||
self.count[hour] += 1;
|
||||
let delta = ret - self.mean[hour];
|
||||
self.mean[hour] += delta / self.count[hour] as f64;
|
||||
let delta2 = ret - self.mean[hour];
|
||||
self.m2[hour] += delta * delta2;
|
||||
}
|
||||
}
|
||||
|
||||
impl Indicator for SeasonalZScore {
|
||||
type Input = Candle;
|
||||
type Output = f64;
|
||||
|
||||
fn update(&mut self, candle: Candle) -> Option<f64> {
|
||||
let civil = civil_from_timestamp(candle.timestamp, self.utc_offset_minutes);
|
||||
let hour = civil.hour as usize;
|
||||
let result = if let Some(prev) = self.prev_close {
|
||||
let ret = if prev == 0.0 {
|
||||
0.0
|
||||
} else {
|
||||
candle.close / prev - 1.0
|
||||
};
|
||||
let z = self.z_for(hour, ret);
|
||||
self.accumulate(hour, ret);
|
||||
z
|
||||
} else {
|
||||
None
|
||||
};
|
||||
self.prev_close = Some(candle.close);
|
||||
if result.is_some() {
|
||||
self.last = result;
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
fn reset(&mut self) {
|
||||
self.prev_close = None;
|
||||
self.count = [0; HOURS];
|
||||
self.mean = [0.0; HOURS];
|
||||
self.m2 = [0.0; HOURS];
|
||||
self.last = None;
|
||||
}
|
||||
|
||||
fn warmup_period(&self) -> usize {
|
||||
2
|
||||
}
|
||||
|
||||
fn is_ready(&self) -> bool {
|
||||
self.last.is_some()
|
||||
}
|
||||
|
||||
fn name(&self) -> &'static str {
|
||||
"SeasonalZScore"
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::traits::BatchExt;
|
||||
use approx::assert_relative_eq;
|
||||
|
||||
const DAY: i64 = 24 * 3_600_000;
|
||||
|
||||
fn c(close: f64, ts: i64) -> Candle {
|
||||
Candle::new(close, close, close, close, 1.0, ts).unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn metadata_and_accessors() {
|
||||
let z = SeasonalZScore::new(120);
|
||||
assert_eq!(z.utc_offset_minutes(), 120);
|
||||
assert_eq!(z.name(), "SeasonalZScore");
|
||||
assert_eq!(z.warmup_period(), 2);
|
||||
assert!(!z.is_ready());
|
||||
assert!(z.value().is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn no_output_until_bucket_has_two_priors() {
|
||||
let mut z = SeasonalZScore::new(0);
|
||||
// Each bar shares the same hour bucket (same time-of-day, daily spacing).
|
||||
assert!(z.update(c(100.0, 0)).is_none()); // first: no return
|
||||
assert!(z.update(c(101.0, DAY)).is_none()); // return #1 -> bucket has 0 priors
|
||||
assert!(z.update(c(102.0, 2 * DAY)).is_none()); // return #2 -> bucket has 1 prior
|
||||
// return #3 -> bucket has 2 priors -> emits.
|
||||
assert!(z.update(c(104.0, 3 * DAY)).is_some());
|
||||
assert!(z.is_ready());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn z_score_matches_manual_welford() {
|
||||
let mut z = SeasonalZScore::new(0);
|
||||
// Returns into one hourly bucket: r1 = 0.01, r2 = 0.02, r3 = 0.03.
|
||||
z.update(c(100.0, 0));
|
||||
z.update(c(101.0, DAY)); // r1 = 0.01
|
||||
z.update(c(103.02, 2 * DAY)); // r2 = 0.02
|
||||
// Priors {0.01, 0.02}: mean 0.015, sample std = sqrt(((.005)^2*2)/1).
|
||||
let mean = 0.015;
|
||||
let std = (((0.01_f64 - mean).powi(2) + (0.02 - mean).powi(2)) / 1.0).sqrt();
|
||||
let r3 = 0.03;
|
||||
let expected = (r3 - mean) / std;
|
||||
let close = 103.02 * (1.0 + r3);
|
||||
let out = z.update(c(close, 3 * DAY)).unwrap();
|
||||
assert_relative_eq!(out, expected, epsilon = 1e-9);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn zero_variance_bucket_reports_zero() {
|
||||
let mut z = SeasonalZScore::new(0);
|
||||
// Constant return into the bucket -> variance 0 -> z = 0.
|
||||
z.update(c(100.0, 0));
|
||||
z.update(c(110.0, DAY)); // r1 = 0.10
|
||||
z.update(c(121.0, 2 * DAY)); // r2 = 0.10
|
||||
let out = z.update(c(133.1, 3 * DAY)).unwrap(); // r3 = 0.10
|
||||
assert_relative_eq!(out, 0.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn zero_prev_close_uses_zero_return() {
|
||||
let mut z = SeasonalZScore::new(0);
|
||||
z.update(c(0.0, 0)); // prev close 0
|
||||
z.update(c(0.0, DAY)); // ret = 0 (guarded), bucket sample
|
||||
z.update(c(0.0, 2 * DAY)); // ret = 0, bucket now 2 priors
|
||||
let out = z.update(c(0.0, 3 * DAY)).unwrap();
|
||||
assert_relative_eq!(out, 0.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reset_clears_state() {
|
||||
let mut z = SeasonalZScore::new(0);
|
||||
for i in 0..4 {
|
||||
z.update(c(100.0 + f64::from(i), i64::from(i) * DAY));
|
||||
}
|
||||
z.reset();
|
||||
assert!(!z.is_ready());
|
||||
assert!(z.value().is_none());
|
||||
assert!(z.update(c(100.0, 4 * DAY)).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn batch_equals_streaming() {
|
||||
let candles: Vec<Candle> = (0..50)
|
||||
.map(|i| c(100.0 + f64::from(i % 9), i64::from(i) * 3 * 3_600_000))
|
||||
.collect();
|
||||
let mut a = SeasonalZScore::new(0);
|
||||
let mut b = SeasonalZScore::new(0);
|
||||
assert_eq!(
|
||||
a.batch(&candles),
|
||||
candles.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,226 @@
|
||||
//! Session High/Low — the running high and low of the current calendar-day
|
||||
//! session, re-anchored automatically at each day boundary.
|
||||
|
||||
use crate::calendar::civil_from_timestamp;
|
||||
use crate::ohlcv::Candle;
|
||||
use crate::traits::Indicator;
|
||||
|
||||
/// Session High/Low output: the high and low established so far in the current
|
||||
/// session.
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub struct SessionHighLowOutput {
|
||||
/// Highest high seen since the current session opened.
|
||||
pub high: f64,
|
||||
/// Lowest low seen since the current session opened.
|
||||
pub low: f64,
|
||||
}
|
||||
|
||||
/// Running high / low of the current session, keyed off the wall-clock day of
|
||||
/// [`Candle::timestamp`](crate::Candle).
|
||||
///
|
||||
/// Unlike [`crate::OpeningRange`] or [`crate::InitialBalance`], which require the
|
||||
/// caller to invoke `reset()` at every session boundary, this indicator detects
|
||||
/// the boundary itself: whenever a candle falls on a different local calendar
|
||||
/// day (after shifting by `utc_offset_minutes`) the high / low are re-anchored to
|
||||
/// that candle. `utc_offset_minutes` lets callers align the day boundary to an
|
||||
/// exchange session — `0` for UTC, `-300` for U.S. Eastern standard time.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// use wickra_core::{Candle, Indicator, SessionHighLow};
|
||||
///
|
||||
/// // One bar per hour; the day rolls over after 24 bars at UTC.
|
||||
/// let mut shl = SessionHighLow::new(0);
|
||||
/// let hour = 3_600_000;
|
||||
/// shl.update(Candle::new(100.0, 105.0, 99.0, 101.0, 1.0, 0).unwrap());
|
||||
/// let v = shl.update(Candle::new(101.0, 108.0, 100.0, 107.0, 1.0, hour).unwrap()).unwrap();
|
||||
/// assert_eq!(v.high, 108.0);
|
||||
/// assert_eq!(v.low, 99.0);
|
||||
/// // A bar on the next day re-anchors to that bar alone.
|
||||
/// let v = shl.update(Candle::new(50.0, 51.0, 49.0, 50.0, 1.0, 24 * hour).unwrap()).unwrap();
|
||||
/// assert_eq!(v.high, 51.0);
|
||||
/// assert_eq!(v.low, 49.0);
|
||||
/// ```
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct SessionHighLow {
|
||||
utc_offset_minutes: i32,
|
||||
day_key: Option<(i64, u32, u32)>,
|
||||
high: f64,
|
||||
low: f64,
|
||||
last: Option<SessionHighLowOutput>,
|
||||
}
|
||||
|
||||
impl SessionHighLow {
|
||||
/// Construct a Session High/Low indicator with the given UTC offset (minutes).
|
||||
pub const fn new(utc_offset_minutes: i32) -> Self {
|
||||
Self {
|
||||
utc_offset_minutes,
|
||||
day_key: None,
|
||||
high: f64::NEG_INFINITY,
|
||||
low: f64::INFINITY,
|
||||
last: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Configured UTC offset in minutes.
|
||||
pub const fn utc_offset_minutes(&self) -> i32 {
|
||||
self.utc_offset_minutes
|
||||
}
|
||||
|
||||
/// Most recent output if at least one bar has been seen.
|
||||
pub const fn value(&self) -> Option<SessionHighLowOutput> {
|
||||
self.last
|
||||
}
|
||||
}
|
||||
|
||||
impl Indicator for SessionHighLow {
|
||||
type Input = Candle;
|
||||
type Output = SessionHighLowOutput;
|
||||
|
||||
fn update(&mut self, candle: Candle) -> Option<SessionHighLowOutput> {
|
||||
let civil = civil_from_timestamp(candle.timestamp, self.utc_offset_minutes);
|
||||
let key = (civil.year, civil.month, civil.day);
|
||||
if self.day_key == Some(key) {
|
||||
if candle.high > self.high {
|
||||
self.high = candle.high;
|
||||
}
|
||||
if candle.low < self.low {
|
||||
self.low = candle.low;
|
||||
}
|
||||
} else {
|
||||
self.day_key = Some(key);
|
||||
self.high = candle.high;
|
||||
self.low = candle.low;
|
||||
}
|
||||
let out = SessionHighLowOutput {
|
||||
high: self.high,
|
||||
low: self.low,
|
||||
};
|
||||
self.last = Some(out);
|
||||
Some(out)
|
||||
}
|
||||
|
||||
fn reset(&mut self) {
|
||||
self.day_key = None;
|
||||
self.high = f64::NEG_INFINITY;
|
||||
self.low = f64::INFINITY;
|
||||
self.last = None;
|
||||
}
|
||||
|
||||
fn warmup_period(&self) -> usize {
|
||||
1
|
||||
}
|
||||
|
||||
fn is_ready(&self) -> bool {
|
||||
self.last.is_some()
|
||||
}
|
||||
|
||||
fn name(&self) -> &'static str {
|
||||
"SessionHighLow"
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::traits::BatchExt;
|
||||
use approx::assert_relative_eq;
|
||||
|
||||
const HOUR: i64 = 3_600_000;
|
||||
|
||||
fn c(high: f64, low: f64, ts: i64) -> Candle {
|
||||
let mid = f64::midpoint(high, low);
|
||||
Candle::new(mid, high, low, mid, 1.0, ts).unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn metadata_and_accessors() {
|
||||
let shl = SessionHighLow::new(-300);
|
||||
assert_eq!(shl.utc_offset_minutes(), -300);
|
||||
assert_eq!(shl.name(), "SessionHighLow");
|
||||
assert_eq!(shl.warmup_period(), 1);
|
||||
assert!(!shl.is_ready());
|
||||
assert!(shl.value().is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tracks_high_low_within_day() {
|
||||
let mut shl = SessionHighLow::new(0);
|
||||
let first = shl.update(c(105.0, 99.0, 0)).unwrap();
|
||||
assert_relative_eq!(first.high, 105.0);
|
||||
assert_relative_eq!(first.low, 99.0);
|
||||
assert!(shl.is_ready());
|
||||
let second = shl.update(c(108.0, 100.0, HOUR)).unwrap();
|
||||
assert_relative_eq!(second.high, 108.0);
|
||||
assert_relative_eq!(second.low, 99.0);
|
||||
// A narrower bar does not shrink the range.
|
||||
let third = shl.update(c(106.0, 101.0, 2 * HOUR)).unwrap();
|
||||
assert_relative_eq!(third.high, 108.0);
|
||||
assert_relative_eq!(third.low, 99.0);
|
||||
// A bar with a lower low extends the range downward (same day).
|
||||
let fourth = shl.update(c(107.0, 95.0, 3 * HOUR)).unwrap();
|
||||
assert_relative_eq!(fourth.high, 108.0);
|
||||
assert_relative_eq!(fourth.low, 95.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn re_anchors_on_new_day() {
|
||||
let mut shl = SessionHighLow::new(0);
|
||||
shl.update(c(105.0, 99.0, 0));
|
||||
shl.update(c(108.0, 100.0, HOUR));
|
||||
let next = shl.update(c(51.0, 49.0, 24 * HOUR)).unwrap();
|
||||
assert_relative_eq!(next.high, 51.0);
|
||||
assert_relative_eq!(next.low, 49.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn utc_offset_shifts_day_boundary() {
|
||||
// Two bars 1h apart straddling UTC midnight. At UTC they are different
|
||||
// days; at +120 min they fall on the same local day.
|
||||
let pre = 23 * HOUR; // 1970-01-01 23:00 UTC
|
||||
let post = 24 * HOUR; // 1970-01-02 00:00 UTC
|
||||
let mut utc = SessionHighLow::new(0);
|
||||
utc.update(c(105.0, 99.0, pre));
|
||||
let rolled = utc.update(c(108.0, 100.0, post)).unwrap();
|
||||
assert_relative_eq!(rolled.high, 108.0);
|
||||
assert_relative_eq!(rolled.low, 100.0); // re-anchored
|
||||
|
||||
let mut shifted = SessionHighLow::new(120);
|
||||
shifted.update(c(105.0, 99.0, pre));
|
||||
let same = shifted.update(c(108.0, 100.0, post)).unwrap();
|
||||
assert_relative_eq!(same.high, 108.0);
|
||||
assert_relative_eq!(same.low, 99.0); // same local day, range kept
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reset_clears_state() {
|
||||
let mut shl = SessionHighLow::new(0);
|
||||
shl.update(c(105.0, 99.0, 0));
|
||||
shl.reset();
|
||||
assert!(!shl.is_ready());
|
||||
assert!(shl.value().is_none());
|
||||
let after = shl.update(c(60.0, 50.0, HOUR)).unwrap();
|
||||
assert_relative_eq!(after.high, 60.0);
|
||||
assert_relative_eq!(after.low, 50.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn batch_equals_streaming() {
|
||||
let candles: Vec<Candle> = (0..30)
|
||||
.map(|i| {
|
||||
c(
|
||||
100.0 + f64::from(i),
|
||||
90.0 + f64::from(i) * 0.5,
|
||||
i64::from(i) * HOUR,
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
let mut a = SessionHighLow::new(0);
|
||||
let mut b = SessionHighLow::new(0);
|
||||
assert_eq!(
|
||||
a.batch(&candles),
|
||||
candles.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,248 @@
|
||||
//! Session Range — the high-minus-low range accumulated within each of the
|
||||
//! three canonical trading sessions (Asia / EU / US) of the current day.
|
||||
|
||||
use crate::calendar::civil_from_timestamp;
|
||||
use crate::ohlcv::Candle;
|
||||
use crate::traits::Indicator;
|
||||
|
||||
/// Session Range output: the current day's range within each session.
|
||||
///
|
||||
/// A session with no bars yet reports `0.0`. All three reset at the local day
|
||||
/// boundary.
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub struct SessionRangeOutput {
|
||||
/// High − low within the Asia session (local hours `00:00..08:00`).
|
||||
pub asia: f64,
|
||||
/// High − low within the EU session (local hours `08:00..16:00`).
|
||||
pub eu: f64,
|
||||
/// High − low within the US session (local hours `16:00..24:00`).
|
||||
pub us: f64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
struct Extent {
|
||||
high: f64,
|
||||
low: f64,
|
||||
}
|
||||
|
||||
impl Extent {
|
||||
const EMPTY: Self = Self {
|
||||
high: f64::NEG_INFINITY,
|
||||
low: f64::INFINITY,
|
||||
};
|
||||
|
||||
fn add(&mut self, candle: Candle) {
|
||||
if candle.high > self.high {
|
||||
self.high = candle.high;
|
||||
}
|
||||
if candle.low < self.low {
|
||||
self.low = candle.low;
|
||||
}
|
||||
}
|
||||
|
||||
fn range(self) -> f64 {
|
||||
if self.high >= self.low {
|
||||
self.high - self.low
|
||||
} else {
|
||||
0.0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Per-session high-low range, keyed off the wall-clock hour of
|
||||
/// [`Candle::timestamp`](crate::Candle).
|
||||
///
|
||||
/// The local day (after shifting by `utc_offset_minutes`) is split into three
|
||||
/// eight-hour sessions: **Asia** `00:00..08:00`, **EU** `08:00..16:00`, **US**
|
||||
/// `16:00..24:00`. Each session accumulates its own high / low; the reported
|
||||
/// range is `high - low`, or `0.0` before that session has seen a bar. All three
|
||||
/// re-anchor automatically at the day boundary.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// use wickra_core::{Candle, Indicator, SessionRange};
|
||||
///
|
||||
/// let hour = 3_600_000;
|
||||
/// let mut sr = SessionRange::new(0);
|
||||
/// // 02:00 UTC — Asia session.
|
||||
/// sr.update(Candle::new(100.0, 104.0, 98.0, 101.0, 1.0, 2 * hour).unwrap());
|
||||
/// // 10:00 UTC — EU session.
|
||||
/// let v = sr.update(Candle::new(101.0, 110.0, 100.0, 109.0, 1.0, 10 * hour).unwrap()).unwrap();
|
||||
/// assert_eq!(v.asia, 6.0);
|
||||
/// assert_eq!(v.eu, 10.0);
|
||||
/// assert_eq!(v.us, 0.0);
|
||||
/// ```
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct SessionRange {
|
||||
utc_offset_minutes: i32,
|
||||
day_key: Option<(i64, u32, u32)>,
|
||||
sessions: [Extent; 3],
|
||||
last: Option<SessionRangeOutput>,
|
||||
}
|
||||
|
||||
impl SessionRange {
|
||||
/// Construct a Session Range indicator with the given UTC offset (minutes).
|
||||
pub const fn new(utc_offset_minutes: i32) -> Self {
|
||||
Self {
|
||||
utc_offset_minutes,
|
||||
day_key: None,
|
||||
sessions: [Extent::EMPTY; 3],
|
||||
last: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Configured UTC offset in minutes.
|
||||
pub const fn utc_offset_minutes(&self) -> i32 {
|
||||
self.utc_offset_minutes
|
||||
}
|
||||
|
||||
/// Most recent output if at least one bar has been seen.
|
||||
pub const fn value(&self) -> Option<SessionRangeOutput> {
|
||||
self.last
|
||||
}
|
||||
|
||||
fn snapshot(&self) -> SessionRangeOutput {
|
||||
SessionRangeOutput {
|
||||
asia: self.sessions[0].range(),
|
||||
eu: self.sessions[1].range(),
|
||||
us: self.sessions[2].range(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Indicator for SessionRange {
|
||||
type Input = Candle;
|
||||
type Output = SessionRangeOutput;
|
||||
|
||||
fn update(&mut self, candle: Candle) -> Option<SessionRangeOutput> {
|
||||
let civil = civil_from_timestamp(candle.timestamp, self.utc_offset_minutes);
|
||||
let key = (civil.year, civil.month, civil.day);
|
||||
if self.day_key != Some(key) {
|
||||
self.day_key = Some(key);
|
||||
self.sessions = [Extent::EMPTY; 3];
|
||||
}
|
||||
let session = (civil.hour / 8) as usize; // 0 Asia, 1 EU, 2 US
|
||||
self.sessions[session].add(candle);
|
||||
let out = self.snapshot();
|
||||
self.last = Some(out);
|
||||
Some(out)
|
||||
}
|
||||
|
||||
fn reset(&mut self) {
|
||||
self.day_key = None;
|
||||
self.sessions = [Extent::EMPTY; 3];
|
||||
self.last = None;
|
||||
}
|
||||
|
||||
fn warmup_period(&self) -> usize {
|
||||
1
|
||||
}
|
||||
|
||||
fn is_ready(&self) -> bool {
|
||||
self.last.is_some()
|
||||
}
|
||||
|
||||
fn name(&self) -> &'static str {
|
||||
"SessionRange"
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::traits::BatchExt;
|
||||
use approx::assert_relative_eq;
|
||||
|
||||
const HOUR: i64 = 3_600_000;
|
||||
|
||||
fn c(high: f64, low: f64, ts: i64) -> Candle {
|
||||
let mid = f64::midpoint(high, low);
|
||||
Candle::new(mid, high, low, mid, 1.0, ts).unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn metadata_and_accessors() {
|
||||
let sr = SessionRange::new(60);
|
||||
assert_eq!(sr.utc_offset_minutes(), 60);
|
||||
assert_eq!(sr.name(), "SessionRange");
|
||||
assert_eq!(sr.warmup_period(), 1);
|
||||
assert!(!sr.is_ready());
|
||||
assert!(sr.value().is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn assigns_bars_to_sessions() {
|
||||
let mut sr = SessionRange::new(0);
|
||||
let asia = sr.update(c(104.0, 98.0, 2 * HOUR)).unwrap();
|
||||
assert_relative_eq!(asia.asia, 6.0);
|
||||
assert_relative_eq!(asia.eu, 0.0);
|
||||
assert_relative_eq!(asia.us, 0.0);
|
||||
assert!(sr.is_ready());
|
||||
let eu = sr.update(c(110.0, 100.0, 10 * HOUR)).unwrap();
|
||||
assert_relative_eq!(eu.eu, 10.0);
|
||||
let us = sr.update(c(120.0, 118.0, 20 * HOUR)).unwrap();
|
||||
assert_relative_eq!(us.us, 2.0);
|
||||
assert_relative_eq!(us.asia, 6.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn widens_within_one_session() {
|
||||
let mut sr = SessionRange::new(0);
|
||||
sr.update(c(104.0, 98.0, HOUR));
|
||||
let wider = sr.update(c(106.0, 95.0, 3 * HOUR)).unwrap();
|
||||
assert_relative_eq!(wider.asia, 11.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resets_sessions_on_new_day() {
|
||||
let mut sr = SessionRange::new(0);
|
||||
sr.update(c(104.0, 98.0, 2 * HOUR));
|
||||
sr.update(c(110.0, 100.0, 10 * HOUR));
|
||||
let next = sr.update(c(101.0, 99.0, (24 + 2) * HOUR)).unwrap();
|
||||
assert_relative_eq!(next.asia, 2.0);
|
||||
assert_relative_eq!(next.eu, 0.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn utc_offset_moves_bar_between_sessions() {
|
||||
// 07:00 UTC is Asia; shifted +120 min it becomes 09:00 -> EU.
|
||||
let mut utc = SessionRange::new(0);
|
||||
let a = utc.update(c(104.0, 98.0, 7 * HOUR)).unwrap();
|
||||
assert_relative_eq!(a.asia, 6.0);
|
||||
assert_relative_eq!(a.eu, 0.0);
|
||||
|
||||
let mut shifted = SessionRange::new(120);
|
||||
let e = shifted.update(c(104.0, 98.0, 7 * HOUR)).unwrap();
|
||||
assert_relative_eq!(e.asia, 0.0);
|
||||
assert_relative_eq!(e.eu, 6.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reset_clears_state() {
|
||||
let mut sr = SessionRange::new(0);
|
||||
sr.update(c(104.0, 98.0, 2 * HOUR));
|
||||
sr.reset();
|
||||
assert!(!sr.is_ready());
|
||||
assert!(sr.value().is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn batch_equals_streaming() {
|
||||
let candles: Vec<Candle> = (0..40)
|
||||
.map(|i| {
|
||||
c(
|
||||
100.0 + f64::from(i % 5),
|
||||
95.0 - f64::from(i % 3),
|
||||
i64::from(i) * HOUR,
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
let mut a = SessionRange::new(0);
|
||||
let mut b = SessionRange::new(0);
|
||||
assert_eq!(
|
||||
a.batch(&candles),
|
||||
candles.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,199 @@
|
||||
//! Session VWAP — the volume-weighted average price accumulated since the start
|
||||
//! of the current calendar-day session, re-anchored automatically each day.
|
||||
|
||||
use crate::calendar::civil_from_timestamp;
|
||||
use crate::ohlcv::Candle;
|
||||
use crate::traits::Indicator;
|
||||
|
||||
/// Volume-weighted average price reset at each local day boundary.
|
||||
///
|
||||
/// Each bar contributes its typical price `(high + low + close) / 3` weighted by
|
||||
/// volume. The running VWAP is `Σ(typical · volume) / Σ volume` over the current
|
||||
/// session; if the session's volume is still zero the indicator falls back to the
|
||||
/// latest typical price so the output is always finite. The session boundary is
|
||||
/// the wall-clock day of [`Candle::timestamp`](crate::Candle) shifted by
|
||||
/// `utc_offset_minutes`.
|
||||
///
|
||||
/// Where [`crate::RollingVwap`] averages over a fixed bar window and
|
||||
/// [`crate::AnchoredVwap`] anchors at a caller-chosen bar, Session VWAP anchors
|
||||
/// at the automatically detected day open.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// use wickra_core::{Candle, Indicator, SessionVwap};
|
||||
///
|
||||
/// let hour = 3_600_000;
|
||||
/// let mut vwap = SessionVwap::new(0);
|
||||
/// // typical = 100, volume 10.
|
||||
/// vwap.update(Candle::new(100.0, 100.0, 100.0, 100.0, 10.0, 0).unwrap());
|
||||
/// // typical = 110, volume 30 -> VWAP = (100*10 + 110*30) / 40 = 107.5.
|
||||
/// let v = vwap.update(Candle::new(110.0, 110.0, 110.0, 110.0, 30.0, hour).unwrap()).unwrap();
|
||||
/// assert!((v - 107.5).abs() < 1e-9);
|
||||
/// ```
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct SessionVwap {
|
||||
utc_offset_minutes: i32,
|
||||
day_key: Option<(i64, u32, u32)>,
|
||||
cum_pv: f64,
|
||||
cum_volume: f64,
|
||||
last: Option<f64>,
|
||||
}
|
||||
|
||||
impl SessionVwap {
|
||||
/// Construct a Session VWAP indicator with the given UTC offset (minutes).
|
||||
pub const fn new(utc_offset_minutes: i32) -> Self {
|
||||
Self {
|
||||
utc_offset_minutes,
|
||||
day_key: None,
|
||||
cum_pv: 0.0,
|
||||
cum_volume: 0.0,
|
||||
last: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Configured UTC offset in minutes.
|
||||
pub const fn utc_offset_minutes(&self) -> i32 {
|
||||
self.utc_offset_minutes
|
||||
}
|
||||
|
||||
/// Most recent VWAP if at least one bar has been seen.
|
||||
pub const fn value(&self) -> Option<f64> {
|
||||
self.last
|
||||
}
|
||||
}
|
||||
|
||||
impl Indicator for SessionVwap {
|
||||
type Input = Candle;
|
||||
type Output = f64;
|
||||
|
||||
fn update(&mut self, candle: Candle) -> Option<f64> {
|
||||
let civil = civil_from_timestamp(candle.timestamp, self.utc_offset_minutes);
|
||||
let key = (civil.year, civil.month, civil.day);
|
||||
if self.day_key != Some(key) {
|
||||
self.day_key = Some(key);
|
||||
self.cum_pv = 0.0;
|
||||
self.cum_volume = 0.0;
|
||||
}
|
||||
let typical = (candle.high + candle.low + candle.close) / 3.0;
|
||||
self.cum_pv += typical * candle.volume;
|
||||
self.cum_volume += candle.volume;
|
||||
let vwap = if self.cum_volume > 0.0 {
|
||||
self.cum_pv / self.cum_volume
|
||||
} else {
|
||||
typical
|
||||
};
|
||||
self.last = Some(vwap);
|
||||
Some(vwap)
|
||||
}
|
||||
|
||||
fn reset(&mut self) {
|
||||
self.day_key = None;
|
||||
self.cum_pv = 0.0;
|
||||
self.cum_volume = 0.0;
|
||||
self.last = None;
|
||||
}
|
||||
|
||||
fn warmup_period(&self) -> usize {
|
||||
1
|
||||
}
|
||||
|
||||
fn is_ready(&self) -> bool {
|
||||
self.last.is_some()
|
||||
}
|
||||
|
||||
fn name(&self) -> &'static str {
|
||||
"SessionVwap"
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::traits::BatchExt;
|
||||
use approx::assert_relative_eq;
|
||||
|
||||
const HOUR: i64 = 3_600_000;
|
||||
|
||||
fn c(price: f64, volume: f64, ts: i64) -> Candle {
|
||||
Candle::new(price, price, price, price, volume, ts).unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn metadata_and_accessors() {
|
||||
let vwap = SessionVwap::new(-480);
|
||||
assert_eq!(vwap.utc_offset_minutes(), -480);
|
||||
assert_eq!(vwap.name(), "SessionVwap");
|
||||
assert_eq!(vwap.warmup_period(), 1);
|
||||
assert!(!vwap.is_ready());
|
||||
assert!(vwap.value().is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn volume_weights_the_average() {
|
||||
let mut vwap = SessionVwap::new(0);
|
||||
let first = vwap.update(c(100.0, 10.0, 0)).unwrap();
|
||||
assert_relative_eq!(first, 100.0);
|
||||
assert!(vwap.is_ready());
|
||||
let second = vwap.update(c(110.0, 30.0, HOUR)).unwrap();
|
||||
assert_relative_eq!(second, 107.5);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn zero_volume_session_falls_back_to_typical() {
|
||||
let mut vwap = SessionVwap::new(0);
|
||||
let v = vwap.update(c(100.0, 0.0, 0)).unwrap();
|
||||
assert_relative_eq!(v, 100.0);
|
||||
let v2 = vwap.update(c(120.0, 0.0, HOUR)).unwrap();
|
||||
assert_relative_eq!(v2, 120.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn re_anchors_on_new_day() {
|
||||
let mut vwap = SessionVwap::new(0);
|
||||
vwap.update(c(100.0, 10.0, 0));
|
||||
vwap.update(c(110.0, 30.0, HOUR));
|
||||
// New day: VWAP restarts from the first bar of day 2.
|
||||
let next = vwap.update(c(200.0, 5.0, 24 * HOUR)).unwrap();
|
||||
assert_relative_eq!(next, 200.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn typical_price_uses_high_low_close() {
|
||||
let mut vwap = SessionVwap::new(0);
|
||||
// typical = (120 + 90 + 102) / 3 = 104.
|
||||
let candle = Candle::new(100.0, 120.0, 90.0, 102.0, 10.0, 0).unwrap();
|
||||
let v = vwap.update(candle).unwrap();
|
||||
assert_relative_eq!(v, 104.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reset_clears_state() {
|
||||
let mut vwap = SessionVwap::new(0);
|
||||
vwap.update(c(100.0, 10.0, 0));
|
||||
vwap.reset();
|
||||
assert!(!vwap.is_ready());
|
||||
assert!(vwap.value().is_none());
|
||||
let after = vwap.update(c(50.0, 1.0, HOUR)).unwrap();
|
||||
assert_relative_eq!(after, 50.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn batch_equals_streaming() {
|
||||
let candles: Vec<Candle> = (0..30)
|
||||
.map(|i| {
|
||||
c(
|
||||
100.0 + f64::from(i),
|
||||
1.0 + f64::from(i % 4),
|
||||
i64::from(i) * HOUR,
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
let mut a = SessionVwap::new(0);
|
||||
let mut b = SessionVwap::new(0);
|
||||
assert_eq!(
|
||||
a.batch(&candles),
|
||||
candles.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,226 @@
|
||||
//! Time-of-Day Return Profile — the mean bar return in each intraday time bucket.
|
||||
|
||||
use crate::calendar::civil_from_timestamp;
|
||||
use crate::error::{Error, Result};
|
||||
use crate::ohlcv::Candle;
|
||||
use crate::traits::Indicator;
|
||||
|
||||
/// Time-of-Day Return Profile output: the per-bucket mean return.
|
||||
///
|
||||
/// `bins[i]` is the mean simple return of all bars whose local time-of-day fell
|
||||
/// in bucket `i`, where bucket `i` spans the minutes
|
||||
/// `[i * 1440 / bins.len(), (i + 1) * 1440 / bins.len())`. Empty buckets read
|
||||
/// `0.0`.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct TimeOfDayReturnProfileOutput {
|
||||
/// Per-bucket mean return, earliest bucket first. Length equals `buckets`.
|
||||
pub bins: Vec<f64>,
|
||||
}
|
||||
|
||||
/// Mean bar return bucketed by local time of day.
|
||||
///
|
||||
/// The local day (the wall-clock day of [`Candle::timestamp`](crate::Candle)
|
||||
/// shifted by `utc_offset_minutes`) is divided into `buckets` equal slices. Each
|
||||
/// bar's simple return `close / previous_close - 1` is accumulated into the bucket
|
||||
/// of its time-of-day, and the profile reports the running mean per bucket. The
|
||||
/// first bar produces no output (no return yet).
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// use wickra_core::{Candle, Indicator, TimeOfDayReturnProfile};
|
||||
///
|
||||
/// let hour = 3_600_000;
|
||||
/// let mut prof = TimeOfDayReturnProfile::new(24, 0).unwrap();
|
||||
/// assert!(prof.update(Candle::new(100.0, 100.0, 100.0, 100.0, 1.0, 0).unwrap()).is_none());
|
||||
/// let out = prof.update(Candle::new(101.0, 101.0, 101.0, 101.0, 1.0, hour).unwrap()).unwrap();
|
||||
/// assert_eq!(out.bins.len(), 24);
|
||||
/// ```
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct TimeOfDayReturnProfile {
|
||||
buckets: usize,
|
||||
utc_offset_minutes: i32,
|
||||
prev_close: Option<f64>,
|
||||
sum: Vec<f64>,
|
||||
count: Vec<u64>,
|
||||
last: Option<TimeOfDayReturnProfileOutput>,
|
||||
}
|
||||
|
||||
impl TimeOfDayReturnProfile {
|
||||
/// Construct a Time-of-Day Return Profile with `buckets` intraday slices.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns [`Error::PeriodZero`] if `buckets == 0`.
|
||||
pub fn new(buckets: usize, utc_offset_minutes: i32) -> Result<Self> {
|
||||
if buckets == 0 {
|
||||
return Err(Error::PeriodZero);
|
||||
}
|
||||
Ok(Self {
|
||||
buckets,
|
||||
utc_offset_minutes,
|
||||
prev_close: None,
|
||||
sum: vec![0.0; buckets],
|
||||
count: vec![0; buckets],
|
||||
last: None,
|
||||
})
|
||||
}
|
||||
|
||||
/// Configured `(buckets, utc_offset_minutes)`.
|
||||
pub const fn params(&self) -> (usize, i32) {
|
||||
(self.buckets, self.utc_offset_minutes)
|
||||
}
|
||||
|
||||
/// Most recent profile if at least one return has been recorded.
|
||||
pub fn value(&self) -> Option<&TimeOfDayReturnProfileOutput> {
|
||||
self.last.as_ref()
|
||||
}
|
||||
|
||||
fn bucket_of(&self, minute_of_day: u32) -> usize {
|
||||
let raw = (minute_of_day as usize * self.buckets) / 1440;
|
||||
raw.min(self.buckets - 1)
|
||||
}
|
||||
|
||||
fn snapshot(&self) -> TimeOfDayReturnProfileOutput {
|
||||
let bins = self
|
||||
.sum
|
||||
.iter()
|
||||
.zip(&self.count)
|
||||
.map(|(total, n)| if *n > 0 { total / *n as f64 } else { 0.0 })
|
||||
.collect();
|
||||
TimeOfDayReturnProfileOutput { bins }
|
||||
}
|
||||
}
|
||||
|
||||
impl Indicator for TimeOfDayReturnProfile {
|
||||
type Input = Candle;
|
||||
type Output = TimeOfDayReturnProfileOutput;
|
||||
|
||||
fn update(&mut self, candle: Candle) -> Option<TimeOfDayReturnProfileOutput> {
|
||||
let civil = civil_from_timestamp(candle.timestamp, self.utc_offset_minutes);
|
||||
let result = if let Some(prev) = self.prev_close {
|
||||
let ret = if prev == 0.0 {
|
||||
0.0
|
||||
} else {
|
||||
candle.close / prev - 1.0
|
||||
};
|
||||
let bucket = self.bucket_of(civil.minute_of_day());
|
||||
self.sum[bucket] += ret;
|
||||
self.count[bucket] += 1;
|
||||
let out = self.snapshot();
|
||||
self.last = Some(out.clone());
|
||||
Some(out)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
self.prev_close = Some(candle.close);
|
||||
result
|
||||
}
|
||||
|
||||
fn reset(&mut self) {
|
||||
self.prev_close = None;
|
||||
self.sum.iter_mut().for_each(|x| *x = 0.0);
|
||||
self.count.iter_mut().for_each(|x| *x = 0);
|
||||
self.last = None;
|
||||
}
|
||||
|
||||
fn warmup_period(&self) -> usize {
|
||||
2
|
||||
}
|
||||
|
||||
fn is_ready(&self) -> bool {
|
||||
self.last.is_some()
|
||||
}
|
||||
|
||||
fn name(&self) -> &'static str {
|
||||
"TimeOfDayReturnProfile"
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::traits::BatchExt;
|
||||
use approx::assert_relative_eq;
|
||||
|
||||
const HOUR: i64 = 3_600_000;
|
||||
|
||||
fn c(close: f64, ts: i64) -> Candle {
|
||||
Candle::new(close, close, close, close, 1.0, ts).unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_zero_buckets() {
|
||||
assert!(matches!(
|
||||
TimeOfDayReturnProfile::new(0, 0),
|
||||
Err(Error::PeriodZero)
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn metadata_and_accessors() {
|
||||
let prof = TimeOfDayReturnProfile::new(24, -300).unwrap();
|
||||
assert_eq!(prof.params(), (24, -300));
|
||||
assert_eq!(prof.name(), "TimeOfDayReturnProfile");
|
||||
assert_eq!(prof.warmup_period(), 2);
|
||||
assert!(!prof.is_ready());
|
||||
assert!(prof.value().is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn buckets_by_hour_and_means_returns() {
|
||||
let mut prof = TimeOfDayReturnProfile::new(24, 0).unwrap();
|
||||
assert!(prof.update(c(100.0, 0)).is_none()); // 00:00, no return
|
||||
// 01:00 return +0.01 -> bucket 1.
|
||||
let out = prof.update(c(101.0, HOUR)).unwrap();
|
||||
assert_eq!(out.bins.len(), 24);
|
||||
assert_relative_eq!(out.bins[1], 0.01);
|
||||
assert_relative_eq!(out.bins[0], 0.0);
|
||||
assert!(prof.is_ready());
|
||||
// 01:00 next day, return -> averages into bucket 1.
|
||||
let out = prof.update(c(102.01, 25 * HOUR)).unwrap();
|
||||
// two returns in bucket 1: 0.01 and 0.01 -> mean 0.01.
|
||||
assert_relative_eq!(out.bins[1], 0.01);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn last_bucket_clamped_for_end_of_day() {
|
||||
let mut prof = TimeOfDayReturnProfile::new(24, 0).unwrap();
|
||||
prof.update(c(100.0, 23 * HOUR));
|
||||
// 23:59 -> minute 1439 -> bucket min(23, 23) = 23.
|
||||
let out = prof.update(c(110.0, 23 * HOUR + 59 * 60_000)).unwrap();
|
||||
assert_relative_eq!(out.bins[23], 0.10);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn zero_prev_close_uses_zero_return() {
|
||||
let mut prof = TimeOfDayReturnProfile::new(4, 0).unwrap();
|
||||
prof.update(c(0.0, 0));
|
||||
let out = prof.update(c(5.0, HOUR)).unwrap();
|
||||
assert_relative_eq!(out.bins[0], 0.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reset_clears_state() {
|
||||
let mut prof = TimeOfDayReturnProfile::new(24, 0).unwrap();
|
||||
prof.update(c(100.0, 0));
|
||||
prof.update(c(101.0, HOUR));
|
||||
prof.reset();
|
||||
assert!(!prof.is_ready());
|
||||
assert!(prof.value().is_none());
|
||||
assert!(prof.update(c(100.0, 2 * HOUR)).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn batch_equals_streaming() {
|
||||
let candles: Vec<Candle> = (0..50)
|
||||
.map(|i| c(100.0 + f64::from(i % 7), i64::from(i) * HOUR))
|
||||
.collect();
|
||||
let mut a = TimeOfDayReturnProfile::new(12, 0).unwrap();
|
||||
let mut b = TimeOfDayReturnProfile::new(12, 0).unwrap();
|
||||
assert_eq!(
|
||||
a.batch(&candles),
|
||||
candles.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,275 @@
|
||||
//! Turn-of-Month Effect — the mean daily return of sessions that fall inside the
|
||||
//! turn-of-month window (the last `n_last` and first `n_first` days of a month).
|
||||
|
||||
use crate::calendar::{civil_from_timestamp, days_in_month};
|
||||
use crate::error::{Error, Result};
|
||||
use crate::ohlcv::Candle;
|
||||
use crate::traits::Indicator;
|
||||
|
||||
/// Whether a day-of-month lies in the turn-of-month window.
|
||||
///
|
||||
/// The window is the first `n_first` calendar days plus the last `n_last` days of
|
||||
/// the month (`days_in_month - n_last < dom`).
|
||||
fn in_turn_window(dom: u32, dim: u32, n_first: u32, n_last: u32) -> bool {
|
||||
dom <= n_first || dom > dim.saturating_sub(n_last)
|
||||
}
|
||||
|
||||
/// Turn-of-Month effect: the running mean of daily close-to-close returns for the
|
||||
/// sessions that fall in the turn-of-month window.
|
||||
///
|
||||
/// Each completed session (the wall-clock day of
|
||||
/// [`Candle::timestamp`](crate::Candle) shifted by `utc_offset_minutes`)
|
||||
/// contributes its return `close / previous_close - 1`. Only sessions whose
|
||||
/// day-of-month is within the first `n_first` or last `n_last` days of their month
|
||||
/// are averaged; the rest are ignored. The classic effect uses `n_first = 3`,
|
||||
/// `n_last = 1`.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// use wickra_core::{Candle, Indicator, TurnOfMonth};
|
||||
///
|
||||
/// let day = 24 * 3_600_000;
|
||||
/// // 2021-01-29 .. 02-02 — all turn-of-month days with n_first=3, n_last=1.
|
||||
/// let mut tom = TurnOfMonth::new(3, 1, 0).unwrap();
|
||||
/// let start = 1_611_878_400_000; // 2021-01-29 00:00 UTC
|
||||
/// let mut last = None;
|
||||
/// for (i, close) in [100.0, 101.0, 102.0, 103.0].iter().enumerate() {
|
||||
/// let ts = start + i as i64 * day;
|
||||
/// last = tom.update(Candle::new(*close, *close, *close, *close, 1.0, ts).unwrap());
|
||||
/// }
|
||||
/// assert!(last.is_some());
|
||||
/// ```
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct TurnOfMonth {
|
||||
n_first: u32,
|
||||
n_last: u32,
|
||||
utc_offset_minutes: i32,
|
||||
day: Option<(i64, u32, u32)>,
|
||||
cur_close: f64,
|
||||
prev_day_close: Option<f64>,
|
||||
sum: f64,
|
||||
count: u64,
|
||||
}
|
||||
|
||||
impl TurnOfMonth {
|
||||
/// Construct a Turn-of-Month indicator.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns [`Error::PeriodZero`] if both `n_first` and `n_last` are zero (the
|
||||
/// window would never include a day).
|
||||
pub fn new(n_first: u32, n_last: u32, utc_offset_minutes: i32) -> Result<Self> {
|
||||
if n_first == 0 && n_last == 0 {
|
||||
return Err(Error::PeriodZero);
|
||||
}
|
||||
Ok(Self {
|
||||
n_first,
|
||||
n_last,
|
||||
utc_offset_minutes,
|
||||
day: None,
|
||||
cur_close: 0.0,
|
||||
prev_day_close: None,
|
||||
sum: 0.0,
|
||||
count: 0,
|
||||
})
|
||||
}
|
||||
|
||||
/// Classic turn-of-month window: first 3 and last 1 day of the month.
|
||||
pub fn classic() -> Self {
|
||||
Self::new(3, 1, 0).expect("classic turn-of-month window is valid")
|
||||
}
|
||||
|
||||
/// Configured `(n_first, n_last, utc_offset_minutes)`.
|
||||
pub const fn params(&self) -> (u32, u32, i32) {
|
||||
(self.n_first, self.n_last, self.utc_offset_minutes)
|
||||
}
|
||||
|
||||
/// Most recent mean turn-of-month return if any in-window day has completed.
|
||||
pub fn value(&self) -> Option<f64> {
|
||||
if self.count == 0 {
|
||||
None
|
||||
} else {
|
||||
Some(self.sum / self.count as f64)
|
||||
}
|
||||
}
|
||||
|
||||
/// Settle the just-finished day `(year, month, dom)` whose last close is
|
||||
/// `self.cur_close`, then start `next_key`.
|
||||
fn roll_into(
|
||||
&mut self,
|
||||
year: i64,
|
||||
month: u32,
|
||||
dom: u32,
|
||||
next_key: (i64, u32, u32),
|
||||
close: f64,
|
||||
) {
|
||||
if let Some(prev) = self.prev_day_close {
|
||||
let ret = if prev == 0.0 {
|
||||
0.0
|
||||
} else {
|
||||
self.cur_close / prev - 1.0
|
||||
};
|
||||
if in_turn_window(dom, days_in_month(year, month), self.n_first, self.n_last) {
|
||||
self.sum += ret;
|
||||
self.count += 1;
|
||||
}
|
||||
}
|
||||
self.prev_day_close = Some(self.cur_close);
|
||||
self.day = Some(next_key);
|
||||
self.cur_close = close;
|
||||
}
|
||||
}
|
||||
|
||||
impl Indicator for TurnOfMonth {
|
||||
type Input = Candle;
|
||||
type Output = f64;
|
||||
|
||||
fn update(&mut self, candle: Candle) -> Option<f64> {
|
||||
let civil = civil_from_timestamp(candle.timestamp, self.utc_offset_minutes);
|
||||
let key = (civil.year, civil.month, civil.day);
|
||||
match self.day {
|
||||
Some(prev) if prev == key => {
|
||||
self.cur_close = candle.close;
|
||||
}
|
||||
Some((year, month, dom)) => {
|
||||
self.roll_into(year, month, dom, key, candle.close);
|
||||
}
|
||||
None => {
|
||||
self.day = Some(key);
|
||||
self.cur_close = candle.close;
|
||||
}
|
||||
}
|
||||
self.value()
|
||||
}
|
||||
|
||||
fn reset(&mut self) {
|
||||
self.day = None;
|
||||
self.cur_close = 0.0;
|
||||
self.prev_day_close = None;
|
||||
self.sum = 0.0;
|
||||
self.count = 0;
|
||||
}
|
||||
|
||||
fn warmup_period(&self) -> usize {
|
||||
2
|
||||
}
|
||||
|
||||
fn is_ready(&self) -> bool {
|
||||
self.count > 0
|
||||
}
|
||||
|
||||
fn name(&self) -> &'static str {
|
||||
"TurnOfMonth"
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::traits::BatchExt;
|
||||
use approx::assert_relative_eq;
|
||||
|
||||
const DAY: i64 = 24 * 3_600_000;
|
||||
// 2021-01-28 00:00 UTC.
|
||||
const JAN28_2021: i64 = 1_611_792_000_000;
|
||||
|
||||
fn c(close: f64, ts: i64) -> Candle {
|
||||
Candle::new(close, close, close, close, 1.0, ts).unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn window_predicate_branches() {
|
||||
// First-days branch.
|
||||
assert!(in_turn_window(1, 31, 3, 1));
|
||||
assert!(in_turn_window(3, 31, 3, 1));
|
||||
assert!(!in_turn_window(4, 31, 3, 1));
|
||||
// Last-days branch.
|
||||
assert!(in_turn_window(31, 31, 3, 1));
|
||||
assert!(!in_turn_window(30, 31, 3, 1));
|
||||
// Saturating subtraction when n_last exceeds the month length.
|
||||
assert!(in_turn_window(1, 28, 0, 40));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_empty_window() {
|
||||
assert!(matches!(TurnOfMonth::new(0, 0, 0), Err(Error::PeriodZero)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn metadata_and_accessors() {
|
||||
let tom = TurnOfMonth::classic();
|
||||
assert_eq!(tom.params(), (3, 1, 0));
|
||||
assert_eq!(tom.name(), "TurnOfMonth");
|
||||
assert_eq!(tom.warmup_period(), 2);
|
||||
assert!(!tom.is_ready());
|
||||
assert!(tom.value().is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn averages_in_window_returns_only() {
|
||||
let mut tom = TurnOfMonth::new(3, 1, 0).unwrap();
|
||||
// 2021-01-28 (out of window, no prior close): close 100.
|
||||
assert!(tom.update(c(100.0, JAN28_2021)).is_none());
|
||||
// 2021-01-29 (out of window: dom 29, dim 31 -> 29 <= 30): return ignored.
|
||||
assert!(tom.update(c(110.0, JAN28_2021 + DAY)).is_none());
|
||||
// 2021-01-30 (out of window): completes 01-29; still none.
|
||||
assert!(tom.update(c(120.0, JAN28_2021 + 2 * DAY)).is_none());
|
||||
// 2021-01-31 (last day, in window): completes 01-30 (out). Still none.
|
||||
assert!(tom.update(c(121.0, JAN28_2021 + 3 * DAY)).is_none());
|
||||
// 2021-02-01 (first day, in window): completes 01-31 (in window).
|
||||
// return = 121 / 120 - 1.
|
||||
let v = tom.update(c(130.0, JAN28_2021 + 4 * DAY)).unwrap();
|
||||
assert_relative_eq!(v, 121.0 / 120.0 - 1.0);
|
||||
assert!(tom.is_ready());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn zero_prev_close_contributes_zero() {
|
||||
let mut tom = TurnOfMonth::new(3, 1, 0).unwrap();
|
||||
// 2021-01-30 closes at 0 — becomes the prior close for 01-31.
|
||||
tom.update(c(0.0, JAN28_2021 + 2 * DAY));
|
||||
// 2021-01-31 (last day, in window): finalizes 01-30 with no prior -> no
|
||||
// contribution, but records prev_day_close = 0.
|
||||
tom.update(c(5.0, JAN28_2021 + 3 * DAY));
|
||||
// 2021-02-01 (in window): finalizes 01-31 with prev_close 0 -> ret 0.
|
||||
let v = tom.update(c(50.0, JAN28_2021 + 4 * DAY)).unwrap();
|
||||
assert_relative_eq!(v, 0.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn same_day_bars_use_latest_close() {
|
||||
let mut tom = TurnOfMonth::new(3, 1, 0).unwrap();
|
||||
// 2021-01-30 closes at 100 (prior day, sets prev_day_close).
|
||||
tom.update(c(100.0, JAN28_2021 + 2 * DAY));
|
||||
// 2021-01-31 two bars on the same day; the later close (120) wins.
|
||||
tom.update(c(110.0, JAN28_2021 + 3 * DAY));
|
||||
tom.update(c(120.0, JAN28_2021 + 3 * DAY + 3_600_000));
|
||||
// 2021-02-01 (in window) finalizes 01-31: return = 120 / 100 - 1 = 0.20.
|
||||
let v = tom.update(c(130.0, JAN28_2021 + 4 * DAY)).unwrap();
|
||||
assert_relative_eq!(v, 0.20);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reset_clears_state() {
|
||||
let mut tom = TurnOfMonth::new(3, 1, 0).unwrap();
|
||||
tom.update(c(121.0, JAN28_2021 + 3 * DAY));
|
||||
tom.update(c(130.0, JAN28_2021 + 4 * DAY));
|
||||
tom.reset();
|
||||
assert!(!tom.is_ready());
|
||||
assert!(tom.value().is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn batch_equals_streaming() {
|
||||
let candles: Vec<Candle> = (0..40)
|
||||
.map(|i| c(100.0 + f64::from(i), JAN28_2021 + i64::from(i) * DAY))
|
||||
.collect();
|
||||
let mut a = TurnOfMonth::new(3, 2, 0).unwrap();
|
||||
let mut b = TurnOfMonth::new(3, 2, 0).unwrap();
|
||||
assert_eq!(
|
||||
a.batch(&candles),
|
||||
candles.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,198 @@
|
||||
//! Volume-by-Time Profile — the mean traded volume in each intraday bucket.
|
||||
|
||||
use crate::calendar::civil_from_timestamp;
|
||||
use crate::error::{Error, Result};
|
||||
use crate::ohlcv::Candle;
|
||||
use crate::traits::Indicator;
|
||||
|
||||
/// Volume-by-Time Profile output: the per-bucket mean volume.
|
||||
///
|
||||
/// `bins[i]` is the mean volume of all bars whose local time-of-day fell in
|
||||
/// bucket `i`. Empty buckets read `0.0`.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct VolumeByTimeProfileOutput {
|
||||
/// Per-bucket mean volume, earliest bucket first. Length equals `buckets`.
|
||||
pub bins: Vec<f64>,
|
||||
}
|
||||
|
||||
/// Mean traded volume bucketed by local time of day.
|
||||
///
|
||||
/// The local day (the wall-clock day of [`Candle::timestamp`](crate::Candle)
|
||||
/// shifted by `utc_offset_minutes`) is split into `buckets` equal slices. Each
|
||||
/// bar's volume is accumulated into the bucket of its time-of-day, and the
|
||||
/// profile reports the running mean volume per bucket. Unlike the return
|
||||
/// profiles, the first bar already produces output (volume needs no prior bar).
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// use wickra_core::{Candle, Indicator, VolumeByTimeProfile};
|
||||
///
|
||||
/// let hour = 3_600_000;
|
||||
/// let mut prof = VolumeByTimeProfile::new(24, 0).unwrap();
|
||||
/// let out = prof.update(Candle::new(100.0, 100.0, 100.0, 100.0, 500.0, hour).unwrap()).unwrap();
|
||||
/// assert_eq!(out.bins.len(), 24);
|
||||
/// assert_eq!(out.bins[1], 500.0);
|
||||
/// ```
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct VolumeByTimeProfile {
|
||||
buckets: usize,
|
||||
utc_offset_minutes: i32,
|
||||
sum: Vec<f64>,
|
||||
count: Vec<u64>,
|
||||
last: Option<VolumeByTimeProfileOutput>,
|
||||
}
|
||||
|
||||
impl VolumeByTimeProfile {
|
||||
/// Construct a Volume-by-Time Profile with `buckets` intraday slices.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns [`Error::PeriodZero`] if `buckets == 0`.
|
||||
pub fn new(buckets: usize, utc_offset_minutes: i32) -> Result<Self> {
|
||||
if buckets == 0 {
|
||||
return Err(Error::PeriodZero);
|
||||
}
|
||||
Ok(Self {
|
||||
buckets,
|
||||
utc_offset_minutes,
|
||||
sum: vec![0.0; buckets],
|
||||
count: vec![0; buckets],
|
||||
last: None,
|
||||
})
|
||||
}
|
||||
|
||||
/// Configured `(buckets, utc_offset_minutes)`.
|
||||
pub const fn params(&self) -> (usize, i32) {
|
||||
(self.buckets, self.utc_offset_minutes)
|
||||
}
|
||||
|
||||
/// Most recent profile if at least one bar has been seen.
|
||||
pub fn value(&self) -> Option<&VolumeByTimeProfileOutput> {
|
||||
self.last.as_ref()
|
||||
}
|
||||
|
||||
fn bucket_of(&self, minute_of_day: u32) -> usize {
|
||||
let raw = (minute_of_day as usize * self.buckets) / 1440;
|
||||
raw.min(self.buckets - 1)
|
||||
}
|
||||
|
||||
fn snapshot(&self) -> VolumeByTimeProfileOutput {
|
||||
let bins = self
|
||||
.sum
|
||||
.iter()
|
||||
.zip(&self.count)
|
||||
.map(|(total, n)| if *n > 0 { total / *n as f64 } else { 0.0 })
|
||||
.collect();
|
||||
VolumeByTimeProfileOutput { bins }
|
||||
}
|
||||
}
|
||||
|
||||
impl Indicator for VolumeByTimeProfile {
|
||||
type Input = Candle;
|
||||
type Output = VolumeByTimeProfileOutput;
|
||||
|
||||
fn update(&mut self, candle: Candle) -> Option<VolumeByTimeProfileOutput> {
|
||||
let civil = civil_from_timestamp(candle.timestamp, self.utc_offset_minutes);
|
||||
let bucket = self.bucket_of(civil.minute_of_day());
|
||||
self.sum[bucket] += candle.volume;
|
||||
self.count[bucket] += 1;
|
||||
let out = self.snapshot();
|
||||
self.last = Some(out.clone());
|
||||
Some(out)
|
||||
}
|
||||
|
||||
fn reset(&mut self) {
|
||||
self.sum.iter_mut().for_each(|x| *x = 0.0);
|
||||
self.count.iter_mut().for_each(|x| *x = 0);
|
||||
self.last = None;
|
||||
}
|
||||
|
||||
fn warmup_period(&self) -> usize {
|
||||
1
|
||||
}
|
||||
|
||||
fn is_ready(&self) -> bool {
|
||||
self.last.is_some()
|
||||
}
|
||||
|
||||
fn name(&self) -> &'static str {
|
||||
"VolumeByTimeProfile"
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::traits::BatchExt;
|
||||
use approx::assert_relative_eq;
|
||||
|
||||
const HOUR: i64 = 3_600_000;
|
||||
|
||||
fn c(volume: f64, ts: i64) -> Candle {
|
||||
Candle::new(100.0, 100.0, 100.0, 100.0, volume, ts).unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_zero_buckets() {
|
||||
assert!(matches!(
|
||||
VolumeByTimeProfile::new(0, 0),
|
||||
Err(Error::PeriodZero)
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn metadata_and_accessors() {
|
||||
let prof = VolumeByTimeProfile::new(24, -60).unwrap();
|
||||
assert_eq!(prof.params(), (24, -60));
|
||||
assert_eq!(prof.name(), "VolumeByTimeProfile");
|
||||
assert_eq!(prof.warmup_period(), 1);
|
||||
assert!(!prof.is_ready());
|
||||
assert!(prof.value().is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn emits_from_first_bar_and_means_volume() {
|
||||
let mut prof = VolumeByTimeProfile::new(24, 0).unwrap();
|
||||
let out = prof.update(c(500.0, HOUR)).unwrap(); // 01:00 -> bucket 1
|
||||
assert_eq!(out.bins.len(), 24);
|
||||
assert_relative_eq!(out.bins[1], 500.0);
|
||||
assert_relative_eq!(out.bins[0], 0.0);
|
||||
assert!(prof.is_ready());
|
||||
// Next day 01:00, volume 700 -> mean (500 + 700) / 2 = 600.
|
||||
let out = prof.update(c(700.0, 25 * HOUR)).unwrap();
|
||||
assert_relative_eq!(out.bins[1], 600.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn last_bucket_clamped() {
|
||||
let mut prof = VolumeByTimeProfile::new(24, 0).unwrap();
|
||||
// 23:59 -> minute 1439 -> bucket 23.
|
||||
let out = prof.update(c(300.0, 23 * HOUR + 59 * 60_000)).unwrap();
|
||||
assert_relative_eq!(out.bins[23], 300.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reset_clears_state() {
|
||||
let mut prof = VolumeByTimeProfile::new(24, 0).unwrap();
|
||||
prof.update(c(500.0, HOUR));
|
||||
prof.reset();
|
||||
assert!(!prof.is_ready());
|
||||
assert!(prof.value().is_none());
|
||||
let out = prof.update(c(100.0, 2 * HOUR)).unwrap();
|
||||
assert_relative_eq!(out.bins[2], 100.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn batch_equals_streaming() {
|
||||
let candles: Vec<Candle> = (0..50)
|
||||
.map(|i| c(100.0 + f64::from(i % 8), i64::from(i) * HOUR))
|
||||
.collect();
|
||||
let mut a = VolumeByTimeProfile::new(12, 0).unwrap();
|
||||
let mut b = VolumeByTimeProfile::new(12, 0).unwrap();
|
||||
assert_eq!(
|
||||
a.batch(&candles),
|
||||
candles.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -42,6 +42,7 @@
|
||||
// builds — library code is still linted for genuinely large stack arrays.
|
||||
#![cfg_attr(test, allow(clippy::large_stack_arrays))]
|
||||
|
||||
mod calendar;
|
||||
mod cross_section;
|
||||
mod derivatives;
|
||||
mod error;
|
||||
@@ -59,17 +60,18 @@ pub use indicators::{
|
||||
AcceleratorOscillator, AdOscillator, AdVolumeLine, AdaptiveCycle, Adl, AdvanceBlock,
|
||||
AdvanceDecline, AdvanceDeclineRatio, Adx, AdxOutput, Adxr, Alligator, AlligatorOutput, Alma,
|
||||
Alpha, AnchoredRsi, AnchoredVwap, Apo, Aroon, AroonOscillator, AroonOutput, Atr, AtrBands,
|
||||
AtrBandsOutput, AtrTrailingStop, Autocorrelation, AverageDrawdown, AvgPrice, AwesomeOscillator,
|
||||
AwesomeOscillatorHistogram, BalanceOfPower, BeltHold, Beta, BetaNeutralSpread, BollingerBands,
|
||||
BollingerBandwidth, BollingerOutput, BreadthThrust, Breakaway, BullishPercentIndex,
|
||||
CalendarSpread, CalmarRatio, Camarilla, CamarillaPivotsOutput, Cci, CenterOfGravity, Cfo,
|
||||
ChaikinMoneyFlow, ChaikinOscillator, ChaikinVolatility, ChandeKrollStop, ChandeKrollStopOutput,
|
||||
ChandelierExit, ChandelierExitOutput, ChoppinessIndex, ClassicPivots, ClassicPivotsOutput,
|
||||
ClosingMarubozu, Cmo, CoefficientOfVariation, Cointegration, CointegrationOutput,
|
||||
ConcealingBabySwallow, ConditionalValueAtRisk, ConnorsRsi, Coppock, Counterattack,
|
||||
CumulativeVolumeDelta, CumulativeVolumeIndex, CyberneticCycle, Decycler, DecyclerOscillator,
|
||||
Dema, DemandIndex, DemarkPivots, DemarkPivotsOutput, DepthSlope, DetrendedStdDev, DistanceSsd,
|
||||
Doji, DojiStar, Donchian, DonchianOutput, DonchianStop, DonchianStopOutput, DoubleBollinger,
|
||||
AtrBandsOutput, AtrTrailingStop, Autocorrelation, AverageDailyRange, AverageDrawdown, AvgPrice,
|
||||
AwesomeOscillator, AwesomeOscillatorHistogram, BalanceOfPower, BeltHold, Beta,
|
||||
BetaNeutralSpread, BollingerBands, BollingerBandwidth, BollingerOutput, BreadthThrust,
|
||||
Breakaway, BullishPercentIndex, CalendarSpread, CalmarRatio, Camarilla, CamarillaPivotsOutput,
|
||||
Cci, CenterOfGravity, Cfo, ChaikinMoneyFlow, ChaikinOscillator, ChaikinVolatility,
|
||||
ChandeKrollStop, ChandeKrollStopOutput, ChandelierExit, ChandelierExitOutput, ChoppinessIndex,
|
||||
ClassicPivots, ClassicPivotsOutput, ClosingMarubozu, Cmo, CoefficientOfVariation,
|
||||
Cointegration, CointegrationOutput, ConcealingBabySwallow, ConditionalValueAtRisk, ConnorsRsi,
|
||||
Coppock, Counterattack, CumulativeVolumeDelta, CumulativeVolumeIndex, CyberneticCycle,
|
||||
DayOfWeekProfile, DayOfWeekProfileOutput, Decycler, DecyclerOscillator, Dema, DemandIndex,
|
||||
DemarkPivots, DemarkPivotsOutput, DepthSlope, DetrendedStdDev, DistanceSsd, Doji, DojiStar,
|
||||
Donchian, DonchianOutput, DonchianStop, DonchianStopOutput, DoubleBollinger,
|
||||
DoubleBollingerOutput, DownsideGapThreeMethods, Dpo, DragonflyDoji, DrawdownDuration, Dx,
|
||||
EaseOfMovement, EffectiveSpread, EhlersStochastic, ElderImpulse, Ema,
|
||||
EmpiricalModeDecomposition, Engulfing, EveningDojiStar, Evwma, FallingThreeMethods, Fama,
|
||||
@@ -81,44 +83,48 @@ pub use indicators::{
|
||||
HistoricalVolatility, Hma, HomingPigeon, HtDcPhase, HtPhasor, HtPhasorOutput, HtTrendMode,
|
||||
HurstChannel, HurstChannelOutput, HurstExponent, Ichimoku, IchimokuOutput, IdenticalThreeCrows,
|
||||
InNeck, Inertia, InformationRatio, InitialBalance, InitialBalanceOutput,
|
||||
InstantaneousTrendline, InverseFisherTransform, InvertedHammer, Jma, KagiBars,
|
||||
KalmanHedgeRatio, KalmanHedgeRatioOutput, Kama, KellyCriterion, Keltner, KeltnerOutput,
|
||||
Kicking, KickingByLength, Kst, KstOutput, Kurtosis, Kvo, KylesLambda, LadderBottom,
|
||||
LaguerreRsi, LeadLagCrossCorrelation, LeadLagCrossCorrelationOutput, LinRegAngle,
|
||||
LinRegChannel, LinRegChannelOutput, LinRegIntercept, LinRegSlope, LinearRegression,
|
||||
LiquidationFeatures, LiquidationFeaturesOutput, LongLeggedDoji, LongLine, LongShortRatio,
|
||||
MaEnvelope, MaEnvelopeOutput, MacdExt, MacdFix, MacdIndicator, MacdOutput, Mama, MamaOutput,
|
||||
MarketFacilitationIndex, Marubozu, MassIndex, MatHold, MatchingLow, MaxDrawdown,
|
||||
McClellanOscillator, McClellanSummationIndex, McGinleyDynamic, MedianAbsoluteDeviation,
|
||||
MedianPrice, Mfi, Microprice, MidPoint, MidPrice, MinusDi, MinusDm, Mom, MorningDojiStar,
|
||||
MorningEveningStar, Natr, NewHighsNewLows, Nvi, OIPriceDivergence, OIWeighted, Obv, OmegaRatio,
|
||||
OnNeck, OpenInterestDelta, OpeningMarubozu, OpeningRange, OpeningRangeOutput,
|
||||
OrderBookImbalanceFull, OrderBookImbalanceTop1, OrderBookImbalanceTopN, OuHalfLife, PainIndex,
|
||||
PairSpreadZScore, PairwiseBeta, ParkinsonVolatility, PearsonCorrelation, PercentAboveMa,
|
||||
PercentB, PercentageTrailingStop, Pgo, PiercingDarkCloud, PlusDi, PlusDm, Pmo,
|
||||
PointAndFigureBars, Ppo, ProfitFactor, Psar, Pvi, QuotedSpread, RSquared, RealizedSpread,
|
||||
RecoveryFactor, RelativeStrengthAB, RelativeStrengthOutput, RenkoBars, RenkoTrailingStop,
|
||||
RickshawMan, RisingThreeMethods, Roc, Rocp, Rocr, Rocr100, RogersSatchellVolatility,
|
||||
RollingCorrelation, RollingCovariance, RollingVwap, RoofingFilter, Rsi, Rvi, RviVolatility,
|
||||
Rwi, RwiOutput, SarExt, SeparatingLines, SharpeRatio, ShootingStar, ShortLine, SignedVolume,
|
||||
SineWave, Skewness, Sma, Smi, Smma, SortinoRatio, SpearmanCorrelation, SpinningTop,
|
||||
SpreadBollingerBands, SpreadBollingerBandsOutput, SpreadHurst, StalledPattern, StandardError,
|
||||
StandardErrorBands, StandardErrorBandsOutput, StarcBands, StarcBandsOutput, Stc, StdDev,
|
||||
StepTrailingStop, StickSandwich, StochRsi, Stochastic, StochasticOutput, SuperSmoother,
|
||||
SuperTrend, SuperTrendOutput, TakerBuySellRatio, Takuri, TasukiGap, TdCombo, TdCountdown,
|
||||
TdDeMarker, TdDifferential, TdLines, TdLinesOutput, TdOpen, TdPressure, TdRangeProjection,
|
||||
InstantaneousTrendline, IntradayVolatilityProfile, IntradayVolatilityProfileOutput,
|
||||
InverseFisherTransform, InvertedHammer, Jma, KagiBars, KalmanHedgeRatio,
|
||||
KalmanHedgeRatioOutput, Kama, KellyCriterion, Keltner, KeltnerOutput, Kicking, KickingByLength,
|
||||
Kst, KstOutput, Kurtosis, Kvo, KylesLambda, LadderBottom, LaguerreRsi, LeadLagCrossCorrelation,
|
||||
LeadLagCrossCorrelationOutput, LinRegAngle, LinRegChannel, LinRegChannelOutput,
|
||||
LinRegIntercept, LinRegSlope, LinearRegression, LiquidationFeatures, LiquidationFeaturesOutput,
|
||||
LongLeggedDoji, LongLine, LongShortRatio, MaEnvelope, MaEnvelopeOutput, MacdExt, MacdFix,
|
||||
MacdIndicator, MacdOutput, Mama, MamaOutput, MarketFacilitationIndex, Marubozu, MassIndex,
|
||||
MatHold, MatchingLow, MaxDrawdown, McClellanOscillator, McClellanSummationIndex,
|
||||
McGinleyDynamic, MedianAbsoluteDeviation, MedianPrice, Mfi, Microprice, MidPoint, MidPrice,
|
||||
MinusDi, MinusDm, Mom, MorningDojiStar, MorningEveningStar, Natr, NewHighsNewLows, Nvi,
|
||||
OIPriceDivergence, OIWeighted, Obv, OmegaRatio, OnNeck, OpenInterestDelta, OpeningMarubozu,
|
||||
OpeningRange, OpeningRangeOutput, OrderBookImbalanceFull, OrderBookImbalanceTop1,
|
||||
OrderBookImbalanceTopN, OuHalfLife, OvernightGap, OvernightIntradayReturn,
|
||||
OvernightIntradayReturnOutput, PainIndex, PairSpreadZScore, PairwiseBeta, ParkinsonVolatility,
|
||||
PearsonCorrelation, PercentAboveMa, PercentB, PercentageTrailingStop, Pgo, PiercingDarkCloud,
|
||||
PlusDi, PlusDm, Pmo, PointAndFigureBars, Ppo, ProfitFactor, Psar, Pvi, QuotedSpread, RSquared,
|
||||
RealizedSpread, RecoveryFactor, RelativeStrengthAB, RelativeStrengthOutput, RenkoBars,
|
||||
RenkoTrailingStop, RickshawMan, RisingThreeMethods, Roc, Rocp, Rocr, Rocr100,
|
||||
RogersSatchellVolatility, RollingCorrelation, RollingCovariance, RollingVwap, RoofingFilter,
|
||||
Rsi, Rvi, RviVolatility, Rwi, RwiOutput, SarExt, SeasonalZScore, SeparatingLines,
|
||||
SessionHighLow, SessionHighLowOutput, SessionRange, SessionRangeOutput, SessionVwap,
|
||||
SharpeRatio, ShootingStar, ShortLine, SignedVolume, SineWave, Skewness, Sma, Smi, Smma,
|
||||
SortinoRatio, SpearmanCorrelation, SpinningTop, SpreadBollingerBands,
|
||||
SpreadBollingerBandsOutput, SpreadHurst, StalledPattern, StandardError, StandardErrorBands,
|
||||
StandardErrorBandsOutput, StarcBands, StarcBandsOutput, Stc, StdDev, StepTrailingStop,
|
||||
StickSandwich, StochRsi, Stochastic, StochasticOutput, SuperSmoother, SuperTrend,
|
||||
SuperTrendOutput, TakerBuySellRatio, Takuri, TasukiGap, TdCombo, TdCountdown, TdDeMarker,
|
||||
TdDifferential, TdLines, TdLinesOutput, TdOpen, TdPressure, TdRangeProjection,
|
||||
TdRangeProjectionOutput, TdRei, TdRiskLevel, TdRiskLevelOutput, TdSequential,
|
||||
TdSequentialOutput, TdSetup, Tema, TermStructureBasis, ThreeInside, ThreeLineStrike,
|
||||
ThreeOutside, ThreeSoldiersOrCrows, ThreeStarsInSouth, Thrusting, TickIndex, Tii, TpoProfile,
|
||||
TpoProfileOutput, TradeImbalance, TreynorRatio, Trima, Trin, Trix, TrueRange, Tsf, Tsi, Tsv,
|
||||
TtmSqueeze, TtmSqueezeOutput, Tweezer, TwoCrows, TypicalPrice, UlcerIndex, UltimateOscillator,
|
||||
ThreeOutside, ThreeSoldiersOrCrows, ThreeStarsInSouth, Thrusting, TickIndex, Tii,
|
||||
TimeOfDayReturnProfile, TimeOfDayReturnProfileOutput, TpoProfile, TpoProfileOutput,
|
||||
TradeImbalance, TreynorRatio, Trima, Trin, Trix, TrueRange, Tsf, Tsi, Tsv, TtmSqueeze,
|
||||
TtmSqueezeOutput, TurnOfMonth, Tweezer, TwoCrows, TypicalPrice, UlcerIndex, UltimateOscillator,
|
||||
UniqueThreeRiver, UpDownVolumeRatio, UpsideGapThreeMethods, UpsideGapTwoCrows, ValueArea,
|
||||
ValueAreaOutput, ValueAtRisk, Variance, VarianceRatio, VerticalHorizontalFilter, Vidya,
|
||||
VoltyStop, VolumeOscillator, VolumePriceTrend, VolumeProfile, VolumeProfileOutput, Vortex,
|
||||
VortexOutput, Vwap, VwapStdDevBands, VwapStdDevBandsOutput, Vwma, Vzo, WaveTrend,
|
||||
WaveTrendOutput, WeightedClose, WilliamsFractals, WilliamsFractalsOutput, WilliamsR, Wma,
|
||||
WoodiePivots, WoodiePivotsOutput, YangZhangVolatility, YoyoExit, ZScore, ZeroLagMacd,
|
||||
ZeroLagMacdOutput, ZigZag, ZigZagOutput, Zlema, FAMILIES, T3,
|
||||
VoltyStop, VolumeByTimeProfile, VolumeByTimeProfileOutput, VolumeOscillator, VolumePriceTrend,
|
||||
VolumeProfile, VolumeProfileOutput, Vortex, VortexOutput, Vwap, VwapStdDevBands,
|
||||
VwapStdDevBandsOutput, Vwma, Vzo, WaveTrend, WaveTrendOutput, WeightedClose, WilliamsFractals,
|
||||
WilliamsFractalsOutput, WilliamsR, Wma, WoodiePivots, WoodiePivotsOutput, YangZhangVolatility,
|
||||
YoyoExit, ZScore, ZeroLagMacd, ZeroLagMacdOutput, ZigZag, ZigZagOutput, Zlema, FAMILIES, T3,
|
||||
};
|
||||
// `FootprintLevel` is a row element of `FootprintOutput`, re-exported on its own
|
||||
// line so the indicator-count tooling (which scans the braced block above and
|
||||
|
||||
+1
-1
@@ -8,7 +8,7 @@ That includes:
|
||||
[Python](https://docs.wickra.org/Quickstart-Python),
|
||||
[Node](https://docs.wickra.org/Quickstart-Node), and
|
||||
[WASM](https://docs.wickra.org/Quickstart-WASM).
|
||||
- A per-indicator deep dive for every one of the **339 indicators** across
|
||||
- A per-indicator deep dive for every one of the **351 indicators** across
|
||||
the sixteen families (Moving Averages, Momentum Oscillators, Trend &
|
||||
Directional, Price Oscillators, Volatility & Bands, Bands & Channels,
|
||||
Trailing Stops, Volume, Price Statistics, Ehlers / Cycle DSP, Pivots &
|
||||
|
||||
Generated
+7
-7
@@ -17,7 +17,7 @@
|
||||
},
|
||||
"../../bindings/node": {
|
||||
"name": "wickra",
|
||||
"version": "0.5.0",
|
||||
"version": "0.5.1",
|
||||
"license": "MIT OR Apache-2.0",
|
||||
"devDependencies": {
|
||||
"@napi-rs/cli": "^2.18.0"
|
||||
@@ -26,12 +26,12 @@
|
||||
"node": ">= 18"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"wickra-darwin-arm64": "0.5.0",
|
||||
"wickra-darwin-x64": "0.5.0",
|
||||
"wickra-linux-arm64-gnu": "0.5.0",
|
||||
"wickra-linux-x64-gnu": "0.5.0",
|
||||
"wickra-win32-arm64-msvc": "0.5.0",
|
||||
"wickra-win32-x64-msvc": "0.5.0"
|
||||
"wickra-darwin-arm64": "0.5.1",
|
||||
"wickra-darwin-x64": "0.5.1",
|
||||
"wickra-linux-arm64-gnu": "0.5.1",
|
||||
"wickra-linux-x64-gnu": "0.5.1",
|
||||
"wickra-win32-arm64-msvc": "0.5.1",
|
||||
"wickra-win32-x64-msvc": "0.5.1"
|
||||
}
|
||||
},
|
||||
"node_modules/wickra": {
|
||||
|
||||
@@ -22,9 +22,7 @@
|
||||
//! WeightedClose.
|
||||
|
||||
use libfuzzer_sys::fuzz_target;
|
||||
use wickra_core::{
|
||||
AbandonedBaby, AccelerationBands, AcceleratorOscillator, AdOscillator, Adl, AdvanceBlock, Adx, Adxr, Alligator, AnchoredVwap, Aroon, AroonOscillator, Atr, AtrBands, AtrTrailingStop, AwesomeOscillator, AwesomeOscillatorHistogram, BalanceOfPower, BatchExt, BeltHold, Breakaway, Camarilla, Candle, Cci, ChaikinMoneyFlow, ChaikinOscillator, ChaikinVolatility, ChandeKrollStop, ChandelierExit, ChoppinessIndex, ClassicPivots, ClosingMarubozu, ConcealingBabySwallow, Counterattack, DemandIndex, DemarkPivots, Doji, DojiStar, Donchian, DonchianStop, DownsideGapThreeMethods, DragonflyDoji, Dx, EaseOfMovement, Engulfing, EveningDojiStar, Evwma, FallingThreeMethods, FibonacciPivots, ForceIndex, FractalChaosBands, GapSideBySideWhite, GarmanKlassVolatility, GravestoneDoji, Hammer, HangingMan, Harami, HeikinAshi, HiLoActivator, HighWave, Hikkake, HikkakeModified, HomingPigeon, HurstChannel, Ichimoku, IdenticalThreeCrows, InNeck, Indicator, Inertia, InitialBalance, InvertedHammer, Keltner, Kicking, KickingByLength, Kvo, LadderBottom, LongLeggedDoji, LongLine, MarketFacilitationIndex, Marubozu, MassIndex, MatHold, MatchingLow, AvgPrice, MedianPrice, Mfi, MidPrice, MinusDi, MinusDm, MorningDojiStar, MorningEveningStar, Natr, Nvi, Obv, OnNeck, OpeningMarubozu, OpeningRange, ParkinsonVolatility, Pgo, PiercingDarkCloud, PlusDi, PlusDm, Psar, Pvi, RickshawMan, RisingThreeMethods, RogersSatchellVolatility, RollingVwap, Rvi, Rwi, SarExt, SeparatingLines, ShootingStar, ShortLine, Smi, SpinningTop, StalledPattern, StarcBands, StickSandwich, Stochastic, SuperTrend, Takuri, TasukiGap, TdCombo, TdCountdown, TdDeMarker, TdDifferential, TdLines, TdOpen, TdPressure, TdRangeProjection, TdRei, TdRiskLevel, TdSequential, TdSetup, ThreeInside, ThreeLineStrike, ThreeOutside, ThreeSoldiersOrCrows, ThreeStarsInSouth, Thrusting, TpoProfile, TrueRange, Tsv, TtmSqueeze, Tweezer, TwoCrows, TypicalPrice, UltimateOscillator, UniqueThreeRiver, UpsideGapThreeMethods, UpsideGapTwoCrows, ValueArea, VoltyStop, VolumeOscillator, VolumePriceTrend, VolumeProfile, Vortex, Vwap, VwapStdDevBands, Vwma, Vzo, WaveTrend, WeightedClose, WilliamsFractals, WilliamsR, WoodiePivots, YangZhangVolatility, YoyoExit, ZigZag
|
||||
};
|
||||
use wickra_core::{AbandonedBaby, AccelerationBands, AcceleratorOscillator, AdOscillator, Adl, AdvanceBlock, Adx, Adxr, Alligator, AnchoredVwap, Aroon, AroonOscillator, Atr, AtrBands, AtrTrailingStop, AverageDailyRange, AwesomeOscillator, AwesomeOscillatorHistogram, BalanceOfPower, BatchExt, BeltHold, Breakaway, Camarilla, Candle, Cci, ChaikinMoneyFlow, ChaikinOscillator, ChaikinVolatility, ChandeKrollStop, ChandelierExit, ChoppinessIndex, ClassicPivots, ClosingMarubozu, ConcealingBabySwallow, Counterattack, DayOfWeekProfile, DemandIndex, DemarkPivots, Doji, DojiStar, Donchian, DonchianStop, DownsideGapThreeMethods, DragonflyDoji, Dx, EaseOfMovement, Engulfing, EveningDojiStar, Evwma, FallingThreeMethods, FibonacciPivots, ForceIndex, FractalChaosBands, GapSideBySideWhite, GarmanKlassVolatility, GravestoneDoji, Hammer, HangingMan, Harami, HeikinAshi, HiLoActivator, HighWave, Hikkake, HikkakeModified, HomingPigeon, HurstChannel, Ichimoku, IdenticalThreeCrows, InNeck, Indicator, Inertia, InitialBalance, IntradayVolatilityProfile, InvertedHammer, Keltner, Kicking, KickingByLength, Kvo, LadderBottom, LongLeggedDoji, LongLine, MarketFacilitationIndex, Marubozu, MassIndex, MatHold, MatchingLow, AvgPrice, MedianPrice, Mfi, MidPrice, MinusDi, MinusDm, MorningDojiStar, MorningEveningStar, Natr, Nvi, Obv, OnNeck, OpeningMarubozu, OpeningRange, OvernightGap, OvernightIntradayReturn, ParkinsonVolatility, Pgo, PiercingDarkCloud, PlusDi, PlusDm, Psar, Pvi, RickshawMan, RisingThreeMethods, RogersSatchellVolatility, RollingVwap, Rvi, Rwi, SarExt, SeasonalZScore, SeparatingLines, SessionHighLow, SessionRange, SessionVwap, ShootingStar, ShortLine, Smi, SpinningTop, StalledPattern, StarcBands, StickSandwich, Stochastic, SuperTrend, Takuri, TasukiGap, TdCombo, TdCountdown, TdDeMarker, TdDifferential, TdLines, TdOpen, TdPressure, TdRangeProjection, TdRei, TdRiskLevel, TdSequential, TdSetup, ThreeInside, ThreeLineStrike, ThreeOutside, ThreeSoldiersOrCrows, ThreeStarsInSouth, Thrusting, TimeOfDayReturnProfile, TpoProfile, TrueRange, Tsv, TtmSqueeze, TurnOfMonth, Tweezer, TwoCrows, TypicalPrice, UltimateOscillator, UniqueThreeRiver, UpsideGapThreeMethods, UpsideGapTwoCrows, ValueArea, VoltyStop, VolumeByTimeProfile, VolumeOscillator, VolumePriceTrend, VolumeProfile, Vortex, Vwap, VwapStdDevBands, Vwma, Vzo, WaveTrend, WeightedClose, WilliamsFractals, WilliamsR, WoodiePivots, YangZhangVolatility, YoyoExit, ZigZag};
|
||||
|
||||
/// Convert a flat `f64` stream into a `Vec<Candle>` by chunking it into
|
||||
/// `[open, high, low, close, volume]` groups. Tuples that fail OHLCV
|
||||
@@ -349,4 +347,29 @@ fuzz_target!(|data: Vec<f64>| {
|
||||
drive(TwoCrows::new, &candles);
|
||||
drive(UpsideGapTwoCrows::new, &candles);
|
||||
drive(IdenticalThreeCrows::new, &candles);
|
||||
// --- Seasonality & Session ---
|
||||
drive(|| VolumeByTimeProfile::new(24, 0).unwrap(), &candles);
|
||||
|
||||
drive(|| IntradayVolatilityProfile::new(24, 0).unwrap(), &candles);
|
||||
|
||||
drive(|| DayOfWeekProfile::new(0), &candles);
|
||||
|
||||
drive(|| TimeOfDayReturnProfile::new(24, 0).unwrap(), &candles);
|
||||
|
||||
drive(|| SeasonalZScore::new(0), &candles);
|
||||
|
||||
drive(|| TurnOfMonth::new(3, 1, 0).unwrap(), &candles);
|
||||
|
||||
drive(|| OvernightIntradayReturn::new(0), &candles);
|
||||
|
||||
drive(|| OvernightGap::new(0), &candles);
|
||||
|
||||
drive(|| AverageDailyRange::new(14, 0).unwrap(), &candles);
|
||||
|
||||
drive(|| SessionRange::new(0), &candles);
|
||||
|
||||
drive(|| SessionHighLow::new(0), &candles);
|
||||
|
||||
drive(|| SessionVwap::new(0), &candles);
|
||||
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user