feat: add Alt-Chart Bars (Renko, Kagi, Point & Figure) via a BarBuilder trait (#146)

Introduces a BarBuilder trait for price-driven chart constructors that emit a variable number of bars per candle (deliberately not Indicator). Adds Renko (box-size bricks, 2-box reversal), Kagi (reversal-amount segments) and Point & Figure (box-size X/O columns, N-box reversal) in a new Alt-Chart Bars family, with custom Python/Node/WASM bindings, a dedicated fuzz target, tests and docs. Indicator count 292 -> 295.
This commit is contained in:
kingchenc
2026-06-02 21:56:00 +02:00
committed by GitHub
parent f37eedd44e
commit d4b3f9dbd1
20 changed files with 1692 additions and 43 deletions
+12 -3
View File
@@ -8,16 +8,25 @@ const test = require('node:test');
const assert = require('node:assert/strict');
const wickra = require('..');
// Bar builders (Renko / Kagi / Point & Figure) implement the `BarBuilder`
// contract, not `Indicator`: they emit a variable number of completed bars per
// candle and have no fixed warmup or ready state. They expose update/batch/reset
// but intentionally not isReady/warmupPeriod, so they are excluded from the
// Indicator completeness contract below (their interface is covered by the
// dedicated bar-builder tests).
const BAR_BUILDERS = new Set(['RenkoBars', 'KagiBars', 'PointAndFigureBars']);
// An "indicator class" is an exported constructor whose prototype carries the
// streaming `update` method. This excludes `version` (a plain function) and any
// non-indicator export.
// streaming `update` method. This excludes `version` (a plain function), the bar
// builders, and any non-indicator export.
function indicatorClasses() {
return Object.keys(wickra).filter((name) => {
const value = wickra[name];
return (
typeof value === 'function' &&
value.prototype &&
typeof value.prototype.update === 'function'
typeof value.prototype.update === 'function' &&
!BAR_BUILDERS.has(name)
);
});
}
@@ -1272,3 +1272,37 @@ test('TpoProfile counts time at price, volume-agnostic', () => {
assert.ok(Math.abs(out.priceHigh - 14) < 1e-9);
assert.deepEqual(out.counts, [1, 2, 2, 1]);
});
test('RenkoBars prints aligned bricks and reverses on two boxes', () => {
const r = new wickra.RenkoBars(1.0);
assert.deepEqual(r.update(10), []); // seed
const up = r.update(13);
assert.equal(up.length, 3);
assert.ok(Math.abs(up[0].open - 10) < 1e-9 && Math.abs(up[0].close - 11) < 1e-9);
assert.ok(up.every((b) => b.direction === 1));
const down = r.update(10);
assert.equal(down.length, 2);
assert.ok(down.every((b) => b.direction === -1));
});
test('KagiBars closes a segment on a reversal', () => {
const k = new wickra.KagiBars(2.0);
k.update(10);
k.update(11);
k.update(15);
const seg = k.update(12);
assert.equal(seg.length, 1);
assert.equal(seg[0].direction, 1);
assert.ok(Math.abs(seg[0].start - 10) < 1e-9 && Math.abs(seg[0].end - 15) < 1e-9);
});
test('PointAndFigureBars closes a column on a 3-box reversal', () => {
const pnf = new wickra.PointAndFigureBars(1.0, 3);
pnf.update(10);
pnf.update(13);
pnf.update(15);
const col = pnf.update(12);
assert.equal(col.length, 1);
assert.equal(col[0].direction, 1);
assert.ok(Math.abs(col[0].high - 15) < 1e-9 && Math.abs(col[0].low - 10) < 1e-9);
});
+40
View File
@@ -310,6 +310,21 @@ export interface LiquidationFeaturesValue {
total: number
imbalance: number
}
export interface RenkoBrickValue {
open: number
close: number
direction: number
}
export interface KagiSegmentValue {
start: number
end: number
direction: number
}
export interface PnfColumnValue {
direction: number
high: number
low: number
}
export type SmaNode = SMA
export declare class SMA {
constructor(period: number)
@@ -3022,6 +3037,31 @@ export declare class InformationRatio {
isReady(): boolean
warmupPeriod(): number
}
export type RenkoBarsNode = RenkoBars
export declare class RenkoBars {
constructor(boxSize: number)
update(close: number): Array<RenkoBrickValue>
batch(close: Array<number>): Array<RenkoBrickValue>
boxSize(): number
reset(): void
}
export type KagiBarsNode = KagiBars
export declare class KagiBars {
constructor(reversal: number)
update(close: number): Array<KagiSegmentValue>
batch(close: Array<number>): Array<KagiSegmentValue>
reversal(): number
reset(): void
}
export type PointAndFigureBarsNode = PointAndFigureBars
export declare class PointAndFigureBars {
constructor(boxSize: number, reversal: number)
update(close: number): Array<PnfColumnValue>
batch(close: Array<number>): Array<PnfColumnValue>
boxSize(): number
reversal(): number
reset(): void
}
export type AlphaNode = Alpha
export declare class Alpha {
constructor(period: number, riskFree: number)
+4 -1
View File
@@ -310,7 +310,7 @@ if (!nativeBinding) {
throw new Error(`Failed to load native binding`)
}
const { version, SMA, EMA, WMA, RSI, DEMA, TEMA, HMA, ROC, TRIX, SMMA, TRIMA, ZLEMA, MOM, CMO, DPO, StdDev, UlcerIndex, VerticalHorizontalFilter, ZScore, McGinleyDynamic, FRAMA, SuperSmoother, FisherTransform, Decycler, CenterOfGravity, CyberneticCycle, InstantaneousTrendline, EhlersStochastic, RVIVolatility, Variance, CoefficientOfVariation, Skewness, Kurtosis, StandardError, DetrendedStdDev, RSquared, MedianAbsoluteDeviation, Autocorrelation, HurstExponent, PearsonCorrelation, Beta, PairwiseBeta, SpearmanCorrelation, PairSpreadZScore, LeadLagCrossCorrelation, Cointegration, RelativeStrengthAB, MACD, BollingerBands, ATR, Stochastic, OBV, ADX, ADXR, CCI, WilliamsR, MFI, PSAR, Keltner, Donchian, VWAP, RollingVWAP, AwesomeOscillator, Aroon, Inertia, ConnorsRSI, LaguerreRSI, SMI, KST, PGO, RVI, AwesomeOscillatorHistogram, STC, ElderImpulse, ZeroLagMACD, CFO, APO, KAMA, EVWMA, Alligator, JMA, VIDYA, ALMA, T3, TSI, PMO, TII, ADL, VolumePriceTrend, ChaikinMoneyFlow, ChaikinOscillator, ForceIndex, NVI, PVI, VolumeOscillator, KVO, WilliamsAD, AnchoredRSI, AnchoredVWAP, DemandIndex, TSV, VZO, MarketFacilitationIndex, EaseOfMovement, SuperTrend, ChandelierExit, ChandeKrollStop, AtrTrailingStop, HiLoActivator, VoltyStop, YoyoExit, DonchianStop, PercentageTrailingStop, StepTrailingStop, RenkoTrailingStop, TypicalPrice, MedianPrice, WeightedClose, LinearRegression, LinRegSlope, AcceleratorOscillator, BalanceOfPower, ChoppinessIndex, TrueRange, ChaikinVolatility, YangZhangVolatility, RogersSatchellVolatility, GarmanKlassVolatility, ParkinsonVolatility, LinRegAngle, BollingerBandwidth, PercentB, NATR, HistoricalVolatility, AroonOscillator, WaveTrend, RWI, Vortex, MassIndex, StochRSI, UltimateOscillator, PPO, Coppock, VWMA, MaEnvelope, AccelerationBands, StarcBands, AtrBands, HurstChannel, LinRegChannel, StandardErrorBands, DoubleBollinger, TtmSqueeze, FractalChaosBands, VwapStdDevBands, ClassicPivots, FibonacciPivots, Camarilla, WoodiePivots, DemarkPivots, WilliamsFractals, ZigZag, TDSetup, TDSequential, TDDeMarker, TDREI, TDPressure, TDCombo, TDCountdown, TDLines, TDRangeProjection, TDDifferential, TDOpen, TDRiskLevel, InverseFisherTransform, DecyclerOscillator, RoofingFilter, EmpiricalModeDecomposition, HilbertDominantCycle, AdaptiveCycle, SineWave, MAMA, FAMA, Ichimoku, HeikinAshi, ValueArea, VolumeProfile, TpoProfile, InitialBalance, OpeningRange, Doji, Hammer, InvertedHammer, HangingMan, ShootingStar, Engulfing, Harami, MorningEveningStar, ThreeSoldiersOrCrows, PiercingDarkCloud, Marubozu, Tweezer, SpinningTop, ThreeInside, ThreeOutside, TwoCrows, UpsideGapTwoCrows, IdenticalThreeCrows, ThreeLineStrike, ThreeStarsInSouth, AbandonedBaby, AdvanceBlock, BeltHold, Breakaway, Counterattack, DojiStar, DragonflyDoji, GravestoneDoji, LongLeggedDoji, RickshawMan, EveningDojiStar, MorningDojiStar, GapSideBySideWhite, HighWave, Hikkake, HikkakeModified, HomingPigeon, OnNeck, InNeck, Thrusting, SeparatingLines, Kicking, KickingByLength, LadderBottom, MatHold, MatchingLow, LongLine, ShortLine, RisingThreeMethods, FallingThreeMethods, UpsideGapThreeMethods, DownsideGapThreeMethods, StalledPattern, StickSandwich, Takuri, ClosingMarubozu, OpeningMarubozu, TasukiGap, UniqueThreeRiver, ConcealingBabySwallow, OrderBookImbalanceTop1, OrderBookImbalanceFull, Microprice, QuotedSpread, DepthSlope, OrderBookImbalanceTopN, SignedVolume, CumulativeVolumeDelta, TradeImbalance, EffectiveSpread, RealizedSpread, KylesLambda, Footprint, FundingRate, FundingRateMean, FundingRateZScore, FundingBasis, OpenInterestDelta, OIPriceDivergence, OIWeighted, LongShortRatio, TakerBuySellRatio, LiquidationFeatures, TermStructureBasis, CalendarSpread, SharpeRatio, SortinoRatio, CalmarRatio, OmegaRatio, MaxDrawdown, AverageDrawdown, DrawdownDuration, PainIndex, ValueAtRisk, ConditionalValueAtRisk, ProfitFactor, GainLossRatio, RecoveryFactor, KellyCriterion, TreynorRatio, InformationRatio, Alpha } = nativeBinding
const { version, SMA, EMA, WMA, RSI, DEMA, TEMA, HMA, ROC, TRIX, SMMA, TRIMA, ZLEMA, MOM, CMO, DPO, StdDev, UlcerIndex, VerticalHorizontalFilter, ZScore, McGinleyDynamic, FRAMA, SuperSmoother, FisherTransform, Decycler, CenterOfGravity, CyberneticCycle, InstantaneousTrendline, EhlersStochastic, RVIVolatility, Variance, CoefficientOfVariation, Skewness, Kurtosis, StandardError, DetrendedStdDev, RSquared, MedianAbsoluteDeviation, Autocorrelation, HurstExponent, PearsonCorrelation, Beta, PairwiseBeta, SpearmanCorrelation, PairSpreadZScore, LeadLagCrossCorrelation, Cointegration, RelativeStrengthAB, MACD, BollingerBands, ATR, Stochastic, OBV, ADX, ADXR, CCI, WilliamsR, MFI, PSAR, Keltner, Donchian, VWAP, RollingVWAP, AwesomeOscillator, Aroon, Inertia, ConnorsRSI, LaguerreRSI, SMI, KST, PGO, RVI, AwesomeOscillatorHistogram, STC, ElderImpulse, ZeroLagMACD, CFO, APO, KAMA, EVWMA, Alligator, JMA, VIDYA, ALMA, T3, TSI, PMO, TII, ADL, VolumePriceTrend, ChaikinMoneyFlow, ChaikinOscillator, ForceIndex, NVI, PVI, VolumeOscillator, KVO, WilliamsAD, AnchoredRSI, AnchoredVWAP, DemandIndex, TSV, VZO, MarketFacilitationIndex, EaseOfMovement, SuperTrend, ChandelierExit, ChandeKrollStop, AtrTrailingStop, HiLoActivator, VoltyStop, YoyoExit, DonchianStop, PercentageTrailingStop, StepTrailingStop, RenkoTrailingStop, TypicalPrice, MedianPrice, WeightedClose, LinearRegression, LinRegSlope, AcceleratorOscillator, BalanceOfPower, ChoppinessIndex, TrueRange, ChaikinVolatility, YangZhangVolatility, RogersSatchellVolatility, GarmanKlassVolatility, ParkinsonVolatility, LinRegAngle, BollingerBandwidth, PercentB, NATR, HistoricalVolatility, AroonOscillator, WaveTrend, RWI, Vortex, MassIndex, StochRSI, UltimateOscillator, PPO, Coppock, VWMA, MaEnvelope, AccelerationBands, StarcBands, AtrBands, HurstChannel, LinRegChannel, StandardErrorBands, DoubleBollinger, TtmSqueeze, FractalChaosBands, VwapStdDevBands, ClassicPivots, FibonacciPivots, Camarilla, WoodiePivots, DemarkPivots, WilliamsFractals, ZigZag, TDSetup, TDSequential, TDDeMarker, TDREI, TDPressure, TDCombo, TDCountdown, TDLines, TDRangeProjection, TDDifferential, TDOpen, TDRiskLevel, InverseFisherTransform, DecyclerOscillator, RoofingFilter, EmpiricalModeDecomposition, HilbertDominantCycle, AdaptiveCycle, SineWave, MAMA, FAMA, Ichimoku, HeikinAshi, ValueArea, VolumeProfile, TpoProfile, InitialBalance, OpeningRange, Doji, Hammer, InvertedHammer, HangingMan, ShootingStar, Engulfing, Harami, MorningEveningStar, ThreeSoldiersOrCrows, PiercingDarkCloud, Marubozu, Tweezer, SpinningTop, ThreeInside, ThreeOutside, TwoCrows, UpsideGapTwoCrows, IdenticalThreeCrows, ThreeLineStrike, ThreeStarsInSouth, AbandonedBaby, AdvanceBlock, BeltHold, Breakaway, Counterattack, DojiStar, DragonflyDoji, GravestoneDoji, LongLeggedDoji, RickshawMan, EveningDojiStar, MorningDojiStar, GapSideBySideWhite, HighWave, Hikkake, HikkakeModified, HomingPigeon, OnNeck, InNeck, Thrusting, SeparatingLines, Kicking, KickingByLength, LadderBottom, MatHold, MatchingLow, LongLine, ShortLine, RisingThreeMethods, FallingThreeMethods, UpsideGapThreeMethods, DownsideGapThreeMethods, StalledPattern, StickSandwich, Takuri, ClosingMarubozu, OpeningMarubozu, TasukiGap, UniqueThreeRiver, ConcealingBabySwallow, OrderBookImbalanceTop1, OrderBookImbalanceFull, Microprice, QuotedSpread, DepthSlope, OrderBookImbalanceTopN, SignedVolume, CumulativeVolumeDelta, TradeImbalance, EffectiveSpread, RealizedSpread, KylesLambda, Footprint, FundingRate, FundingRateMean, FundingRateZScore, FundingBasis, OpenInterestDelta, OIPriceDivergence, OIWeighted, LongShortRatio, TakerBuySellRatio, LiquidationFeatures, TermStructureBasis, CalendarSpread, SharpeRatio, SortinoRatio, CalmarRatio, OmegaRatio, MaxDrawdown, AverageDrawdown, DrawdownDuration, PainIndex, ValueAtRisk, ConditionalValueAtRisk, ProfitFactor, GainLossRatio, RecoveryFactor, KellyCriterion, TreynorRatio, InformationRatio, RenkoBars, KagiBars, PointAndFigureBars, Alpha } = nativeBinding
module.exports.version = version
module.exports.SMA = SMA
@@ -604,4 +604,7 @@ module.exports.RecoveryFactor = RecoveryFactor
module.exports.KellyCriterion = KellyCriterion
module.exports.TreynorRatio = TreynorRatio
module.exports.InformationRatio = InformationRatio
module.exports.RenkoBars = RenkoBars
module.exports.KagiBars = KagiBars
module.exports.PointAndFigureBars = PointAndFigureBars
module.exports.Alpha = Alpha
+185 -1
View File
@@ -16,7 +16,7 @@ use napi::Error as NapiError;
use napi::Status;
use napi_derive::napi;
use wickra_core as wc;
use wickra_core::{BatchExt, Indicator};
use wickra_core::{BarBuilder, BatchExt, Indicator};
fn map_err(e: wc::Error) -> NapiError {
NapiError::new(Status::InvalidArg, e.to_string())
@@ -11060,6 +11060,190 @@ impl InformationRatioNode {
}
}
// ============================== Alt-Chart Bars ==============================
//
// Bar builders consume close prices and emit a variable number of completed bars
// per input. `update(close)` returns the bars finished on that close; `batch`
// returns all completed bars concatenated.
#[napi(object)]
pub struct RenkoBrickValue {
pub open: f64,
pub close: f64,
pub direction: i32,
}
#[napi(js_name = "RenkoBars")]
pub struct RenkoBarsNode {
inner: wc::RenkoBars,
}
#[napi]
impl RenkoBarsNode {
#[napi(constructor)]
pub fn new(box_size: f64) -> napi::Result<Self> {
Ok(Self {
inner: wc::RenkoBars::new(box_size).map_err(map_err)?,
})
}
#[napi]
pub fn update(&mut self, close: f64) -> napi::Result<Vec<RenkoBrickValue>> {
let candle = wc::Candle::new(close, close, close, close, 1.0, 0).map_err(map_err)?;
Ok(self
.inner
.update(candle)
.into_iter()
.map(|b| RenkoBrickValue {
open: b.open,
close: b.close,
direction: i32::from(b.direction),
})
.collect())
}
#[napi]
pub fn batch(&mut self, close: Vec<f64>) -> napi::Result<Vec<RenkoBrickValue>> {
let mut out = Vec::new();
for price in close {
let candle = wc::Candle::new(price, price, price, price, 1.0, 0).map_err(map_err)?;
for b in self.inner.update(candle) {
out.push(RenkoBrickValue {
open: b.open,
close: b.close,
direction: i32::from(b.direction),
});
}
}
Ok(out)
}
#[napi(js_name = "boxSize")]
pub fn box_size(&self) -> f64 {
self.inner.box_size()
}
#[napi]
pub fn reset(&mut self) {
self.inner.reset();
}
}
#[napi(object)]
pub struct KagiSegmentValue {
pub start: f64,
pub end: f64,
pub direction: i32,
}
#[napi(js_name = "KagiBars")]
pub struct KagiBarsNode {
inner: wc::KagiBars,
}
#[napi]
impl KagiBarsNode {
#[napi(constructor)]
pub fn new(reversal: f64) -> napi::Result<Self> {
Ok(Self {
inner: wc::KagiBars::new(reversal).map_err(map_err)?,
})
}
#[napi]
pub fn update(&mut self, close: f64) -> napi::Result<Vec<KagiSegmentValue>> {
let candle = wc::Candle::new(close, close, close, close, 1.0, 0).map_err(map_err)?;
Ok(self
.inner
.update(candle)
.into_iter()
.map(|b| KagiSegmentValue {
start: b.start,
end: b.end,
direction: i32::from(b.direction),
})
.collect())
}
#[napi]
pub fn batch(&mut self, close: Vec<f64>) -> napi::Result<Vec<KagiSegmentValue>> {
let mut out = Vec::new();
for price in close {
let candle = wc::Candle::new(price, price, price, price, 1.0, 0).map_err(map_err)?;
for b in self.inner.update(candle) {
out.push(KagiSegmentValue {
start: b.start,
end: b.end,
direction: i32::from(b.direction),
});
}
}
Ok(out)
}
#[napi]
pub fn reversal(&self) -> f64 {
self.inner.reversal()
}
#[napi]
pub fn reset(&mut self) {
self.inner.reset();
}
}
#[napi(object)]
pub struct PnfColumnValue {
pub direction: i32,
pub high: f64,
pub low: f64,
}
#[napi(js_name = "PointAndFigureBars")]
pub struct PointAndFigureBarsNode {
inner: wc::PointAndFigureBars,
}
#[napi]
impl PointAndFigureBarsNode {
#[napi(constructor)]
pub fn new(box_size: f64, reversal: u32) -> napi::Result<Self> {
Ok(Self {
inner: wc::PointAndFigureBars::new(box_size, reversal as usize).map_err(map_err)?,
})
}
#[napi]
pub fn update(&mut self, close: f64) -> napi::Result<Vec<PnfColumnValue>> {
let candle = wc::Candle::new(close, close, close, close, 1.0, 0).map_err(map_err)?;
Ok(self
.inner
.update(candle)
.into_iter()
.map(|col| PnfColumnValue {
direction: i32::from(col.direction),
high: col.high,
low: col.low,
})
.collect())
}
#[napi]
pub fn batch(&mut self, close: Vec<f64>) -> napi::Result<Vec<PnfColumnValue>> {
let mut out = Vec::new();
for price in close {
let candle = wc::Candle::new(price, price, price, price, 1.0, 0).map_err(map_err)?;
for col in self.inner.update(candle) {
out.push(PnfColumnValue {
direction: i32::from(col.direction),
high: col.high,
low: col.low,
});
}
}
Ok(out)
}
#[napi(js_name = "boxSize")]
pub fn box_size(&self) -> f64 {
self.inner.box_size()
}
#[napi]
pub fn reversal(&self) -> u32 {
self.inner.reversal() as u32
}
#[napi]
pub fn reset(&mut self) {
self.inner.reset();
}
}
#[napi(js_name = "Alpha")]
pub struct AlphaNode {
inner: wc::Alpha,
@@ -227,6 +227,10 @@ from ._wickra import (
TpoProfile,
InitialBalance,
OpeningRange,
# Alt-Chart Bars
RenkoBars,
KagiBars,
PointAndFigureBars,
# Candlestick patterns
Doji,
Hammer,
@@ -542,6 +546,10 @@ __all__ = [
"TpoProfile",
"InitialBalance",
"OpeningRange",
# Alt-Chart Bars
"RenkoBars",
"KagiBars",
"PointAndFigureBars",
# Candlestick patterns
"Doji",
"Hammer",
+203 -1
View File
@@ -17,7 +17,7 @@ use pyo3::exceptions::{PyTypeError, PyValueError};
use pyo3::prelude::*;
use pyo3::types::PyDict;
use wickra_core as wc;
use wickra_core::{BatchExt, Indicator};
use wickra_core::{BarBuilder, BatchExt, Indicator};
fn map_err(e: wc::Error) -> PyErr {
match e {
@@ -14210,6 +14210,205 @@ impl PyAlpha {
}
}
// ============================== Alt-Chart Bars ==============================
//
// Bar builders consume close prices and emit a variable number of completed bars
// per input. `update(close)` returns the bars finished on that close; `batch`
// returns a `(k, 3)` array of all completed bars concatenated.
#[pyclass(name = "RenkoBars", module = "wickra._wickra", skip_from_py_object)]
#[derive(Clone)]
struct PyRenkoBars {
inner: wc::RenkoBars,
}
#[pymethods]
impl PyRenkoBars {
#[new]
fn new(box_size: f64) -> PyResult<Self> {
Ok(Self {
inner: wc::RenkoBars::new(box_size).map_err(map_err)?,
})
}
/// Feed one close; returns bricks completed on it as `(open, close, direction)`.
fn update(&mut self, close: f64) -> PyResult<Vec<(f64, f64, i64)>> {
let candle = wc::Candle::new(close, close, close, close, 1.0, 0).map_err(map_err)?;
Ok(self
.inner
.update(candle)
.into_iter()
.map(|b| (b.open, b.close, i64::from(b.direction)))
.collect())
}
/// Batch over a close column. Returns shape `(k, 3)` of `[open, close, direction]`.
fn batch<'py>(
&mut self,
py: Python<'py>,
close: PyReadonlyArray1<'py, f64>,
) -> PyResult<Bound<'py, PyArray2<f64>>> {
let prices = close
.as_slice()
.map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?;
let mut rows: Vec<f64> = Vec::new();
let mut k = 0usize;
for &price in prices {
let candle = wc::Candle::new(price, price, price, price, 1.0, 0).map_err(map_err)?;
for b in self.inner.update(candle) {
rows.push(b.open);
rows.push(b.close);
rows.push(f64::from(b.direction));
k += 1;
}
}
Ok(numpy::ndarray::Array2::from_shape_vec((k, 3), rows)
.expect("shape consistent")
.into_pyarray(py))
}
#[getter]
fn box_size(&self) -> f64 {
self.inner.box_size()
}
fn reset(&mut self) {
self.inner.reset();
}
fn __repr__(&self) -> String {
format!("RenkoBars(box_size={})", self.inner.box_size())
}
}
#[pyclass(name = "KagiBars", module = "wickra._wickra", skip_from_py_object)]
#[derive(Clone)]
struct PyKagiBars {
inner: wc::KagiBars,
}
#[pymethods]
impl PyKagiBars {
#[new]
fn new(reversal: f64) -> PyResult<Self> {
Ok(Self {
inner: wc::KagiBars::new(reversal).map_err(map_err)?,
})
}
/// Feed one close; returns completed segments as `(start, end, direction)`.
fn update(&mut self, close: f64) -> PyResult<Vec<(f64, f64, i64)>> {
let candle = wc::Candle::new(close, close, close, close, 1.0, 0).map_err(map_err)?;
Ok(self
.inner
.update(candle)
.into_iter()
.map(|b| (b.start, b.end, i64::from(b.direction)))
.collect())
}
/// Batch over a close column. Returns shape `(k, 3)` of `[start, end, direction]`.
fn batch<'py>(
&mut self,
py: Python<'py>,
close: PyReadonlyArray1<'py, f64>,
) -> PyResult<Bound<'py, PyArray2<f64>>> {
let prices = close
.as_slice()
.map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?;
let mut rows: Vec<f64> = Vec::new();
let mut k = 0usize;
for &price in prices {
let candle = wc::Candle::new(price, price, price, price, 1.0, 0).map_err(map_err)?;
for b in self.inner.update(candle) {
rows.push(b.start);
rows.push(b.end);
rows.push(f64::from(b.direction));
k += 1;
}
}
Ok(numpy::ndarray::Array2::from_shape_vec((k, 3), rows)
.expect("shape consistent")
.into_pyarray(py))
}
#[getter]
fn reversal(&self) -> f64 {
self.inner.reversal()
}
fn reset(&mut self) {
self.inner.reset();
}
fn __repr__(&self) -> String {
format!("KagiBars(reversal={})", self.inner.reversal())
}
}
#[pyclass(
name = "PointAndFigureBars",
module = "wickra._wickra",
skip_from_py_object
)]
#[derive(Clone)]
struct PyPointAndFigureBars {
inner: wc::PointAndFigureBars,
}
#[pymethods]
impl PyPointAndFigureBars {
#[new]
#[pyo3(signature = (box_size, reversal=3))]
fn new(box_size: f64, reversal: usize) -> PyResult<Self> {
Ok(Self {
inner: wc::PointAndFigureBars::new(box_size, reversal).map_err(map_err)?,
})
}
/// Feed one close; returns completed columns as `(direction, high, low)`.
fn update(&mut self, close: f64) -> PyResult<Vec<(i64, f64, f64)>> {
let candle = wc::Candle::new(close, close, close, close, 1.0, 0).map_err(map_err)?;
Ok(self
.inner
.update(candle)
.into_iter()
.map(|c| (i64::from(c.direction), c.high, c.low))
.collect())
}
/// Batch over a close column. Returns shape `(k, 3)` of `[direction, high, low]`.
fn batch<'py>(
&mut self,
py: Python<'py>,
close: PyReadonlyArray1<'py, f64>,
) -> PyResult<Bound<'py, PyArray2<f64>>> {
let prices = close
.as_slice()
.map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?;
let mut rows: Vec<f64> = Vec::new();
let mut k = 0usize;
for &price in prices {
let candle = wc::Candle::new(price, price, price, price, 1.0, 0).map_err(map_err)?;
for col in self.inner.update(candle) {
rows.push(f64::from(col.direction));
rows.push(col.high);
rows.push(col.low);
k += 1;
}
}
Ok(numpy::ndarray::Array2::from_shape_vec((k, 3), rows)
.expect("shape consistent")
.into_pyarray(py))
}
#[getter]
fn box_size(&self) -> f64 {
self.inner.box_size()
}
#[getter]
fn reversal(&self) -> usize {
self.inner.reversal()
}
fn reset(&mut self) {
self.inner.reset();
}
fn __repr__(&self) -> String {
format!(
"PointAndFigureBars(box_size={}, reversal={})",
self.inner.box_size(),
self.inner.reversal()
)
}
}
// ============================== Module ==============================
#[pymodule]
@@ -14407,6 +14606,9 @@ fn _wickra(_py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> {
m.add_class::<PyValueArea>()?;
m.add_class::<PyVolumeProfile>()?;
m.add_class::<PyTpoProfile>()?;
m.add_class::<PyRenkoBars>()?;
m.add_class::<PyKagiBars>()?;
m.add_class::<PyPointAndFigureBars>()?;
m.add_class::<PyInitialBalance>()?;
m.add_class::<PyOpeningRange>()?;
// Candlestick patterns.
@@ -2649,3 +2649,44 @@ def test_basis_indicators_streaming_equals_batch():
[streamer.update(futures[i], mark[i]) for i in range(n)], dtype=np.float64
)
assert _eq_nan(batch, streamed)
# --- Alt-Chart Bars ------------------------------------------------------
def test_renko_bars_reference():
r = ta.RenkoBars(1.0)
assert r.update(10.0) == [] # seed
assert r.update(13.0) == [(10.0, 11.0, 1), (11.0, 12.0, 1), (12.0, 13.0, 1)]
assert r.update(10.0) == [(12.0, 11.0, -1), (11.0, 10.0, -1)] # 2-box reversal
def test_renko_bars_batch_shape():
r = ta.RenkoBars(1.0)
out = r.batch(np.array([10.0, 11.0, 12.0, 13.0]))
assert out.shape == (3, 3)
np.testing.assert_allclose(out[:, 2], [1.0, 1.0, 1.0])
def test_kagi_bars_reference():
k = ta.KagiBars(2.0)
assert k.update(10.0) == [] # seed
assert k.update(11.0) == [] # establishes up
assert k.update(15.0) == [] # extends
assert k.update(12.0) == [(10.0, 15.0, 1)] # reversal closes up segment
def test_point_and_figure_bars_reference():
pnf = ta.PointAndFigureBars(1.0, 3)
assert pnf.update(10.0) == [] # seed
assert pnf.update(13.0) == [] # starts X column
assert pnf.update(15.0) == [] # extends up
assert pnf.update(12.0) == [(1, 15.0, 10.0)] # 3-box reversal closes X column
def test_bar_builders_reset():
r = ta.RenkoBars(1.0)
r.update(10.0)
r.update(15.0)
r.reset()
assert r.update(50.0) == [] # re-seeds after reset
+156 -1
View File
@@ -12,7 +12,7 @@
use js_sys::{Array, Float64Array, Object, Reflect};
use wasm_bindgen::prelude::*;
use wickra_core as wc;
use wickra_core::{BatchExt, Indicator};
use wickra_core::{BarBuilder, BatchExt, Indicator};
fn map_err(e: wc::Error) -> JsError {
JsError::new(&e.to_string())
@@ -8517,6 +8517,161 @@ impl WasmInformationRatio {
}
}
// ============================== Alt-Chart Bars ==============================
//
// Bar builders consume close prices and emit a variable number of completed bars
// per input. `update(close)` returns a JS array of the bars finished on that
// close; `batch` returns all completed bars concatenated.
#[wasm_bindgen(js_name = RenkoBars)]
pub struct WasmRenkoBars {
inner: wc::RenkoBars,
}
#[wasm_bindgen(js_class = RenkoBars)]
impl WasmRenkoBars {
#[wasm_bindgen(constructor)]
pub fn new(box_size: f64) -> Result<WasmRenkoBars, JsError> {
Ok(Self {
inner: wc::RenkoBars::new(box_size).map_err(map_err)?,
})
}
/// Returns an array of `{ open, close, direction }` bricks completed on this close.
pub fn update(&mut self, close: f64) -> Result<Array, JsError> {
let candle = wc::Candle::new(close, close, close, close, 1.0, 0).map_err(map_err)?;
let arr = Array::new();
for b in self.inner.update(candle) {
let obj = Object::new();
Reflect::set(&obj, &"open".into(), &b.open.into()).ok();
Reflect::set(&obj, &"close".into(), &b.close.into()).ok();
Reflect::set(&obj, &"direction".into(), &f64::from(b.direction).into()).ok();
arr.push(&obj);
}
Ok(arr)
}
pub fn batch(&mut self, close: &[f64]) -> Result<Array, JsError> {
let arr = Array::new();
for &price in close {
let candle = wc::Candle::new(price, price, price, price, 1.0, 0).map_err(map_err)?;
for b in self.inner.update(candle) {
let obj = Object::new();
Reflect::set(&obj, &"open".into(), &b.open.into()).ok();
Reflect::set(&obj, &"close".into(), &b.close.into()).ok();
Reflect::set(&obj, &"direction".into(), &f64::from(b.direction).into()).ok();
arr.push(&obj);
}
}
Ok(arr)
}
#[wasm_bindgen(js_name = boxSize)]
pub fn box_size(&self) -> f64 {
self.inner.box_size()
}
pub fn reset(&mut self) {
self.inner.reset();
}
}
#[wasm_bindgen(js_name = KagiBars)]
pub struct WasmKagiBars {
inner: wc::KagiBars,
}
#[wasm_bindgen(js_class = KagiBars)]
impl WasmKagiBars {
#[wasm_bindgen(constructor)]
pub fn new(reversal: f64) -> Result<WasmKagiBars, JsError> {
Ok(Self {
inner: wc::KagiBars::new(reversal).map_err(map_err)?,
})
}
/// Returns an array of `{ start, end, direction }` segments completed on this close.
pub fn update(&mut self, close: f64) -> Result<Array, JsError> {
let candle = wc::Candle::new(close, close, close, close, 1.0, 0).map_err(map_err)?;
let arr = Array::new();
for b in self.inner.update(candle) {
let obj = Object::new();
Reflect::set(&obj, &"start".into(), &b.start.into()).ok();
Reflect::set(&obj, &"end".into(), &b.end.into()).ok();
Reflect::set(&obj, &"direction".into(), &f64::from(b.direction).into()).ok();
arr.push(&obj);
}
Ok(arr)
}
pub fn batch(&mut self, close: &[f64]) -> Result<Array, JsError> {
let arr = Array::new();
for &price in close {
let candle = wc::Candle::new(price, price, price, price, 1.0, 0).map_err(map_err)?;
for b in self.inner.update(candle) {
let obj = Object::new();
Reflect::set(&obj, &"start".into(), &b.start.into()).ok();
Reflect::set(&obj, &"end".into(), &b.end.into()).ok();
Reflect::set(&obj, &"direction".into(), &f64::from(b.direction).into()).ok();
arr.push(&obj);
}
}
Ok(arr)
}
pub fn reversal(&self) -> f64 {
self.inner.reversal()
}
pub fn reset(&mut self) {
self.inner.reset();
}
}
#[wasm_bindgen(js_name = PointAndFigureBars)]
pub struct WasmPointAndFigureBars {
inner: wc::PointAndFigureBars,
}
#[wasm_bindgen(js_class = PointAndFigureBars)]
impl WasmPointAndFigureBars {
#[wasm_bindgen(constructor)]
pub fn new(box_size: f64, reversal: usize) -> Result<WasmPointAndFigureBars, JsError> {
Ok(Self {
inner: wc::PointAndFigureBars::new(box_size, reversal).map_err(map_err)?,
})
}
/// Returns an array of `{ direction, high, low }` columns completed on this close.
pub fn update(&mut self, close: f64) -> Result<Array, JsError> {
let candle = wc::Candle::new(close, close, close, close, 1.0, 0).map_err(map_err)?;
let arr = Array::new();
for col in self.inner.update(candle) {
let obj = Object::new();
Reflect::set(&obj, &"direction".into(), &f64::from(col.direction).into()).ok();
Reflect::set(&obj, &"high".into(), &col.high.into()).ok();
Reflect::set(&obj, &"low".into(), &col.low.into()).ok();
arr.push(&obj);
}
Ok(arr)
}
pub fn batch(&mut self, close: &[f64]) -> Result<Array, JsError> {
let arr = Array::new();
for &price in close {
let candle = wc::Candle::new(price, price, price, price, 1.0, 0).map_err(map_err)?;
for col in self.inner.update(candle) {
let obj = Object::new();
Reflect::set(&obj, &"direction".into(), &f64::from(col.direction).into()).ok();
Reflect::set(&obj, &"high".into(), &col.high.into()).ok();
Reflect::set(&obj, &"low".into(), &col.low.into()).ok();
arr.push(&obj);
}
}
Ok(arr)
}
#[wasm_bindgen(js_name = boxSize)]
pub fn box_size(&self) -> f64 {
self.inner.box_size()
}
pub fn reversal(&self) -> usize {
self.inner.reversal()
}
pub fn reset(&mut self) {
self.inner.reset();
}
}
#[wasm_bindgen(js_name = Alpha)]
pub struct WasmAlpha {
inner: wc::Alpha,