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:
@@ -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);
|
||||
});
|
||||
|
||||
Vendored
+40
@@ -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)
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user