Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fc6f619550 | ||
|
|
91aa6fffbf |
+9
-1
@@ -7,6 +7,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
## [0.7.2] - 2026-06-08
|
||||||
|
- **Composite Profile** — multi-session composite volume profile exposing POC, VAH and VAL (`CompositeProfile`).
|
||||||
|
- **High/Low Volume Nodes** — highest- and lowest-volume price nodes in the profile (`HighLowVolumeNodes`).
|
||||||
|
- **Profile Shape** — profile shape classification (b/P/D normal) as a numeric code (`ProfileShape`).
|
||||||
|
- **Single Prints** — count of single-print (low-activity) price levels in the profile (`SinglePrints`).
|
||||||
|
- **Naked POC** — most recent untouched (naked) point of control level (`NakedPoc`).
|
||||||
|
|
||||||
## [0.7.1] - 2026-06-08
|
## [0.7.1] - 2026-06-08
|
||||||
- **Open-Interest Momentum** — rate-of-change of open interest over a rolling window (`OpenInterestMomentum`).
|
- **Open-Interest Momentum** — rate-of-change of open interest over a rolling window (`OpenInterestMomentum`).
|
||||||
- **Funding-Implied APR** — annualised funding rate (per-interval funding times intervals per year) (`FundingImpliedApr`).
|
- **Funding-Implied APR** — annualised funding rate (per-interval funding times intervals per year) (`FundingImpliedApr`).
|
||||||
@@ -1389,7 +1396,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
optional Binance live feed.
|
optional Binance live feed.
|
||||||
- Bindings for Python, Node.js, and WebAssembly.
|
- Bindings for Python, Node.js, and WebAssembly.
|
||||||
|
|
||||||
[Unreleased]: https://github.com/wickra-lib/wickra/compare/v0.7.1...HEAD
|
[Unreleased]: https://github.com/wickra-lib/wickra/compare/v0.7.2...HEAD
|
||||||
|
[0.7.2]: https://github.com/wickra-lib/wickra/compare/v0.7.1...v0.7.2
|
||||||
[0.7.1]: https://github.com/wickra-lib/wickra/compare/v0.7.0...v0.7.1
|
[0.7.1]: https://github.com/wickra-lib/wickra/compare/v0.7.0...v0.7.1
|
||||||
[0.7.0]: https://github.com/wickra-lib/wickra/compare/v0.6.9...v0.7.0
|
[0.7.0]: https://github.com/wickra-lib/wickra/compare/v0.6.9...v0.7.0
|
||||||
[0.6.9]: https://github.com/wickra-lib/wickra/compare/v0.6.8...v0.6.9
|
[0.6.9]: https://github.com/wickra-lib/wickra/compare/v0.6.8...v0.6.9
|
||||||
|
|||||||
Generated
+8
-8
@@ -1944,7 +1944,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wickra"
|
name = "wickra"
|
||||||
version = "0.7.1"
|
version = "0.7.2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"approx",
|
"approx",
|
||||||
"criterion",
|
"criterion",
|
||||||
@@ -1955,7 +1955,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wickra-bench"
|
name = "wickra-bench"
|
||||||
version = "0.7.1"
|
version = "0.7.2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"criterion",
|
"criterion",
|
||||||
"kand",
|
"kand",
|
||||||
@@ -1967,7 +1967,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wickra-core"
|
name = "wickra-core"
|
||||||
version = "0.7.1"
|
version = "0.7.2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"approx",
|
"approx",
|
||||||
"proptest",
|
"proptest",
|
||||||
@@ -1977,7 +1977,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wickra-data"
|
name = "wickra-data"
|
||||||
version = "0.7.1"
|
version = "0.7.2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"approx",
|
"approx",
|
||||||
"csv",
|
"csv",
|
||||||
@@ -1994,7 +1994,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wickra-examples"
|
name = "wickra-examples"
|
||||||
version = "0.7.1"
|
version = "0.7.2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"tokio",
|
"tokio",
|
||||||
@@ -2004,7 +2004,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wickra-node"
|
name = "wickra-node"
|
||||||
version = "0.7.1"
|
version = "0.7.2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"napi",
|
"napi",
|
||||||
"napi-build",
|
"napi-build",
|
||||||
@@ -2014,7 +2014,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wickra-python"
|
name = "wickra-python"
|
||||||
version = "0.7.1"
|
version = "0.7.2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"numpy",
|
"numpy",
|
||||||
"pyo3",
|
"pyo3",
|
||||||
@@ -2023,7 +2023,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wickra-wasm"
|
name = "wickra-wasm"
|
||||||
version = "0.7.1"
|
version = "0.7.2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"console_error_panic_hook",
|
"console_error_panic_hook",
|
||||||
"js-sys",
|
"js-sys",
|
||||||
|
|||||||
+2
-2
@@ -13,7 +13,7 @@ members = [
|
|||||||
exclude = ["fuzz"]
|
exclude = ["fuzz"]
|
||||||
|
|
||||||
[workspace.package]
|
[workspace.package]
|
||||||
version = "0.7.1"
|
version = "0.7.2"
|
||||||
authors = ["kingchenc <support@wickra.org>"]
|
authors = ["kingchenc <support@wickra.org>"]
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
rust-version = "1.86"
|
rust-version = "1.86"
|
||||||
@@ -25,7 +25,7 @@ keywords = ["finance", "trading", "indicators", "technical-analysis", "ta"]
|
|||||||
categories = ["finance", "mathematics", "science"]
|
categories = ["finance", "mathematics", "science"]
|
||||||
|
|
||||||
[workspace.dependencies]
|
[workspace.dependencies]
|
||||||
wickra-core = { path = "crates/wickra-core", version = "0.7.1" }
|
wickra-core = { path = "crates/wickra-core", version = "0.7.2" }
|
||||||
|
|
||||||
thiserror = "2"
|
thiserror = "2"
|
||||||
rayon = "1.10"
|
rayon = "1.10"
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<p align="center">
|
<p align="center">
|
||||||
<a href="https://wickra.org"><img src="https://raw.githubusercontent.com/wickra-lib/.github/main/profile/wickra-banner.webp?v=493" 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=498" alt="Wickra — streaming-first technical indicators" width="100%"></a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
[](https://github.com/wickra-lib/wickra/actions/workflows/ci.yml)
|
[](https://github.com/wickra-lib/wickra/actions/workflows/ci.yml)
|
||||||
@@ -48,7 +48,7 @@ Full documentation lives at **[docs.wickra.org](https://docs.wickra.org)**:
|
|||||||
[Node](https://docs.wickra.org/Quickstart-Node),
|
[Node](https://docs.wickra.org/Quickstart-Node),
|
||||||
[WASM](https://docs.wickra.org/Quickstart-WASM).
|
[WASM](https://docs.wickra.org/Quickstart-WASM).
|
||||||
- **Indicators** — a per-indicator deep dive (formula, parameters, warmup) for
|
- **Indicators** — a per-indicator deep dive (formula, parameters, warmup) for
|
||||||
every one of the 493 indicators; start at the
|
every one of the 498 indicators; start at the
|
||||||
[indicators overview](https://docs.wickra.org/Indicators-Overview).
|
[indicators overview](https://docs.wickra.org/Indicators-Overview).
|
||||||
- **Reference** — [warmup periods](https://docs.wickra.org/Warmup-Periods),
|
- **Reference** — [warmup periods](https://docs.wickra.org/Warmup-Periods),
|
||||||
[streaming vs batch](https://docs.wickra.org/Streaming-vs-Batch),
|
[streaming vs batch](https://docs.wickra.org/Streaming-vs-Batch),
|
||||||
@@ -66,7 +66,7 @@ an afterthought — **live, tick-by-tick data** — without giving up the breadt
|
|||||||
a full batch library, and without making you reimplement your indicators four
|
a full batch library, and without making you reimplement your indicators four
|
||||||
times to get there.
|
times to get there.
|
||||||
|
|
||||||
- **The biggest streaming-native catalogue, period.** 493 indicators across 24
|
- **The biggest streaming-native catalogue, period.** 498 indicators across 24
|
||||||
families — candlesticks, harmonic & chart patterns, market profile, market
|
families — candlesticks, harmonic & chart patterns, market profile, market
|
||||||
breadth, Renko/Kagi/Point&Figure bars, Ehlers DSP cycles, risk/performance
|
breadth, Renko/Kagi/Point&Figure bars, Ehlers DSP cycles, risk/performance
|
||||||
metrics — every single one updating in **O(1) per tick**. TA-Lib ships ~150 and
|
metrics — every single one updating in **O(1) per tick**. TA-Lib ships ~150 and
|
||||||
@@ -77,7 +77,7 @@ times to get there.
|
|||||||
- **Correct by construction, not by hope.** Every `update` validates its input,
|
- **Correct by construction, not by hope.** Every `update` validates its input,
|
||||||
runs a real warmup, and returns an `Option` so a single bad tick can't silently
|
runs a real warmup, and returns an `Option` so a single bad tick can't silently
|
||||||
poison state. `batch == streaming` is **bit-exact, fuzzed and 100 %-line-covered
|
poison state. `batch == streaming` is **bit-exact, fuzzed and 100 %-line-covered
|
||||||
for all 493 indicators**.
|
for all 498 indicators**.
|
||||||
- **Orders of magnitude faster where it counts.** In streaming Wickra is **11–56×**
|
- **Orders of magnitude faster where it counts.** In streaming Wickra is **11–56×**
|
||||||
faster than the only other incremental peer and **thousands of times** faster
|
faster than the only other incremental peer and **thousands of times** faster
|
||||||
than recompute-on-every-tick libraries. On batch it wins several rows outright
|
than recompute-on-every-tick libraries. On batch it wins several rows outright
|
||||||
@@ -95,7 +95,7 @@ Every other library forces one of those compromises. Wickra doesn't:
|
|||||||
|
|
||||||
| Library | Install | Streaming | Languages | Indicators | Active |
|
| Library | Install | Streaming | Languages | Indicators | Active |
|
||||||
|------------------|-------------|-------------|-----------------------------|-----------:|--------|
|
|------------------|-------------|-------------|-----------------------------|-----------:|--------|
|
||||||
| **★ Wickra**| **clean** | **yes, O(1)** | **Python · Node · WASM · Rust** | **493** | **yes** |
|
| **★ Wickra**| **clean** | **yes, O(1)** | **Python · Node · WASM · Rust** | **498** | **yes** |
|
||||||
| kand | clean | yes | Python · WASM · Rust | ~60 | yes |
|
| kand | clean | yes | Python · WASM · Rust | ~60 | yes |
|
||||||
| ta-rs | clean | yes | Rust only | ~30 | stale |
|
| ta-rs | clean | yes | Rust only | ~30 | stale |
|
||||||
| yata | clean | partial | Rust only | ~35 | yes |
|
| yata | clean | partial | Rust only | ~35 | yes |
|
||||||
@@ -128,7 +128,7 @@ Full tables (Rust + Python, streaming + batch) and how to reproduce them live in
|
|||||||
|
|
||||||
## Indicators
|
## Indicators
|
||||||
|
|
||||||
493 streaming-first indicators across twenty-four families. Every one passes the
|
498 streaming-first indicators across twenty-four families. Every one passes the
|
||||||
`batch == streaming` equivalence test, reference-value tests, and reset
|
`batch == streaming` equivalence test, reference-value tests, and reset
|
||||||
semantics tests. Each has a per-indicator deep dive (formula, parameters,
|
semantics tests. Each has a per-indicator deep dive (formula, parameters,
|
||||||
warmup) at [docs.wickra.org](https://docs.wickra.org/Indicators-Overview).
|
warmup) at [docs.wickra.org](https://docs.wickra.org/Indicators-Overview).
|
||||||
@@ -155,7 +155,7 @@ warmup) at [docs.wickra.org](https://docs.wickra.org/Indicators-Overview).
|
|||||||
| Fibonacci | Fibonacci Retracement, Fibonacci Extension, Fibonacci Projection, Auto-Fibonacci, Golden Pocket, Fibonacci Confluence, Fibonacci Fan, Fibonacci Arcs, Fibonacci Channel, Fibonacci Time Zones |
|
| Fibonacci | Fibonacci Retracement, Fibonacci Extension, Fibonacci Projection, Auto-Fibonacci, Golden Pocket, Fibonacci Confluence, Fibonacci Fan, Fibonacci Arcs, Fibonacci Channel, Fibonacci Time Zones |
|
||||||
| Microstructure | Order-Book Imbalance (Top-1 / Top-N / Full), Microprice, Quoted Spread, Depth Slope, Signed Volume, Cumulative Volume Delta, Trade Imbalance, Effective Spread, Realized Spread, Kyle's Lambda, Footprint, Order Flow Imbalance, VPIN, Amihud Illiquidity, Roll Measure, Trade-Sign Autocorrelation, Hasbrouck Information Share |
|
| Microstructure | Order-Book Imbalance (Top-1 / Top-N / Full), Microprice, Quoted Spread, Depth Slope, Signed Volume, Cumulative Volume Delta, Trade Imbalance, Effective Spread, Realized Spread, Kyle's Lambda, Footprint, Order Flow Imbalance, VPIN, Amihud Illiquidity, Roll Measure, Trade-Sign Autocorrelation, Hasbrouck Information Share |
|
||||||
| Derivatives | Funding Rate, Funding Rate Mean, Funding Rate Z-Score, Funding Basis, Open-Interest Delta, OI / Price Divergence, OI-Weighted Price, Long/Short Ratio, Taker Buy/Sell Ratio, Liquidation Features, Term-Structure Basis, Calendar Spread, Estimated Leverage Ratio, OI-to-Volume Ratio, Perpetual Premium Index, Funding-Implied APR, Open-Interest Momentum |
|
| Derivatives | Funding Rate, Funding Rate Mean, Funding Rate Z-Score, Funding Basis, Open-Interest Delta, OI / Price Divergence, OI-Weighted Price, Long/Short Ratio, Taker Buy/Sell Ratio, Liquidation Features, Term-Structure Basis, Calendar Spread, Estimated Leverage Ratio, OI-to-Volume Ratio, Perpetual Premium Index, Funding-Implied APR, Open-Interest Momentum |
|
||||||
| Market Profile | Value Area (POC / VAH / VAL), Volume Profile (histogram), TPO Profile, Initial Balance, Opening Range |
|
| Market Profile | Value Area (POC / VAH / VAL), Volume Profile (histogram), TPO Profile, Initial Balance, Opening Range, Naked POC, Single Prints, Profile Shape, High/Low Volume Nodes, Composite Profile |
|
||||||
| 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 |
|
| Market Breadth | Advance/Decline Line, Advance/Decline Ratio, Advance/Decline Volume Line, McClellan Oscillator, McClellan Summation Index, TRIN / Arms Index, Breadth Thrust, New Highs - New Lows, High-Low Index, Percent Above Moving Average, Up/Down Volume Ratio, Bullish Percent Index, Cumulative Volume Index, Absolute Breadth Index, TICK Index |
|
||||||
| Risk / Performance | Sharpe Ratio, Sortino Ratio, Calmar Ratio, Omega Ratio, Max Drawdown, Average Drawdown, Drawdown Duration, Pain Index, Value at Risk, Conditional Value at Risk (CVaR), Profit Factor, Gain/Loss Ratio, Recovery Factor, Kelly Criterion, Treynor Ratio, Information Ratio, Alpha (Jensen) |
|
| Risk / Performance | Sharpe Ratio, Sortino Ratio, Calmar Ratio, Omega Ratio, Max Drawdown, Average Drawdown, Drawdown Duration, Pain Index, Value at Risk, Conditional Value at Risk (CVaR), Profit Factor, Gain/Loss Ratio, Recovery Factor, Kelly Criterion, Treynor Ratio, Information Ratio, Alpha (Jensen) |
|
||||||
| 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 |
|
| 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 |
|
||||||
@@ -237,7 +237,7 @@ A Python live-trading example using the public `websockets` package lives at
|
|||||||
```
|
```
|
||||||
wickra/
|
wickra/
|
||||||
├── crates/
|
├── crates/
|
||||||
│ ├── wickra-core/ core engine + all 493 indicators
|
│ ├── wickra-core/ core engine + all 498 indicators
|
||||||
│ ├── wickra/ top-level facade crate (publishes on crates.io) + benches/
|
│ ├── wickra/ top-level facade crate (publishes on crates.io) + benches/
|
||||||
│ ├── wickra-data/ CSV reader, tick aggregator, live exchange feeds
|
│ ├── wickra-data/ CSV reader, tick aggregator, live exchange feeds
|
||||||
│ └── wickra-bench/ internal cross-library benchmark harness (not published)
|
│ └── wickra-bench/ internal cross-library benchmark harness (not published)
|
||||||
|
|||||||
@@ -392,6 +392,9 @@ const candleScalar = {
|
|||||||
DumplingTop: { make: () => new wickra.DumplingTop(9), step: (ind, i) => ind.update(high[i], low[i], close[i]), batch: (ind) => ind.batch(high, low, close) },
|
DumplingTop: { make: () => new wickra.DumplingTop(9), step: (ind, i) => ind.update(high[i], low[i], close[i]), batch: (ind) => ind.batch(high, low, close) },
|
||||||
NewPriceLines: { make: () => new wickra.NewPriceLines(5), step: (ind, i) => ind.update(high[i], low[i], close[i]), batch: (ind) => ind.batch(high, low, close) },
|
NewPriceLines: { make: () => new wickra.NewPriceLines(5), step: (ind, i) => ind.update(high[i], low[i], close[i]), batch: (ind) => ind.batch(high, low, close) },
|
||||||
FryPanBottom: { make: () => new wickra.FryPanBottom(9), step: (ind, i) => ind.update(high[i], low[i], close[i]), batch: (ind) => ind.batch(high, low, close) },
|
FryPanBottom: { make: () => new wickra.FryPanBottom(9), step: (ind, i) => ind.update(high[i], low[i], close[i]), batch: (ind) => ind.batch(high, low, close) },
|
||||||
|
NakedPoc: { make: () => new wickra.NakedPoc(20, 24), step: (ind, i) => ind.update(high[i], low[i], close[i], volume[i]), batch: (ind) => ind.batch(high, low, close, volume) },
|
||||||
|
SinglePrints: { make: () => new wickra.SinglePrints(20, 24), step: (ind, i) => ind.update(high[i], low[i]), batch: (ind) => ind.batch(high, low) },
|
||||||
|
ProfileShape: { make: () => new wickra.ProfileShape(20, 24), step: (ind, i) => ind.update(high[i], low[i], volume[i]), batch: (ind) => ind.batch(high, low, volume) },
|
||||||
};
|
};
|
||||||
|
|
||||||
for (const [name, d] of Object.entries(candleScalar)) {
|
for (const [name, d] of Object.entries(candleScalar)) {
|
||||||
@@ -497,6 +500,8 @@ const multi = {
|
|||||||
SmoothedHeikinAshi: { make: () => new wickra.SmoothedHeikinAshi(5), fields: ['open', 'high', 'low', 'close'], step: (ind, i) => ind.update(open[i], high[i], low[i], close[i]), batch: (ind) => ind.batch(open, high, low, close) },
|
SmoothedHeikinAshi: { make: () => new wickra.SmoothedHeikinAshi(5), fields: ['open', 'high', 'low', 'close'], step: (ind, i) => ind.update(open[i], high[i], low[i], close[i]), batch: (ind) => ind.batch(open, high, low, close) },
|
||||||
Equivolume: { make: () => new wickra.Equivolume(20), fields: ['height', 'width'], step: (ind, i) => ind.update(high[i], low[i], volume[i]), batch: (ind) => ind.batch(high, low, volume) },
|
Equivolume: { make: () => new wickra.Equivolume(20), fields: ['height', 'width'], step: (ind, i) => ind.update(high[i], low[i], volume[i]), batch: (ind) => ind.batch(high, low, volume) },
|
||||||
CandleVolume: { make: () => new wickra.CandleVolume(20), fields: ['body', 'width'], step: (ind, i) => ind.update(open[i], close[i], volume[i]), batch: (ind) => ind.batch(open, close, volume) },
|
CandleVolume: { make: () => new wickra.CandleVolume(20), fields: ['body', 'width'], step: (ind, i) => ind.update(open[i], close[i], volume[i]), batch: (ind) => ind.batch(open, close, volume) },
|
||||||
|
HighLowVolumeNodes: { make: () => new wickra.HighLowVolumeNodes(20, 24), fields: ['hvn', 'lvn'], step: (ind, i) => ind.update(high[i], low[i], volume[i]), batch: (ind) => ind.batch(high, low, volume) },
|
||||||
|
CompositeProfile: { make: () => new wickra.CompositeProfile(20, 24, 0.7), fields: ['poc', 'vah', 'val'], step: (ind, i) => ind.update(high[i], low[i], volume[i]), batch: (ind) => ind.batch(high, low, volume) },
|
||||||
};
|
};
|
||||||
|
|
||||||
for (const [name, d] of Object.entries(multi)) {
|
for (const [name, d] of Object.entries(multi)) {
|
||||||
|
|||||||
Vendored
+54
@@ -409,6 +409,15 @@ export interface VolumeProfileValue {
|
|||||||
priceHigh: number
|
priceHigh: number
|
||||||
bins: Array<number>
|
bins: Array<number>
|
||||||
}
|
}
|
||||||
|
export interface HighLowVolumeNodesValue {
|
||||||
|
hvn: number
|
||||||
|
lvn: number
|
||||||
|
}
|
||||||
|
export interface CompositeProfileValue {
|
||||||
|
poc: number
|
||||||
|
vah: number
|
||||||
|
val: number
|
||||||
|
}
|
||||||
export interface TpoProfileValue {
|
export interface TpoProfileValue {
|
||||||
priceLow: number
|
priceLow: number
|
||||||
priceHigh: number
|
priceHigh: number
|
||||||
@@ -3470,6 +3479,51 @@ export declare class ValueArea {
|
|||||||
update(high: number, low: number, volume: number): ValueAreaValue | null
|
update(high: number, low: number, volume: number): ValueAreaValue | null
|
||||||
batch(high: Array<number>, low: Array<number>, volume: Array<number>): Array<number>
|
batch(high: Array<number>, low: Array<number>, volume: Array<number>): Array<number>
|
||||||
}
|
}
|
||||||
|
export type NakedPocNode = NakedPoc
|
||||||
|
export declare class NakedPoc {
|
||||||
|
constructor(sessionLen: number, binCount: number)
|
||||||
|
update(high: number, low: number, close: number, volume: number): number | null
|
||||||
|
batch(high: Array<number>, low: Array<number>, close: Array<number>, volume: Array<number>): Array<number>
|
||||||
|
reset(): void
|
||||||
|
isReady(): boolean
|
||||||
|
warmupPeriod(): number
|
||||||
|
}
|
||||||
|
export type SinglePrintsNode = SinglePrints
|
||||||
|
export declare class SinglePrints {
|
||||||
|
constructor(period: number, binCount: number)
|
||||||
|
update(high: number, low: number): number | null
|
||||||
|
batch(high: Array<number>, low: Array<number>): Array<number>
|
||||||
|
reset(): void
|
||||||
|
isReady(): boolean
|
||||||
|
warmupPeriod(): number
|
||||||
|
}
|
||||||
|
export type ProfileShapeNode = ProfileShape
|
||||||
|
export declare class ProfileShape {
|
||||||
|
constructor(period: number, binCount: number)
|
||||||
|
update(high: number, low: number, volume: number): number | null
|
||||||
|
batch(high: Array<number>, low: Array<number>, volume: Array<number>): Array<number>
|
||||||
|
reset(): void
|
||||||
|
isReady(): boolean
|
||||||
|
warmupPeriod(): number
|
||||||
|
}
|
||||||
|
export type HighLowVolumeNodesNode = HighLowVolumeNodes
|
||||||
|
export declare class HighLowVolumeNodes {
|
||||||
|
constructor(period: number, binCount: number)
|
||||||
|
update(high: number, low: number, volume: number): HighLowVolumeNodesValue | null
|
||||||
|
batch(high: Array<number>, low: Array<number>, volume: Array<number>): Array<number>
|
||||||
|
reset(): void
|
||||||
|
isReady(): boolean
|
||||||
|
warmupPeriod(): number
|
||||||
|
}
|
||||||
|
export type CompositeProfileNode = CompositeProfile
|
||||||
|
export declare class CompositeProfile {
|
||||||
|
constructor(period: number, binCount: number, valueAreaPct: number)
|
||||||
|
update(high: number, low: number, volume: number): CompositeProfileValue | null
|
||||||
|
batch(high: Array<number>, low: Array<number>, volume: Array<number>): Array<number>
|
||||||
|
reset(): void
|
||||||
|
isReady(): boolean
|
||||||
|
warmupPeriod(): number
|
||||||
|
}
|
||||||
export type VolumeProfileNode = VolumeProfile
|
export type VolumeProfileNode = VolumeProfile
|
||||||
export declare class VolumeProfile {
|
export declare class VolumeProfile {
|
||||||
constructor(period: number, binCount: number)
|
constructor(period: number, binCount: number)
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "wickra-darwin-arm64",
|
"name": "wickra-darwin-arm64",
|
||||||
"version": "0.7.1",
|
"version": "0.7.2",
|
||||||
"description": "Native binding for wickra (macOS Apple Silicon). Installed automatically as an optional dependency of wickra on matching platforms.",
|
"description": "Native binding for wickra (macOS Apple Silicon). Installed automatically as an optional dependency of wickra on matching platforms.",
|
||||||
"main": "wickra.darwin-arm64.node",
|
"main": "wickra.darwin-arm64.node",
|
||||||
"files": [
|
"files": [
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "wickra-darwin-x64",
|
"name": "wickra-darwin-x64",
|
||||||
"version": "0.7.1",
|
"version": "0.7.2",
|
||||||
"description": "Native binding for wickra (macOS Intel). Installed automatically as an optional dependency of wickra on matching platforms.",
|
"description": "Native binding for wickra (macOS Intel). Installed automatically as an optional dependency of wickra on matching platforms.",
|
||||||
"main": "wickra.darwin-x64.node",
|
"main": "wickra.darwin-x64.node",
|
||||||
"files": [
|
"files": [
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "wickra-linux-arm64-gnu",
|
"name": "wickra-linux-arm64-gnu",
|
||||||
"version": "0.7.1",
|
"version": "0.7.2",
|
||||||
"description": "Native binding for wickra (linux arm64 GNU). Installed automatically as an optional dependency of wickra on matching platforms.",
|
"description": "Native binding for wickra (linux arm64 GNU). Installed automatically as an optional dependency of wickra on matching platforms.",
|
||||||
"main": "wickra.linux-arm64-gnu.node",
|
"main": "wickra.linux-arm64-gnu.node",
|
||||||
"files": [
|
"files": [
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "wickra-linux-x64-gnu",
|
"name": "wickra-linux-x64-gnu",
|
||||||
"version": "0.7.1",
|
"version": "0.7.2",
|
||||||
"description": "Native binding for wickra (linux x64 GNU). Installed automatically as an optional dependency of wickra on matching platforms.",
|
"description": "Native binding for wickra (linux x64 GNU). Installed automatically as an optional dependency of wickra on matching platforms.",
|
||||||
"main": "wickra.linux-x64-gnu.node",
|
"main": "wickra.linux-x64-gnu.node",
|
||||||
"files": [
|
"files": [
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "wickra-win32-arm64-msvc",
|
"name": "wickra-win32-arm64-msvc",
|
||||||
"version": "0.7.1",
|
"version": "0.7.2",
|
||||||
"description": "Native binding for wickra (Windows arm64 MSVC). Installed automatically as an optional dependency of wickra on matching platforms.",
|
"description": "Native binding for wickra (Windows arm64 MSVC). Installed automatically as an optional dependency of wickra on matching platforms.",
|
||||||
"main": "wickra.win32-arm64-msvc.node",
|
"main": "wickra.win32-arm64-msvc.node",
|
||||||
"files": [
|
"files": [
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "wickra-win32-x64-msvc",
|
"name": "wickra-win32-x64-msvc",
|
||||||
"version": "0.7.1",
|
"version": "0.7.2",
|
||||||
"description": "Native binding for wickra (Windows x64 MSVC). Installed automatically as an optional dependency of wickra on matching platforms.",
|
"description": "Native binding for wickra (Windows x64 MSVC). Installed automatically as an optional dependency of wickra on matching platforms.",
|
||||||
"main": "wickra.win32-x64-msvc.node",
|
"main": "wickra.win32-x64-msvc.node",
|
||||||
"files": [
|
"files": [
|
||||||
|
|||||||
Generated
+20
-20
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "wickra",
|
"name": "wickra",
|
||||||
"version": "0.7.1",
|
"version": "0.7.2",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "wickra",
|
"name": "wickra",
|
||||||
"version": "0.7.1",
|
"version": "0.7.2",
|
||||||
"license": "MIT OR Apache-2.0",
|
"license": "MIT OR Apache-2.0",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@napi-rs/cli": "^2.18.0"
|
"@napi-rs/cli": "^2.18.0"
|
||||||
@@ -15,12 +15,12 @@
|
|||||||
"node": ">= 18"
|
"node": ">= 18"
|
||||||
},
|
},
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
"wickra-darwin-arm64": "0.7.1",
|
"wickra-darwin-arm64": "0.7.2",
|
||||||
"wickra-darwin-x64": "0.7.1",
|
"wickra-darwin-x64": "0.7.2",
|
||||||
"wickra-linux-arm64-gnu": "0.7.1",
|
"wickra-linux-arm64-gnu": "0.7.2",
|
||||||
"wickra-linux-x64-gnu": "0.7.1",
|
"wickra-linux-x64-gnu": "0.7.2",
|
||||||
"wickra-win32-arm64-msvc": "0.7.1",
|
"wickra-win32-arm64-msvc": "0.7.2",
|
||||||
"wickra-win32-x64-msvc": "0.7.1"
|
"wickra-win32-x64-msvc": "0.7.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@napi-rs/cli": {
|
"node_modules/@napi-rs/cli": {
|
||||||
@@ -41,8 +41,8 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/wickra-darwin-arm64": {
|
"node_modules/wickra-darwin-arm64": {
|
||||||
"version": "0.7.1",
|
"version": "0.7.2",
|
||||||
"resolved": "https://registry.npmjs.org/wickra-darwin-arm64/-/wickra-darwin-arm64-0.7.1.tgz",
|
"resolved": "https://registry.npmjs.org/wickra-darwin-arm64/-/wickra-darwin-arm64-0.7.2.tgz",
|
||||||
"integrity": "sha512-4eZiBR/yGUdr4nzhEUFy2i69XgNx64iI2ax/LPamsThgylC0KpHOZKK19QzJ2d9KbK4C8nMjME5FLuR+4GNEwQ==",
|
"integrity": "sha512-4eZiBR/yGUdr4nzhEUFy2i69XgNx64iI2ax/LPamsThgylC0KpHOZKK19QzJ2d9KbK4C8nMjME5FLuR+4GNEwQ==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
@@ -57,8 +57,8 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/wickra-darwin-x64": {
|
"node_modules/wickra-darwin-x64": {
|
||||||
"version": "0.7.1",
|
"version": "0.7.2",
|
||||||
"resolved": "https://registry.npmjs.org/wickra-darwin-x64/-/wickra-darwin-x64-0.7.1.tgz",
|
"resolved": "https://registry.npmjs.org/wickra-darwin-x64/-/wickra-darwin-x64-0.7.2.tgz",
|
||||||
"integrity": "sha512-6hf8zI3QPjTFp4zCpmgUwDvNtu6jHqNUHKD5e55POo0CgA52HkpyxSPtVm8TGTIZDI7kPjlbOdBM8CJ76mmXwA==",
|
"integrity": "sha512-6hf8zI3QPjTFp4zCpmgUwDvNtu6jHqNUHKD5e55POo0CgA52HkpyxSPtVm8TGTIZDI7kPjlbOdBM8CJ76mmXwA==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
@@ -73,8 +73,8 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/wickra-linux-arm64-gnu": {
|
"node_modules/wickra-linux-arm64-gnu": {
|
||||||
"version": "0.7.1",
|
"version": "0.7.2",
|
||||||
"resolved": "https://registry.npmjs.org/wickra-linux-arm64-gnu/-/wickra-linux-arm64-gnu-0.7.1.tgz",
|
"resolved": "https://registry.npmjs.org/wickra-linux-arm64-gnu/-/wickra-linux-arm64-gnu-0.7.2.tgz",
|
||||||
"integrity": "sha512-kSe6y0xBMSiqdPLXNjwop5WZdHtvdBNKSEBCwZ4hFq33p4apW25/wrlzv9/oDuyD4kuPabJEhCCnFOplh58CUg==",
|
"integrity": "sha512-kSe6y0xBMSiqdPLXNjwop5WZdHtvdBNKSEBCwZ4hFq33p4apW25/wrlzv9/oDuyD4kuPabJEhCCnFOplh58CUg==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
@@ -89,8 +89,8 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/wickra-linux-x64-gnu": {
|
"node_modules/wickra-linux-x64-gnu": {
|
||||||
"version": "0.7.1",
|
"version": "0.7.2",
|
||||||
"resolved": "https://registry.npmjs.org/wickra-linux-x64-gnu/-/wickra-linux-x64-gnu-0.7.1.tgz",
|
"resolved": "https://registry.npmjs.org/wickra-linux-x64-gnu/-/wickra-linux-x64-gnu-0.7.2.tgz",
|
||||||
"integrity": "sha512-tWBWS4qz7hxM4xnpFb59bhf6TaLwXq0Z3jEa/2l7r8PiHA94g8r8S53NRMiT+4yiL5hSWe/nUiC/YXdRrhEZ4g==",
|
"integrity": "sha512-tWBWS4qz7hxM4xnpFb59bhf6TaLwXq0Z3jEa/2l7r8PiHA94g8r8S53NRMiT+4yiL5hSWe/nUiC/YXdRrhEZ4g==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
@@ -105,8 +105,8 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/wickra-win32-arm64-msvc": {
|
"node_modules/wickra-win32-arm64-msvc": {
|
||||||
"version": "0.7.1",
|
"version": "0.7.2",
|
||||||
"resolved": "https://registry.npmjs.org/wickra-win32-arm64-msvc/-/wickra-win32-arm64-msvc-0.7.1.tgz",
|
"resolved": "https://registry.npmjs.org/wickra-win32-arm64-msvc/-/wickra-win32-arm64-msvc-0.7.2.tgz",
|
||||||
"integrity": "sha512-EXIckHxAtF75PUGDKRzXyqMe9ldP0JjSdu68WFN6iJfp+McYrGu6h40TEJlQ/oUEIoPqiZB/xhVyo/el5Lg7zw==",
|
"integrity": "sha512-EXIckHxAtF75PUGDKRzXyqMe9ldP0JjSdu68WFN6iJfp+McYrGu6h40TEJlQ/oUEIoPqiZB/xhVyo/el5Lg7zw==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
@@ -121,8 +121,8 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/wickra-win32-x64-msvc": {
|
"node_modules/wickra-win32-x64-msvc": {
|
||||||
"version": "0.7.1",
|
"version": "0.7.2",
|
||||||
"resolved": "https://registry.npmjs.org/wickra-win32-x64-msvc/-/wickra-win32-x64-msvc-0.7.1.tgz",
|
"resolved": "https://registry.npmjs.org/wickra-win32-x64-msvc/-/wickra-win32-x64-msvc-0.7.2.tgz",
|
||||||
"integrity": "sha512-Yfsqq1Xwp6hdxMyLze411vNdo7BDwI6+lPSe7A9XdqyPecNDbtKwYLpsal2r8EHbNzqM+R8XnuRtUaEQS5VlUQ==",
|
"integrity": "sha512-Yfsqq1Xwp6hdxMyLze411vNdo7BDwI6+lPSe7A9XdqyPecNDbtKwYLpsal2r8EHbNzqM+R8XnuRtUaEQS5VlUQ==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "wickra",
|
"name": "wickra",
|
||||||
"version": "0.7.1",
|
"version": "0.7.2",
|
||||||
"description": "Streaming-first technical indicators: incremental, fast, install-free. Node bindings powered by Rust.",
|
"description": "Streaming-first technical indicators: incremental, fast, install-free. Node bindings powered by Rust.",
|
||||||
"author": "kingchenc <support@wickra.org>",
|
"author": "kingchenc <support@wickra.org>",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
@@ -47,12 +47,12 @@
|
|||||||
"node": ">= 18"
|
"node": ">= 18"
|
||||||
},
|
},
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
"wickra-linux-x64-gnu": "0.7.1",
|
"wickra-linux-x64-gnu": "0.7.2",
|
||||||
"wickra-linux-arm64-gnu": "0.7.1",
|
"wickra-linux-arm64-gnu": "0.7.2",
|
||||||
"wickra-darwin-x64": "0.7.1",
|
"wickra-darwin-x64": "0.7.2",
|
||||||
"wickra-darwin-arm64": "0.7.1",
|
"wickra-darwin-arm64": "0.7.2",
|
||||||
"wickra-win32-x64-msvc": "0.7.1",
|
"wickra-win32-x64-msvc": "0.7.2",
|
||||||
"wickra-win32-arm64-msvc": "0.7.1"
|
"wickra-win32-arm64-msvc": "0.7.2"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "napi build --platform --release",
|
"build": "napi build --platform --release",
|
||||||
|
|||||||
@@ -12874,6 +12874,332 @@ pub struct VolumeProfileValue {
|
|||||||
pub bins: Vec<f64>,
|
pub bins: Vec<f64>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Naked POC: most recent untouched point-of-control level (Candle -> f64).
|
||||||
|
#[napi(js_name = "NakedPoc")]
|
||||||
|
pub struct NakedPocNode {
|
||||||
|
inner: wc::NakedPoc,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[napi]
|
||||||
|
impl NakedPocNode {
|
||||||
|
#[napi(constructor)]
|
||||||
|
pub fn new(session_len: u32, bin_count: u32) -> napi::Result<Self> {
|
||||||
|
Ok(Self {
|
||||||
|
inner: wc::NakedPoc::new(session_len as usize, bin_count as usize).map_err(map_err)?,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
#[napi]
|
||||||
|
pub fn update(
|
||||||
|
&mut self,
|
||||||
|
high: f64,
|
||||||
|
low: f64,
|
||||||
|
close: f64,
|
||||||
|
volume: f64,
|
||||||
|
) -> napi::Result<Option<f64>> {
|
||||||
|
Ok(self.inner.update(cnd(high, low, close, volume)?))
|
||||||
|
}
|
||||||
|
#[napi]
|
||||||
|
pub fn batch(
|
||||||
|
&mut self,
|
||||||
|
high: Vec<f64>,
|
||||||
|
low: Vec<f64>,
|
||||||
|
close: Vec<f64>,
|
||||||
|
volume: Vec<f64>,
|
||||||
|
) -> napi::Result<Vec<f64>> {
|
||||||
|
if high.len() != low.len() || low.len() != close.len() || close.len() != volume.len() {
|
||||||
|
return Err(NapiError::from_reason(
|
||||||
|
"high, low, close, volume must be equal length".to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let mut out = Vec::with_capacity(close.len());
|
||||||
|
for i in 0..close.len() {
|
||||||
|
out.push(
|
||||||
|
self.inner
|
||||||
|
.update(cnd(high[i], low[i], close[i], volume[i])?)
|
||||||
|
.unwrap_or(f64::NAN),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Single Prints: count of single-print price levels (Candle -> f64).
|
||||||
|
#[napi(js_name = "SinglePrints")]
|
||||||
|
pub struct SinglePrintsNode {
|
||||||
|
inner: wc::SinglePrints,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[napi]
|
||||||
|
impl SinglePrintsNode {
|
||||||
|
#[napi(constructor)]
|
||||||
|
pub fn new(period: u32, bin_count: u32) -> napi::Result<Self> {
|
||||||
|
Ok(Self {
|
||||||
|
inner: wc::SinglePrints::new(period as usize, bin_count as usize).map_err(map_err)?,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
#[napi]
|
||||||
|
pub fn update(&mut self, high: f64, low: f64) -> napi::Result<Option<f64>> {
|
||||||
|
let mid = f64::midpoint(high, low);
|
||||||
|
Ok(self
|
||||||
|
.inner
|
||||||
|
.update(wc::Candle::new(mid, high, low, mid, 0.0, 0).map_err(map_err)?))
|
||||||
|
}
|
||||||
|
#[napi]
|
||||||
|
pub fn batch(&mut self, high: Vec<f64>, low: Vec<f64>) -> napi::Result<Vec<f64>> {
|
||||||
|
if high.len() != low.len() {
|
||||||
|
return Err(NapiError::from_reason(
|
||||||
|
"high and low must be equal length".to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let mut out = Vec::with_capacity(high.len());
|
||||||
|
for i in 0..high.len() {
|
||||||
|
let mid = f64::midpoint(high[i], low[i]);
|
||||||
|
out.push(
|
||||||
|
self.inner
|
||||||
|
.update(wc::Candle::new(mid, high[i], low[i], mid, 0.0, 0).map_err(map_err)?)
|
||||||
|
.unwrap_or(f64::NAN),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Profile Shape: b/P/D classification as a numeric code (Candle -> f64).
|
||||||
|
#[napi(js_name = "ProfileShape")]
|
||||||
|
pub struct ProfileShapeNode {
|
||||||
|
inner: wc::ProfileShape,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[napi]
|
||||||
|
impl ProfileShapeNode {
|
||||||
|
#[napi(constructor)]
|
||||||
|
pub fn new(period: u32, bin_count: u32) -> napi::Result<Self> {
|
||||||
|
Ok(Self {
|
||||||
|
inner: wc::ProfileShape::new(period as usize, bin_count as usize).map_err(map_err)?,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
#[napi]
|
||||||
|
pub fn update(&mut self, high: f64, low: f64, volume: f64) -> napi::Result<Option<f64>> {
|
||||||
|
let mid = f64::midpoint(high, low);
|
||||||
|
Ok(self
|
||||||
|
.inner
|
||||||
|
.update(wc::Candle::new(mid, high, low, mid, volume, 0).map_err(map_err)?))
|
||||||
|
}
|
||||||
|
#[napi]
|
||||||
|
pub fn batch(
|
||||||
|
&mut self,
|
||||||
|
high: Vec<f64>,
|
||||||
|
low: Vec<f64>,
|
||||||
|
volume: Vec<f64>,
|
||||||
|
) -> napi::Result<Vec<f64>> {
|
||||||
|
if high.len() != low.len() || low.len() != volume.len() {
|
||||||
|
return Err(NapiError::from_reason(
|
||||||
|
"high, low, volume must be equal length".to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let mut out = Vec::with_capacity(high.len());
|
||||||
|
for i in 0..high.len() {
|
||||||
|
let mid = f64::midpoint(high[i], low[i]);
|
||||||
|
out.push(
|
||||||
|
self.inner
|
||||||
|
.update(
|
||||||
|
wc::Candle::new(mid, high[i], low[i], mid, volume[i], 0)
|
||||||
|
.map_err(map_err)?,
|
||||||
|
)
|
||||||
|
.unwrap_or(f64::NAN),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
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 HighLowVolumeNodesValue {
|
||||||
|
pub hvn: f64,
|
||||||
|
pub lvn: f64,
|
||||||
|
}
|
||||||
|
|
||||||
|
// High/Low Volume Nodes: highest- and lowest-volume price nodes (Candle -> struct).
|
||||||
|
#[napi(js_name = "HighLowVolumeNodes")]
|
||||||
|
pub struct HighLowVolumeNodesNode {
|
||||||
|
inner: wc::HighLowVolumeNodes,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[napi]
|
||||||
|
impl HighLowVolumeNodesNode {
|
||||||
|
#[napi(constructor)]
|
||||||
|
pub fn new(period: u32, bin_count: u32) -> napi::Result<Self> {
|
||||||
|
Ok(Self {
|
||||||
|
inner: wc::HighLowVolumeNodes::new(period as usize, bin_count as usize)
|
||||||
|
.map_err(map_err)?,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
#[napi]
|
||||||
|
pub fn update(
|
||||||
|
&mut self,
|
||||||
|
high: f64,
|
||||||
|
low: f64,
|
||||||
|
volume: f64,
|
||||||
|
) -> napi::Result<Option<HighLowVolumeNodesValue>> {
|
||||||
|
let mid = f64::midpoint(high, low);
|
||||||
|
let candle = wc::Candle::new(mid, high, low, mid, volume, 0).map_err(map_err)?;
|
||||||
|
Ok(self.inner.update(candle).map(|o| HighLowVolumeNodesValue {
|
||||||
|
hvn: o.hvn,
|
||||||
|
lvn: o.lvn,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
#[napi]
|
||||||
|
pub fn batch(
|
||||||
|
&mut self,
|
||||||
|
high: Vec<f64>,
|
||||||
|
low: Vec<f64>,
|
||||||
|
volume: Vec<f64>,
|
||||||
|
) -> napi::Result<Vec<f64>> {
|
||||||
|
if high.len() != low.len() || low.len() != volume.len() {
|
||||||
|
return Err(NapiError::from_reason(
|
||||||
|
"high, low, volume must be equal length".to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let n = high.len();
|
||||||
|
let mut out = vec![f64::NAN; n * 2];
|
||||||
|
for i in 0..n {
|
||||||
|
let mid = f64::midpoint(high[i], low[i]);
|
||||||
|
let candle =
|
||||||
|
wc::Candle::new(mid, high[i], low[i], mid, volume[i], 0).map_err(map_err)?;
|
||||||
|
if let Some(o) = self.inner.update(candle) {
|
||||||
|
out[i * 2] = o.hvn;
|
||||||
|
out[i * 2 + 1] = o.lvn;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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 CompositeProfileValue {
|
||||||
|
pub poc: f64,
|
||||||
|
pub vah: f64,
|
||||||
|
pub val: f64,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Composite Profile: multi-session composite volume profile (Candle -> struct).
|
||||||
|
#[napi(js_name = "CompositeProfile")]
|
||||||
|
pub struct CompositeProfileNode {
|
||||||
|
inner: wc::CompositeProfile,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[napi]
|
||||||
|
impl CompositeProfileNode {
|
||||||
|
#[napi(constructor)]
|
||||||
|
pub fn new(period: u32, bin_count: u32, value_area_pct: f64) -> napi::Result<Self> {
|
||||||
|
Ok(Self {
|
||||||
|
inner: wc::CompositeProfile::new(period as usize, bin_count as usize, value_area_pct)
|
||||||
|
.map_err(map_err)?,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
#[napi]
|
||||||
|
pub fn update(
|
||||||
|
&mut self,
|
||||||
|
high: f64,
|
||||||
|
low: f64,
|
||||||
|
volume: f64,
|
||||||
|
) -> napi::Result<Option<CompositeProfileValue>> {
|
||||||
|
let mid = f64::midpoint(high, low);
|
||||||
|
let candle = wc::Candle::new(mid, high, low, mid, volume, 0).map_err(map_err)?;
|
||||||
|
Ok(self.inner.update(candle).map(|o| CompositeProfileValue {
|
||||||
|
poc: o.poc,
|
||||||
|
vah: o.vah,
|
||||||
|
val: o.val,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
#[napi]
|
||||||
|
pub fn batch(
|
||||||
|
&mut self,
|
||||||
|
high: Vec<f64>,
|
||||||
|
low: Vec<f64>,
|
||||||
|
volume: Vec<f64>,
|
||||||
|
) -> napi::Result<Vec<f64>> {
|
||||||
|
if high.len() != low.len() || low.len() != volume.len() {
|
||||||
|
return Err(NapiError::from_reason(
|
||||||
|
"high, low, volume must be equal length".to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let n = high.len();
|
||||||
|
let mut out = vec![f64::NAN; n * 3];
|
||||||
|
for i in 0..n {
|
||||||
|
let mid = f64::midpoint(high[i], low[i]);
|
||||||
|
let candle =
|
||||||
|
wc::Candle::new(mid, high[i], low[i], mid, volume[i], 0).map_err(map_err)?;
|
||||||
|
if let Some(o) = self.inner.update(candle) {
|
||||||
|
out[i * 3] = o.poc;
|
||||||
|
out[i * 3 + 1] = o.vah;
|
||||||
|
out[i * 3 + 2] = o.val;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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 = "VolumeProfile")]
|
#[napi(js_name = "VolumeProfile")]
|
||||||
pub struct VolumeProfileNode {
|
pub struct VolumeProfileNode {
|
||||||
inner: wc::VolumeProfile,
|
inner: wc::VolumeProfile,
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ build-backend = "maturin"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "wickra"
|
name = "wickra"
|
||||||
version = "0.7.1"
|
version = "0.7.2"
|
||||||
description = "Streaming-first technical indicators: incremental, fast, install-free."
|
description = "Streaming-first technical indicators: incremental, fast, install-free."
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
license = "MIT OR Apache-2.0"
|
license = "MIT OR Apache-2.0"
|
||||||
|
|||||||
@@ -350,6 +350,11 @@ from ._wickra import (
|
|||||||
Equivolume,
|
Equivolume,
|
||||||
CandleVolume,
|
CandleVolume,
|
||||||
# Market Profile
|
# Market Profile
|
||||||
|
CompositeProfile,
|
||||||
|
HighLowVolumeNodes,
|
||||||
|
ProfileShape,
|
||||||
|
SinglePrints,
|
||||||
|
NakedPoc,
|
||||||
ValueArea,
|
ValueArea,
|
||||||
VolumeProfile,
|
VolumeProfile,
|
||||||
TpoProfile,
|
TpoProfile,
|
||||||
@@ -873,6 +878,11 @@ __all__ = [
|
|||||||
"Equivolume",
|
"Equivolume",
|
||||||
"CandleVolume",
|
"CandleVolume",
|
||||||
# Market Profile
|
# Market Profile
|
||||||
|
"CompositeProfile",
|
||||||
|
"HighLowVolumeNodes",
|
||||||
|
"ProfileShape",
|
||||||
|
"SinglePrints",
|
||||||
|
"NakedPoc",
|
||||||
"ValueArea",
|
"ValueArea",
|
||||||
"VolumeProfile",
|
"VolumeProfile",
|
||||||
"TpoProfile",
|
"TpoProfile",
|
||||||
|
|||||||
@@ -18140,6 +18140,349 @@ impl PyOpeningRange {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Naked POC: most recent untouched point-of-control level (Candle -> f64).
|
||||||
|
#[pyclass(name = "NakedPoc", module = "wickra._wickra", skip_from_py_object)]
|
||||||
|
#[derive(Clone)]
|
||||||
|
struct PyNakedPoc {
|
||||||
|
inner: wc::NakedPoc,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[pymethods]
|
||||||
|
impl PyNakedPoc {
|
||||||
|
#[new]
|
||||||
|
#[pyo3(signature = (session_len=20, bin_count=24))]
|
||||||
|
fn new(session_len: usize, bin_count: usize) -> PyResult<Self> {
|
||||||
|
Ok(Self {
|
||||||
|
inner: wc::NakedPoc::new(session_len, bin_count).map_err(map_err)?,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
fn update(&mut self, candle: &Bound<'_, PyAny>) -> PyResult<Option<f64>> {
|
||||||
|
let c = extract_candle(candle)?;
|
||||||
|
Ok(self.inner.update(c))
|
||||||
|
}
|
||||||
|
/// Batch over numpy high, low, close, volume arrays (all 1-D, equal length).
|
||||||
|
fn batch<'py>(
|
||||||
|
&mut self,
|
||||||
|
py: Python<'py>,
|
||||||
|
high: PyReadonlyArray1<'py, f64>,
|
||||||
|
low: PyReadonlyArray1<'py, f64>,
|
||||||
|
close: PyReadonlyArray1<'py, f64>,
|
||||||
|
volume: PyReadonlyArray1<'py, f64>,
|
||||||
|
) -> PyResult<Bound<'py, PyArray1<f64>>> {
|
||||||
|
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))?;
|
||||||
|
if h.len() != l.len() || l.len() != c.len() || c.len() != v.len() {
|
||||||
|
return Err(PyValueError::new_err(
|
||||||
|
"high, low, close, volume must be equal length",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let mut out = Vec::with_capacity(c.len());
|
||||||
|
for i in 0..c.len() {
|
||||||
|
let candle = wc::Candle::new(c[i], h[i], l[i], c[i], v[i], 0).map_err(map_err)?;
|
||||||
|
out.push(self.inner.update(candle).unwrap_or(f64::NAN));
|
||||||
|
}
|
||||||
|
Ok(out.into_pyarray(py))
|
||||||
|
}
|
||||||
|
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 (s, b) = self.inner.params();
|
||||||
|
format!("NakedPoc(session_len={s}, bin_count={b})")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Single Prints: count of single-print price levels in the profile (Candle -> f64).
|
||||||
|
#[pyclass(name = "SinglePrints", module = "wickra._wickra", skip_from_py_object)]
|
||||||
|
#[derive(Clone)]
|
||||||
|
struct PySinglePrints {
|
||||||
|
inner: wc::SinglePrints,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[pymethods]
|
||||||
|
impl PySinglePrints {
|
||||||
|
#[new]
|
||||||
|
#[pyo3(signature = (period=20, bin_count=24))]
|
||||||
|
fn new(period: usize, bin_count: usize) -> PyResult<Self> {
|
||||||
|
Ok(Self {
|
||||||
|
inner: wc::SinglePrints::new(period, bin_count).map_err(map_err)?,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
fn update(&mut self, candle: &Bound<'_, PyAny>) -> PyResult<Option<f64>> {
|
||||||
|
let c = extract_candle(candle)?;
|
||||||
|
Ok(self.inner.update(c))
|
||||||
|
}
|
||||||
|
/// Batch over numpy high, low arrays (1-D, equal length).
|
||||||
|
fn batch<'py>(
|
||||||
|
&mut self,
|
||||||
|
py: Python<'py>,
|
||||||
|
high: PyReadonlyArray1<'py, f64>,
|
||||||
|
low: PyReadonlyArray1<'py, f64>,
|
||||||
|
) -> PyResult<Bound<'py, PyArray1<f64>>> {
|
||||||
|
let h = high
|
||||||
|
.as_slice()
|
||||||
|
.map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?;
|
||||||
|
let l = low
|
||||||
|
.as_slice()
|
||||||
|
.map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?;
|
||||||
|
if h.len() != l.len() {
|
||||||
|
return Err(PyValueError::new_err("high and low must be equal length"));
|
||||||
|
}
|
||||||
|
let mut out = Vec::with_capacity(h.len());
|
||||||
|
for i in 0..h.len() {
|
||||||
|
let mid = f64::midpoint(h[i], l[i]);
|
||||||
|
let candle = wc::Candle::new(mid, h[i], l[i], mid, 0.0, 0).map_err(map_err)?;
|
||||||
|
out.push(self.inner.update(candle).unwrap_or(f64::NAN));
|
||||||
|
}
|
||||||
|
Ok(out.into_pyarray(py))
|
||||||
|
}
|
||||||
|
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 (p, b) = self.inner.params();
|
||||||
|
format!("SinglePrints(period={p}, bin_count={b})")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Profile Shape: b/P/D shape classification as a numeric code (Candle -> f64).
|
||||||
|
#[pyclass(name = "ProfileShape", module = "wickra._wickra", skip_from_py_object)]
|
||||||
|
#[derive(Clone)]
|
||||||
|
struct PyProfileShape {
|
||||||
|
inner: wc::ProfileShape,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[pymethods]
|
||||||
|
impl PyProfileShape {
|
||||||
|
#[new]
|
||||||
|
#[pyo3(signature = (period=20, bin_count=24))]
|
||||||
|
fn new(period: usize, bin_count: usize) -> PyResult<Self> {
|
||||||
|
Ok(Self {
|
||||||
|
inner: wc::ProfileShape::new(period, bin_count).map_err(map_err)?,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
fn update(&mut self, candle: &Bound<'_, PyAny>) -> PyResult<Option<f64>> {
|
||||||
|
let c = extract_candle(candle)?;
|
||||||
|
Ok(self.inner.update(c))
|
||||||
|
}
|
||||||
|
/// Batch over numpy high, low, volume arrays (1-D, equal length).
|
||||||
|
fn batch<'py>(
|
||||||
|
&mut self,
|
||||||
|
py: Python<'py>,
|
||||||
|
high: PyReadonlyArray1<'py, f64>,
|
||||||
|
low: PyReadonlyArray1<'py, f64>,
|
||||||
|
volume: PyReadonlyArray1<'py, f64>,
|
||||||
|
) -> PyResult<Bound<'py, PyArray1<f64>>> {
|
||||||
|
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 v = volume
|
||||||
|
.as_slice()
|
||||||
|
.map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?;
|
||||||
|
if h.len() != l.len() || l.len() != v.len() {
|
||||||
|
return Err(PyValueError::new_err(
|
||||||
|
"high, low, volume must be equal length",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let mut out = Vec::with_capacity(h.len());
|
||||||
|
for i in 0..h.len() {
|
||||||
|
let mid = f64::midpoint(h[i], l[i]);
|
||||||
|
let candle = wc::Candle::new(mid, h[i], l[i], mid, v[i], 0).map_err(map_err)?;
|
||||||
|
out.push(self.inner.update(candle).unwrap_or(f64::NAN));
|
||||||
|
}
|
||||||
|
Ok(out.into_pyarray(py))
|
||||||
|
}
|
||||||
|
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 (p, b) = self.inner.params();
|
||||||
|
format!("ProfileShape(period={p}, bin_count={b})")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// High/Low Volume Nodes: highest- and lowest-volume price nodes (Candle -> struct).
|
||||||
|
#[pyclass(
|
||||||
|
name = "HighLowVolumeNodes",
|
||||||
|
module = "wickra._wickra",
|
||||||
|
skip_from_py_object
|
||||||
|
)]
|
||||||
|
#[derive(Clone)]
|
||||||
|
struct PyHighLowVolumeNodes {
|
||||||
|
inner: wc::HighLowVolumeNodes,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[pymethods]
|
||||||
|
impl PyHighLowVolumeNodes {
|
||||||
|
#[new]
|
||||||
|
#[pyo3(signature = (period=20, bin_count=24))]
|
||||||
|
fn new(period: usize, bin_count: usize) -> PyResult<Self> {
|
||||||
|
Ok(Self {
|
||||||
|
inner: wc::HighLowVolumeNodes::new(period, bin_count).map_err(map_err)?,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
fn update(&mut self, candle: &Bound<'_, PyAny>) -> PyResult<Option<(f64, f64)>> {
|
||||||
|
let c = extract_candle(candle)?;
|
||||||
|
Ok(self.inner.update(c).map(|o| (o.hvn, o.lvn)))
|
||||||
|
}
|
||||||
|
/// Batch over numpy high, low, volume. Returns shape `(n, 2)` `[hvn, lvn]`.
|
||||||
|
fn batch<'py>(
|
||||||
|
&mut self,
|
||||||
|
py: Python<'py>,
|
||||||
|
high: PyReadonlyArray1<'py, f64>,
|
||||||
|
low: PyReadonlyArray1<'py, f64>,
|
||||||
|
volume: PyReadonlyArray1<'py, f64>,
|
||||||
|
) -> PyResult<Bound<'py, PyArray2<f64>>> {
|
||||||
|
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 v = volume
|
||||||
|
.as_slice()
|
||||||
|
.map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?;
|
||||||
|
if h.len() != l.len() || l.len() != v.len() {
|
||||||
|
return Err(PyValueError::new_err(
|
||||||
|
"high, low, volume must be equal length",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let n = h.len();
|
||||||
|
let mut out = vec![f64::NAN; n * 2];
|
||||||
|
for i in 0..n {
|
||||||
|
let mid = f64::midpoint(h[i], l[i]);
|
||||||
|
let candle = wc::Candle::new(mid, h[i], l[i], mid, v[i], 0).map_err(map_err)?;
|
||||||
|
if let Some(o) = self.inner.update(candle) {
|
||||||
|
out[i * 2] = o.hvn;
|
||||||
|
out[i * 2 + 1] = o.lvn;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(numpy::ndarray::Array2::from_shape_vec((n, 2), out)
|
||||||
|
.expect("shape consistent")
|
||||||
|
.into_pyarray(py))
|
||||||
|
}
|
||||||
|
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 (p, b) = self.inner.params();
|
||||||
|
format!("HighLowVolumeNodes(period={p}, bin_count={b})")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Composite Profile: multi-session composite volume profile (Candle -> struct).
|
||||||
|
#[pyclass(
|
||||||
|
name = "CompositeProfile",
|
||||||
|
module = "wickra._wickra",
|
||||||
|
skip_from_py_object
|
||||||
|
)]
|
||||||
|
#[derive(Clone)]
|
||||||
|
struct PyCompositeProfile {
|
||||||
|
inner: wc::CompositeProfile,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[pymethods]
|
||||||
|
impl PyCompositeProfile {
|
||||||
|
#[new]
|
||||||
|
#[pyo3(signature = (period=20, bin_count=24, value_area_pct=0.70))]
|
||||||
|
fn new(period: usize, bin_count: usize, value_area_pct: f64) -> PyResult<Self> {
|
||||||
|
Ok(Self {
|
||||||
|
inner: wc::CompositeProfile::new(period, bin_count, value_area_pct).map_err(map_err)?,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
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.poc, o.vah, o.val)))
|
||||||
|
}
|
||||||
|
/// Batch over numpy high, low, volume. Returns shape `(n, 3)` `[poc, vah, val]`.
|
||||||
|
fn batch<'py>(
|
||||||
|
&mut self,
|
||||||
|
py: Python<'py>,
|
||||||
|
high: PyReadonlyArray1<'py, f64>,
|
||||||
|
low: PyReadonlyArray1<'py, f64>,
|
||||||
|
volume: PyReadonlyArray1<'py, f64>,
|
||||||
|
) -> PyResult<Bound<'py, PyArray2<f64>>> {
|
||||||
|
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 v = volume
|
||||||
|
.as_slice()
|
||||||
|
.map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?;
|
||||||
|
if h.len() != l.len() || l.len() != v.len() {
|
||||||
|
return Err(PyValueError::new_err(
|
||||||
|
"high, low, volume must be equal length",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let n = h.len();
|
||||||
|
let mut out = vec![f64::NAN; n * 3];
|
||||||
|
for i in 0..n {
|
||||||
|
let mid = f64::midpoint(h[i], l[i]);
|
||||||
|
let candle = wc::Candle::new(mid, h[i], l[i], mid, v[i], 0).map_err(map_err)?;
|
||||||
|
if let Some(o) = self.inner.update(candle) {
|
||||||
|
out[i * 3] = o.poc;
|
||||||
|
out[i * 3 + 1] = o.vah;
|
||||||
|
out[i * 3 + 2] = o.val;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(numpy::ndarray::Array2::from_shape_vec((n, 3), out)
|
||||||
|
.expect("shape consistent")
|
||||||
|
.into_pyarray(py))
|
||||||
|
}
|
||||||
|
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 (p, b, pct) = self.inner.params();
|
||||||
|
format!("CompositeProfile(period={p}, bin_count={b}, value_area_pct={pct})")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ============================== Candlestick Patterns ==============================
|
// ============================== Candlestick Patterns ==============================
|
||||||
//
|
//
|
||||||
// All 15 patterns take Candles and emit a signed f64 signal per bar:
|
// All 15 patterns take Candles and emit a signed f64 signal per bar:
|
||||||
@@ -25272,6 +25615,11 @@ fn _wickra(_py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> {
|
|||||||
m.add_class::<PyPointAndFigureBars>()?;
|
m.add_class::<PyPointAndFigureBars>()?;
|
||||||
m.add_class::<PyInitialBalance>()?;
|
m.add_class::<PyInitialBalance>()?;
|
||||||
m.add_class::<PyOpeningRange>()?;
|
m.add_class::<PyOpeningRange>()?;
|
||||||
|
m.add_class::<PyNakedPoc>()?;
|
||||||
|
m.add_class::<PySinglePrints>()?;
|
||||||
|
m.add_class::<PyProfileShape>()?;
|
||||||
|
m.add_class::<PyHighLowVolumeNodes>()?;
|
||||||
|
m.add_class::<PyCompositeProfile>()?;
|
||||||
// Candlestick patterns.
|
// Candlestick patterns.
|
||||||
m.add_class::<PyDoji>()?;
|
m.add_class::<PyDoji>()?;
|
||||||
m.add_class::<PyHammer>()?;
|
m.add_class::<PyHammer>()?;
|
||||||
|
|||||||
@@ -383,6 +383,18 @@ def test_relative_strength_streaming_matches_batch():
|
|||||||
# 6-tuple candle; the batch helper takes only the columns it needs.
|
# 6-tuple candle; the batch helper takes only the columns it needs.
|
||||||
|
|
||||||
CANDLE_SCALAR = {
|
CANDLE_SCALAR = {
|
||||||
|
"ProfileShape": (
|
||||||
|
lambda: ta.ProfileShape(20, 24),
|
||||||
|
lambda ind, h, l, c, v: ind.batch(h, l, v),
|
||||||
|
),
|
||||||
|
"SinglePrints": (
|
||||||
|
lambda: ta.SinglePrints(20, 24),
|
||||||
|
lambda ind, h, l, c, v: ind.batch(h, l),
|
||||||
|
),
|
||||||
|
"NakedPoc": (
|
||||||
|
lambda: ta.NakedPoc(20, 24),
|
||||||
|
lambda ind, h, l, c, v: ind.batch(h, l, c, v),
|
||||||
|
),
|
||||||
"FryPanBottom": (
|
"FryPanBottom": (
|
||||||
lambda: ta.FryPanBottom(9),
|
lambda: ta.FryPanBottom(9),
|
||||||
lambda ind, h, l, c, v: ind.batch(h, l, c),
|
lambda ind, h, l, c, v: ind.batch(h, l, c),
|
||||||
@@ -1009,6 +1021,16 @@ def test_candle_scalar_streaming_matches_batch(name, ohlcv):
|
|||||||
# --- Candle-input, multi-output indicators --------------------------------
|
# --- Candle-input, multi-output indicators --------------------------------
|
||||||
|
|
||||||
MULTI = {
|
MULTI = {
|
||||||
|
"CompositeProfile": (
|
||||||
|
lambda: ta.CompositeProfile(20, 24, 0.7),
|
||||||
|
lambda ind, h, l, c, v: ind.batch(h, l, v),
|
||||||
|
3,
|
||||||
|
),
|
||||||
|
"HighLowVolumeNodes": (
|
||||||
|
lambda: ta.HighLowVolumeNodes(20, 24),
|
||||||
|
lambda ind, h, l, c, v: ind.batch(h, l, v),
|
||||||
|
2,
|
||||||
|
),
|
||||||
"CandleVolume": (
|
"CandleVolume": (
|
||||||
lambda: ta.CandleVolume(20),
|
lambda: ta.CandleVolume(20),
|
||||||
lambda ind, h, l, c, v: ind.batch(c, c, v),
|
lambda ind, h, l, c, v: ind.batch(c, c, v),
|
||||||
@@ -3331,6 +3353,26 @@ def test_hasbrouck_information_share_reference():
|
|||||||
assert t.update(7.0, 9.0) is None
|
assert t.update(7.0, 9.0) is None
|
||||||
assert t.update(7.0, 9.0) == pytest.approx(0.5)
|
assert t.update(7.0, 9.0) == pytest.approx(0.5)
|
||||||
|
|
||||||
|
|
||||||
|
def test_naked_poc_reference():
|
||||||
|
t = ta.NakedPoc(20, 24)
|
||||||
|
|
||||||
|
|
||||||
|
def test_single_prints_reference():
|
||||||
|
t = ta.SinglePrints(20, 24)
|
||||||
|
|
||||||
|
|
||||||
|
def test_profile_shape_reference():
|
||||||
|
t = ta.ProfileShape(20, 24)
|
||||||
|
|
||||||
|
|
||||||
|
def test_high_low_volume_nodes_reference():
|
||||||
|
t = ta.HighLowVolumeNodes(20, 24)
|
||||||
|
|
||||||
|
|
||||||
|
def test_composite_profile_reference():
|
||||||
|
t = ta.CompositeProfile(20, 24, 0.7)
|
||||||
|
|
||||||
# --- Lifecycle ------------------------------------------------------------
|
# --- Lifecycle ------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -8526,6 +8526,298 @@ impl WasmValueArea {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Naked POC: most recent untouched point-of-control level (Candle -> f64).
|
||||||
|
#[wasm_bindgen(js_name = NakedPoc)]
|
||||||
|
pub struct WasmNakedPoc {
|
||||||
|
inner: wc::NakedPoc,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[wasm_bindgen(js_class = NakedPoc)]
|
||||||
|
impl WasmNakedPoc {
|
||||||
|
#[wasm_bindgen(constructor)]
|
||||||
|
pub fn new(session_len: usize, bin_count: usize) -> Result<WasmNakedPoc, JsError> {
|
||||||
|
Ok(Self {
|
||||||
|
inner: wc::NakedPoc::new(session_len, bin_count).map_err(map_err)?,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
pub fn update(
|
||||||
|
&mut self,
|
||||||
|
high: f64,
|
||||||
|
low: f64,
|
||||||
|
close: f64,
|
||||||
|
volume: f64,
|
||||||
|
) -> Result<Option<f64>, JsError> {
|
||||||
|
Ok(self.inner.update(make_candle(high, low, close, volume)?))
|
||||||
|
}
|
||||||
|
pub fn batch(
|
||||||
|
&mut self,
|
||||||
|
high: &[f64],
|
||||||
|
low: &[f64],
|
||||||
|
close: &[f64],
|
||||||
|
volume: &[f64],
|
||||||
|
) -> Result<Float64Array, JsError> {
|
||||||
|
if high.len() != low.len() || low.len() != close.len() || close.len() != volume.len() {
|
||||||
|
return Err(JsError::new(
|
||||||
|
"high, low, close, volume must be equal length",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let mut out = Vec::with_capacity(close.len());
|
||||||
|
for i in 0..close.len() {
|
||||||
|
out.push(
|
||||||
|
self.inner
|
||||||
|
.update(make_candle(high[i], low[i], close[i], volume[i])?)
|
||||||
|
.unwrap_or(f64::NAN),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Ok(Float64Array::from(out.as_slice()))
|
||||||
|
}
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Single Prints: count of single-print price levels (Candle -> f64).
|
||||||
|
#[wasm_bindgen(js_name = SinglePrints)]
|
||||||
|
pub struct WasmSinglePrints {
|
||||||
|
inner: wc::SinglePrints,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[wasm_bindgen(js_class = SinglePrints)]
|
||||||
|
impl WasmSinglePrints {
|
||||||
|
#[wasm_bindgen(constructor)]
|
||||||
|
pub fn new(period: usize, bin_count: usize) -> Result<WasmSinglePrints, JsError> {
|
||||||
|
Ok(Self {
|
||||||
|
inner: wc::SinglePrints::new(period, bin_count).map_err(map_err)?,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
pub fn update(&mut self, high: f64, low: f64) -> Result<Option<f64>, JsError> {
|
||||||
|
let mid = f64::midpoint(high, low);
|
||||||
|
Ok(self
|
||||||
|
.inner
|
||||||
|
.update(wc::Candle::new(mid, high, low, mid, 0.0, 0).map_err(map_err)?))
|
||||||
|
}
|
||||||
|
pub fn batch(&mut self, high: &[f64], low: &[f64]) -> Result<Float64Array, JsError> {
|
||||||
|
if high.len() != low.len() {
|
||||||
|
return Err(JsError::new("high and low must be equal length"));
|
||||||
|
}
|
||||||
|
let mut out = Vec::with_capacity(high.len());
|
||||||
|
for i in 0..high.len() {
|
||||||
|
let mid = f64::midpoint(high[i], low[i]);
|
||||||
|
out.push(
|
||||||
|
self.inner
|
||||||
|
.update(wc::Candle::new(mid, high[i], low[i], mid, 0.0, 0).map_err(map_err)?)
|
||||||
|
.unwrap_or(f64::NAN),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Ok(Float64Array::from(out.as_slice()))
|
||||||
|
}
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Profile Shape: b/P/D classification as a numeric code (Candle -> f64).
|
||||||
|
#[wasm_bindgen(js_name = ProfileShape)]
|
||||||
|
pub struct WasmProfileShape {
|
||||||
|
inner: wc::ProfileShape,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[wasm_bindgen(js_class = ProfileShape)]
|
||||||
|
impl WasmProfileShape {
|
||||||
|
#[wasm_bindgen(constructor)]
|
||||||
|
pub fn new(period: usize, bin_count: usize) -> Result<WasmProfileShape, JsError> {
|
||||||
|
Ok(Self {
|
||||||
|
inner: wc::ProfileShape::new(period, bin_count).map_err(map_err)?,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
pub fn update(&mut self, high: f64, low: f64, volume: f64) -> Result<Option<f64>, JsError> {
|
||||||
|
let mid = f64::midpoint(high, low);
|
||||||
|
Ok(self
|
||||||
|
.inner
|
||||||
|
.update(wc::Candle::new(mid, high, low, mid, volume, 0).map_err(map_err)?))
|
||||||
|
}
|
||||||
|
pub fn batch(
|
||||||
|
&mut self,
|
||||||
|
high: &[f64],
|
||||||
|
low: &[f64],
|
||||||
|
volume: &[f64],
|
||||||
|
) -> Result<Float64Array, JsError> {
|
||||||
|
if high.len() != low.len() || low.len() != volume.len() {
|
||||||
|
return Err(JsError::new("high, low, volume must be equal length"));
|
||||||
|
}
|
||||||
|
let mut out = Vec::with_capacity(high.len());
|
||||||
|
for i in 0..high.len() {
|
||||||
|
let mid = f64::midpoint(high[i], low[i]);
|
||||||
|
out.push(
|
||||||
|
self.inner
|
||||||
|
.update(
|
||||||
|
wc::Candle::new(mid, high[i], low[i], mid, volume[i], 0)
|
||||||
|
.map_err(map_err)?,
|
||||||
|
)
|
||||||
|
.unwrap_or(f64::NAN),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Ok(Float64Array::from(out.as_slice()))
|
||||||
|
}
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// High/Low Volume Nodes: highest- and lowest-volume price nodes (Candle -> struct).
|
||||||
|
#[wasm_bindgen(js_name = HighLowVolumeNodes)]
|
||||||
|
pub struct WasmHighLowVolumeNodes {
|
||||||
|
inner: wc::HighLowVolumeNodes,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[wasm_bindgen(js_class = HighLowVolumeNodes)]
|
||||||
|
impl WasmHighLowVolumeNodes {
|
||||||
|
#[wasm_bindgen(constructor)]
|
||||||
|
pub fn new(period: usize, bin_count: usize) -> Result<WasmHighLowVolumeNodes, JsError> {
|
||||||
|
Ok(Self {
|
||||||
|
inner: wc::HighLowVolumeNodes::new(period, bin_count).map_err(map_err)?,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
pub fn batch(
|
||||||
|
&mut self,
|
||||||
|
high: &[f64],
|
||||||
|
low: &[f64],
|
||||||
|
volume: &[f64],
|
||||||
|
) -> Result<Float64Array, JsError> {
|
||||||
|
if high.len() != low.len() || low.len() != volume.len() {
|
||||||
|
return Err(JsError::new("high, low, volume must be equal length"));
|
||||||
|
}
|
||||||
|
let n = high.len();
|
||||||
|
let mut out = vec![f64::NAN; n * 2];
|
||||||
|
for i in 0..n {
|
||||||
|
let mid = f64::midpoint(high[i], low[i]);
|
||||||
|
let c = wc::Candle::new(mid, high[i], low[i], mid, volume[i], 0).map_err(map_err)?;
|
||||||
|
if let Some(o) = self.inner.update(c) {
|
||||||
|
out[i * 2] = o.hvn;
|
||||||
|
out[i * 2 + 1] = o.lvn;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(Float64Array::from(out.as_slice()))
|
||||||
|
}
|
||||||
|
/// Streaming update. Returns `{ hvn, lvn }` once warm, else `null`.
|
||||||
|
pub fn update(&mut self, high: f64, low: f64, volume: f64) -> Result<JsValue, JsError> {
|
||||||
|
let mid = f64::midpoint(high, low);
|
||||||
|
let c = wc::Candle::new(mid, high, low, mid, volume, 0).map_err(map_err)?;
|
||||||
|
Ok(match self.inner.update(c) {
|
||||||
|
Some(o) => {
|
||||||
|
let obj = Object::new();
|
||||||
|
Reflect::set(&obj, &"hvn".into(), &o.hvn.into()).ok();
|
||||||
|
Reflect::set(&obj, &"lvn".into(), &o.lvn.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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Composite Profile: multi-session composite volume profile (Candle -> struct).
|
||||||
|
#[wasm_bindgen(js_name = CompositeProfile)]
|
||||||
|
pub struct WasmCompositeProfile {
|
||||||
|
inner: wc::CompositeProfile,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[wasm_bindgen(js_class = CompositeProfile)]
|
||||||
|
impl WasmCompositeProfile {
|
||||||
|
#[wasm_bindgen(constructor)]
|
||||||
|
pub fn new(
|
||||||
|
period: usize,
|
||||||
|
bin_count: usize,
|
||||||
|
value_area_pct: f64,
|
||||||
|
) -> Result<WasmCompositeProfile, JsError> {
|
||||||
|
Ok(Self {
|
||||||
|
inner: wc::CompositeProfile::new(period, bin_count, value_area_pct).map_err(map_err)?,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
pub fn batch(
|
||||||
|
&mut self,
|
||||||
|
high: &[f64],
|
||||||
|
low: &[f64],
|
||||||
|
volume: &[f64],
|
||||||
|
) -> Result<Float64Array, JsError> {
|
||||||
|
if high.len() != low.len() || low.len() != volume.len() {
|
||||||
|
return Err(JsError::new("high, low, volume must be equal length"));
|
||||||
|
}
|
||||||
|
let n = high.len();
|
||||||
|
let mut out = vec![f64::NAN; n * 3];
|
||||||
|
for i in 0..n {
|
||||||
|
let mid = f64::midpoint(high[i], low[i]);
|
||||||
|
let c = wc::Candle::new(mid, high[i], low[i], mid, volume[i], 0).map_err(map_err)?;
|
||||||
|
if let Some(o) = self.inner.update(c) {
|
||||||
|
out[i * 3] = o.poc;
|
||||||
|
out[i * 3 + 1] = o.vah;
|
||||||
|
out[i * 3 + 2] = o.val;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(Float64Array::from(out.as_slice()))
|
||||||
|
}
|
||||||
|
/// Streaming update. Returns `{ poc, vah, val }` once warm, else `null`.
|
||||||
|
pub fn update(&mut self, high: f64, low: f64, volume: f64) -> Result<JsValue, JsError> {
|
||||||
|
let mid = f64::midpoint(high, low);
|
||||||
|
let c = wc::Candle::new(mid, high, low, mid, volume, 0).map_err(map_err)?;
|
||||||
|
Ok(match self.inner.update(c) {
|
||||||
|
Some(o) => {
|
||||||
|
let obj = Object::new();
|
||||||
|
Reflect::set(&obj, &"poc".into(), &o.poc.into()).ok();
|
||||||
|
Reflect::set(&obj, &"vah".into(), &o.vah.into()).ok();
|
||||||
|
Reflect::set(&obj, &"val".into(), &o.val.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 = VolumeProfile)]
|
#[wasm_bindgen(js_name = VolumeProfile)]
|
||||||
pub struct WasmVolumeProfile {
|
pub struct WasmVolumeProfile {
|
||||||
inner: wc::VolumeProfile,
|
inner: wc::VolumeProfile,
|
||||||
|
|||||||
@@ -0,0 +1,347 @@
|
|||||||
|
//! Composite Profile — POC and value area over a long composite window.
|
||||||
|
|
||||||
|
use std::collections::VecDeque;
|
||||||
|
|
||||||
|
use crate::error::{Error, Result};
|
||||||
|
use crate::ohlcv::Candle;
|
||||||
|
use crate::traits::Indicator;
|
||||||
|
|
||||||
|
/// Output of [`CompositeProfile`]: the point of control and the value-area bounds.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||||
|
pub struct CompositeProfileOutput {
|
||||||
|
/// Point of Control — the price (bin centre) with the most volume.
|
||||||
|
pub poc: f64,
|
||||||
|
/// Value-Area High — top of the band holding `value_area_pct` of volume.
|
||||||
|
pub vah: f64,
|
||||||
|
/// Value-Area Low — bottom of that band.
|
||||||
|
pub val: f64,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Composite Profile — a multi-session volume profile reduced to its **point of
|
||||||
|
/// control** and **value area**, built over a long composite window.
|
||||||
|
///
|
||||||
|
/// ```text
|
||||||
|
/// build a `bins`-bucket volume profile over the last `period` candles
|
||||||
|
/// POC = bin with the most volume
|
||||||
|
/// expand from the POC, always adding the heavier adjacent bin, until the
|
||||||
|
/// accumulated volume reaches `value_area_pct` of the total
|
||||||
|
/// VAH / VAL = the highest / lowest price included
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// A composite profile merges many sessions into one structure to reveal the
|
||||||
|
/// dominant value area and control price across a longer horizon — the levels that
|
||||||
|
/// matter for swing positioning rather than a single day. The point of control is
|
||||||
|
/// the fairest price (heaviest trade); the value area (classically 70% of volume)
|
||||||
|
/// brackets where the market spent most of its time. Price inside the value area is
|
||||||
|
/// "in balance"; acceptance outside it signals a value migration.
|
||||||
|
///
|
||||||
|
/// The first value lands after `period` candles; each `update` rebuilds the
|
||||||
|
/// profile in O(`period · bins`).
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// use wickra_core::{Candle, Indicator, CompositeProfile};
|
||||||
|
///
|
||||||
|
/// let mut indicator = CompositeProfile::new(100, 50, 0.70).unwrap();
|
||||||
|
/// let mut last = None;
|
||||||
|
/// for i in 0..150 {
|
||||||
|
/// let base = 100.0 + (f64::from(i) * 0.1).sin() * 8.0;
|
||||||
|
/// let c = Candle::new(base, base + 1.0, base - 1.0, base, 1_000.0, 0).unwrap();
|
||||||
|
/// last = indicator.update(c);
|
||||||
|
/// }
|
||||||
|
/// assert!(last.is_some());
|
||||||
|
/// ```
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct CompositeProfile {
|
||||||
|
period: usize,
|
||||||
|
bins: usize,
|
||||||
|
value_area_pct: f64,
|
||||||
|
window: VecDeque<Candle>,
|
||||||
|
last: Option<CompositeProfileOutput>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CompositeProfile {
|
||||||
|
/// Construct a Composite Profile.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// Returns [`Error::PeriodZero`] if `period` or `bins` is zero, or
|
||||||
|
/// [`Error::InvalidParameter`] if `value_area_pct` is not in `(0, 1]`.
|
||||||
|
pub fn new(period: usize, bins: usize, value_area_pct: f64) -> Result<Self> {
|
||||||
|
if period == 0 || bins == 0 {
|
||||||
|
return Err(Error::PeriodZero);
|
||||||
|
}
|
||||||
|
if !value_area_pct.is_finite() || value_area_pct <= 0.0 || value_area_pct > 1.0 {
|
||||||
|
return Err(Error::InvalidParameter {
|
||||||
|
message: "value_area_pct must be in (0, 1]",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Ok(Self {
|
||||||
|
period,
|
||||||
|
bins,
|
||||||
|
value_area_pct,
|
||||||
|
window: VecDeque::with_capacity(period),
|
||||||
|
last: None,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Configured `(period, bins, value_area_pct)`.
|
||||||
|
pub const fn params(&self) -> (usize, usize, f64) {
|
||||||
|
(self.period, self.bins, self.value_area_pct)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Current value if available.
|
||||||
|
pub const fn value(&self) -> Option<CompositeProfileOutput> {
|
||||||
|
self.last
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
|
||||||
|
fn compute(&self) -> CompositeProfileOutput {
|
||||||
|
let mut low = f64::INFINITY;
|
||||||
|
let mut high = f64::NEG_INFINITY;
|
||||||
|
for c in &self.window {
|
||||||
|
low = low.min(c.low);
|
||||||
|
high = high.max(c.high);
|
||||||
|
}
|
||||||
|
let span = high - low;
|
||||||
|
if span <= 0.0 {
|
||||||
|
return CompositeProfileOutput {
|
||||||
|
poc: low,
|
||||||
|
vah: low,
|
||||||
|
val: low,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
let width = span / self.bins as f64;
|
||||||
|
let centre = |idx: usize| low + (idx as f64 + 0.5) * width;
|
||||||
|
let mut hist = vec![0.0; self.bins];
|
||||||
|
for c in &self.window {
|
||||||
|
if c.volume == 0.0 {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let lo_idx = (((c.low - low) / width).floor() as usize).min(self.bins - 1);
|
||||||
|
let hi_idx = (((c.high - low) / width).floor() as usize).min(self.bins - 1);
|
||||||
|
let share = c.volume / (hi_idx - lo_idx + 1) as f64;
|
||||||
|
for bin in hist.iter_mut().take(hi_idx + 1).skip(lo_idx) {
|
||||||
|
*bin += share;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let total: f64 = hist.iter().sum();
|
||||||
|
let mut poc = 0;
|
||||||
|
let mut poc_vol = f64::NEG_INFINITY;
|
||||||
|
for (idx, &vol) in hist.iter().enumerate() {
|
||||||
|
if vol > poc_vol {
|
||||||
|
poc_vol = vol;
|
||||||
|
poc = idx;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let target = total * self.value_area_pct;
|
||||||
|
let mut acc = hist[poc];
|
||||||
|
let mut top = poc;
|
||||||
|
let mut bottom = poc;
|
||||||
|
while acc < target && (top < self.bins - 1 || bottom > 0) {
|
||||||
|
let above = if top < self.bins - 1 {
|
||||||
|
hist[top + 1]
|
||||||
|
} else {
|
||||||
|
f64::NEG_INFINITY
|
||||||
|
};
|
||||||
|
let below = if bottom > 0 {
|
||||||
|
hist[bottom - 1]
|
||||||
|
} else {
|
||||||
|
f64::NEG_INFINITY
|
||||||
|
};
|
||||||
|
if above >= below {
|
||||||
|
top += 1;
|
||||||
|
acc += hist[top];
|
||||||
|
} else {
|
||||||
|
bottom -= 1;
|
||||||
|
acc += hist[bottom];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
CompositeProfileOutput {
|
||||||
|
poc: centre(poc),
|
||||||
|
vah: centre(top),
|
||||||
|
val: centre(bottom),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Indicator for CompositeProfile {
|
||||||
|
type Input = Candle;
|
||||||
|
type Output = CompositeProfileOutput;
|
||||||
|
|
||||||
|
fn update(&mut self, candle: Candle) -> Option<CompositeProfileOutput> {
|
||||||
|
if self.window.len() == self.period {
|
||||||
|
self.window.pop_front();
|
||||||
|
}
|
||||||
|
self.window.push_back(candle);
|
||||||
|
if self.window.len() < self.period {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let out = self.compute();
|
||||||
|
self.last = Some(out);
|
||||||
|
Some(out)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn reset(&mut self) {
|
||||||
|
self.window.clear();
|
||||||
|
self.last = None;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn warmup_period(&self) -> usize {
|
||||||
|
self.period
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_ready(&self) -> bool {
|
||||||
|
self.last.is_some()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn name(&self) -> &'static str {
|
||||||
|
"CompositeProfile"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::traits::BatchExt;
|
||||||
|
|
||||||
|
fn c(high: f64, low: f64, volume: f64) -> Candle {
|
||||||
|
Candle::new_unchecked(
|
||||||
|
f64::midpoint(high, low),
|
||||||
|
high,
|
||||||
|
low,
|
||||||
|
f64::midpoint(high, low),
|
||||||
|
volume,
|
||||||
|
0,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn rejects_invalid_params() {
|
||||||
|
assert!(matches!(
|
||||||
|
CompositeProfile::new(0, 50, 0.7),
|
||||||
|
Err(Error::PeriodZero)
|
||||||
|
));
|
||||||
|
assert!(matches!(
|
||||||
|
CompositeProfile::new(100, 0, 0.7),
|
||||||
|
Err(Error::PeriodZero)
|
||||||
|
));
|
||||||
|
assert!(matches!(
|
||||||
|
CompositeProfile::new(100, 50, 0.0),
|
||||||
|
Err(Error::InvalidParameter { .. })
|
||||||
|
));
|
||||||
|
assert!(matches!(
|
||||||
|
CompositeProfile::new(100, 50, 1.5),
|
||||||
|
Err(Error::InvalidParameter { .. })
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn accessors_and_metadata() {
|
||||||
|
let p = CompositeProfile::new(100, 50, 0.7).unwrap();
|
||||||
|
assert_eq!(p.params(), (100, 50, 0.7));
|
||||||
|
assert_eq!(p.warmup_period(), 100);
|
||||||
|
assert_eq!(p.name(), "CompositeProfile");
|
||||||
|
assert!(!p.is_ready());
|
||||||
|
assert_eq!(p.value(), None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn first_emission_at_warmup_period() {
|
||||||
|
let mut p = CompositeProfile::new(4, 8, 0.7).unwrap();
|
||||||
|
let candles: Vec<Candle> = (0..6).map(|_| c(110.0, 90.0, 1_000.0)).collect();
|
||||||
|
let out = p.batch(&candles);
|
||||||
|
for v in out.iter().take(3) {
|
||||||
|
assert!(v.is_none());
|
||||||
|
}
|
||||||
|
assert!(out[3].is_some());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn value_area_brackets_poc() {
|
||||||
|
let mut p = CompositeProfile::new(20, 30, 0.7).unwrap();
|
||||||
|
let candles: Vec<Candle> = (0..40)
|
||||||
|
.map(|i| {
|
||||||
|
c(
|
||||||
|
110.0 + (f64::from(i) * 0.3).sin() * 8.0,
|
||||||
|
90.0 + (f64::from(i) * 0.3).cos() * 8.0,
|
||||||
|
1_000.0,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
for o in p.batch(&candles).into_iter().flatten() {
|
||||||
|
assert!(o.val <= o.poc && o.poc <= o.vah);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn poc_at_heavy_cluster() {
|
||||||
|
// Volume clustered at ~100; thin pokes elsewhere -> POC near 100.
|
||||||
|
let mut p = CompositeProfile::new(6, 30, 0.7).unwrap();
|
||||||
|
let mut candles: Vec<Candle> = (0..5).map(|_| c(101.0, 99.0, 5_000.0)).collect();
|
||||||
|
candles.push(c(140.0, 60.0, 50.0));
|
||||||
|
let out = p.batch(&candles).into_iter().flatten().last().unwrap();
|
||||||
|
assert!(
|
||||||
|
(out.poc - 100.0).abs() < 5.0,
|
||||||
|
"POC should sit at the cluster, got {}",
|
||||||
|
out.poc
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn reset_clears_state() {
|
||||||
|
let mut p = CompositeProfile::new(4, 8, 0.7).unwrap();
|
||||||
|
p.batch(&[c(110.0, 90.0, 1_000.0); 6]);
|
||||||
|
assert!(p.is_ready());
|
||||||
|
p.reset();
|
||||||
|
assert!(!p.is_ready());
|
||||||
|
assert_eq!(p.value(), None);
|
||||||
|
assert_eq!(p.update(c(110.0, 90.0, 1_000.0)), None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn batch_equals_streaming() {
|
||||||
|
let candles: Vec<Candle> = (0..120)
|
||||||
|
.map(|i| {
|
||||||
|
c(
|
||||||
|
110.0 + (f64::from(i) * 0.25).sin() * 9.0,
|
||||||
|
90.0,
|
||||||
|
1_000.0 + f64::from(i),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
let batch = CompositeProfile::new(50, 50, 0.7).unwrap().batch(&candles);
|
||||||
|
let mut b = CompositeProfile::new(50, 50, 0.7).unwrap();
|
||||||
|
let streamed: Vec<_> = candles.iter().map(|x| b.update(*x)).collect();
|
||||||
|
assert_eq!(batch, streamed);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn flat_window_collapses_to_price() {
|
||||||
|
// Zero high-low span returns the price for POC, VAH and VAL.
|
||||||
|
let mut cp = CompositeProfile::new(2, 4, 0.7).unwrap();
|
||||||
|
cp.update(c(50.0, 50.0, 10.0));
|
||||||
|
let out = cp.update(c(50.0, 50.0, 10.0)).unwrap();
|
||||||
|
assert_eq!(out.poc, out.vah);
|
||||||
|
assert_eq!(out.poc, out.val);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn zero_volume_window_is_handled() {
|
||||||
|
// Non-flat window of zero-volume candles hits the skip path.
|
||||||
|
let mut cp = CompositeProfile::new(2, 4, 0.7).unwrap();
|
||||||
|
cp.update(c(60.0, 40.0, 0.0));
|
||||||
|
assert!(cp.update(c(60.0, 40.0, 0.0)).is_some());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn value_area_expands_down_from_top_poc() {
|
||||||
|
// POC sits in the top bin; with a wide value-area target the area runs
|
||||||
|
// out of bins above (the ceiling branch) and keeps expanding downward.
|
||||||
|
let mut cp = CompositeProfile::new(2, 3, 0.9).unwrap();
|
||||||
|
cp.update(c(100.0, 0.0, 30.0)); // thin spread across all three bins
|
||||||
|
let out = cp.update(c(100.0, 67.0, 60.0)).unwrap(); // heavy in the top bin
|
||||||
|
assert!(out.val <= out.poc && out.poc <= out.vah);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,307 @@
|
|||||||
|
//! High/Low Volume Nodes (HVN / LVN) — the busiest and quietest price levels.
|
||||||
|
|
||||||
|
use std::collections::VecDeque;
|
||||||
|
|
||||||
|
use crate::error::{Error, Result};
|
||||||
|
use crate::ohlcv::Candle;
|
||||||
|
use crate::traits::Indicator;
|
||||||
|
|
||||||
|
/// Output of [`HighLowVolumeNodes`]: the price of the highest- and lowest-volume
|
||||||
|
/// node in the profile.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||||
|
pub struct HighLowVolumeNodesOutput {
|
||||||
|
/// High Volume Node — the price level (bin centre) with the most volume.
|
||||||
|
pub hvn: f64,
|
||||||
|
/// Low Volume Node — the traded price level with the least volume.
|
||||||
|
pub lvn: f64,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// High/Low Volume Nodes — the price levels of greatest and least acceptance in a
|
||||||
|
/// rolling volume profile.
|
||||||
|
///
|
||||||
|
/// ```text
|
||||||
|
/// build a `bins`-bucket volume profile over the last `period` candles
|
||||||
|
/// HVN = bin centre of the bucket with the most volume
|
||||||
|
/// LVN = bin centre of the traded bucket with the least volume
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// A volume profile reveals where the market spent the most effort. A **High Volume
|
||||||
|
/// Node** (HVN) is a price the market accepted and traded heavily — it acts as a
|
||||||
|
/// magnet and as strong support/resistance. A **Low Volume Node** (LVN) is a price
|
||||||
|
/// the market rejected quickly — moves tend to accelerate through LVNs and they
|
||||||
|
/// often mark the edges between balance areas. Each candle's volume is spread
|
||||||
|
/// across the price bins its high-low range spans (as in
|
||||||
|
/// [`VolumeProfile`](crate::VolumeProfile)).
|
||||||
|
///
|
||||||
|
/// The first value lands after `period` candles; each `update` rebuilds the profile
|
||||||
|
/// in O(`period · bins`). A degenerate flat window puts both nodes at the price.
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// use wickra_core::{Candle, Indicator, HighLowVolumeNodes};
|
||||||
|
///
|
||||||
|
/// let mut indicator = HighLowVolumeNodes::new(20, 24).unwrap();
|
||||||
|
/// let mut last = None;
|
||||||
|
/// for i in 0..40 {
|
||||||
|
/// let base = 100.0 + (f64::from(i) * 0.3).sin() * 5.0;
|
||||||
|
/// let c = Candle::new(base, base + 1.0, base - 1.0, base, 1_000.0, 0).unwrap();
|
||||||
|
/// last = indicator.update(c);
|
||||||
|
/// }
|
||||||
|
/// assert!(last.is_some());
|
||||||
|
/// ```
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct HighLowVolumeNodes {
|
||||||
|
period: usize,
|
||||||
|
bins: usize,
|
||||||
|
window: VecDeque<Candle>,
|
||||||
|
last: Option<HighLowVolumeNodesOutput>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl HighLowVolumeNodes {
|
||||||
|
/// Construct a High/Low Volume Nodes indicator.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// Returns [`Error::PeriodZero`] if `period` or `bins` is zero.
|
||||||
|
pub fn new(period: usize, bins: usize) -> Result<Self> {
|
||||||
|
if period == 0 || bins == 0 {
|
||||||
|
return Err(Error::PeriodZero);
|
||||||
|
}
|
||||||
|
Ok(Self {
|
||||||
|
period,
|
||||||
|
bins,
|
||||||
|
window: VecDeque::with_capacity(period),
|
||||||
|
last: None,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Configured `(period, bins)`.
|
||||||
|
pub const fn params(&self) -> (usize, usize) {
|
||||||
|
(self.period, self.bins)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Current value if available.
|
||||||
|
pub const fn value(&self) -> Option<HighLowVolumeNodesOutput> {
|
||||||
|
self.last
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Build the volume histogram; returns `(low, bin_width, bins)`.
|
||||||
|
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
|
||||||
|
fn profile(&self) -> (f64, f64, Vec<f64>) {
|
||||||
|
let mut low = f64::INFINITY;
|
||||||
|
let mut high = f64::NEG_INFINITY;
|
||||||
|
for c in &self.window {
|
||||||
|
low = low.min(c.low);
|
||||||
|
high = high.max(c.high);
|
||||||
|
}
|
||||||
|
let mut hist = vec![0.0; self.bins];
|
||||||
|
let span = high - low;
|
||||||
|
if span <= 0.0 {
|
||||||
|
hist[0] = self.window.iter().map(|c| c.volume).sum();
|
||||||
|
return (low, 0.0, hist);
|
||||||
|
}
|
||||||
|
let width = span / self.bins as f64;
|
||||||
|
for c in &self.window {
|
||||||
|
if c.volume == 0.0 {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let lo_idx = (((c.low - low) / width).floor() as usize).min(self.bins - 1);
|
||||||
|
let hi_idx = (((c.high - low) / width).floor() as usize).min(self.bins - 1);
|
||||||
|
let touched = hi_idx - lo_idx + 1;
|
||||||
|
let share = c.volume / touched as f64;
|
||||||
|
for bin in hist.iter_mut().take(hi_idx + 1).skip(lo_idx) {
|
||||||
|
*bin += share;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
(low, width, hist)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Indicator for HighLowVolumeNodes {
|
||||||
|
type Input = Candle;
|
||||||
|
type Output = HighLowVolumeNodesOutput;
|
||||||
|
|
||||||
|
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
|
||||||
|
fn update(&mut self, candle: Candle) -> Option<HighLowVolumeNodesOutput> {
|
||||||
|
if self.window.len() == self.period {
|
||||||
|
self.window.pop_front();
|
||||||
|
}
|
||||||
|
self.window.push_back(candle);
|
||||||
|
if self.window.len() < self.period {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let (low, width, hist) = self.profile();
|
||||||
|
let centre = |idx: usize| low + (idx as f64 + 0.5) * width;
|
||||||
|
|
||||||
|
let mut hvn_idx = 0;
|
||||||
|
let mut hvn_vol = f64::NEG_INFINITY;
|
||||||
|
let mut lvn_idx = 0;
|
||||||
|
let mut lvn_vol = f64::INFINITY;
|
||||||
|
for (idx, &vol) in hist.iter().enumerate() {
|
||||||
|
if vol > hvn_vol {
|
||||||
|
hvn_vol = vol;
|
||||||
|
hvn_idx = idx;
|
||||||
|
}
|
||||||
|
if vol > 0.0 && vol < lvn_vol {
|
||||||
|
lvn_vol = vol;
|
||||||
|
lvn_idx = idx;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// If no traded bin was found (all zero volume), both default to bin 0.
|
||||||
|
if !lvn_vol.is_finite() {
|
||||||
|
lvn_idx = hvn_idx;
|
||||||
|
}
|
||||||
|
let out = HighLowVolumeNodesOutput {
|
||||||
|
hvn: centre(hvn_idx),
|
||||||
|
lvn: centre(lvn_idx),
|
||||||
|
};
|
||||||
|
self.last = Some(out);
|
||||||
|
Some(out)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn reset(&mut self) {
|
||||||
|
self.window.clear();
|
||||||
|
self.last = None;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn warmup_period(&self) -> usize {
|
||||||
|
self.period
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_ready(&self) -> bool {
|
||||||
|
self.last.is_some()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn name(&self) -> &'static str {
|
||||||
|
"HighLowVolumeNodes"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::traits::BatchExt;
|
||||||
|
|
||||||
|
fn c(high: f64, low: f64, volume: f64) -> Candle {
|
||||||
|
Candle::new_unchecked(
|
||||||
|
f64::midpoint(high, low),
|
||||||
|
high,
|
||||||
|
low,
|
||||||
|
f64::midpoint(high, low),
|
||||||
|
volume,
|
||||||
|
0,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn rejects_zero_params() {
|
||||||
|
assert!(matches!(
|
||||||
|
HighLowVolumeNodes::new(0, 24),
|
||||||
|
Err(Error::PeriodZero)
|
||||||
|
));
|
||||||
|
assert!(matches!(
|
||||||
|
HighLowVolumeNodes::new(20, 0),
|
||||||
|
Err(Error::PeriodZero)
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn accessors_and_metadata() {
|
||||||
|
let h = HighLowVolumeNodes::new(20, 24).unwrap();
|
||||||
|
assert_eq!(h.params(), (20, 24));
|
||||||
|
assert_eq!(h.warmup_period(), 20);
|
||||||
|
assert_eq!(h.name(), "HighLowVolumeNodes");
|
||||||
|
assert!(!h.is_ready());
|
||||||
|
assert_eq!(h.value(), None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn first_emission_at_warmup_period() {
|
||||||
|
let mut h = HighLowVolumeNodes::new(4, 8).unwrap();
|
||||||
|
let candles: Vec<Candle> = (0..6).map(|_| c(110.0, 90.0, 1_000.0)).collect();
|
||||||
|
let out = h.batch(&candles);
|
||||||
|
for v in out.iter().take(3) {
|
||||||
|
assert!(v.is_none());
|
||||||
|
}
|
||||||
|
assert!(out[3].is_some());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn hvn_at_heavy_price() {
|
||||||
|
// Most bars cluster at ~100 (heavy volume); one bar pokes up to 120 lightly.
|
||||||
|
let mut h = HighLowVolumeNodes::new(6, 24).unwrap();
|
||||||
|
let mut candles: Vec<Candle> = (0..5).map(|_| c(101.0, 99.0, 5_000.0)).collect();
|
||||||
|
candles.push(c(121.0, 119.0, 100.0));
|
||||||
|
let out = h.batch(&candles).into_iter().flatten().last().unwrap();
|
||||||
|
// HVN should sit near the heavy 100 cluster, well below the light 120 poke.
|
||||||
|
assert!(
|
||||||
|
out.hvn < 110.0,
|
||||||
|
"HVN should be at the heavy cluster, got {}",
|
||||||
|
out.hvn
|
||||||
|
);
|
||||||
|
assert!(out.lvn >= out.hvn - 1e9); // lvn is a valid level
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn hvn_at_or_above_low() {
|
||||||
|
let mut h = HighLowVolumeNodes::new(10, 24).unwrap();
|
||||||
|
let candles: Vec<Candle> = (0..30)
|
||||||
|
.map(|i| {
|
||||||
|
c(
|
||||||
|
110.0 + (f64::from(i) * 0.3).sin() * 5.0,
|
||||||
|
90.0,
|
||||||
|
1_000.0 + f64::from(i),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
for o in h.batch(&candles).into_iter().flatten() {
|
||||||
|
assert!(o.hvn.is_finite() && o.lvn.is_finite());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn reset_clears_state() {
|
||||||
|
let mut h = HighLowVolumeNodes::new(4, 8).unwrap();
|
||||||
|
h.batch(&[c(110.0, 90.0, 1_000.0); 6]);
|
||||||
|
assert!(h.is_ready());
|
||||||
|
h.reset();
|
||||||
|
assert!(!h.is_ready());
|
||||||
|
assert_eq!(h.value(), None);
|
||||||
|
assert_eq!(h.update(c(110.0, 90.0, 1_000.0)), None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn batch_equals_streaming() {
|
||||||
|
let candles: Vec<Candle> = (0..80)
|
||||||
|
.map(|i| {
|
||||||
|
c(
|
||||||
|
110.0 + (f64::from(i) * 0.25).sin() * 9.0,
|
||||||
|
90.0,
|
||||||
|
1_000.0 + f64::from(i),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
let batch = HighLowVolumeNodes::new(20, 24).unwrap().batch(&candles);
|
||||||
|
let mut b = HighLowVolumeNodes::new(20, 24).unwrap();
|
||||||
|
let streamed: Vec<_> = candles.iter().map(|x| b.update(*x)).collect();
|
||||||
|
assert_eq!(batch, streamed);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn flat_window_is_handled() {
|
||||||
|
// Zero high-low span dumps all volume into bin 0 and returns early.
|
||||||
|
let mut h = HighLowVolumeNodes::new(2, 4).unwrap();
|
||||||
|
h.update(c(50.0, 50.0, 10.0));
|
||||||
|
assert!(h.update(c(50.0, 50.0, 10.0)).is_some());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn zero_volume_window_falls_back() {
|
||||||
|
// All-zero volume leaves no traded bin; the LVN falls back to the HVN.
|
||||||
|
let mut h = HighLowVolumeNodes::new(2, 4).unwrap();
|
||||||
|
h.update(c(60.0, 40.0, 0.0));
|
||||||
|
let out = h.update(c(60.0, 40.0, 0.0)).unwrap();
|
||||||
|
assert_eq!(out.hvn, out.lvn);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -84,6 +84,7 @@ mod cmf;
|
|||||||
mod cmo;
|
mod cmo;
|
||||||
mod coefficient_of_variation;
|
mod coefficient_of_variation;
|
||||||
mod cointegration;
|
mod cointegration;
|
||||||
|
mod composite_profile;
|
||||||
mod concealing_baby_swallow;
|
mod concealing_baby_swallow;
|
||||||
mod conditional_value_at_risk;
|
mod conditional_value_at_risk;
|
||||||
mod connors_rsi;
|
mod connors_rsi;
|
||||||
@@ -182,6 +183,7 @@ mod heikin_ashi;
|
|||||||
mod heikin_ashi_oscillator;
|
mod heikin_ashi_oscillator;
|
||||||
mod high_low_index;
|
mod high_low_index;
|
||||||
mod high_low_range;
|
mod high_low_range;
|
||||||
|
mod high_low_volume_nodes;
|
||||||
mod high_wave;
|
mod high_wave;
|
||||||
mod highpass_filter;
|
mod highpass_filter;
|
||||||
mod hikkake;
|
mod hikkake;
|
||||||
@@ -269,6 +271,7 @@ mod mom;
|
|||||||
mod morning_doji_star;
|
mod morning_doji_star;
|
||||||
mod morning_evening_star;
|
mod morning_evening_star;
|
||||||
mod murrey_math_lines;
|
mod murrey_math_lines;
|
||||||
|
mod naked_poc;
|
||||||
mod natr;
|
mod natr;
|
||||||
mod new_highs_new_lows;
|
mod new_highs_new_lows;
|
||||||
mod new_price_lines;
|
mod new_price_lines;
|
||||||
@@ -311,6 +314,7 @@ mod point_and_figure_bars;
|
|||||||
mod polarized_fractal_efficiency;
|
mod polarized_fractal_efficiency;
|
||||||
mod ppo;
|
mod ppo;
|
||||||
mod ppo_histogram;
|
mod ppo_histogram;
|
||||||
|
mod profile_shape;
|
||||||
mod profit_factor;
|
mod profit_factor;
|
||||||
mod projection_bands;
|
mod projection_bands;
|
||||||
mod projection_oscillator;
|
mod projection_oscillator;
|
||||||
@@ -366,6 +370,7 @@ mod short_line;
|
|||||||
mod signed_volume;
|
mod signed_volume;
|
||||||
mod sine_wave;
|
mod sine_wave;
|
||||||
mod sine_weighted_ma;
|
mod sine_weighted_ma;
|
||||||
|
mod single_prints;
|
||||||
mod skewness;
|
mod skewness;
|
||||||
mod sma;
|
mod sma;
|
||||||
mod smi;
|
mod smi;
|
||||||
@@ -577,6 +582,7 @@ pub use cmf::ChaikinMoneyFlow;
|
|||||||
pub use cmo::Cmo;
|
pub use cmo::Cmo;
|
||||||
pub use coefficient_of_variation::CoefficientOfVariation;
|
pub use coefficient_of_variation::CoefficientOfVariation;
|
||||||
pub use cointegration::{Cointegration, CointegrationOutput};
|
pub use cointegration::{Cointegration, CointegrationOutput};
|
||||||
|
pub use composite_profile::{CompositeProfile, CompositeProfileOutput};
|
||||||
pub use concealing_baby_swallow::ConcealingBabySwallow;
|
pub use concealing_baby_swallow::ConcealingBabySwallow;
|
||||||
pub use conditional_value_at_risk::ConditionalValueAtRisk;
|
pub use conditional_value_at_risk::ConditionalValueAtRisk;
|
||||||
pub use connors_rsi::ConnorsRsi;
|
pub use connors_rsi::ConnorsRsi;
|
||||||
@@ -675,6 +681,7 @@ pub use heikin_ashi::{HeikinAshi, HeikinAshiOutput};
|
|||||||
pub use heikin_ashi_oscillator::HeikinAshiOscillator;
|
pub use heikin_ashi_oscillator::HeikinAshiOscillator;
|
||||||
pub use high_low_index::HighLowIndex;
|
pub use high_low_index::HighLowIndex;
|
||||||
pub use high_low_range::HighLowRange;
|
pub use high_low_range::HighLowRange;
|
||||||
|
pub use high_low_volume_nodes::{HighLowVolumeNodes, HighLowVolumeNodesOutput};
|
||||||
pub use high_wave::HighWave;
|
pub use high_wave::HighWave;
|
||||||
pub use highpass_filter::HighpassFilter;
|
pub use highpass_filter::HighpassFilter;
|
||||||
pub use hikkake::Hikkake;
|
pub use hikkake::Hikkake;
|
||||||
@@ -762,6 +769,7 @@ pub use mom::Mom;
|
|||||||
pub use morning_doji_star::MorningDojiStar;
|
pub use morning_doji_star::MorningDojiStar;
|
||||||
pub use morning_evening_star::MorningEveningStar;
|
pub use morning_evening_star::MorningEveningStar;
|
||||||
pub use murrey_math_lines::{MurreyMathLines, MurreyMathLinesOutput};
|
pub use murrey_math_lines::{MurreyMathLines, MurreyMathLinesOutput};
|
||||||
|
pub use naked_poc::NakedPoc;
|
||||||
pub use natr::Natr;
|
pub use natr::Natr;
|
||||||
pub use new_highs_new_lows::NewHighsNewLows;
|
pub use new_highs_new_lows::NewHighsNewLows;
|
||||||
pub use new_price_lines::NewPriceLines;
|
pub use new_price_lines::NewPriceLines;
|
||||||
@@ -804,6 +812,7 @@ pub use point_and_figure_bars::{PnfColumn, PointAndFigureBars};
|
|||||||
pub use polarized_fractal_efficiency::PolarizedFractalEfficiency;
|
pub use polarized_fractal_efficiency::PolarizedFractalEfficiency;
|
||||||
pub use ppo::Ppo;
|
pub use ppo::Ppo;
|
||||||
pub use ppo_histogram::PpoHistogram;
|
pub use ppo_histogram::PpoHistogram;
|
||||||
|
pub use profile_shape::ProfileShape;
|
||||||
pub use profit_factor::ProfitFactor;
|
pub use profit_factor::ProfitFactor;
|
||||||
pub use projection_bands::{ProjectionBands, ProjectionBandsOutput};
|
pub use projection_bands::{ProjectionBands, ProjectionBandsOutput};
|
||||||
pub use projection_oscillator::ProjectionOscillator;
|
pub use projection_oscillator::ProjectionOscillator;
|
||||||
@@ -859,6 +868,7 @@ pub use short_line::ShortLine;
|
|||||||
pub use signed_volume::SignedVolume;
|
pub use signed_volume::SignedVolume;
|
||||||
pub use sine_wave::SineWave;
|
pub use sine_wave::SineWave;
|
||||||
pub use sine_weighted_ma::SineWeightedMa;
|
pub use sine_weighted_ma::SineWeightedMa;
|
||||||
|
pub use single_prints::SinglePrints;
|
||||||
pub use skewness::Skewness;
|
pub use skewness::Skewness;
|
||||||
pub use sma::Sma;
|
pub use sma::Sma;
|
||||||
pub use smi::Smi;
|
pub use smi::Smi;
|
||||||
@@ -1503,6 +1513,11 @@ pub const FAMILIES: &[(&str, &[&str])] = &[
|
|||||||
"OpeningRange",
|
"OpeningRange",
|
||||||
"VolumeProfile",
|
"VolumeProfile",
|
||||||
"TpoProfile",
|
"TpoProfile",
|
||||||
|
"NakedPoc",
|
||||||
|
"SinglePrints",
|
||||||
|
"ProfileShape",
|
||||||
|
"HighLowVolumeNodes",
|
||||||
|
"CompositeProfile",
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
@@ -1639,6 +1654,6 @@ mod family_tests {
|
|||||||
// the actual indicator count is the early-warning signal that an
|
// the actual indicator count is the early-warning signal that an
|
||||||
// indicator was added without being assigned a family.
|
// indicator was added without being assigned a family.
|
||||||
let total: usize = FAMILIES.iter().map(|(_, ns)| ns.len()).sum();
|
let total: usize = FAMILIES.iter().map(|(_, ns)| ns.len()).sum();
|
||||||
assert_eq!(total, 493, "FAMILIES total drifted from indicator count");
|
assert_eq!(total, 498, "FAMILIES total drifted from indicator count");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,318 @@
|
|||||||
|
//! Naked POC — the nearest prior-session point of control price has not yet revisited.
|
||||||
|
|
||||||
|
use std::collections::VecDeque;
|
||||||
|
|
||||||
|
use crate::error::{Error, Result};
|
||||||
|
use crate::ohlcv::Candle;
|
||||||
|
use crate::traits::Indicator;
|
||||||
|
|
||||||
|
/// Naked (Virgin) POC — the nearest **untested** point of control from a prior
|
||||||
|
/// session: a heavily-traded price the market has not traded back through since.
|
||||||
|
///
|
||||||
|
/// ```text
|
||||||
|
/// every `session_len` candles forms a session; its POC (heaviest-volume price) is
|
||||||
|
/// recorded as "naked"
|
||||||
|
/// a naked POC becomes "tested" once a later candle's high-low range covers it
|
||||||
|
/// output = the nearest still-naked POC to the current close (or the close itself
|
||||||
|
/// if every prior POC has been revisited)
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// A point of control is a magnet — price tends to return to fair value. A *naked*
|
||||||
|
/// (or virgin) POC is one that has not yet been revisited, so it carries an
|
||||||
|
/// outstanding "pull": untested POCs are high-probability targets and
|
||||||
|
/// support/resistance on the approach. This indicator records each completed
|
||||||
|
/// session's POC, marks them tested as price trades through them, and reports the
|
||||||
|
/// closest one still outstanding.
|
||||||
|
///
|
||||||
|
/// The first value lands after `session_len` candles (the first session's POC).
|
||||||
|
/// Each `update` is O(`session_len · bins` + naked-count).
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// use wickra_core::{Candle, Indicator, NakedPoc};
|
||||||
|
///
|
||||||
|
/// let mut indicator = NakedPoc::new(20, 24).unwrap();
|
||||||
|
/// let mut last = None;
|
||||||
|
/// for i in 0..60 {
|
||||||
|
/// let base = 100.0 + (f64::from(i) * 0.2).sin() * 5.0;
|
||||||
|
/// let c = Candle::new(base, base + 1.0, base - 1.0, base, 1_000.0, 0).unwrap();
|
||||||
|
/// last = indicator.update(c);
|
||||||
|
/// }
|
||||||
|
/// assert!(last.is_some());
|
||||||
|
/// ```
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct NakedPoc {
|
||||||
|
session_len: usize,
|
||||||
|
bins: usize,
|
||||||
|
session: VecDeque<Candle>,
|
||||||
|
naked: Vec<f64>,
|
||||||
|
last_close: f64,
|
||||||
|
ready: bool,
|
||||||
|
last: Option<f64>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl NakedPoc {
|
||||||
|
/// Construct a Naked POC tracker with the given `session_len` and profile
|
||||||
|
/// `bins`.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// Returns [`Error::PeriodZero`] if `session_len` or `bins` is zero.
|
||||||
|
pub fn new(session_len: usize, bins: usize) -> Result<Self> {
|
||||||
|
if session_len == 0 || bins == 0 {
|
||||||
|
return Err(Error::PeriodZero);
|
||||||
|
}
|
||||||
|
Ok(Self {
|
||||||
|
session_len,
|
||||||
|
bins,
|
||||||
|
session: VecDeque::with_capacity(session_len),
|
||||||
|
naked: Vec::new(),
|
||||||
|
last_close: 0.0,
|
||||||
|
ready: false,
|
||||||
|
last: None,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Configured `(session_len, bins)`.
|
||||||
|
pub const fn params(&self) -> (usize, usize) {
|
||||||
|
(self.session_len, self.bins)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Number of currently-naked POCs.
|
||||||
|
pub fn naked_count(&self) -> usize {
|
||||||
|
self.naked.len()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Current value if available.
|
||||||
|
pub const fn value(&self) -> Option<f64> {
|
||||||
|
self.last
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
|
||||||
|
fn session_poc(&self) -> f64 {
|
||||||
|
let mut low = f64::INFINITY;
|
||||||
|
let mut high = f64::NEG_INFINITY;
|
||||||
|
for c in &self.session {
|
||||||
|
low = low.min(c.low);
|
||||||
|
high = high.max(c.high);
|
||||||
|
}
|
||||||
|
let span = high - low;
|
||||||
|
if span <= 0.0 {
|
||||||
|
return low;
|
||||||
|
}
|
||||||
|
let width = span / self.bins as f64;
|
||||||
|
let mut hist = vec![0.0; self.bins];
|
||||||
|
for c in &self.session {
|
||||||
|
if c.volume == 0.0 {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let lo_idx = (((c.low - low) / width).floor() as usize).min(self.bins - 1);
|
||||||
|
let hi_idx = (((c.high - low) / width).floor() as usize).min(self.bins - 1);
|
||||||
|
let share = c.volume / (hi_idx - lo_idx + 1) as f64;
|
||||||
|
for bin in hist.iter_mut().take(hi_idx + 1).skip(lo_idx) {
|
||||||
|
*bin += share;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let mut poc = 0;
|
||||||
|
let mut poc_vol = f64::NEG_INFINITY;
|
||||||
|
for (idx, &vol) in hist.iter().enumerate() {
|
||||||
|
if vol > poc_vol {
|
||||||
|
poc_vol = vol;
|
||||||
|
poc = idx;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
low + (poc as f64 + 0.5) * width
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Indicator for NakedPoc {
|
||||||
|
type Input = Candle;
|
||||||
|
type Output = f64;
|
||||||
|
|
||||||
|
fn update(&mut self, candle: Candle) -> Option<f64> {
|
||||||
|
// Test outstanding naked POCs against this candle's range.
|
||||||
|
self.naked
|
||||||
|
.retain(|&poc| !(candle.low <= poc && poc <= candle.high));
|
||||||
|
self.last_close = candle.close;
|
||||||
|
|
||||||
|
// Accumulate the session; finalize a POC at the boundary.
|
||||||
|
self.session.push_back(candle);
|
||||||
|
if self.session.len() == self.session_len {
|
||||||
|
let poc = self.session_poc();
|
||||||
|
self.naked.push(poc);
|
||||||
|
self.session.clear();
|
||||||
|
self.ready = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if !self.ready {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let nearest = self
|
||||||
|
.naked
|
||||||
|
.iter()
|
||||||
|
.copied()
|
||||||
|
.min_by(|a, b| {
|
||||||
|
(a - self.last_close)
|
||||||
|
.abs()
|
||||||
|
.total_cmp(&(b - self.last_close).abs())
|
||||||
|
})
|
||||||
|
.unwrap_or(self.last_close);
|
||||||
|
self.last = Some(nearest);
|
||||||
|
Some(nearest)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn reset(&mut self) {
|
||||||
|
self.session.clear();
|
||||||
|
self.naked.clear();
|
||||||
|
self.last_close = 0.0;
|
||||||
|
self.ready = false;
|
||||||
|
self.last = None;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn warmup_period(&self) -> usize {
|
||||||
|
self.session_len
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_ready(&self) -> bool {
|
||||||
|
self.last.is_some()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn name(&self) -> &'static str {
|
||||||
|
"NakedPoc"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::traits::BatchExt;
|
||||||
|
|
||||||
|
fn c(high: f64, low: f64, close: f64, volume: f64) -> Candle {
|
||||||
|
Candle::new_unchecked(f64::midpoint(high, low), high, low, close, volume, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn rejects_zero_params() {
|
||||||
|
assert!(matches!(NakedPoc::new(0, 24), Err(Error::PeriodZero)));
|
||||||
|
assert!(matches!(NakedPoc::new(20, 0), Err(Error::PeriodZero)));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn accessors_and_metadata() {
|
||||||
|
let n = NakedPoc::new(20, 24).unwrap();
|
||||||
|
assert_eq!(n.params(), (20, 24));
|
||||||
|
assert_eq!(n.naked_count(), 0);
|
||||||
|
assert_eq!(n.warmup_period(), 20);
|
||||||
|
assert_eq!(n.name(), "NakedPoc");
|
||||||
|
assert!(!n.is_ready());
|
||||||
|
assert_eq!(n.value(), None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn first_emission_at_session_end() {
|
||||||
|
let mut n = NakedPoc::new(4, 8).unwrap();
|
||||||
|
let candles: Vec<Candle> = (0..6).map(|_| c(101.0, 99.0, 100.0, 1_000.0)).collect();
|
||||||
|
let out = n.batch(&candles);
|
||||||
|
for v in out.iter().take(3) {
|
||||||
|
assert!(v.is_none());
|
||||||
|
}
|
||||||
|
assert!(out[3].is_some());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn records_session_poc() {
|
||||||
|
let mut n = NakedPoc::new(4, 16).unwrap();
|
||||||
|
// A session clustered around 100 -> POC near 100.
|
||||||
|
n.batch(&[c(101.0, 99.0, 100.0, 5_000.0); 4]);
|
||||||
|
assert_eq!(n.naked_count(), 1);
|
||||||
|
let poc = n.value().unwrap();
|
||||||
|
assert!(
|
||||||
|
(poc - 100.0).abs() < 2.0,
|
||||||
|
"POC should be near 100, got {poc}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn revisit_marks_poc_tested() {
|
||||||
|
let mut n = NakedPoc::new(4, 16).unwrap();
|
||||||
|
// Session 1 around 100 -> naked POC ~100.
|
||||||
|
n.batch(&[c(101.0, 99.0, 100.0, 5_000.0); 4]);
|
||||||
|
assert_eq!(n.naked_count(), 1);
|
||||||
|
// Trade away at 120 (does not cover 100) -> still naked.
|
||||||
|
n.update(c(121.0, 119.0, 120.0, 1_000.0));
|
||||||
|
assert_eq!(n.naked_count(), 1);
|
||||||
|
// A candle whose range covers 100 -> POC tested -> removed.
|
||||||
|
n.update(c(121.0, 95.0, 100.0, 1_000.0));
|
||||||
|
assert_eq!(n.naked_count(), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn empty_naked_reports_close() {
|
||||||
|
let mut n = NakedPoc::new(4, 16).unwrap();
|
||||||
|
n.batch(&[c(101.0, 99.0, 100.0, 5_000.0); 4]);
|
||||||
|
// Wipe the naked POC with a covering candle.
|
||||||
|
let out = n.update(c(121.0, 95.0, 117.0, 1_000.0)).unwrap();
|
||||||
|
assert_eq!(n.naked_count(), 0);
|
||||||
|
assert!(
|
||||||
|
(out - 117.0).abs() < 1e-9,
|
||||||
|
"with no naked POC, output is the close"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn reset_clears_state() {
|
||||||
|
let mut n = NakedPoc::new(4, 8).unwrap();
|
||||||
|
n.batch(&[c(101.0, 99.0, 100.0, 1_000.0); 6]);
|
||||||
|
assert!(n.is_ready());
|
||||||
|
n.reset();
|
||||||
|
assert!(!n.is_ready());
|
||||||
|
assert_eq!(n.value(), None);
|
||||||
|
assert_eq!(n.naked_count(), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn batch_equals_streaming() {
|
||||||
|
let candles: Vec<Candle> = (0..80)
|
||||||
|
.map(|i| {
|
||||||
|
let b = 100.0 + (f64::from(i) * 0.25).sin() * 9.0;
|
||||||
|
c(b + 1.0, b - 1.0, b, 1_000.0 + f64::from(i))
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
let batch = NakedPoc::new(20, 24).unwrap().batch(&candles);
|
||||||
|
let mut b = NakedPoc::new(20, 24).unwrap();
|
||||||
|
let streamed: Vec<_> = candles.iter().map(|x| b.update(*x)).collect();
|
||||||
|
assert_eq!(batch, streamed);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn flat_session_reports_price() {
|
||||||
|
// A session with zero high-low span returns the session price directly.
|
||||||
|
let mut n = NakedPoc::new(2, 4).unwrap();
|
||||||
|
n.update(c(50.0, 50.0, 50.0, 10.0));
|
||||||
|
assert_eq!(n.update(c(50.0, 50.0, 50.0, 10.0)), Some(50.0));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn zero_volume_session_is_handled() {
|
||||||
|
// Zero-volume candles are skipped in the histogram; a POC still emits.
|
||||||
|
let mut n = NakedPoc::new(2, 4).unwrap();
|
||||||
|
n.update(c(60.0, 40.0, 50.0, 0.0));
|
||||||
|
assert!(n.update(c(60.0, 40.0, 50.0, 0.0)).is_some());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn nearest_of_two_naked_pocs() {
|
||||||
|
// Two untouched POCs at distant prices accumulate; the one nearest the
|
||||||
|
// last close is reported (exercises the min-by comparison).
|
||||||
|
let mut n = NakedPoc::new(2, 4).unwrap();
|
||||||
|
n.update(c(11.0, 9.0, 10.0, 100.0));
|
||||||
|
n.update(c(11.0, 9.0, 10.0, 100.0)); // POC near 10
|
||||||
|
n.update(c(101.0, 99.0, 100.0, 100.0));
|
||||||
|
let v = n.update(c(101.0, 99.0, 100.0, 100.0)).unwrap(); // POC near 100
|
||||||
|
assert!(
|
||||||
|
v > 50.0,
|
||||||
|
"nearest to close 100 should be the upper POC, got {v}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,285 @@
|
|||||||
|
//! Profile Shape — classifies the volume profile as b-shape, P-shape, or D/normal.
|
||||||
|
|
||||||
|
use std::collections::VecDeque;
|
||||||
|
|
||||||
|
use crate::error::{Error, Result};
|
||||||
|
use crate::ohlcv::Candle;
|
||||||
|
use crate::traits::Indicator;
|
||||||
|
|
||||||
|
/// Profile Shape — classifies a rolling volume profile by where its point of
|
||||||
|
/// control (POC) sits within the range: `b`, `P`, or `D` (normal).
|
||||||
|
///
|
||||||
|
/// ```text
|
||||||
|
/// build a `bins`-bucket volume profile over the last `period` candles
|
||||||
|
/// poc_idx = bin with the most volume
|
||||||
|
/// +1 P-shape : POC in the upper third (heavy top, thin tail down) — short-covering / accumulation
|
||||||
|
/// −1 b-shape : POC in the lower third (heavy bottom, thin tail up) — long-liquidation / distribution
|
||||||
|
/// 0 D/normal: POC in the middle third (balanced bell)
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// Market Profile readers classify the day's shape by the location of the heaviest
|
||||||
|
/// trading. A **P-shape** (control high, a thin tail beneath) typically marks
|
||||||
|
/// short-covering or the start of accumulation; a **b-shape** (control low, thin
|
||||||
|
/// tail above) marks long liquidation or distribution; a **D-shape** is a balanced,
|
||||||
|
/// two-sided day. Reducing the profile to this three-way code gives a compact,
|
||||||
|
/// streaming read of market posture.
|
||||||
|
///
|
||||||
|
/// The output is `+1` / `0` / `−1`. The first value lands after `period` candles;
|
||||||
|
/// each `update` rebuilds the profile in O(`period · bins`).
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// use wickra_core::{Candle, Indicator, ProfileShape};
|
||||||
|
///
|
||||||
|
/// let mut indicator = ProfileShape::new(20, 24).unwrap();
|
||||||
|
/// let mut last = None;
|
||||||
|
/// for i in 0..40 {
|
||||||
|
/// let base = 100.0 + (f64::from(i) * 0.3).sin() * 5.0;
|
||||||
|
/// let c = Candle::new(base, base + 1.0, base - 1.0, base, 1_000.0, 0).unwrap();
|
||||||
|
/// last = indicator.update(c);
|
||||||
|
/// }
|
||||||
|
/// assert!(last.is_some());
|
||||||
|
/// ```
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct ProfileShape {
|
||||||
|
period: usize,
|
||||||
|
bins: usize,
|
||||||
|
window: VecDeque<Candle>,
|
||||||
|
last: Option<f64>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ProfileShape {
|
||||||
|
/// Construct a Profile Shape classifier.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// Returns [`Error::PeriodZero`] if `period` is zero, or
|
||||||
|
/// [`Error::InvalidPeriod`] if `bins < 3` (the three-way split needs three
|
||||||
|
/// zones).
|
||||||
|
pub fn new(period: usize, bins: usize) -> Result<Self> {
|
||||||
|
if period == 0 {
|
||||||
|
return Err(Error::PeriodZero);
|
||||||
|
}
|
||||||
|
if bins < 3 {
|
||||||
|
return Err(Error::InvalidPeriod {
|
||||||
|
message: "profile shape needs bins >= 3",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Ok(Self {
|
||||||
|
period,
|
||||||
|
bins,
|
||||||
|
window: VecDeque::with_capacity(period),
|
||||||
|
last: None,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Configured `(period, bins)`.
|
||||||
|
pub const fn params(&self) -> (usize, usize) {
|
||||||
|
(self.period, self.bins)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Current value if available.
|
||||||
|
pub const fn value(&self) -> Option<f64> {
|
||||||
|
self.last
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
|
||||||
|
fn poc_index(&self) -> usize {
|
||||||
|
let mut low = f64::INFINITY;
|
||||||
|
let mut high = f64::NEG_INFINITY;
|
||||||
|
for c in &self.window {
|
||||||
|
low = low.min(c.low);
|
||||||
|
high = high.max(c.high);
|
||||||
|
}
|
||||||
|
let mut hist = vec![0.0; self.bins];
|
||||||
|
let span = high - low;
|
||||||
|
if span > 0.0 {
|
||||||
|
let width = span / self.bins as f64;
|
||||||
|
for c in &self.window {
|
||||||
|
if c.volume == 0.0 {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let lo_idx = (((c.low - low) / width).floor() as usize).min(self.bins - 1);
|
||||||
|
let hi_idx = (((c.high - low) / width).floor() as usize).min(self.bins - 1);
|
||||||
|
let share = c.volume / (hi_idx - lo_idx + 1) as f64;
|
||||||
|
for bin in hist.iter_mut().take(hi_idx + 1).skip(lo_idx) {
|
||||||
|
*bin += share;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let mut poc_idx = 0;
|
||||||
|
let mut poc_vol = f64::NEG_INFINITY;
|
||||||
|
for (idx, &vol) in hist.iter().enumerate() {
|
||||||
|
if vol > poc_vol {
|
||||||
|
poc_vol = vol;
|
||||||
|
poc_idx = idx;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
poc_idx
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Indicator for ProfileShape {
|
||||||
|
type Input = Candle;
|
||||||
|
type Output = f64;
|
||||||
|
|
||||||
|
fn update(&mut self, candle: Candle) -> Option<f64> {
|
||||||
|
if self.window.len() == self.period {
|
||||||
|
self.window.pop_front();
|
||||||
|
}
|
||||||
|
self.window.push_back(candle);
|
||||||
|
if self.window.len() < self.period {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let poc = self.poc_index();
|
||||||
|
let lower = self.bins / 3;
|
||||||
|
let upper = self.bins - self.bins / 3;
|
||||||
|
let shape = if poc >= upper {
|
||||||
|
1.0
|
||||||
|
} else if poc < lower {
|
||||||
|
-1.0
|
||||||
|
} else {
|
||||||
|
0.0
|
||||||
|
};
|
||||||
|
self.last = Some(shape);
|
||||||
|
Some(shape)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn reset(&mut self) {
|
||||||
|
self.window.clear();
|
||||||
|
self.last = None;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn warmup_period(&self) -> usize {
|
||||||
|
self.period
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_ready(&self) -> bool {
|
||||||
|
self.last.is_some()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn name(&self) -> &'static str {
|
||||||
|
"ProfileShape"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::traits::BatchExt;
|
||||||
|
|
||||||
|
fn c(high: f64, low: f64, volume: f64) -> Candle {
|
||||||
|
Candle::new_unchecked(
|
||||||
|
f64::midpoint(high, low),
|
||||||
|
high,
|
||||||
|
low,
|
||||||
|
f64::midpoint(high, low),
|
||||||
|
volume,
|
||||||
|
0,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn rejects_invalid_params() {
|
||||||
|
assert!(matches!(ProfileShape::new(0, 24), Err(Error::PeriodZero)));
|
||||||
|
assert!(matches!(
|
||||||
|
ProfileShape::new(20, 2),
|
||||||
|
Err(Error::InvalidPeriod { .. })
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn accessors_and_metadata() {
|
||||||
|
let p = ProfileShape::new(20, 24).unwrap();
|
||||||
|
assert_eq!(p.params(), (20, 24));
|
||||||
|
assert_eq!(p.warmup_period(), 20);
|
||||||
|
assert_eq!(p.name(), "ProfileShape");
|
||||||
|
assert!(!p.is_ready());
|
||||||
|
assert_eq!(p.value(), None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn first_emission_at_warmup_period() {
|
||||||
|
let mut p = ProfileShape::new(4, 9).unwrap();
|
||||||
|
let candles: Vec<Candle> = (0..6).map(|_| c(110.0, 90.0, 1_000.0)).collect();
|
||||||
|
let out = p.batch(&candles);
|
||||||
|
for v in out.iter().take(3) {
|
||||||
|
assert!(v.is_none());
|
||||||
|
}
|
||||||
|
assert!(out[3].is_some());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn heavy_top_is_p_shape() {
|
||||||
|
// Volume concentrated near the top of the range -> P-shape -> +1.
|
||||||
|
let mut p = ProfileShape::new(6, 9).unwrap();
|
||||||
|
let mut candles: Vec<Candle> = (0..5).map(|_| c(119.0, 117.0, 5_000.0)).collect();
|
||||||
|
candles.push(c(119.0, 80.0, 50.0)); // a thin tail down to 80
|
||||||
|
let last = p.batch(&candles).into_iter().flatten().last().unwrap();
|
||||||
|
assert_eq!(last, 1.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn heavy_bottom_is_b_shape() {
|
||||||
|
let mut p = ProfileShape::new(6, 9).unwrap();
|
||||||
|
let mut candles: Vec<Candle> = (0..5).map(|_| c(83.0, 81.0, 5_000.0)).collect();
|
||||||
|
candles.push(c(120.0, 81.0, 50.0)); // a thin tail up to 120
|
||||||
|
let last = p.batch(&candles).into_iter().flatten().last().unwrap();
|
||||||
|
assert_eq!(last, -1.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn balanced_is_d_shape() {
|
||||||
|
// Volume concentrated in the middle -> D/normal -> 0.
|
||||||
|
let mut p = ProfileShape::new(6, 9).unwrap();
|
||||||
|
let mut candles: Vec<Candle> = (0..5).map(|_| c(101.0, 99.0, 5_000.0)).collect();
|
||||||
|
candles.push(c(120.0, 80.0, 50.0)); // thin tails both ways, POC in the middle
|
||||||
|
let last = p.batch(&candles).into_iter().flatten().last().unwrap();
|
||||||
|
assert_eq!(last, 0.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn reset_clears_state() {
|
||||||
|
let mut p = ProfileShape::new(4, 9).unwrap();
|
||||||
|
p.batch(&[c(110.0, 90.0, 1_000.0); 6]);
|
||||||
|
assert!(p.is_ready());
|
||||||
|
p.reset();
|
||||||
|
assert!(!p.is_ready());
|
||||||
|
assert_eq!(p.value(), None);
|
||||||
|
assert_eq!(p.update(c(110.0, 90.0, 1_000.0)), None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn batch_equals_streaming() {
|
||||||
|
let candles: Vec<Candle> = (0..80)
|
||||||
|
.map(|i| {
|
||||||
|
c(
|
||||||
|
110.0 + (f64::from(i) * 0.25).sin() * 9.0,
|
||||||
|
90.0,
|
||||||
|
1_000.0 + f64::from(i),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
let batch = ProfileShape::new(20, 24).unwrap().batch(&candles);
|
||||||
|
let mut b = ProfileShape::new(20, 24).unwrap();
|
||||||
|
let streamed: Vec<_> = candles.iter().map(|x| b.update(*x)).collect();
|
||||||
|
assert_eq!(batch, streamed);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn flat_window_is_handled() {
|
||||||
|
// Zero high-low span skips the histogram pass entirely.
|
||||||
|
let mut p = ProfileShape::new(2, 4).unwrap();
|
||||||
|
p.update(c(50.0, 50.0, 10.0));
|
||||||
|
assert!(p.update(c(50.0, 50.0, 10.0)).is_some());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn zero_volume_window_is_handled() {
|
||||||
|
// Non-flat window of zero-volume candles hits the skip path.
|
||||||
|
let mut p = ProfileShape::new(2, 4).unwrap();
|
||||||
|
p.update(c(60.0, 40.0, 0.0));
|
||||||
|
assert!(p.update(c(60.0, 40.0, 0.0)).is_some());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,253 @@
|
|||||||
|
//! Single Prints — count of price levels touched by exactly one bar (low acceptance).
|
||||||
|
|
||||||
|
use std::collections::VecDeque;
|
||||||
|
|
||||||
|
use crate::error::{Error, Result};
|
||||||
|
use crate::ohlcv::Candle;
|
||||||
|
use crate::traits::Indicator;
|
||||||
|
|
||||||
|
/// Single Prints — the number of price levels (bins) in the rolling profile that
|
||||||
|
/// were touched by **exactly one** bar, marking zones of low acceptance / fast
|
||||||
|
/// movement.
|
||||||
|
///
|
||||||
|
/// ```text
|
||||||
|
/// for each of `bins` price levels over the last `period` candles:
|
||||||
|
/// touches = number of bars whose high-low range covers that level
|
||||||
|
/// SinglePrints = count of levels with touches == 1
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// In Market Profile a "single print" is a price the market traded through so
|
||||||
|
/// quickly that only one time-period printed there — a footprint of an aggressive,
|
||||||
|
/// one-sided move with little two-way trade. Single prints often act as support or
|
||||||
|
/// resistance on a retest (the imbalance gets "repaired") and mark the edges of
|
||||||
|
/// rapid moves. Counting them per profile gives a streaming gauge of how much of
|
||||||
|
/// the recent range was traversed without acceptance: a high count means a fast,
|
||||||
|
/// trending, low-rotation market; a low count means a balanced, well-traded range.
|
||||||
|
///
|
||||||
|
/// The output is a non-negative count. The first value lands after `period`
|
||||||
|
/// candles; each `update` rebuilds the touch histogram in O(`period · bins`).
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// use wickra_core::{Candle, Indicator, SinglePrints};
|
||||||
|
///
|
||||||
|
/// let mut indicator = SinglePrints::new(20, 24).unwrap();
|
||||||
|
/// let mut last = None;
|
||||||
|
/// for i in 0..40 {
|
||||||
|
/// let base = 100.0 + f64::from(i); // a one-directional ramp -> many single prints
|
||||||
|
/// let c = Candle::new(base, base + 0.5, base - 0.5, base, 1_000.0, 0).unwrap();
|
||||||
|
/// last = indicator.update(c);
|
||||||
|
/// }
|
||||||
|
/// assert!(last.is_some());
|
||||||
|
/// ```
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct SinglePrints {
|
||||||
|
period: usize,
|
||||||
|
bins: usize,
|
||||||
|
window: VecDeque<Candle>,
|
||||||
|
last: Option<f64>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SinglePrints {
|
||||||
|
/// Construct a Single Prints counter.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// Returns [`Error::PeriodZero`] if `period` or `bins` is zero.
|
||||||
|
pub fn new(period: usize, bins: usize) -> Result<Self> {
|
||||||
|
if period == 0 || bins == 0 {
|
||||||
|
return Err(Error::PeriodZero);
|
||||||
|
}
|
||||||
|
Ok(Self {
|
||||||
|
period,
|
||||||
|
bins,
|
||||||
|
window: VecDeque::with_capacity(period),
|
||||||
|
last: None,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Configured `(period, bins)`.
|
||||||
|
pub const fn params(&self) -> (usize, usize) {
|
||||||
|
(self.period, self.bins)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Current value if available.
|
||||||
|
pub const fn value(&self) -> Option<f64> {
|
||||||
|
self.last
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
|
||||||
|
fn count_single_prints(&self) -> usize {
|
||||||
|
let mut low = f64::INFINITY;
|
||||||
|
let mut high = f64::NEG_INFINITY;
|
||||||
|
for c in &self.window {
|
||||||
|
low = low.min(c.low);
|
||||||
|
high = high.max(c.high);
|
||||||
|
}
|
||||||
|
let span = high - low;
|
||||||
|
if span <= 0.0 {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
let width = span / self.bins as f64;
|
||||||
|
let mut touches = vec![0u32; self.bins];
|
||||||
|
for c in &self.window {
|
||||||
|
let lo_idx = (((c.low - low) / width).floor() as usize).min(self.bins - 1);
|
||||||
|
let hi_idx = (((c.high - low) / width).floor() as usize).min(self.bins - 1);
|
||||||
|
for t in touches.iter_mut().take(hi_idx + 1).skip(lo_idx) {
|
||||||
|
*t += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
touches.iter().filter(|&&t| t == 1).count()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Indicator for SinglePrints {
|
||||||
|
type Input = Candle;
|
||||||
|
type Output = f64;
|
||||||
|
|
||||||
|
fn update(&mut self, candle: Candle) -> Option<f64> {
|
||||||
|
if self.window.len() == self.period {
|
||||||
|
self.window.pop_front();
|
||||||
|
}
|
||||||
|
self.window.push_back(candle);
|
||||||
|
if self.window.len() < self.period {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let count = self.count_single_prints() as f64;
|
||||||
|
self.last = Some(count);
|
||||||
|
Some(count)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn reset(&mut self) {
|
||||||
|
self.window.clear();
|
||||||
|
self.last = None;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn warmup_period(&self) -> usize {
|
||||||
|
self.period
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_ready(&self) -> bool {
|
||||||
|
self.last.is_some()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn name(&self) -> &'static str {
|
||||||
|
"SinglePrints"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::traits::BatchExt;
|
||||||
|
|
||||||
|
fn c(high: f64, low: f64) -> Candle {
|
||||||
|
Candle::new_unchecked(
|
||||||
|
f64::midpoint(high, low),
|
||||||
|
high,
|
||||||
|
low,
|
||||||
|
f64::midpoint(high, low),
|
||||||
|
1_000.0,
|
||||||
|
0,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn rejects_zero_params() {
|
||||||
|
assert!(matches!(SinglePrints::new(0, 24), Err(Error::PeriodZero)));
|
||||||
|
assert!(matches!(SinglePrints::new(20, 0), Err(Error::PeriodZero)));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn accessors_and_metadata() {
|
||||||
|
let s = SinglePrints::new(20, 24).unwrap();
|
||||||
|
assert_eq!(s.params(), (20, 24));
|
||||||
|
assert_eq!(s.warmup_period(), 20);
|
||||||
|
assert_eq!(s.name(), "SinglePrints");
|
||||||
|
assert!(!s.is_ready());
|
||||||
|
assert_eq!(s.value(), None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn first_emission_at_warmup_period() {
|
||||||
|
let mut s = SinglePrints::new(4, 8).unwrap();
|
||||||
|
let candles: Vec<Candle> = (0..6)
|
||||||
|
.map(|i| c(101.0 + f64::from(i), 99.0 + f64::from(i)))
|
||||||
|
.collect();
|
||||||
|
let out = s.batch(&candles);
|
||||||
|
for v in out.iter().take(3) {
|
||||||
|
assert!(v.is_none());
|
||||||
|
}
|
||||||
|
assert!(out[3].is_some());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn flat_range_has_no_single_prints() {
|
||||||
|
// Every bar covers the same single price -> zero span -> 0.
|
||||||
|
let mut s = SinglePrints::new(4, 8).unwrap();
|
||||||
|
let last = s
|
||||||
|
.batch(&[c(100.0, 100.0); 6])
|
||||||
|
.into_iter()
|
||||||
|
.flatten()
|
||||||
|
.last()
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(last, 0.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn ramp_has_many_single_prints() {
|
||||||
|
// A one-directional ramp visits most levels exactly once.
|
||||||
|
let mut s = SinglePrints::new(10, 24).unwrap();
|
||||||
|
let candles: Vec<Candle> = (0..10)
|
||||||
|
.map(|i| c(100.5 + f64::from(i), 99.5 + f64::from(i)))
|
||||||
|
.collect();
|
||||||
|
let last = s.batch(&candles).into_iter().flatten().last().unwrap();
|
||||||
|
assert!(
|
||||||
|
last > 0.0,
|
||||||
|
"a ramp should produce single prints, got {last}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn output_non_negative() {
|
||||||
|
let mut s = SinglePrints::new(14, 24).unwrap();
|
||||||
|
for v in s
|
||||||
|
.batch(
|
||||||
|
&(0..60)
|
||||||
|
.map(|i| c(110.0 + (f64::from(i) * 0.3).sin() * 8.0, 90.0))
|
||||||
|
.collect::<Vec<_>>(),
|
||||||
|
)
|
||||||
|
.into_iter()
|
||||||
|
.flatten()
|
||||||
|
{
|
||||||
|
assert!(v >= 0.0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn reset_clears_state() {
|
||||||
|
let mut s = SinglePrints::new(4, 8).unwrap();
|
||||||
|
s.batch(
|
||||||
|
&(0..6)
|
||||||
|
.map(|i| c(101.0 + f64::from(i), 99.0 + f64::from(i)))
|
||||||
|
.collect::<Vec<_>>(),
|
||||||
|
);
|
||||||
|
assert!(s.is_ready());
|
||||||
|
s.reset();
|
||||||
|
assert!(!s.is_ready());
|
||||||
|
assert_eq!(s.value(), None);
|
||||||
|
assert_eq!(s.update(c(101.0, 99.0)), None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn batch_equals_streaming() {
|
||||||
|
let candles: Vec<Candle> = (0..80)
|
||||||
|
.map(|i| c(110.0 + (f64::from(i) * 0.25).sin() * 9.0, 90.0))
|
||||||
|
.collect();
|
||||||
|
let batch = SinglePrints::new(20, 24).unwrap().batch(&candles);
|
||||||
|
let mut b = SinglePrints::new(20, 24).unwrap();
|
||||||
|
let streamed: Vec<_> = candles.iter().map(|x| b.update(*x)).collect();
|
||||||
|
assert_eq!(batch, streamed);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -72,37 +72,38 @@ pub use indicators::{
|
|||||||
ChaikinVolatility, ChandeKrollStop, ChandeKrollStopOutput, ChandelierExit,
|
ChaikinVolatility, ChandeKrollStop, ChandeKrollStopOutput, ChandelierExit,
|
||||||
ChandelierExitOutput, ChoppinessIndex, ClassicPivots, ClassicPivotsOutput, CloseVsOpen,
|
ChandelierExitOutput, ChoppinessIndex, ClassicPivots, ClassicPivotsOutput, CloseVsOpen,
|
||||||
ClosingMarubozu, Cmo, CoefficientOfVariation, Cointegration, CointegrationOutput,
|
ClosingMarubozu, Cmo, CoefficientOfVariation, Cointegration, CointegrationOutput,
|
||||||
ConcealingBabySwallow, ConditionalValueAtRisk, ConnorsRsi, Coppock, CorrelationTrendIndicator,
|
CompositeProfile, CompositeProfileOutput, ConcealingBabySwallow, ConditionalValueAtRisk,
|
||||||
Counterattack, Crab, CumulativeVolumeDelta, CumulativeVolumeIndex, CupAndHandle,
|
ConnorsRsi, Coppock, CorrelationTrendIndicator, Counterattack, Crab, CumulativeVolumeDelta,
|
||||||
CyberneticCycle, Cypher, DayOfWeekProfile, DayOfWeekProfileOutput, Decycler,
|
CumulativeVolumeIndex, CupAndHandle, CyberneticCycle, Cypher, DayOfWeekProfile,
|
||||||
DecyclerOscillator, Dema, DemandIndex, DemarkPivots, DemarkPivotsOutput, DepthSlope,
|
DayOfWeekProfileOutput, Decycler, DecyclerOscillator, Dema, DemandIndex, DemarkPivots,
|
||||||
DerivativeOscillator, DetrendedStdDev, DisparityIndex, DistanceSsd, Doji, DojiStar, Donchian,
|
DemarkPivotsOutput, DepthSlope, DerivativeOscillator, DetrendedStdDev, DisparityIndex,
|
||||||
DonchianOutput, DonchianStop, DonchianStopOutput, DoubleBollinger, DoubleBollingerOutput,
|
DistanceSsd, Doji, DojiStar, Donchian, DonchianOutput, DonchianStop, DonchianStopOutput,
|
||||||
DoubleTopBottom, DownsideGapThreeMethods, Dpo, DragonflyDoji, DrawdownDuration, DumplingTop,
|
DoubleBollinger, DoubleBollingerOutput, DoubleTopBottom, DownsideGapThreeMethods, Dpo,
|
||||||
Dx, DynamicMomentumIndex, EaseOfMovement, EffectiveSpread, EhlersStochastic, Ehma,
|
DragonflyDoji, DrawdownDuration, DumplingTop, Dx, DynamicMomentumIndex, EaseOfMovement,
|
||||||
ElderImpulse, ElderRay, ElderRayOutput, ElderSafeZone, ElderSafeZoneOutput, Ema,
|
EffectiveSpread, EhlersStochastic, Ehma, ElderImpulse, ElderRay, ElderRayOutput, ElderSafeZone,
|
||||||
EmpiricalModeDecomposition, Engulfing, Equivolume, EquivolumeOutput, EstimatedLeverageRatio,
|
ElderSafeZoneOutput, Ema, EmpiricalModeDecomposition, Engulfing, Equivolume, EquivolumeOutput,
|
||||||
EvenBetterSinewave, EveningDojiStar, Evwma, EwmaVolatility, Expectancy, FallingThreeMethods,
|
EstimatedLeverageRatio, EvenBetterSinewave, EveningDojiStar, Evwma, EwmaVolatility, Expectancy,
|
||||||
Fama, FibArcs, FibArcsOutput, FibChannel, FibChannelOutput, FibConfluence, FibConfluenceOutput,
|
FallingThreeMethods, Fama, FibArcs, FibArcsOutput, FibChannel, FibChannelOutput, FibConfluence,
|
||||||
FibExtension, FibExtensionOutput, FibFan, FibFanOutput, FibProjection, FibProjectionOutput,
|
FibConfluenceOutput, FibExtension, FibExtensionOutput, FibFan, FibFanOutput, FibProjection,
|
||||||
FibRetracement, FibRetracementOutput, FibTimeZones, FibTimeZonesOutput, FibonacciPivots,
|
FibProjectionOutput, FibRetracement, FibRetracementOutput, FibTimeZones, FibTimeZonesOutput,
|
||||||
FibonacciPivotsOutput, FisherRsi, FisherTransform, FlagPennant, Footprint, FootprintOutput,
|
FibonacciPivots, FibonacciPivotsOutput, FisherRsi, FisherTransform, FlagPennant, Footprint,
|
||||||
ForceIndex, FractalChaosBands, FractalChaosBandsOutput, Frama, FryPanBottom, FundingBasis,
|
FootprintOutput, ForceIndex, FractalChaosBands, FractalChaosBandsOutput, Frama, FryPanBottom,
|
||||||
FundingImpliedApr, FundingRate, FundingRateMean, FundingRateZScore, GainLossRatio,
|
FundingBasis, FundingImpliedApr, FundingRate, FundingRateMean, FundingRateZScore,
|
||||||
GapSideBySideWhite, Garch11, GarmanKlassVolatility, Gartley, GatorOscillator,
|
GainLossRatio, GapSideBySideWhite, Garch11, GarmanKlassVolatility, Gartley, GatorOscillator,
|
||||||
GatorOscillatorOutput, GeneralizedDema, GeometricMa, GoldenPocket, GoldenPocketOutput,
|
GatorOscillatorOutput, GeneralizedDema, GeometricMa, GoldenPocket, GoldenPocketOutput,
|
||||||
GrangerCausality, GravestoneDoji, Hammer, HangingMan, Harami, HaramiCross,
|
GrangerCausality, GravestoneDoji, Hammer, HangingMan, Harami, HaramiCross,
|
||||||
HasbrouckInformationShare, HeadAndShoulders, HeikinAshi, HeikinAshiOscillator,
|
HasbrouckInformationShare, HeadAndShoulders, HeikinAshi, HeikinAshiOscillator,
|
||||||
HeikinAshiOutput, HiLoActivator, HighLowIndex, HighLowRange, HighWave, HighpassFilter, Hikkake,
|
HeikinAshiOutput, HiLoActivator, HighLowIndex, HighLowRange, HighLowVolumeNodes,
|
||||||
HikkakeModified, HilbertDominantCycle, HistoricalVolatility, Hma, HoltWinters, HomingPigeon,
|
HighLowVolumeNodesOutput, HighWave, HighpassFilter, Hikkake, HikkakeModified,
|
||||||
HtDcPhase, HtPhasor, HtPhasorOutput, HtTrendMode, HurstChannel, HurstChannelOutput,
|
HilbertDominantCycle, HistoricalVolatility, Hma, HoltWinters, HomingPigeon, HtDcPhase,
|
||||||
HurstExponent, Ichimoku, IchimokuOutput, IdenticalThreeCrows, InNeck, Inertia,
|
HtPhasor, HtPhasorOutput, HtTrendMode, HurstChannel, HurstChannelOutput, HurstExponent,
|
||||||
InformationRatio, InitialBalance, InitialBalanceOutput, InstantaneousTrendline,
|
Ichimoku, IchimokuOutput, IdenticalThreeCrows, InNeck, Inertia, InformationRatio,
|
||||||
IntradayIntensity, IntradayMomentumIndex, IntradayVolatilityProfile,
|
InitialBalance, InitialBalanceOutput, InstantaneousTrendline, IntradayIntensity,
|
||||||
IntradayVolatilityProfileOutput, InverseFisherTransform, InvertedHammer, JarqueBera, Jma,
|
IntradayMomentumIndex, IntradayVolatilityProfile, IntradayVolatilityProfileOutput,
|
||||||
JumpIndicator, KagiBars, KalmanHedgeRatio, KalmanHedgeRatioOutput, Kama, KaseDevStop,
|
InverseFisherTransform, InvertedHammer, JarqueBera, Jma, JumpIndicator, KagiBars,
|
||||||
KaseDevStopOutput, KasePermissionStochastic, KasePermissionStochasticOutput, KellyCriterion,
|
KalmanHedgeRatio, KalmanHedgeRatioOutput, Kama, KaseDevStop, KaseDevStopOutput,
|
||||||
Keltner, KeltnerOutput, KendallTau, Kicking, KickingByLength, Kst, KstOutput, Kurtosis, Kvo,
|
KasePermissionStochastic, KasePermissionStochasticOutput, KellyCriterion, Keltner,
|
||||||
|
KeltnerOutput, KendallTau, Kicking, KickingByLength, Kst, KstOutput, Kurtosis, Kvo,
|
||||||
KylesLambda, LadderBottom, LaguerreRsi, LeadLagCrossCorrelation, LeadLagCrossCorrelationOutput,
|
KylesLambda, LadderBottom, LaguerreRsi, LeadLagCrossCorrelation, LeadLagCrossCorrelationOutput,
|
||||||
LinRegAngle, LinRegChannel, LinRegChannelOutput, LinRegIntercept, LinRegSlope,
|
LinRegAngle, LinRegChannel, LinRegChannelOutput, LinRegIntercept, LinRegSlope,
|
||||||
LinearRegression, LiquidationFeatures, LiquidationFeaturesOutput, LogReturn, LongLeggedDoji,
|
LinearRegression, LiquidationFeatures, LiquidationFeaturesOutput, LogReturn, LongLeggedDoji,
|
||||||
@@ -112,7 +113,7 @@ pub use indicators::{
|
|||||||
McGinleyDynamic, MedianAbsoluteDeviation, MedianChannel, MedianChannelOutput, MedianMa,
|
McGinleyDynamic, MedianAbsoluteDeviation, MedianChannel, MedianChannelOutput, MedianMa,
|
||||||
MedianPrice, Mfi, Microprice, MidPoint, MidPrice, MinusDi, MinusDm, ModifiedMaStop,
|
MedianPrice, Mfi, Microprice, MidPoint, MidPrice, MinusDi, MinusDm, ModifiedMaStop,
|
||||||
ModifiedMaStopOutput, Mom, MorningDojiStar, MorningEveningStar, MurreyMathLines,
|
ModifiedMaStopOutput, Mom, MorningDojiStar, MorningEveningStar, MurreyMathLines,
|
||||||
MurreyMathLinesOutput, Natr, NewHighsNewLows, NewPriceLines, Nrtr, NrtrOutput, Nvi,
|
MurreyMathLinesOutput, NakedPoc, Natr, NewHighsNewLows, NewPriceLines, Nrtr, NrtrOutput, Nvi,
|
||||||
OIPriceDivergence, OIWeighted, Obv, OiToVolumeRatio, OmegaRatio, OnNeck, OpenInterestDelta,
|
OIPriceDivergence, OIWeighted, Obv, OiToVolumeRatio, OmegaRatio, OnNeck, OpenInterestDelta,
|
||||||
OpenInterestMomentum, OpeningMarubozu, OpeningRange, OpeningRangeOutput,
|
OpenInterestMomentum, OpeningMarubozu, OpeningRange, OpeningRangeOutput,
|
||||||
OrderBookImbalanceFull, OrderBookImbalanceTop1, OrderBookImbalanceTopN, OrderFlowImbalance,
|
OrderBookImbalanceFull, OrderBookImbalanceTop1, OrderBookImbalanceTopN, OrderFlowImbalance,
|
||||||
@@ -120,39 +121,39 @@ pub use indicators::{
|
|||||||
PairSpreadZScore, PairwiseBeta, ParkinsonVolatility, PearsonCorrelation, PercentAboveMa,
|
PairSpreadZScore, PairwiseBeta, ParkinsonVolatility, PearsonCorrelation, PercentAboveMa,
|
||||||
PercentB, PercentageTrailingStop, PerpetualPremiumIndex, Pgo, PiercingDarkCloud, Pin,
|
PercentB, PercentageTrailingStop, PerpetualPremiumIndex, Pgo, PiercingDarkCloud, Pin,
|
||||||
PivotReversal, PlusDi, PlusDm, Pmo, PointAndFigureBars, PolarizedFractalEfficiency, Ppo,
|
PivotReversal, PlusDi, PlusDm, Pmo, PointAndFigureBars, PolarizedFractalEfficiency, Ppo,
|
||||||
PpoHistogram, ProfitFactor, ProjectionBands, ProjectionBandsOutput, ProjectionOscillator, Psar,
|
PpoHistogram, ProfileShape, ProfitFactor, ProjectionBands, ProjectionBandsOutput,
|
||||||
Pvi, Qqe, QqeOutput, Qstick, QuartileBands, QuartileBandsOutput, QuotedSpread, RSquared,
|
ProjectionOscillator, Psar, Pvi, Qqe, QqeOutput, Qstick, QuartileBands, QuartileBandsOutput,
|
||||||
RealizedSpread, RealizedVolatility, RecoveryFactor, RectangleRange, Reflex, RegimeLabel,
|
QuotedSpread, RSquared, RealizedSpread, RealizedVolatility, RecoveryFactor, RectangleRange,
|
||||||
RelativeStrengthAB, RelativeStrengthOutput, RenkoBars, RenkoTrailingStop, RickshawMan,
|
Reflex, RegimeLabel, RelativeStrengthAB, RelativeStrengthOutput, RenkoBars, RenkoTrailingStop,
|
||||||
RisingThreeMethods, Rmi, Roc, Rocp, Rocr, Rocr100, RogersSatchellVolatility, RollMeasure,
|
RickshawMan, RisingThreeMethods, Rmi, Roc, Rocp, Rocr, Rocr100, RogersSatchellVolatility,
|
||||||
RollingCorrelation, RollingCovariance, RollingIqr, RollingMinMaxScaler, RollingPercentileRank,
|
RollMeasure, RollingCorrelation, RollingCovariance, RollingIqr, RollingMinMaxScaler,
|
||||||
RollingQuantile, RollingVwap, RoofingFilter, Rsi, Rsx, Rvi, RviVolatility, Rwi, RwiOutput,
|
RollingPercentileRank, RollingQuantile, RollingVwap, RoofingFilter, Rsi, Rsx, Rvi,
|
||||||
SampleEntropy, SarExt, SeasonalZScore, SeparatingLines, SessionHighLow, SessionHighLowOutput,
|
RviVolatility, Rwi, RwiOutput, SampleEntropy, SarExt, SeasonalZScore, SeparatingLines,
|
||||||
SessionRange, SessionRangeOutput, SessionVwap, ShannonEntropy, Shark, SharpeRatio,
|
SessionHighLow, SessionHighLowOutput, SessionRange, SessionRangeOutput, SessionVwap,
|
||||||
ShootingStar, ShortLine, SignedVolume, SineWave, SineWeightedMa, Skewness, Sma, Smi, Smma,
|
ShannonEntropy, Shark, SharpeRatio, ShootingStar, ShortLine, SignedVolume, SineWave,
|
||||||
SmoothedHeikinAshi, SmoothedHeikinAshiOutput, SortinoRatio, SpearmanCorrelation, SpinningTop,
|
SineWeightedMa, SinglePrints, Skewness, Sma, Smi, Smma, SmoothedHeikinAshi,
|
||||||
SpreadAr1Coefficient, SpreadBollingerBands, SpreadBollingerBandsOutput, SpreadHurst,
|
SmoothedHeikinAshiOutput, SortinoRatio, SpearmanCorrelation, SpinningTop, SpreadAr1Coefficient,
|
||||||
StalledPattern, StandardError, StandardErrorBands, StandardErrorBandsOutput, StarcBands,
|
SpreadBollingerBands, SpreadBollingerBandsOutput, SpreadHurst, StalledPattern, StandardError,
|
||||||
StarcBandsOutput, Stc, StdDev, StepTrailingStop, StickSandwich, StochRsi, Stochastic,
|
StandardErrorBands, StandardErrorBandsOutput, StarcBands, StarcBandsOutput, Stc, StdDev,
|
||||||
StochasticCci, StochasticOutput, SuperSmoother, SuperTrend, SuperTrendOutput,
|
StepTrailingStop, StickSandwich, StochRsi, Stochastic, StochasticCci, StochasticOutput,
|
||||||
TakerBuySellRatio, Takuri, TasukiGap, TdCamouflage, TdClop, TdClopwin, TdCombo, TdCountdown,
|
SuperSmoother, SuperTrend, SuperTrendOutput, TakerBuySellRatio, Takuri, TasukiGap,
|
||||||
TdDWave, TdDeMarker, TdDifferential, TdLines, TdLinesOutput, TdMovingAverage,
|
TdCamouflage, TdClop, TdClopwin, TdCombo, TdCountdown, TdDWave, TdDeMarker, TdDifferential,
|
||||||
TdMovingAverageOutput, TdOpen, TdPressure, TdPropulsion, TdRangeProjection,
|
TdLines, TdLinesOutput, TdMovingAverage, TdMovingAverageOutput, TdOpen, TdPressure,
|
||||||
TdRangeProjectionOutput, TdRei, TdRiskLevel, TdRiskLevelOutput, TdSequential,
|
TdPropulsion, TdRangeProjection, TdRangeProjectionOutput, TdRei, TdRiskLevel,
|
||||||
TdSequentialOutput, TdSetup, TdTrap, Tema, TermStructureBasis, ThreeDrives, ThreeInside,
|
TdRiskLevelOutput, TdSequential, TdSequentialOutput, TdSetup, TdTrap, Tema, TermStructureBasis,
|
||||||
ThreeLineBreak, ThreeLineStrike, ThreeOutside, ThreeSoldiersOrCrows, ThreeStarsInSouth,
|
ThreeDrives, ThreeInside, ThreeLineBreak, ThreeLineStrike, ThreeOutside, ThreeSoldiersOrCrows,
|
||||||
Thrusting, TickIndex, Tii, TimeBasedStop, TimeOfDayReturnProfile, TimeOfDayReturnProfileOutput,
|
ThreeStarsInSouth, Thrusting, TickIndex, Tii, TimeBasedStop, TimeOfDayReturnProfile,
|
||||||
TowerTopBottom, TpoProfile, TpoProfileOutput, TradeImbalance, TradeSignAutocorrelation,
|
TimeOfDayReturnProfileOutput, TowerTopBottom, TpoProfile, TpoProfileOutput, TradeImbalance,
|
||||||
TradeVolumeIndex, TrendLabel, TrendStrengthIndex, Trendflex, TreynorRatio, Triangle, Trima,
|
TradeSignAutocorrelation, TradeVolumeIndex, TrendLabel, TrendStrengthIndex, Trendflex,
|
||||||
Trin, TripleTopBottom, Tristar, Trix, TrueRange, Tsf, TsfOscillator, Tsi, Tsv, TtmSqueeze,
|
TreynorRatio, Triangle, Trima, Trin, TripleTopBottom, Tristar, Trix, TrueRange, Tsf,
|
||||||
TtmSqueezeOutput, TtmTrend, TurnOfMonth, Tweezer, TwiggsMoneyFlow, TwoCrows, TypicalPrice,
|
TsfOscillator, Tsi, Tsv, TtmSqueeze, TtmSqueezeOutput, TtmTrend, TurnOfMonth, Tweezer,
|
||||||
UlcerIndex, UltimateOscillator, UniqueThreeRiver, UniversalOscillator, UpDownVolumeRatio,
|
TwiggsMoneyFlow, TwoCrows, TypicalPrice, UlcerIndex, UltimateOscillator, UniqueThreeRiver,
|
||||||
UpsideGapThreeMethods, UpsideGapTwoCrows, ValueArea, ValueAreaOutput, ValueAtRisk, Variance,
|
UniversalOscillator, UpDownVolumeRatio, UpsideGapThreeMethods, UpsideGapTwoCrows, ValueArea,
|
||||||
VarianceRatio, VerticalHorizontalFilter, Vidya, VolatilityCone, VolatilityConeOutput,
|
ValueAreaOutput, ValueAtRisk, Variance, VarianceRatio, VerticalHorizontalFilter, Vidya,
|
||||||
VolatilityOfVolatility, VolatilityRatio, VoltyStop, VolumeByTimeProfile,
|
VolatilityCone, VolatilityConeOutput, VolatilityOfVolatility, VolatilityRatio, VoltyStop,
|
||||||
VolumeByTimeProfileOutput, VolumeOscillator, VolumePriceTrend, VolumeProfile,
|
VolumeByTimeProfile, VolumeByTimeProfileOutput, VolumeOscillator, VolumePriceTrend,
|
||||||
VolumeProfileOutput, VolumeRsi, VolumeWeightedMacd, VolumeWeightedMacdOutput, VolumeWeightedSr,
|
VolumeProfile, VolumeProfileOutput, VolumeRsi, VolumeWeightedMacd, VolumeWeightedMacdOutput,
|
||||||
VolumeWeightedSrOutput, Vortex, VortexOutput, Vpin, Vwap, VwapStdDevBands,
|
VolumeWeightedSr, VolumeWeightedSrOutput, Vortex, VortexOutput, Vpin, Vwap, VwapStdDevBands,
|
||||||
VwapStdDevBandsOutput, Vwma, Vzo, Wad, WavePm, WaveTrend, WaveTrendOutput, Wedge,
|
VwapStdDevBandsOutput, Vwma, Vzo, Wad, WavePm, WaveTrend, WaveTrendOutput, Wedge,
|
||||||
WeightedClose, WickRatio, WilliamsFractals, WilliamsFractalsOutput, WilliamsR, WinRate, Wma,
|
WeightedClose, WickRatio, WilliamsFractals, WilliamsFractalsOutput, WilliamsR, WinRate, Wma,
|
||||||
WoodiePivots, WoodiePivotsOutput, YangZhangVolatility, YoyoExit, ZScore, ZeroLagMacd,
|
WoodiePivots, WoodiePivotsOutput, YangZhangVolatility, YoyoExit, ZScore, ZeroLagMacd,
|
||||||
|
|||||||
+1
-1
@@ -8,7 +8,7 @@ That includes:
|
|||||||
[Python](https://docs.wickra.org/Quickstart-Python),
|
[Python](https://docs.wickra.org/Quickstart-Python),
|
||||||
[Node](https://docs.wickra.org/Quickstart-Node), and
|
[Node](https://docs.wickra.org/Quickstart-Node), and
|
||||||
[WASM](https://docs.wickra.org/Quickstart-WASM).
|
[WASM](https://docs.wickra.org/Quickstart-WASM).
|
||||||
- A per-indicator deep dive for every one of the **493 indicators** across
|
- A per-indicator deep dive for every one of the **498 indicators** across
|
||||||
the sixteen families (Moving Averages, Momentum Oscillators, Trend &
|
the sixteen families (Moving Averages, Momentum Oscillators, Trend &
|
||||||
Directional, Price Oscillators, Volatility & Bands, Bands & Channels,
|
Directional, Price Oscillators, Volatility & Bands, Bands & Channels,
|
||||||
Trailing Stops, Volume, Price Statistics, Ehlers / Cycle DSP, Pivots &
|
Trailing Stops, Volume, Price Statistics, Ehlers / Cycle DSP, Pivots &
|
||||||
|
|||||||
Generated
+7
-7
@@ -17,7 +17,7 @@
|
|||||||
},
|
},
|
||||||
"../../bindings/node": {
|
"../../bindings/node": {
|
||||||
"name": "wickra",
|
"name": "wickra",
|
||||||
"version": "0.7.1",
|
"version": "0.7.2",
|
||||||
"license": "MIT OR Apache-2.0",
|
"license": "MIT OR Apache-2.0",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@napi-rs/cli": "^2.18.0"
|
"@napi-rs/cli": "^2.18.0"
|
||||||
@@ -26,12 +26,12 @@
|
|||||||
"node": ">= 18"
|
"node": ">= 18"
|
||||||
},
|
},
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
"wickra-darwin-arm64": "0.7.1",
|
"wickra-darwin-arm64": "0.7.2",
|
||||||
"wickra-darwin-x64": "0.7.1",
|
"wickra-darwin-x64": "0.7.2",
|
||||||
"wickra-linux-arm64-gnu": "0.7.1",
|
"wickra-linux-arm64-gnu": "0.7.2",
|
||||||
"wickra-linux-x64-gnu": "0.7.1",
|
"wickra-linux-x64-gnu": "0.7.2",
|
||||||
"wickra-win32-arm64-msvc": "0.7.1",
|
"wickra-win32-arm64-msvc": "0.7.2",
|
||||||
"wickra-win32-x64-msvc": "0.7.1"
|
"wickra-win32-x64-msvc": "0.7.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/wickra": {
|
"node_modules/wickra": {
|
||||||
|
|||||||
@@ -22,7 +22,7 @@
|
|||||||
//! WeightedClose.
|
//! WeightedClose.
|
||||||
|
|
||||||
use libfuzzer_sys::fuzz_target;
|
use libfuzzer_sys::fuzz_target;
|
||||||
use wickra_core::{AbandonedBaby, Abcd, AccelerationBands, AcceleratorOscillator, AdOscillator, AdaptiveCci, Adl, AdvanceBlock, Adx, Adxr, Alligator, AnchoredVwap, AndrewsPitchfork, Aroon, AroonOscillator, Atr, AtrBands, AtrRatchet, AtrTrailingStop, AutoFib, AverageDailyRange, AwesomeOscillator, AwesomeOscillatorHistogram, BalanceOfPower, Bat, BatchExt, BeltHold, BetterVolume, BodySizePct, Breakaway, Butterfly, Camarilla, Candle, CandleVolume, Cci, CentralPivotRange, ChaikinMoneyFlow, ChaikinOscillator, ChaikinVolatility, ChandeKrollStop, ChandelierExit, ChoppinessIndex, ClassicPivots, CloseVsOpen, ClosingMarubozu, ConcealingBabySwallow, Counterattack, Crab, CupAndHandle, Cypher, DayOfWeekProfile, DemandIndex, DemarkPivots, Doji, DojiStar, Donchian, DonchianStop, DoubleTopBottom, DownsideGapThreeMethods, DragonflyDoji, DumplingTop, Dx, EaseOfMovement, ElderRay, ElderSafeZone, Engulfing, Equivolume, EveningDojiStar, Evwma, FallingThreeMethods, FibArcs, FibChannel, FibConfluence, FibExtension, FibFan, FibProjection, FibRetracement, FibTimeZones, FibonacciPivots, FlagPennant, ForceIndex, FractalChaosBands, FryPanBottom, GapSideBySideWhite, GarmanKlassVolatility, Gartley, GatorOscillator, GoldenPocket, GravestoneDoji, Hammer, HangingMan, Harami, HaramiCross, HeadAndShoulders, HeikinAshi, HeikinAshiOscillator, HiLoActivator, HighLowRange, HighWave, Hikkake, HikkakeModified, HomingPigeon, HurstChannel, Ichimoku, IdenticalThreeCrows, InNeck, Indicator, Inertia, InitialBalance, IntradayIntensity, IntradayMomentumIndex, IntradayVolatilityProfile, InvertedHammer, KaseDevStop, KasePermissionStochastic, Keltner, Kicking, KickingByLength, Kvo, LadderBottom, LongLeggedDoji, LongLine, MarketFacilitationIndex, Marubozu, MassIndex, MatHold, MatchingLow, AvgPrice, MedianPrice, Mfi, MidPrice, MinusDi, MinusDm, ModifiedMaStop, MorningDojiStar, MorningEveningStar, MurreyMathLines, Natr, NewPriceLines, Nrtr, Nvi, Obv, OnNeck, OpeningMarubozu, OpeningRange, OvernightGap, OvernightIntradayReturn, ParkinsonVolatility, Pgo, PiercingDarkCloud, PivotReversal, PlusDi, PlusDm, ProjectionBands, ProjectionOscillator, Psar, Pvi, Qstick, RectangleRange, RickshawMan, RisingThreeMethods, RogersSatchellVolatility, RollingVwap, Rvi, Rwi, SarExt, SeasonalZScore, SeparatingLines, SessionHighLow, SessionRange, SessionVwap, Shark, ShootingStar, ShortLine, Smi, SmoothedHeikinAshi, SpinningTop, StalledPattern, StarcBands, StickSandwich, Stochastic, StochasticCci, SuperTrend, Takuri, TasukiGap, TdCamouflage, TdClop, TdClopwin, TdCombo, TdCountdown, TdDWave, TdDeMarker, TdDifferential, TdLines, TdMovingAverage, TdOpen, TdPressure, TdPropulsion, TdRangeProjection, TdRei, TdRiskLevel, TdSequential, TdSetup, TdTrap, ThreeDrives, ThreeInside, ThreeLineBreak, ThreeLineStrike, ThreeOutside, ThreeSoldiersOrCrows, ThreeStarsInSouth, Thrusting, TimeBasedStop, TimeOfDayReturnProfile, TowerTopBottom, TpoProfile, TradeVolumeIndex, Triangle, TripleTopBottom, Tristar, TrueRange, Tsv, TtmSqueeze, TtmTrend, TurnOfMonth, Tweezer, TwiggsMoneyFlow, TwoCrows, TypicalPrice, UltimateOscillator, UniqueThreeRiver, UpsideGapThreeMethods, UpsideGapTwoCrows, ValueArea, VolatilityCone, VolatilityRatio, VoltyStop, VolumeByTimeProfile, VolumeOscillator, VolumePriceTrend, VolumeProfile, VolumeRsi, VolumeWeightedMacd, VolumeWeightedSr, Vortex, Vwap, VwapStdDevBands, Vwma, Vzo, WaveTrend, Wedge, WeightedClose, WickRatio, Wad, WilliamsFractals, WilliamsR, WoodiePivots, YangZhangVolatility, YoyoExit, ZigZag};
|
use wickra_core::{AbandonedBaby, Abcd, AccelerationBands, AcceleratorOscillator, AdOscillator, AdaptiveCci, Adl, AdvanceBlock, Adx, Adxr, Alligator, AnchoredVwap, AndrewsPitchfork, Aroon, AroonOscillator, Atr, AtrBands, AtrRatchet, AtrTrailingStop, AutoFib, AverageDailyRange, AwesomeOscillator, AwesomeOscillatorHistogram, BalanceOfPower, Bat, BatchExt, BeltHold, BetterVolume, BodySizePct, Breakaway, Butterfly, Camarilla, Candle, CandleVolume, Cci, CentralPivotRange, ChaikinMoneyFlow, ChaikinOscillator, ChaikinVolatility, ChandeKrollStop, ChandelierExit, ChoppinessIndex, ClassicPivots, CloseVsOpen, ClosingMarubozu, CompositeProfile, ConcealingBabySwallow, Counterattack, Crab, CupAndHandle, Cypher, DayOfWeekProfile, DemandIndex, DemarkPivots, Doji, DojiStar, Donchian, DonchianStop, DoubleTopBottom, DownsideGapThreeMethods, DragonflyDoji, DumplingTop, Dx, EaseOfMovement, ElderRay, ElderSafeZone, Engulfing, Equivolume, EveningDojiStar, Evwma, FallingThreeMethods, FibArcs, FibChannel, FibConfluence, FibExtension, FibFan, FibProjection, FibRetracement, FibTimeZones, FibonacciPivots, FlagPennant, ForceIndex, FractalChaosBands, FryPanBottom, GapSideBySideWhite, GarmanKlassVolatility, Gartley, GatorOscillator, GoldenPocket, GravestoneDoji, Hammer, HangingMan, Harami, HaramiCross, HeadAndShoulders, HeikinAshi, HeikinAshiOscillator, HiLoActivator, HighLowRange, HighLowVolumeNodes, HighWave, Hikkake, HikkakeModified, HomingPigeon, HurstChannel, Ichimoku, IdenticalThreeCrows, InNeck, Indicator, Inertia, InitialBalance, IntradayIntensity, IntradayMomentumIndex, IntradayVolatilityProfile, InvertedHammer, KaseDevStop, KasePermissionStochastic, Keltner, Kicking, KickingByLength, Kvo, LadderBottom, LongLeggedDoji, LongLine, MarketFacilitationIndex, Marubozu, MassIndex, MatHold, MatchingLow, AvgPrice, MedianPrice, Mfi, MidPrice, MinusDi, MinusDm, ModifiedMaStop, MorningDojiStar, MorningEveningStar, MurreyMathLines, NakedPoc, Natr, NewPriceLines, Nrtr, Nvi, Obv, OnNeck, OpeningMarubozu, OpeningRange, OvernightGap, OvernightIntradayReturn, ParkinsonVolatility, Pgo, PiercingDarkCloud, PivotReversal, PlusDi, PlusDm, ProfileShape, ProjectionBands, ProjectionOscillator, Psar, Pvi, Qstick, RectangleRange, RickshawMan, RisingThreeMethods, RogersSatchellVolatility, RollingVwap, Rvi, Rwi, SarExt, SeasonalZScore, SeparatingLines, SessionHighLow, SessionRange, SessionVwap, Shark, ShootingStar, ShortLine, SinglePrints, Smi, SmoothedHeikinAshi, SpinningTop, StalledPattern, StarcBands, StickSandwich, Stochastic, StochasticCci, SuperTrend, Takuri, TasukiGap, TdCamouflage, TdClop, TdClopwin, TdCombo, TdCountdown, TdDWave, TdDeMarker, TdDifferential, TdLines, TdMovingAverage, TdOpen, TdPressure, TdPropulsion, TdRangeProjection, TdRei, TdRiskLevel, TdSequential, TdSetup, TdTrap, ThreeDrives, ThreeInside, ThreeLineBreak, ThreeLineStrike, ThreeOutside, ThreeSoldiersOrCrows, ThreeStarsInSouth, Thrusting, TimeBasedStop, TimeOfDayReturnProfile, TowerTopBottom, TpoProfile, TradeVolumeIndex, Triangle, TripleTopBottom, Tristar, TrueRange, Tsv, TtmSqueeze, TtmTrend, TurnOfMonth, Tweezer, TwiggsMoneyFlow, TwoCrows, TypicalPrice, UltimateOscillator, UniqueThreeRiver, UpsideGapThreeMethods, UpsideGapTwoCrows, ValueArea, VolatilityCone, VolatilityRatio, VoltyStop, VolumeByTimeProfile, VolumeOscillator, VolumePriceTrend, VolumeProfile, VolumeRsi, VolumeWeightedMacd, VolumeWeightedSr, Vortex, Vwap, VwapStdDevBands, Vwma, Vzo, WaveTrend, Wedge, WeightedClose, WickRatio, Wad, WilliamsFractals, WilliamsR, WoodiePivots, YangZhangVolatility, YoyoExit, ZigZag};
|
||||||
|
|
||||||
/// Convert a flat `f64` stream into a `Vec<Candle>` by chunking it into
|
/// Convert a flat `f64` stream into a `Vec<Candle>` by chunking it into
|
||||||
/// `[open, high, low, close, volume]` groups. Tuples that fail OHLCV
|
/// `[open, high, low, close, volume]` groups. Tuples that fail OHLCV
|
||||||
@@ -188,6 +188,11 @@ fuzz_target!(|data: Vec<f64>| {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// --- Market Profile (multi-output) ---
|
// --- Market Profile (multi-output) ---
|
||||||
|
drive(|| CompositeProfile::new(20, 24, 0.7).unwrap(), &candles);
|
||||||
|
drive(|| HighLowVolumeNodes::new(20, 24).unwrap(), &candles);
|
||||||
|
drive(|| NakedPoc::new(20, 24).unwrap(), &candles);
|
||||||
|
drive(|| SinglePrints::new(20, 24).unwrap(), &candles);
|
||||||
|
drive(|| ProfileShape::new(20, 24).unwrap(), &candles);
|
||||||
drive(|| ValueArea::new(20, 50, 0.70).unwrap(), &candles);
|
drive(|| ValueArea::new(20, 50, 0.70).unwrap(), &candles);
|
||||||
drive(|| VolumeProfile::new(20, 50).unwrap(), &candles);
|
drive(|| VolumeProfile::new(20, 50).unwrap(), &candles);
|
||||||
drive(|| TpoProfile::new(20, 50).unwrap(), &candles);
|
drive(|| TpoProfile::new(20, 50).unwrap(), &candles);
|
||||||
|
|||||||
Reference in New Issue
Block a user