feat: TA-Lib candlestick patterns — separating/kicking/ladder/mat-hold (part 6 of 9) (#138)
This commit is contained in:
@@ -52,6 +52,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
- **On-Neck** — a long black candle then a white candle closing at its low (the neckline), a bearish continuation; -1 (`CDLONNECK`).
|
- **On-Neck** — a long black candle then a white candle closing at its low (the neckline), a bearish continuation; -1 (`CDLONNECK`).
|
||||||
- **In-Neck** — a long black candle then a white candle closing just into its body, a bearish continuation; -1 (`CDLINNECK`).
|
- **In-Neck** — a long black candle then a white candle closing just into its body, a bearish continuation; -1 (`CDLINNECK`).
|
||||||
- **Thrusting** — a long black candle then a white candle closing well into but below the midpoint of its body, a bearish continuation; -1 (`CDLTHRUSTING`).
|
- **Thrusting** — a long black candle then a white candle closing well into but below the midpoint of its body, a bearish continuation; -1 (`CDLTHRUSTING`).
|
||||||
|
- **Separating Lines** — opposite-coloured candles sharing the same open, the second an opening marubozu resuming the trend; bullish +1, bearish -1 (`CDLSEPARATINGLINES`).
|
||||||
|
- **Kicking** — two opposite-coloured marubozu separated by a gap; bullish +1, bearish -1 (`CDLKICKING`).
|
||||||
|
- **Kicking by Length** — a kicking pattern signalled by the colour of the longer marubozu; +1 / -1 (`CDLKICKINGBYLENGTH`).
|
||||||
|
- **Ladder Bottom** — three descending black candles, a fourth with an upper shadow, then a white candle gapping up, a bullish reversal; +1 (`CDLLADDERBOTTOM`).
|
||||||
|
- **Mat Hold** — a long white candle, a holding three-bar pullback, then a new-high white candle, a bullish continuation; +1 (`CDLMATHOLD`).
|
||||||
- **Derivatives family — funding & open interest (part 1).** A new family of
|
- **Derivatives family — funding & open interest (part 1).** A new family of
|
||||||
indicators that consume a perpetual / futures tick (`DerivativesTick`,
|
indicators that consume a perpetual / futures tick (`DerivativesTick`,
|
||||||
bundling funding rate, mark / index / futures price, open interest,
|
bundling funding rate, mark / index / futures price, open interest,
|
||||||
|
|||||||
@@ -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=269" 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=274" 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)
|
||||||
@@ -47,7 +47,7 @@ Full documentation lives at **[docs.wickra.org](https://docs.wickra.org)**:
|
|||||||
[Node](https://docs.wickra.org/Quickstart-Node),
|
[Node](https://docs.wickra.org/Quickstart-Node),
|
||||||
[WASM](https://docs.wickra.org/Quickstart-WASM).
|
[WASM](https://docs.wickra.org/Quickstart-WASM).
|
||||||
- **Indicators** — a per-indicator deep dive (formula, parameters, warmup) for
|
- **Indicators** — a per-indicator deep dive (formula, parameters, warmup) for
|
||||||
every one of the 269 indicators; start at the
|
every one of the 274 indicators; start at the
|
||||||
[indicators overview](https://docs.wickra.org/Indicators-Overview).
|
[indicators overview](https://docs.wickra.org/Indicators-Overview).
|
||||||
- **Reference** — [warmup periods](https://docs.wickra.org/Warmup-Periods),
|
- **Reference** — [warmup periods](https://docs.wickra.org/Warmup-Periods),
|
||||||
[streaming vs batch](https://docs.wickra.org/Streaming-vs-Batch),
|
[streaming vs batch](https://docs.wickra.org/Streaming-vs-Batch),
|
||||||
@@ -135,7 +135,7 @@ python -m benchmarks.compare_libraries
|
|||||||
|
|
||||||
## Indicators
|
## Indicators
|
||||||
|
|
||||||
269 streaming-first indicators across eighteen families. Every one passes the
|
274 streaming-first indicators across eighteen 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).
|
|||||||
| Pivots & S/R | Classic Pivots, Fibonacci Pivots, Camarilla, Woodie Pivots, DeMark Pivots, Williams Fractals, ZigZag |
|
| Pivots & S/R | Classic Pivots, Fibonacci Pivots, Camarilla, Woodie Pivots, DeMark Pivots, Williams Fractals, ZigZag |
|
||||||
| DeMark | TD Setup, TD Sequential, TD DeMarker, TD REI, TD Pressure, TD Combo, TD Countdown, TD Lines, TD Range Projection, TD Differential, TD Open, TD Risk Level |
|
| DeMark | TD Setup, TD Sequential, TD DeMarker, TD REI, TD Pressure, TD Combo, TD Countdown, TD Lines, TD Range Projection, TD Differential, TD Open, TD Risk Level |
|
||||||
| Ichimoku & Charts | Ichimoku Kinko Hyo (Tenkan, Kijun, Senkou A/B, Chikou), Heikin-Ashi |
|
| Ichimoku & Charts | Ichimoku Kinko Hyo (Tenkan, Kijun, Senkou A/B, Chikou), Heikin-Ashi |
|
||||||
| Candlestick Patterns | Doji, Hammer, Inverted Hammer, Hanging Man, Shooting Star, Engulfing, Harami, Morning/Evening Star, Three White Soldiers/Black Crows, Piercing Line/Dark Cloud Cover, Marubozu, Tweezer, Spinning Top, Three Inside Up/Down, Three Outside Up/Down, Two Crows, Upside Gap Two Crows, Identical Three Crows, Three Line Strike, Three Stars in the South, Abandoned Baby, Advance Block, Belt-hold, Breakaway, Counterattack, Doji Star, Dragonfly Doji, Gravestone Doji, Long-Legged Doji, Rickshaw Man, Evening Doji Star, Morning Doji Star, Gap Side-by-Side White, High-Wave, Hikkake, Modified Hikkake, Homing Pigeon, On-Neck, In-Neck, Thrusting |
|
| Candlestick Patterns | Doji, Hammer, Inverted Hammer, Hanging Man, Shooting Star, Engulfing, Harami, Morning/Evening Star, Three White Soldiers/Black Crows, Piercing Line/Dark Cloud Cover, Marubozu, Tweezer, Spinning Top, Three Inside Up/Down, Three Outside Up/Down, Two Crows, Upside Gap Two Crows, Identical Three Crows, Three Line Strike, Three Stars in the South, Abandoned Baby, Advance Block, Belt-hold, Breakaway, Counterattack, Doji Star, Dragonfly Doji, Gravestone Doji, Long-Legged Doji, Rickshaw Man, Evening Doji Star, Morning Doji Star, Gap Side-by-Side White, High-Wave, Hikkake, Modified Hikkake, Homing Pigeon, On-Neck, In-Neck, Thrusting, Separating Lines, Kicking, Kicking by Length, Ladder Bottom, Mat Hold |
|
||||||
| Microstructure | Order-Book Imbalance (Top-1 / Top-N / Full), Microprice, Quoted Spread, Depth Slope, Signed Volume, Cumulative Volume Delta, Trade Imbalance, Effective Spread, Realized Spread, Kyle's Lambda, Footprint |
|
| Microstructure | Order-Book Imbalance (Top-1 / Top-N / Full), Microprice, Quoted Spread, Depth Slope, Signed Volume, Cumulative Volume Delta, Trade Imbalance, Effective Spread, Realized Spread, Kyle's Lambda, Footprint |
|
||||||
| Derivatives | Funding Rate, Funding Rate Mean, Funding Rate Z-Score, Funding Basis, Open-Interest Delta, OI / Price Divergence, OI-Weighted Price, Long/Short Ratio, Taker Buy/Sell Ratio, Liquidation Features, Term-Structure Basis, Calendar Spread |
|
| Derivatives | Funding Rate, Funding Rate Mean, Funding Rate Z-Score, Funding Basis, Open-Interest Delta, OI / Price Divergence, OI-Weighted Price, Long/Short Ratio, Taker Buy/Sell Ratio, Liquidation Features, Term-Structure Basis, Calendar Spread |
|
||||||
| Market Profile | Value Area (POC / VAH / VAL), Initial Balance, Opening Range |
|
| Market Profile | Value Area (POC / VAH / VAL), Initial Balance, Opening Range |
|
||||||
@@ -238,7 +238,7 @@ A Python live-trading example using the public `websockets` package lives at
|
|||||||
```
|
```
|
||||||
wickra/
|
wickra/
|
||||||
├── crates/
|
├── crates/
|
||||||
│ ├── wickra-core/ core engine + all 269 indicators
|
│ ├── wickra-core/ core engine + all 274 indicators
|
||||||
│ ├── wickra/ top-level facade crate (publishes on crates.io) + benches/
|
│ ├── wickra/ top-level facade crate (publishes on crates.io) + benches/
|
||||||
│ └── wickra-data/ CSV reader, tick aggregator, live exchange feeds
|
│ └── wickra-data/ CSV reader, tick aggregator, live exchange feeds
|
||||||
├── bindings/
|
├── bindings/
|
||||||
|
|||||||
@@ -260,6 +260,11 @@ const candleScalar = {
|
|||||||
OnNeck: { make: () => new wickra.OnNeck(), step: (ind, i) => ind.update(open[i], high[i], low[i], close[i]), batch: (ind) => ind.batch(open, high, low, close) },
|
OnNeck: { make: () => new wickra.OnNeck(), step: (ind, i) => ind.update(open[i], high[i], low[i], close[i]), batch: (ind) => ind.batch(open, high, low, close) },
|
||||||
InNeck: { make: () => new wickra.InNeck(), step: (ind, i) => ind.update(open[i], high[i], low[i], close[i]), batch: (ind) => ind.batch(open, high, low, close) },
|
InNeck: { make: () => new wickra.InNeck(), step: (ind, i) => ind.update(open[i], high[i], low[i], close[i]), batch: (ind) => ind.batch(open, high, low, close) },
|
||||||
Thrusting: { make: () => new wickra.Thrusting(), step: (ind, i) => ind.update(open[i], high[i], low[i], close[i]), batch: (ind) => ind.batch(open, high, low, close) },
|
Thrusting: { make: () => new wickra.Thrusting(), step: (ind, i) => ind.update(open[i], high[i], low[i], close[i]), batch: (ind) => ind.batch(open, high, low, close) },
|
||||||
|
SeparatingLines: { make: () => new wickra.SeparatingLines(), step: (ind, i) => ind.update(open[i], high[i], low[i], close[i]), batch: (ind) => ind.batch(open, high, low, close) },
|
||||||
|
Kicking: { make: () => new wickra.Kicking(), step: (ind, i) => ind.update(open[i], high[i], low[i], close[i]), batch: (ind) => ind.batch(open, high, low, close) },
|
||||||
|
KickingByLength: { make: () => new wickra.KickingByLength(), step: (ind, i) => ind.update(open[i], high[i], low[i], close[i]), batch: (ind) => ind.batch(open, high, low, close) },
|
||||||
|
LadderBottom: { make: () => new wickra.LadderBottom(), step: (ind, i) => ind.update(open[i], high[i], low[i], close[i]), batch: (ind) => ind.batch(open, high, low, close) },
|
||||||
|
MatHold: { make: () => new wickra.MatHold(), step: (ind, i) => ind.update(open[i], high[i], low[i], close[i]), batch: (ind) => ind.batch(open, high, low, close) },
|
||||||
};
|
};
|
||||||
|
|
||||||
for (const [name, d] of Object.entries(candleScalar)) {
|
for (const [name, d] of Object.entries(candleScalar)) {
|
||||||
|
|||||||
Vendored
+45
@@ -2435,6 +2435,51 @@ export declare class Thrusting {
|
|||||||
isReady(): boolean
|
isReady(): boolean
|
||||||
warmupPeriod(): number
|
warmupPeriod(): number
|
||||||
}
|
}
|
||||||
|
export type SeparatingLinesNode = SeparatingLines
|
||||||
|
export declare class SeparatingLines {
|
||||||
|
constructor()
|
||||||
|
update(open: number, high: number, low: number, close: number): number | null
|
||||||
|
batch(open: Array<number>, high: Array<number>, low: Array<number>, close: Array<number>): Array<number>
|
||||||
|
reset(): void
|
||||||
|
isReady(): boolean
|
||||||
|
warmupPeriod(): number
|
||||||
|
}
|
||||||
|
export type KickingNode = Kicking
|
||||||
|
export declare class Kicking {
|
||||||
|
constructor()
|
||||||
|
update(open: number, high: number, low: number, close: number): number | null
|
||||||
|
batch(open: Array<number>, high: Array<number>, low: Array<number>, close: Array<number>): Array<number>
|
||||||
|
reset(): void
|
||||||
|
isReady(): boolean
|
||||||
|
warmupPeriod(): number
|
||||||
|
}
|
||||||
|
export type KickingByLengthNode = KickingByLength
|
||||||
|
export declare class KickingByLength {
|
||||||
|
constructor()
|
||||||
|
update(open: number, high: number, low: number, close: number): number | null
|
||||||
|
batch(open: Array<number>, high: Array<number>, low: Array<number>, close: Array<number>): Array<number>
|
||||||
|
reset(): void
|
||||||
|
isReady(): boolean
|
||||||
|
warmupPeriod(): number
|
||||||
|
}
|
||||||
|
export type LadderBottomNode = LadderBottom
|
||||||
|
export declare class LadderBottom {
|
||||||
|
constructor()
|
||||||
|
update(open: number, high: number, low: number, close: number): number | null
|
||||||
|
batch(open: Array<number>, high: Array<number>, low: Array<number>, close: Array<number>): Array<number>
|
||||||
|
reset(): void
|
||||||
|
isReady(): boolean
|
||||||
|
warmupPeriod(): number
|
||||||
|
}
|
||||||
|
export type MatHoldNode = MatHold
|
||||||
|
export declare class MatHold {
|
||||||
|
constructor()
|
||||||
|
update(open: number, high: number, low: number, close: number): number | null
|
||||||
|
batch(open: Array<number>, high: Array<number>, low: Array<number>, close: Array<number>): Array<number>
|
||||||
|
reset(): void
|
||||||
|
isReady(): boolean
|
||||||
|
warmupPeriod(): number
|
||||||
|
}
|
||||||
export type OrderBookImbalanceTop1Node = OrderBookImbalanceTop1
|
export type OrderBookImbalanceTop1Node = OrderBookImbalanceTop1
|
||||||
export declare class OrderBookImbalanceTop1 {
|
export declare class OrderBookImbalanceTop1 {
|
||||||
constructor()
|
constructor()
|
||||||
|
|||||||
@@ -310,7 +310,7 @@ if (!nativeBinding) {
|
|||||||
throw new Error(`Failed to load native binding`)
|
throw new Error(`Failed to load native binding`)
|
||||||
}
|
}
|
||||||
|
|
||||||
const { version, SMA, EMA, WMA, RSI, DEMA, TEMA, HMA, ROC, TRIX, SMMA, TRIMA, ZLEMA, MOM, CMO, DPO, StdDev, UlcerIndex, VerticalHorizontalFilter, ZScore, McGinleyDynamic, FRAMA, SuperSmoother, FisherTransform, Decycler, CenterOfGravity, CyberneticCycle, InstantaneousTrendline, EhlersStochastic, RVIVolatility, Variance, CoefficientOfVariation, Skewness, Kurtosis, StandardError, DetrendedStdDev, RSquared, MedianAbsoluteDeviation, 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, 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, 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, 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, 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, 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, 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
|
||||||
|
|
||||||
module.exports.version = version
|
module.exports.version = version
|
||||||
module.exports.SMA = SMA
|
module.exports.SMA = SMA
|
||||||
@@ -540,6 +540,11 @@ module.exports.HomingPigeon = HomingPigeon
|
|||||||
module.exports.OnNeck = OnNeck
|
module.exports.OnNeck = OnNeck
|
||||||
module.exports.InNeck = InNeck
|
module.exports.InNeck = InNeck
|
||||||
module.exports.Thrusting = Thrusting
|
module.exports.Thrusting = Thrusting
|
||||||
|
module.exports.SeparatingLines = SeparatingLines
|
||||||
|
module.exports.Kicking = Kicking
|
||||||
|
module.exports.KickingByLength = KickingByLength
|
||||||
|
module.exports.LadderBottom = LadderBottom
|
||||||
|
module.exports.MatHold = MatHold
|
||||||
module.exports.OrderBookImbalanceTop1 = OrderBookImbalanceTop1
|
module.exports.OrderBookImbalanceTop1 = OrderBookImbalanceTop1
|
||||||
module.exports.OrderBookImbalanceFull = OrderBookImbalanceFull
|
module.exports.OrderBookImbalanceFull = OrderBookImbalanceFull
|
||||||
module.exports.Microprice = Microprice
|
module.exports.Microprice = Microprice
|
||||||
|
|||||||
@@ -8794,6 +8794,11 @@ node_candle_pattern!(HomingPigeonNode, wc::HomingPigeon, "HomingPigeon");
|
|||||||
node_candle_pattern!(OnNeckNode, wc::OnNeck, "OnNeck");
|
node_candle_pattern!(OnNeckNode, wc::OnNeck, "OnNeck");
|
||||||
node_candle_pattern!(InNeckNode, wc::InNeck, "InNeck");
|
node_candle_pattern!(InNeckNode, wc::InNeck, "InNeck");
|
||||||
node_candle_pattern!(ThrustingNode, wc::Thrusting, "Thrusting");
|
node_candle_pattern!(ThrustingNode, wc::Thrusting, "Thrusting");
|
||||||
|
node_candle_pattern!(SeparatingLinesNode, wc::SeparatingLines, "SeparatingLines");
|
||||||
|
node_candle_pattern!(KickingNode, wc::Kicking, "Kicking");
|
||||||
|
node_candle_pattern!(KickingByLengthNode, wc::KickingByLength, "KickingByLength");
|
||||||
|
node_candle_pattern!(LadderBottomNode, wc::LadderBottom, "LadderBottom");
|
||||||
|
node_candle_pattern!(MatHoldNode, wc::MatHold, "MatHold");
|
||||||
|
|
||||||
// ============================== Microstructure: Order Book ==============================
|
// ============================== Microstructure: Order Book ==============================
|
||||||
//
|
//
|
||||||
|
|||||||
@@ -265,6 +265,11 @@ from ._wickra import (
|
|||||||
OnNeck,
|
OnNeck,
|
||||||
InNeck,
|
InNeck,
|
||||||
Thrusting,
|
Thrusting,
|
||||||
|
SeparatingLines,
|
||||||
|
Kicking,
|
||||||
|
KickingByLength,
|
||||||
|
LadderBottom,
|
||||||
|
MatHold,
|
||||||
# Microstructure: order book
|
# Microstructure: order book
|
||||||
OrderBookImbalanceTop1,
|
OrderBookImbalanceTop1,
|
||||||
OrderBookImbalanceTopN,
|
OrderBookImbalanceTopN,
|
||||||
@@ -557,6 +562,11 @@ __all__ = [
|
|||||||
"OnNeck",
|
"OnNeck",
|
||||||
"InNeck",
|
"InNeck",
|
||||||
"Thrusting",
|
"Thrusting",
|
||||||
|
"SeparatingLines",
|
||||||
|
"Kicking",
|
||||||
|
"KickingByLength",
|
||||||
|
"LadderBottom",
|
||||||
|
"MatHold",
|
||||||
# Microstructure: order book
|
# Microstructure: order book
|
||||||
"OrderBookImbalanceTop1",
|
"OrderBookImbalanceTop1",
|
||||||
"OrderBookImbalanceTopN",
|
"OrderBookImbalanceTopN",
|
||||||
|
|||||||
@@ -11657,6 +11657,11 @@ candle_pattern_no_param!(PyHomingPigeon, wc::HomingPigeon, "HomingPigeon");
|
|||||||
candle_pattern_no_param!(PyOnNeck, wc::OnNeck, "OnNeck");
|
candle_pattern_no_param!(PyOnNeck, wc::OnNeck, "OnNeck");
|
||||||
candle_pattern_no_param!(PyInNeck, wc::InNeck, "InNeck");
|
candle_pattern_no_param!(PyInNeck, wc::InNeck, "InNeck");
|
||||||
candle_pattern_no_param!(PyThrusting, wc::Thrusting, "Thrusting");
|
candle_pattern_no_param!(PyThrusting, wc::Thrusting, "Thrusting");
|
||||||
|
candle_pattern_no_param!(PySeparatingLines, wc::SeparatingLines, "SeparatingLines");
|
||||||
|
candle_pattern_no_param!(PyKicking, wc::Kicking, "Kicking");
|
||||||
|
candle_pattern_no_param!(PyKickingByLength, wc::KickingByLength, "KickingByLength");
|
||||||
|
candle_pattern_no_param!(PyLadderBottom, wc::LadderBottom, "LadderBottom");
|
||||||
|
candle_pattern_no_param!(PyMatHold, wc::MatHold, "MatHold");
|
||||||
// ============================== Microstructure: Order Book ==============================
|
// ============================== Microstructure: Order Book ==============================
|
||||||
//
|
//
|
||||||
// Order-book indicators consume a depth snapshot rather than OHLCV. Streaming
|
// Order-book indicators consume a depth snapshot rather than OHLCV. Streaming
|
||||||
@@ -14178,6 +14183,11 @@ fn _wickra(_py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> {
|
|||||||
m.add_class::<PyOnNeck>()?;
|
m.add_class::<PyOnNeck>()?;
|
||||||
m.add_class::<PyInNeck>()?;
|
m.add_class::<PyInNeck>()?;
|
||||||
m.add_class::<PyThrusting>()?;
|
m.add_class::<PyThrusting>()?;
|
||||||
|
m.add_class::<PySeparatingLines>()?;
|
||||||
|
m.add_class::<PyKicking>()?;
|
||||||
|
m.add_class::<PyKickingByLength>()?;
|
||||||
|
m.add_class::<PyLadderBottom>()?;
|
||||||
|
m.add_class::<PyMatHold>()?;
|
||||||
// Microstructure: order book.
|
// Microstructure: order book.
|
||||||
m.add_class::<PyOrderBookImbalanceTop1>()?;
|
m.add_class::<PyOrderBookImbalanceTop1>()?;
|
||||||
m.add_class::<PyOrderBookImbalanceTopN>()?;
|
m.add_class::<PyOrderBookImbalanceTopN>()?;
|
||||||
|
|||||||
@@ -623,6 +623,26 @@ CANDLE_SCALAR = {
|
|||||||
lambda: ta.Thrusting(),
|
lambda: ta.Thrusting(),
|
||||||
lambda ind, h, l, c, v: ind.batch(c, h, l, c),
|
lambda ind, h, l, c, v: ind.batch(c, h, l, c),
|
||||||
),
|
),
|
||||||
|
"SeparatingLines": (
|
||||||
|
lambda: ta.SeparatingLines(),
|
||||||
|
lambda ind, h, l, c, v: ind.batch(c, h, l, c),
|
||||||
|
),
|
||||||
|
"Kicking": (
|
||||||
|
lambda: ta.Kicking(),
|
||||||
|
lambda ind, h, l, c, v: ind.batch(c, h, l, c),
|
||||||
|
),
|
||||||
|
"KickingByLength": (
|
||||||
|
lambda: ta.KickingByLength(),
|
||||||
|
lambda ind, h, l, c, v: ind.batch(c, h, l, c),
|
||||||
|
),
|
||||||
|
"LadderBottom": (
|
||||||
|
lambda: ta.LadderBottom(),
|
||||||
|
lambda ind, h, l, c, v: ind.batch(c, h, l, c),
|
||||||
|
),
|
||||||
|
"MatHold": (
|
||||||
|
lambda: ta.MatHold(),
|
||||||
|
lambda ind, h, l, c, v: ind.batch(c, h, l, c),
|
||||||
|
),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -1835,6 +1855,42 @@ def test_thrusting_reference():
|
|||||||
assert t.update((15.0, 15.1, 9.0, 10.0, 1.0, 0)) == pytest.approx(0.0)
|
assert t.update((15.0, 15.1, 9.0, 10.0, 1.0, 0)) == pytest.approx(0.0)
|
||||||
assert t.update((7.0, 11.6, 6.9, 11.5, 1.0, 1)) == pytest.approx(-1.0)
|
assert t.update((7.0, 11.6, 6.9, 11.5, 1.0, 1)) == pytest.approx(-1.0)
|
||||||
|
|
||||||
|
|
||||||
|
def test_separating_lines_reference():
|
||||||
|
t = ta.SeparatingLines()
|
||||||
|
assert t.update((12.0, 12.1, 9.9, 10.0, 1.0, 0)) == pytest.approx(0.0)
|
||||||
|
assert t.update((12.0, 14.1, 12.0, 14.0, 1.0, 1)) == pytest.approx(1.0)
|
||||||
|
|
||||||
|
|
||||||
|
def test_kicking_reference():
|
||||||
|
t = ta.Kicking()
|
||||||
|
assert t.update((12.0, 12.0, 10.0, 10.0, 1.0, 0)) == pytest.approx(0.0)
|
||||||
|
assert t.update((14.0, 16.0, 14.0, 16.0, 1.0, 1)) == pytest.approx(1.0)
|
||||||
|
|
||||||
|
|
||||||
|
def test_kicking_by_length_reference():
|
||||||
|
t = ta.KickingByLength()
|
||||||
|
assert t.update((12.0, 12.0, 10.0, 10.0, 1.0, 0)) == pytest.approx(0.0)
|
||||||
|
assert t.update((14.0, 20.0, 14.0, 20.0, 1.0, 1)) == pytest.approx(1.0)
|
||||||
|
|
||||||
|
|
||||||
|
def test_ladder_bottom_reference():
|
||||||
|
t = ta.LadderBottom()
|
||||||
|
assert t.update((20.0, 20.1, 17.9, 18.0, 1.0, 0)) == pytest.approx(0.0)
|
||||||
|
assert t.update((18.0, 18.1, 15.9, 16.0, 1.0, 1)) == pytest.approx(0.0)
|
||||||
|
assert t.update((16.0, 16.1, 13.9, 14.0, 1.0, 2)) == pytest.approx(0.0)
|
||||||
|
assert t.update((14.0, 15.0, 12.4, 12.5, 1.0, 3)) == pytest.approx(0.0)
|
||||||
|
assert t.update((15.0, 17.1, 14.9, 17.0, 1.0, 4)) == pytest.approx(1.0)
|
||||||
|
|
||||||
|
|
||||||
|
def test_mat_hold_reference():
|
||||||
|
t = ta.MatHold()
|
||||||
|
assert t.update((10.0, 15.1, 9.9, 15.0, 1.0, 0)) == pytest.approx(0.0)
|
||||||
|
assert t.update((16.0, 16.1, 15.4, 15.5, 1.0, 1)) == pytest.approx(0.0)
|
||||||
|
assert t.update((15.5, 15.6, 14.9, 15.0, 1.0, 2)) == pytest.approx(0.0)
|
||||||
|
assert t.update((15.0, 15.1, 14.4, 14.5, 1.0, 3)) == pytest.approx(0.0)
|
||||||
|
assert t.update((14.5, 17.1, 14.4, 17.0, 1.0, 4)) == pytest.approx(1.0)
|
||||||
|
|
||||||
# --- Lifecycle ------------------------------------------------------------
|
# --- Lifecycle ------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -6371,6 +6371,11 @@ wasm_candle_pattern!(WasmHomingPigeon, wc::HomingPigeon, HomingPigeon);
|
|||||||
wasm_candle_pattern!(WasmOnNeck, wc::OnNeck, OnNeck);
|
wasm_candle_pattern!(WasmOnNeck, wc::OnNeck, OnNeck);
|
||||||
wasm_candle_pattern!(WasmInNeck, wc::InNeck, InNeck);
|
wasm_candle_pattern!(WasmInNeck, wc::InNeck, InNeck);
|
||||||
wasm_candle_pattern!(WasmThrusting, wc::Thrusting, Thrusting);
|
wasm_candle_pattern!(WasmThrusting, wc::Thrusting, Thrusting);
|
||||||
|
wasm_candle_pattern!(WasmSeparatingLines, wc::SeparatingLines, SeparatingLines);
|
||||||
|
wasm_candle_pattern!(WasmKicking, wc::Kicking, Kicking);
|
||||||
|
wasm_candle_pattern!(WasmKickingByLength, wc::KickingByLength, KickingByLength);
|
||||||
|
wasm_candle_pattern!(WasmLadderBottom, wc::LadderBottom, LadderBottom);
|
||||||
|
wasm_candle_pattern!(WasmMatHold, wc::MatHold, MatHold);
|
||||||
|
|
||||||
// ============================== Microstructure: Order Book ==============================
|
// ============================== Microstructure: Order Book ==============================
|
||||||
//
|
//
|
||||||
|
|||||||
@@ -0,0 +1,189 @@
|
|||||||
|
//! Kicking candlestick pattern.
|
||||||
|
|
||||||
|
use crate::ohlcv::Candle;
|
||||||
|
use crate::traits::Indicator;
|
||||||
|
|
||||||
|
/// Kicking — a 2-bar reversal of two opposite-coloured marubozu separated by a
|
||||||
|
/// gap. A shadowless candle is "kicked" the other way by a shadowless candle of
|
||||||
|
/// the opposite colour that gaps clear of it — a violent change of control. It is
|
||||||
|
/// trend-agnostic: the gap direction alone defines the signal.
|
||||||
|
///
|
||||||
|
/// ```text
|
||||||
|
/// marubozu = |close − open| >= 0.95 * (high − low) (no meaningful shadows)
|
||||||
|
/// bullish (+1.0): black marubozu, then a white marubozu gapping UP (low2 > high1)
|
||||||
|
/// bearish (−1.0): white marubozu, then a black marubozu gapping DOWN (high2 < low1)
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// Output is `+1.0` (bullish) or `−1.0` (bearish) when the pattern completes and
|
||||||
|
/// `0.0` otherwise. The first bar always returns `0.0` because the two-bar window
|
||||||
|
/// is not yet filled. The marubozu threshold follows the geometric house style
|
||||||
|
/// rather than TA-Lib's rolling averages. Pattern-shape check only — no trend
|
||||||
|
/// filter is applied; combine with a trend indicator for actionable signals.
|
||||||
|
///
|
||||||
|
/// # Signed ±1 encoding
|
||||||
|
///
|
||||||
|
/// This detector emits the uniform candlestick sign convention shared across the
|
||||||
|
/// pattern family — `+1.0` bullish, `−1.0` bearish, `0.0` no pattern — so it
|
||||||
|
/// drops straight into a machine-learning feature matrix where the two directions
|
||||||
|
/// occupy a single dimension.
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// use wickra_core::{Candle, Indicator, Kicking};
|
||||||
|
///
|
||||||
|
/// let mut indicator = Kicking::new();
|
||||||
|
/// indicator.update(Candle::new(12.0, 12.0, 10.0, 10.0, 1.0, 0).unwrap());
|
||||||
|
/// let out = indicator
|
||||||
|
/// .update(Candle::new(14.0, 16.0, 14.0, 16.0, 1.0, 1).unwrap());
|
||||||
|
/// assert_eq!(out, Some(1.0));
|
||||||
|
/// ```
|
||||||
|
#[derive(Debug, Clone, Default)]
|
||||||
|
pub struct Kicking {
|
||||||
|
prev: Option<Candle>,
|
||||||
|
has_emitted: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Kicking {
|
||||||
|
/// Construct a new Kicking detector.
|
||||||
|
pub const fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
prev: None,
|
||||||
|
has_emitted: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Whether `candle` is a marubozu (body fills at least 95 % of its range).
|
||||||
|
fn is_marubozu(candle: &Candle) -> bool {
|
||||||
|
let range = candle.high - candle.low;
|
||||||
|
range > 0.0 && (candle.close - candle.open).abs() >= 0.95 * range
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Indicator for Kicking {
|
||||||
|
type Input = Candle;
|
||||||
|
type Output = f64;
|
||||||
|
|
||||||
|
fn update(&mut self, candle: Candle) -> Option<f64> {
|
||||||
|
self.has_emitted = true;
|
||||||
|
let prev = self.prev;
|
||||||
|
self.prev = Some(candle);
|
||||||
|
let Some(bar1) = prev else {
|
||||||
|
return Some(0.0);
|
||||||
|
};
|
||||||
|
if !is_marubozu(&bar1) || !is_marubozu(&candle) {
|
||||||
|
return Some(0.0);
|
||||||
|
}
|
||||||
|
// Bullish: black marubozu kicked up by a white marubozu gapping above it.
|
||||||
|
if bar1.close < bar1.open && candle.close > candle.open && candle.low > bar1.high {
|
||||||
|
return Some(1.0);
|
||||||
|
}
|
||||||
|
// Bearish: white marubozu kicked down by a black marubozu gapping below it.
|
||||||
|
if bar1.close > bar1.open && candle.close < candle.open && candle.high < bar1.low {
|
||||||
|
return Some(-1.0);
|
||||||
|
}
|
||||||
|
Some(0.0)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn reset(&mut self) {
|
||||||
|
self.prev = None;
|
||||||
|
self.has_emitted = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn warmup_period(&self) -> usize {
|
||||||
|
2
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_ready(&self) -> bool {
|
||||||
|
self.has_emitted
|
||||||
|
}
|
||||||
|
|
||||||
|
fn name(&self) -> &'static str {
|
||||||
|
"Kicking"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::traits::BatchExt;
|
||||||
|
|
||||||
|
fn c(open: f64, high: f64, low: f64, close: f64, ts: i64) -> Candle {
|
||||||
|
Candle::new(open, high, low, close, 1.0, ts).unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn accessors_and_metadata() {
|
||||||
|
let t = Kicking::new();
|
||||||
|
assert_eq!(t.name(), "Kicking");
|
||||||
|
assert_eq!(t.warmup_period(), 2);
|
||||||
|
assert!(!t.is_ready());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn bullish_kicking_is_plus_one() {
|
||||||
|
let mut t = Kicking::new();
|
||||||
|
assert_eq!(t.update(c(12.0, 12.0, 10.0, 10.0, 0)), Some(0.0));
|
||||||
|
assert_eq!(t.update(c(14.0, 16.0, 14.0, 16.0, 1)), Some(1.0));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn bearish_kicking_is_minus_one() {
|
||||||
|
let mut t = Kicking::new();
|
||||||
|
assert_eq!(t.update(c(10.0, 12.0, 10.0, 12.0, 0)), Some(0.0));
|
||||||
|
assert_eq!(t.update(c(8.0, 8.0, 6.0, 6.0, 1)), Some(-1.0));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn not_marubozu_yields_zero() {
|
||||||
|
let mut t = Kicking::new();
|
||||||
|
// bar1 has long shadows -> not a marubozu.
|
||||||
|
t.update(c(12.0, 14.0, 8.0, 10.0, 0));
|
||||||
|
assert_eq!(t.update(c(14.0, 16.0, 14.0, 16.0, 1)), Some(0.0));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn no_gap_yields_zero() {
|
||||||
|
let mut t = Kicking::new();
|
||||||
|
t.update(c(12.0, 12.0, 10.0, 10.0, 0));
|
||||||
|
// White marubozu but it overlaps bar1 (no gap up).
|
||||||
|
assert_eq!(t.update(c(11.0, 13.0, 11.0, 13.0, 1)), Some(0.0));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn first_bar_returns_zero() {
|
||||||
|
let mut t = Kicking::new();
|
||||||
|
assert_eq!(t.update(c(12.0, 12.0, 10.0, 10.0, 0)), Some(0.0));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn batch_equals_streaming() {
|
||||||
|
let candles: Vec<Candle> = (0..40)
|
||||||
|
.map(|i| {
|
||||||
|
let base = 100.0 + i as f64 * 5.0;
|
||||||
|
if i % 2 == 0 {
|
||||||
|
c(base + 2.0, base + 2.0, base, base, i)
|
||||||
|
} else {
|
||||||
|
c(base + 3.0, base + 5.0, base + 3.0, base + 5.0, i)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
let mut a = Kicking::new();
|
||||||
|
let mut b = Kicking::new();
|
||||||
|
assert_eq!(
|
||||||
|
a.batch(&candles),
|
||||||
|
candles.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn reset_clears_state() {
|
||||||
|
let mut t = Kicking::new();
|
||||||
|
t.update(c(12.0, 12.0, 10.0, 10.0, 0));
|
||||||
|
t.update(c(14.0, 16.0, 14.0, 16.0, 1));
|
||||||
|
assert!(t.is_ready());
|
||||||
|
t.reset();
|
||||||
|
assert!(!t.is_ready());
|
||||||
|
assert_eq!(t.update(c(12.0, 12.0, 10.0, 10.0, 0)), Some(0.0));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,197 @@
|
|||||||
|
//! Kicking-by-Length candlestick pattern.
|
||||||
|
|
||||||
|
use crate::ohlcv::Candle;
|
||||||
|
use crate::traits::Indicator;
|
||||||
|
|
||||||
|
/// Kicking-by-Length — the [`Kicking`](crate::Kicking) pattern with the signal
|
||||||
|
/// taken from the *longer* of the two marubozu rather than from the gap direction.
|
||||||
|
/// When the two shadowless candles differ in size, the bigger one is treated as
|
||||||
|
/// the dominant force.
|
||||||
|
///
|
||||||
|
/// ```text
|
||||||
|
/// marubozu = |close − open| >= 0.95 * (high − low)
|
||||||
|
/// setup: two opposite-coloured marubozu separated by a gap
|
||||||
|
/// black then white gapping UP, or white then black gapping DOWN
|
||||||
|
/// signal = colour of the LONGER marubozu (white -> +1.0, black -> −1.0)
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// Output is `+1.0` or `−1.0` when the kicking setup is present and `0.0`
|
||||||
|
/// otherwise. Note this can disagree with [`Kicking`](crate::Kicking): a black
|
||||||
|
/// marubozu kicked up by a *shorter* white marubozu reports `−1.0` here. The first
|
||||||
|
/// bar always returns `0.0` because the two-bar window is not yet filled. The
|
||||||
|
/// marubozu threshold follows the geometric house style rather than TA-Lib's
|
||||||
|
/// rolling averages. Pattern-shape check only — no trend filter is applied;
|
||||||
|
/// combine with a trend indicator for actionable signals.
|
||||||
|
///
|
||||||
|
/// # Signed ±1 encoding
|
||||||
|
///
|
||||||
|
/// This detector emits the uniform candlestick sign convention shared across the
|
||||||
|
/// pattern family — `+1.0` bullish, `−1.0` bearish, `0.0` no pattern — so it
|
||||||
|
/// drops straight into a machine-learning feature matrix as a single dimension.
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// use wickra_core::{Candle, Indicator, KickingByLength};
|
||||||
|
///
|
||||||
|
/// let mut indicator = KickingByLength::new();
|
||||||
|
/// indicator.update(Candle::new(12.0, 12.0, 10.0, 10.0, 1.0, 0).unwrap());
|
||||||
|
/// // White marubozu gaps up and is the longer body -> +1.
|
||||||
|
/// let out = indicator
|
||||||
|
/// .update(Candle::new(14.0, 20.0, 14.0, 20.0, 1.0, 1).unwrap());
|
||||||
|
/// assert_eq!(out, Some(1.0));
|
||||||
|
/// ```
|
||||||
|
#[derive(Debug, Clone, Default)]
|
||||||
|
pub struct KickingByLength {
|
||||||
|
prev: Option<Candle>,
|
||||||
|
has_emitted: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl KickingByLength {
|
||||||
|
/// Construct a new Kicking-by-Length detector.
|
||||||
|
pub const fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
prev: None,
|
||||||
|
has_emitted: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_marubozu(candle: &Candle) -> bool {
|
||||||
|
let range = candle.high - candle.low;
|
||||||
|
range > 0.0 && (candle.close - candle.open).abs() >= 0.95 * range
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Indicator for KickingByLength {
|
||||||
|
type Input = Candle;
|
||||||
|
type Output = f64;
|
||||||
|
|
||||||
|
fn update(&mut self, candle: Candle) -> Option<f64> {
|
||||||
|
self.has_emitted = true;
|
||||||
|
let prev = self.prev;
|
||||||
|
self.prev = Some(candle);
|
||||||
|
let Some(bar1) = prev else {
|
||||||
|
return Some(0.0);
|
||||||
|
};
|
||||||
|
if !is_marubozu(&bar1) || !is_marubozu(&candle) {
|
||||||
|
return Some(0.0);
|
||||||
|
}
|
||||||
|
let body1 = bar1.close - bar1.open;
|
||||||
|
let body2 = candle.close - candle.open;
|
||||||
|
let bullish_setup = body1 < 0.0 && body2 > 0.0 && candle.low > bar1.high;
|
||||||
|
let bearish_setup = body1 > 0.0 && body2 < 0.0 && candle.high < bar1.low;
|
||||||
|
if !(bullish_setup || bearish_setup) {
|
||||||
|
return Some(0.0);
|
||||||
|
}
|
||||||
|
// The longer marubozu's colour is the signal.
|
||||||
|
let longer_is_white = if body1.abs() >= body2.abs() {
|
||||||
|
body1 > 0.0
|
||||||
|
} else {
|
||||||
|
body2 > 0.0
|
||||||
|
};
|
||||||
|
Some(if longer_is_white { 1.0 } else { -1.0 })
|
||||||
|
}
|
||||||
|
|
||||||
|
fn reset(&mut self) {
|
||||||
|
self.prev = None;
|
||||||
|
self.has_emitted = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn warmup_period(&self) -> usize {
|
||||||
|
2
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_ready(&self) -> bool {
|
||||||
|
self.has_emitted
|
||||||
|
}
|
||||||
|
|
||||||
|
fn name(&self) -> &'static str {
|
||||||
|
"KickingByLength"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::traits::BatchExt;
|
||||||
|
|
||||||
|
fn c(open: f64, high: f64, low: f64, close: f64, ts: i64) -> Candle {
|
||||||
|
Candle::new(open, high, low, close, 1.0, ts).unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn accessors_and_metadata() {
|
||||||
|
let t = KickingByLength::new();
|
||||||
|
assert_eq!(t.name(), "KickingByLength");
|
||||||
|
assert_eq!(t.warmup_period(), 2);
|
||||||
|
assert!(!t.is_ready());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn longer_white_is_plus_one() {
|
||||||
|
let mut t = KickingByLength::new();
|
||||||
|
assert_eq!(t.update(c(12.0, 12.0, 10.0, 10.0, 0)), Some(0.0));
|
||||||
|
// White marubozu (length 6) longer than the black one (length 2).
|
||||||
|
assert_eq!(t.update(c(14.0, 20.0, 14.0, 20.0, 1)), Some(1.0));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn longer_black_is_minus_one() {
|
||||||
|
let mut t = KickingByLength::new();
|
||||||
|
// Black marubozu (length 6), then a shorter white marubozu (length 2)
|
||||||
|
// gapping up -> the longer black body wins, so -1.
|
||||||
|
assert_eq!(t.update(c(16.0, 16.0, 10.0, 10.0, 0)), Some(0.0));
|
||||||
|
assert_eq!(t.update(c(18.0, 20.0, 18.0, 20.0, 1)), Some(-1.0));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn not_marubozu_yields_zero() {
|
||||||
|
let mut t = KickingByLength::new();
|
||||||
|
t.update(c(12.0, 14.0, 8.0, 10.0, 0));
|
||||||
|
assert_eq!(t.update(c(14.0, 20.0, 14.0, 20.0, 1)), Some(0.0));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn no_gap_yields_zero() {
|
||||||
|
let mut t = KickingByLength::new();
|
||||||
|
t.update(c(12.0, 12.0, 10.0, 10.0, 0));
|
||||||
|
assert_eq!(t.update(c(11.0, 13.0, 11.0, 13.0, 1)), Some(0.0));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn first_bar_returns_zero() {
|
||||||
|
let mut t = KickingByLength::new();
|
||||||
|
assert_eq!(t.update(c(12.0, 12.0, 10.0, 10.0, 0)), Some(0.0));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn batch_equals_streaming() {
|
||||||
|
let candles: Vec<Candle> = (0..40)
|
||||||
|
.map(|i| {
|
||||||
|
let base = 100.0 + i as f64 * 5.0;
|
||||||
|
if i % 2 == 0 {
|
||||||
|
c(base + 2.0, base + 2.0, base, base, i)
|
||||||
|
} else {
|
||||||
|
c(base + 3.0, base + 5.0, base + 3.0, base + 5.0, i)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
let mut a = KickingByLength::new();
|
||||||
|
let mut b = KickingByLength::new();
|
||||||
|
assert_eq!(
|
||||||
|
a.batch(&candles),
|
||||||
|
candles.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn reset_clears_state() {
|
||||||
|
let mut t = KickingByLength::new();
|
||||||
|
t.update(c(12.0, 12.0, 10.0, 10.0, 0));
|
||||||
|
t.update(c(14.0, 20.0, 14.0, 20.0, 1));
|
||||||
|
assert!(t.is_ready());
|
||||||
|
t.reset();
|
||||||
|
assert!(!t.is_ready());
|
||||||
|
assert_eq!(t.update(c(12.0, 12.0, 10.0, 10.0, 0)), Some(0.0));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,207 @@
|
|||||||
|
//! Ladder Bottom candlestick pattern.
|
||||||
|
|
||||||
|
use crate::ohlcv::Candle;
|
||||||
|
use crate::traits::Indicator;
|
||||||
|
|
||||||
|
/// Ladder Bottom — a 5-bar bullish reversal. Three long black candles step the
|
||||||
|
/// market down like rungs of a ladder, a fourth black candle finally shows an
|
||||||
|
/// upper shadow (the first sign of buying), and a white candle then gaps up into
|
||||||
|
/// its body to confirm the turn.
|
||||||
|
///
|
||||||
|
/// ```text
|
||||||
|
/// bar1, bar2, bar3 black, with consecutively lower opens AND closes
|
||||||
|
/// bar4 black with an upper shadow (high4 > open4)
|
||||||
|
/// bar5 white, opens above bar4's body (open5 > open4) and closes up
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// Output is `+1.0` when the pattern completes and `0.0` otherwise. Ladder Bottom
|
||||||
|
/// is a single-direction (bullish-only) reversal, so it never emits `−1.0`. The
|
||||||
|
/// first four bars always return `0.0` because the five-bar window is not yet
|
||||||
|
/// filled. Pattern-shape check only — no trend filter is applied; combine with a
|
||||||
|
/// trend indicator for actionable signals.
|
||||||
|
///
|
||||||
|
/// # Signed ±1 encoding
|
||||||
|
///
|
||||||
|
/// This detector emits the uniform candlestick sign convention shared across the
|
||||||
|
/// pattern family — `+1.0` bullish, `0.0` no pattern — so it drops straight into
|
||||||
|
/// a machine-learning feature matrix as a single dimension.
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// use wickra_core::{Candle, Indicator, LadderBottom};
|
||||||
|
///
|
||||||
|
/// let mut indicator = LadderBottom::new();
|
||||||
|
/// indicator.update(Candle::new(20.0, 20.1, 17.9, 18.0, 1.0, 0).unwrap());
|
||||||
|
/// indicator.update(Candle::new(18.0, 18.1, 15.9, 16.0, 1.0, 1).unwrap());
|
||||||
|
/// indicator.update(Candle::new(16.0, 16.1, 13.9, 14.0, 1.0, 2).unwrap());
|
||||||
|
/// indicator.update(Candle::new(14.0, 15.0, 12.4, 12.5, 1.0, 3).unwrap());
|
||||||
|
/// let out = indicator
|
||||||
|
/// .update(Candle::new(15.0, 17.1, 14.9, 17.0, 1.0, 4).unwrap());
|
||||||
|
/// assert_eq!(out, Some(1.0));
|
||||||
|
/// ```
|
||||||
|
#[derive(Debug, Clone, Default)]
|
||||||
|
pub struct LadderBottom {
|
||||||
|
c1: Option<Candle>,
|
||||||
|
c2: Option<Candle>,
|
||||||
|
c3: Option<Candle>,
|
||||||
|
c4: Option<Candle>,
|
||||||
|
has_emitted: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl LadderBottom {
|
||||||
|
/// Construct a new Ladder Bottom detector.
|
||||||
|
pub const fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
c1: None,
|
||||||
|
c2: None,
|
||||||
|
c3: None,
|
||||||
|
c4: None,
|
||||||
|
has_emitted: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Indicator for LadderBottom {
|
||||||
|
type Input = Candle;
|
||||||
|
type Output = f64;
|
||||||
|
|
||||||
|
fn update(&mut self, candle: Candle) -> Option<f64> {
|
||||||
|
self.has_emitted = true;
|
||||||
|
let bar1 = self.c1;
|
||||||
|
let bar2 = self.c2;
|
||||||
|
let bar3 = self.c3;
|
||||||
|
let bar4 = self.c4;
|
||||||
|
self.c1 = self.c2;
|
||||||
|
self.c2 = self.c3;
|
||||||
|
self.c3 = self.c4;
|
||||||
|
self.c4 = Some(candle);
|
||||||
|
let (Some(bar1), Some(bar2), Some(bar3), Some(bar4)) = (bar1, bar2, bar3, bar4) else {
|
||||||
|
return Some(0.0);
|
||||||
|
};
|
||||||
|
if bar1.close < bar1.open
|
||||||
|
&& bar2.close < bar2.open
|
||||||
|
&& bar3.close < bar3.open
|
||||||
|
&& bar2.open < bar1.open
|
||||||
|
&& bar2.close < bar1.close
|
||||||
|
&& bar3.open < bar2.open
|
||||||
|
&& bar3.close < bar2.close
|
||||||
|
&& bar4.close < bar4.open
|
||||||
|
&& bar4.high > bar4.open
|
||||||
|
&& candle.close > candle.open
|
||||||
|
&& candle.open > bar4.open
|
||||||
|
{
|
||||||
|
return Some(1.0);
|
||||||
|
}
|
||||||
|
Some(0.0)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn reset(&mut self) {
|
||||||
|
self.c1 = None;
|
||||||
|
self.c2 = None;
|
||||||
|
self.c3 = None;
|
||||||
|
self.c4 = None;
|
||||||
|
self.has_emitted = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn warmup_period(&self) -> usize {
|
||||||
|
5
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_ready(&self) -> bool {
|
||||||
|
self.has_emitted
|
||||||
|
}
|
||||||
|
|
||||||
|
fn name(&self) -> &'static str {
|
||||||
|
"LadderBottom"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::traits::BatchExt;
|
||||||
|
|
||||||
|
fn c(open: f64, high: f64, low: f64, close: f64, ts: i64) -> Candle {
|
||||||
|
Candle::new(open, high, low, close, 1.0, ts).unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn accessors_and_metadata() {
|
||||||
|
let t = LadderBottom::new();
|
||||||
|
assert_eq!(t.name(), "LadderBottom");
|
||||||
|
assert_eq!(t.warmup_period(), 5);
|
||||||
|
assert!(!t.is_ready());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn ladder_bottom_is_plus_one() {
|
||||||
|
let mut t = LadderBottom::new();
|
||||||
|
assert_eq!(t.update(c(20.0, 20.1, 17.9, 18.0, 0)), Some(0.0));
|
||||||
|
assert_eq!(t.update(c(18.0, 18.1, 15.9, 16.0, 1)), Some(0.0));
|
||||||
|
assert_eq!(t.update(c(16.0, 16.1, 13.9, 14.0, 2)), Some(0.0));
|
||||||
|
assert_eq!(t.update(c(14.0, 15.0, 12.4, 12.5, 3)), Some(0.0));
|
||||||
|
assert_eq!(t.update(c(15.0, 17.1, 14.9, 17.0, 4)), Some(1.0));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn fourth_bar_without_upper_shadow_yields_zero() {
|
||||||
|
let mut t = LadderBottom::new();
|
||||||
|
t.update(c(20.0, 20.1, 17.9, 18.0, 0));
|
||||||
|
t.update(c(18.0, 18.1, 15.9, 16.0, 1));
|
||||||
|
t.update(c(16.0, 16.1, 13.9, 14.0, 2));
|
||||||
|
// bar4 opens at its high -> no upper shadow.
|
||||||
|
t.update(c(14.0, 14.0, 12.4, 12.5, 3));
|
||||||
|
assert_eq!(t.update(c(15.0, 17.1, 14.9, 17.0, 4)), Some(0.0));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn not_three_descending_blacks_yields_zero() {
|
||||||
|
let mut t = LadderBottom::new();
|
||||||
|
// bar2 is not lower than bar1.
|
||||||
|
t.update(c(20.0, 20.1, 17.9, 18.0, 0));
|
||||||
|
t.update(c(21.0, 21.1, 18.9, 19.0, 1));
|
||||||
|
t.update(c(16.0, 16.1, 13.9, 14.0, 2));
|
||||||
|
t.update(c(14.0, 15.0, 12.4, 12.5, 3));
|
||||||
|
assert_eq!(t.update(c(15.0, 17.1, 14.9, 17.0, 4)), Some(0.0));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn first_four_bars_return_zero() {
|
||||||
|
let mut t = LadderBottom::new();
|
||||||
|
assert_eq!(t.update(c(20.0, 20.1, 17.9, 18.0, 0)), Some(0.0));
|
||||||
|
assert_eq!(t.update(c(18.0, 18.1, 15.9, 16.0, 1)), Some(0.0));
|
||||||
|
assert_eq!(t.update(c(16.0, 16.1, 13.9, 14.0, 2)), Some(0.0));
|
||||||
|
assert_eq!(t.update(c(14.0, 15.0, 12.4, 12.5, 3)), Some(0.0));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn batch_equals_streaming() {
|
||||||
|
let candles: Vec<Candle> = (0..40)
|
||||||
|
.map(|i| {
|
||||||
|
let base = 200.0 - i as f64;
|
||||||
|
c(base, base + 0.1, base - 2.1, base - 2.0, i)
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
let mut a = LadderBottom::new();
|
||||||
|
let mut b = LadderBottom::new();
|
||||||
|
assert_eq!(
|
||||||
|
a.batch(&candles),
|
||||||
|
candles.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn reset_clears_state() {
|
||||||
|
let mut t = LadderBottom::new();
|
||||||
|
t.update(c(20.0, 20.1, 17.9, 18.0, 0));
|
||||||
|
t.update(c(18.0, 18.1, 15.9, 16.0, 1));
|
||||||
|
t.update(c(16.0, 16.1, 13.9, 14.0, 2));
|
||||||
|
t.update(c(14.0, 15.0, 12.4, 12.5, 3));
|
||||||
|
t.update(c(15.0, 17.1, 14.9, 17.0, 4));
|
||||||
|
assert!(t.is_ready());
|
||||||
|
t.reset();
|
||||||
|
assert!(!t.is_ready());
|
||||||
|
assert_eq!(t.update(c(20.0, 20.1, 17.9, 18.0, 0)), Some(0.0));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,274 @@
|
|||||||
|
//! Mat Hold candlestick pattern.
|
||||||
|
|
||||||
|
use crate::error::{Error, Result};
|
||||||
|
use crate::ohlcv::Candle;
|
||||||
|
use crate::traits::Indicator;
|
||||||
|
|
||||||
|
/// Mat Hold — a 5-bar bullish continuation. A long white candle is followed by a
|
||||||
|
/// brief three-bar pullback that gaps up and then drifts on small bodies *without*
|
||||||
|
/// surrendering much ground, after which a white candle breaks to a new high and
|
||||||
|
/// the uptrend resumes.
|
||||||
|
///
|
||||||
|
/// ```text
|
||||||
|
/// long body = |close − open| >= 0.5 * (high − low)
|
||||||
|
/// bar1 white & long
|
||||||
|
/// bar2 small body gapping up above bar1 (min(o2,c2) > close1)
|
||||||
|
/// bar2, bar3, bar4 each small (|body| <= 0.5 · body1)
|
||||||
|
/// the pullback holds (min low of bars 2..4 > close1 − penetration·body1)
|
||||||
|
/// bar5 white, closing at a new high (close5 > max high of bars 1..4)
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// Output is `+1.0` when the pattern completes and `0.0` otherwise. Mat Hold is a
|
||||||
|
/// single-direction (bullish-only) continuation, so it never emits `−1.0`. The
|
||||||
|
/// first four bars always return `0.0` because the five-bar window is not yet
|
||||||
|
/// filled. `penetration` is how far the pullback may retrace into the first body;
|
||||||
|
/// it defaults to `0.5` (TA-Lib's `CDLMATHOLD` default) and must lie in `[0, 1)`.
|
||||||
|
/// Body thresholds follow the geometric house style rather than TA-Lib's rolling
|
||||||
|
/// averages. Pattern-shape check only — no trend filter is applied; combine with a
|
||||||
|
/// trend indicator for actionable signals.
|
||||||
|
///
|
||||||
|
/// # Signed ±1 encoding
|
||||||
|
///
|
||||||
|
/// This detector emits the uniform candlestick sign convention shared across the
|
||||||
|
/// pattern family — `+1.0` bullish, `0.0` no pattern — so it drops straight into
|
||||||
|
/// a machine-learning feature matrix as a single dimension.
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// use wickra_core::{Candle, Indicator, MatHold};
|
||||||
|
///
|
||||||
|
/// let mut indicator = MatHold::new();
|
||||||
|
/// indicator.update(Candle::new(10.0, 15.1, 9.9, 15.0, 1.0, 0).unwrap());
|
||||||
|
/// indicator.update(Candle::new(16.0, 16.1, 15.4, 15.5, 1.0, 1).unwrap());
|
||||||
|
/// indicator.update(Candle::new(15.5, 15.6, 14.9, 15.0, 1.0, 2).unwrap());
|
||||||
|
/// indicator.update(Candle::new(15.0, 15.1, 14.4, 14.5, 1.0, 3).unwrap());
|
||||||
|
/// let out = indicator
|
||||||
|
/// .update(Candle::new(14.5, 17.1, 14.4, 17.0, 1.0, 4).unwrap());
|
||||||
|
/// assert_eq!(out, Some(1.0));
|
||||||
|
/// ```
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct MatHold {
|
||||||
|
penetration: f64,
|
||||||
|
c1: Option<Candle>,
|
||||||
|
c2: Option<Candle>,
|
||||||
|
c3: Option<Candle>,
|
||||||
|
c4: Option<Candle>,
|
||||||
|
has_emitted: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for MatHold {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MatHold {
|
||||||
|
/// Construct a Mat Hold detector with the default 0.5 penetration.
|
||||||
|
pub const fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
penetration: 0.5,
|
||||||
|
c1: None,
|
||||||
|
c2: None,
|
||||||
|
c3: None,
|
||||||
|
c4: None,
|
||||||
|
has_emitted: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Construct a Mat Hold detector with a custom penetration fraction.
|
||||||
|
///
|
||||||
|
/// `penetration` must lie in `[0, 1)`.
|
||||||
|
pub fn with_penetration(penetration: f64) -> Result<Self> {
|
||||||
|
if !(0.0..1.0).contains(&penetration) {
|
||||||
|
return Err(Error::InvalidPeriod {
|
||||||
|
message: "mat hold penetration must lie in [0, 1)",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Ok(Self {
|
||||||
|
penetration,
|
||||||
|
c1: None,
|
||||||
|
c2: None,
|
||||||
|
c3: None,
|
||||||
|
c4: None,
|
||||||
|
has_emitted: false,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Configured penetration fraction.
|
||||||
|
pub fn penetration(&self) -> f64 {
|
||||||
|
self.penetration
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Indicator for MatHold {
|
||||||
|
type Input = Candle;
|
||||||
|
type Output = f64;
|
||||||
|
|
||||||
|
fn update(&mut self, candle: Candle) -> Option<f64> {
|
||||||
|
self.has_emitted = true;
|
||||||
|
let bar1 = self.c1;
|
||||||
|
let bar2 = self.c2;
|
||||||
|
let bar3 = self.c3;
|
||||||
|
let bar4 = self.c4;
|
||||||
|
self.c1 = self.c2;
|
||||||
|
self.c2 = self.c3;
|
||||||
|
self.c3 = self.c4;
|
||||||
|
self.c4 = Some(candle);
|
||||||
|
let (Some(bar1), Some(bar2), Some(bar3), Some(bar4)) = (bar1, bar2, bar3, bar4) else {
|
||||||
|
return Some(0.0);
|
||||||
|
};
|
||||||
|
let range1 = bar1.high - bar1.low;
|
||||||
|
if range1 <= 0.0 {
|
||||||
|
return Some(0.0);
|
||||||
|
}
|
||||||
|
let body1 = bar1.close - bar1.open;
|
||||||
|
if body1 < 0.5 * range1 {
|
||||||
|
return Some(0.0); // bar1 must be a long white body
|
||||||
|
}
|
||||||
|
let small = 0.5 * body1;
|
||||||
|
if (bar2.close - bar2.open).abs() > small
|
||||||
|
|| (bar3.close - bar3.open).abs() > small
|
||||||
|
|| (bar4.close - bar4.open).abs() > small
|
||||||
|
{
|
||||||
|
return Some(0.0); // the three pullback bars must be small
|
||||||
|
}
|
||||||
|
// bar2 gaps up above bar1's body.
|
||||||
|
if bar2.open.min(bar2.close) <= bar1.close {
|
||||||
|
return Some(0.0);
|
||||||
|
}
|
||||||
|
// The pullback must hold above the penetration line.
|
||||||
|
let hold_line = bar1.close - self.penetration * body1;
|
||||||
|
if bar2.low.min(bar3.low).min(bar4.low) <= hold_line {
|
||||||
|
return Some(0.0);
|
||||||
|
}
|
||||||
|
// bar5 breaks to a new high on a white body.
|
||||||
|
let max_high = bar1.high.max(bar2.high).max(bar3.high).max(bar4.high);
|
||||||
|
if candle.close > candle.open && candle.close > max_high {
|
||||||
|
return Some(1.0);
|
||||||
|
}
|
||||||
|
Some(0.0)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn reset(&mut self) {
|
||||||
|
self.c1 = None;
|
||||||
|
self.c2 = None;
|
||||||
|
self.c3 = None;
|
||||||
|
self.c4 = None;
|
||||||
|
self.has_emitted = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn warmup_period(&self) -> usize {
|
||||||
|
5
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_ready(&self) -> bool {
|
||||||
|
self.has_emitted
|
||||||
|
}
|
||||||
|
|
||||||
|
fn name(&self) -> &'static str {
|
||||||
|
"MatHold"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::traits::BatchExt;
|
||||||
|
|
||||||
|
fn c(open: f64, high: f64, low: f64, close: f64, ts: i64) -> Candle {
|
||||||
|
Candle::new(open, high, low, close, 1.0, ts).unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn rejects_invalid_penetration() {
|
||||||
|
assert!(MatHold::with_penetration(-0.01).is_err());
|
||||||
|
assert!(MatHold::with_penetration(1.0).is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn accepts_valid_penetration() {
|
||||||
|
let t = MatHold::with_penetration(0.3).unwrap();
|
||||||
|
assert!((t.penetration() - 0.3).abs() < 1e-12);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn accessors_and_metadata() {
|
||||||
|
let t = MatHold::default();
|
||||||
|
assert_eq!(t.name(), "MatHold");
|
||||||
|
assert_eq!(t.warmup_period(), 5);
|
||||||
|
assert!(!t.is_ready());
|
||||||
|
assert!((t.penetration() - 0.5).abs() < 1e-12);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn mat_hold_is_plus_one() {
|
||||||
|
let mut t = MatHold::new();
|
||||||
|
assert_eq!(t.update(c(10.0, 15.1, 9.9, 15.0, 0)), Some(0.0));
|
||||||
|
assert_eq!(t.update(c(16.0, 16.1, 15.4, 15.5, 1)), Some(0.0));
|
||||||
|
assert_eq!(t.update(c(15.5, 15.6, 14.9, 15.0, 2)), Some(0.0));
|
||||||
|
assert_eq!(t.update(c(15.0, 15.1, 14.4, 14.5, 3)), Some(0.0));
|
||||||
|
assert_eq!(t.update(c(14.5, 17.1, 14.4, 17.0, 4)), Some(1.0));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn pullback_breaks_hold_yields_zero() {
|
||||||
|
let mut t = MatHold::new();
|
||||||
|
t.update(c(10.0, 15.1, 9.9, 15.0, 0));
|
||||||
|
t.update(c(16.0, 16.1, 15.4, 15.5, 1));
|
||||||
|
t.update(c(15.5, 15.6, 14.9, 15.0, 2));
|
||||||
|
// bar4 dips below the hold line (close1 - 0.5*body1 = 12.5).
|
||||||
|
t.update(c(13.0, 13.1, 12.0, 12.4, 3));
|
||||||
|
assert_eq!(t.update(c(14.5, 17.1, 12.0, 17.0, 4)), Some(0.0));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn no_new_high_yields_zero() {
|
||||||
|
let mut t = MatHold::new();
|
||||||
|
t.update(c(10.0, 15.1, 9.9, 15.0, 0));
|
||||||
|
t.update(c(16.0, 16.1, 15.4, 15.5, 1));
|
||||||
|
t.update(c(15.5, 15.6, 14.9, 15.0, 2));
|
||||||
|
t.update(c(15.0, 15.1, 14.4, 14.5, 3));
|
||||||
|
// bar5 white but closes below the prior max high (16.1).
|
||||||
|
assert_eq!(t.update(c(14.5, 16.0, 14.4, 15.9, 4)), Some(0.0));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn first_four_bars_return_zero() {
|
||||||
|
let mut t = MatHold::new();
|
||||||
|
assert_eq!(t.update(c(10.0, 15.1, 9.9, 15.0, 0)), Some(0.0));
|
||||||
|
assert_eq!(t.update(c(16.0, 16.1, 15.4, 15.5, 1)), Some(0.0));
|
||||||
|
assert_eq!(t.update(c(15.5, 15.6, 14.9, 15.0, 2)), Some(0.0));
|
||||||
|
assert_eq!(t.update(c(15.0, 15.1, 14.4, 14.5, 3)), Some(0.0));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn batch_equals_streaming() {
|
||||||
|
let candles: Vec<Candle> = (0..40)
|
||||||
|
.map(|i| {
|
||||||
|
let base = 100.0 + i as f64;
|
||||||
|
c(base, base + 5.2, base - 0.1, base + 5.0, i)
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
let mut a = MatHold::new();
|
||||||
|
let mut b = MatHold::new();
|
||||||
|
assert_eq!(
|
||||||
|
a.batch(&candles),
|
||||||
|
candles.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn reset_clears_state() {
|
||||||
|
let mut t = MatHold::new();
|
||||||
|
t.update(c(10.0, 15.1, 9.9, 15.0, 0));
|
||||||
|
t.update(c(16.0, 16.1, 15.4, 15.5, 1));
|
||||||
|
t.update(c(15.5, 15.6, 14.9, 15.0, 2));
|
||||||
|
t.update(c(15.0, 15.1, 14.4, 14.5, 3));
|
||||||
|
t.update(c(14.5, 17.1, 14.4, 17.0, 4));
|
||||||
|
assert!(t.is_ready());
|
||||||
|
t.reset();
|
||||||
|
assert!(!t.is_ready());
|
||||||
|
assert_eq!(t.update(c(10.0, 15.1, 9.9, 15.0, 0)), Some(0.0));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -121,10 +121,13 @@ mod jma;
|
|||||||
mod kama;
|
mod kama;
|
||||||
mod kelly_criterion;
|
mod kelly_criterion;
|
||||||
mod keltner;
|
mod keltner;
|
||||||
|
mod kicking;
|
||||||
|
mod kicking_by_length;
|
||||||
mod kst;
|
mod kst;
|
||||||
mod kurtosis;
|
mod kurtosis;
|
||||||
mod kvo;
|
mod kvo;
|
||||||
mod kyles_lambda;
|
mod kyles_lambda;
|
||||||
|
mod ladder_bottom;
|
||||||
mod laguerre_rsi;
|
mod laguerre_rsi;
|
||||||
mod lead_lag_cross_correlation;
|
mod lead_lag_cross_correlation;
|
||||||
mod linreg;
|
mod linreg;
|
||||||
@@ -140,6 +143,7 @@ mod mama;
|
|||||||
mod market_facilitation_index;
|
mod market_facilitation_index;
|
||||||
mod marubozu;
|
mod marubozu;
|
||||||
mod mass_index;
|
mod mass_index;
|
||||||
|
mod mat_hold;
|
||||||
mod max_drawdown;
|
mod max_drawdown;
|
||||||
mod mcginley_dynamic;
|
mod mcginley_dynamic;
|
||||||
mod median_absolute_deviation;
|
mod median_absolute_deviation;
|
||||||
@@ -189,6 +193,7 @@ mod rsi;
|
|||||||
mod rvi;
|
mod rvi;
|
||||||
mod rvi_volatility;
|
mod rvi_volatility;
|
||||||
mod rwi;
|
mod rwi;
|
||||||
|
mod separating_lines;
|
||||||
mod sharpe_ratio;
|
mod sharpe_ratio;
|
||||||
mod shooting_star;
|
mod shooting_star;
|
||||||
mod signed_volume;
|
mod signed_volume;
|
||||||
@@ -390,10 +395,13 @@ pub use jma::Jma;
|
|||||||
pub use kama::Kama;
|
pub use kama::Kama;
|
||||||
pub use kelly_criterion::KellyCriterion;
|
pub use kelly_criterion::KellyCriterion;
|
||||||
pub use keltner::{Keltner, KeltnerOutput};
|
pub use keltner::{Keltner, KeltnerOutput};
|
||||||
|
pub use kicking::Kicking;
|
||||||
|
pub use kicking_by_length::KickingByLength;
|
||||||
pub use kst::{Kst, KstOutput};
|
pub use kst::{Kst, KstOutput};
|
||||||
pub use kurtosis::Kurtosis;
|
pub use kurtosis::Kurtosis;
|
||||||
pub use kvo::Kvo;
|
pub use kvo::Kvo;
|
||||||
pub use kyles_lambda::KylesLambda;
|
pub use kyles_lambda::KylesLambda;
|
||||||
|
pub use ladder_bottom::LadderBottom;
|
||||||
pub use laguerre_rsi::LaguerreRsi;
|
pub use laguerre_rsi::LaguerreRsi;
|
||||||
pub use lead_lag_cross_correlation::{LeadLagCrossCorrelation, LeadLagCrossCorrelationOutput};
|
pub use lead_lag_cross_correlation::{LeadLagCrossCorrelation, LeadLagCrossCorrelationOutput};
|
||||||
pub use linreg::LinearRegression;
|
pub use linreg::LinearRegression;
|
||||||
@@ -409,6 +417,7 @@ pub use mama::{Mama, MamaOutput};
|
|||||||
pub use market_facilitation_index::MarketFacilitationIndex;
|
pub use market_facilitation_index::MarketFacilitationIndex;
|
||||||
pub use marubozu::Marubozu;
|
pub use marubozu::Marubozu;
|
||||||
pub use mass_index::MassIndex;
|
pub use mass_index::MassIndex;
|
||||||
|
pub use mat_hold::MatHold;
|
||||||
pub use max_drawdown::MaxDrawdown;
|
pub use max_drawdown::MaxDrawdown;
|
||||||
pub use mcginley_dynamic::McGinleyDynamic;
|
pub use mcginley_dynamic::McGinleyDynamic;
|
||||||
pub use median_absolute_deviation::MedianAbsoluteDeviation;
|
pub use median_absolute_deviation::MedianAbsoluteDeviation;
|
||||||
@@ -458,6 +467,7 @@ pub use rsi::Rsi;
|
|||||||
pub use rvi::Rvi;
|
pub use rvi::Rvi;
|
||||||
pub use rvi_volatility::RviVolatility;
|
pub use rvi_volatility::RviVolatility;
|
||||||
pub use rwi::{Rwi, RwiOutput};
|
pub use rwi::{Rwi, RwiOutput};
|
||||||
|
pub use separating_lines::SeparatingLines;
|
||||||
pub use sharpe_ratio::SharpeRatio;
|
pub use sharpe_ratio::SharpeRatio;
|
||||||
pub use shooting_star::ShootingStar;
|
pub use shooting_star::ShootingStar;
|
||||||
pub use signed_volume::SignedVolume;
|
pub use signed_volume::SignedVolume;
|
||||||
@@ -830,6 +840,11 @@ pub const FAMILIES: &[(&str, &[&str])] = &[
|
|||||||
"OnNeck",
|
"OnNeck",
|
||||||
"InNeck",
|
"InNeck",
|
||||||
"Thrusting",
|
"Thrusting",
|
||||||
|
"SeparatingLines",
|
||||||
|
"Kicking",
|
||||||
|
"KickingByLength",
|
||||||
|
"LadderBottom",
|
||||||
|
"MatHold",
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
@@ -921,6 +936,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, 264, "FAMILIES total drifted from indicator count");
|
assert_eq!(total, 269, "FAMILIES total drifted from indicator count");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,202 @@
|
|||||||
|
//! Separating Lines candlestick pattern.
|
||||||
|
|
||||||
|
use crate::ohlcv::Candle;
|
||||||
|
use crate::traits::Indicator;
|
||||||
|
|
||||||
|
/// Separating Lines — a 2-bar continuation. After a counter-trend candle, the next
|
||||||
|
/// candle of the *opposite* colour opens right back at the prior open and runs as
|
||||||
|
/// an opening marubozu in the trend direction, so the trend "separates" from the
|
||||||
|
/// pullback and resumes.
|
||||||
|
///
|
||||||
|
/// ```text
|
||||||
|
/// long body = |close − open| >= 0.5 * (high − low)
|
||||||
|
/// bar1, bar2 opposite colours
|
||||||
|
/// bar2 opens at bar1's open (|open2 − open1| <= 0.05 · range1)
|
||||||
|
/// bar2 is a long opening marubozu in its direction
|
||||||
|
/// white bar2: open2 == low2 (no lower shadow) -> +1.0
|
||||||
|
/// black bar2: open2 == high2 (no upper shadow) -> −1.0
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// Output is `+1.0` (bullish continuation) or `−1.0` (bearish continuation) when
|
||||||
|
/// the pattern completes and `0.0` otherwise. The first bar always returns `0.0`
|
||||||
|
/// because the two-bar window is not yet filled. Open-equality and marubozu
|
||||||
|
/// thresholds follow the geometric house style rather than TA-Lib's rolling
|
||||||
|
/// averages. Pattern-shape check only — no trend filter is applied; combine with
|
||||||
|
/// a trend indicator for actionable signals.
|
||||||
|
///
|
||||||
|
/// # Signed ±1 encoding
|
||||||
|
///
|
||||||
|
/// This detector emits the uniform candlestick sign convention shared across the
|
||||||
|
/// pattern family — `+1.0` bullish, `−1.0` bearish, `0.0` no pattern — so it
|
||||||
|
/// drops straight into a machine-learning feature matrix where the two directions
|
||||||
|
/// occupy a single dimension.
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// use wickra_core::{Candle, Indicator, SeparatingLines};
|
||||||
|
///
|
||||||
|
/// let mut indicator = SeparatingLines::new();
|
||||||
|
/// indicator.update(Candle::new(12.0, 12.1, 9.9, 10.0, 1.0, 0).unwrap());
|
||||||
|
/// let out = indicator
|
||||||
|
/// .update(Candle::new(12.0, 14.1, 12.0, 14.0, 1.0, 1).unwrap());
|
||||||
|
/// assert_eq!(out, Some(1.0));
|
||||||
|
/// ```
|
||||||
|
#[derive(Debug, Clone, Default)]
|
||||||
|
pub struct SeparatingLines {
|
||||||
|
prev: Option<Candle>,
|
||||||
|
has_emitted: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SeparatingLines {
|
||||||
|
/// Construct a new Separating Lines detector.
|
||||||
|
pub const fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
prev: None,
|
||||||
|
has_emitted: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Indicator for SeparatingLines {
|
||||||
|
type Input = Candle;
|
||||||
|
type Output = f64;
|
||||||
|
|
||||||
|
fn update(&mut self, candle: Candle) -> Option<f64> {
|
||||||
|
self.has_emitted = true;
|
||||||
|
let prev = self.prev;
|
||||||
|
self.prev = Some(candle);
|
||||||
|
let Some(bar1) = prev else {
|
||||||
|
return Some(0.0);
|
||||||
|
};
|
||||||
|
let range1 = bar1.high - bar1.low;
|
||||||
|
let range2 = candle.high - candle.low;
|
||||||
|
if range1 <= 0.0 || range2 <= 0.0 {
|
||||||
|
return Some(0.0);
|
||||||
|
}
|
||||||
|
// Opens must coincide.
|
||||||
|
if (candle.open - bar1.open).abs() > 0.05 * range1 {
|
||||||
|
return Some(0.0);
|
||||||
|
}
|
||||||
|
let body2 = candle.close - candle.open;
|
||||||
|
if body2.abs() < 0.5 * range2 {
|
||||||
|
return Some(0.0); // bar2 must be a long body
|
||||||
|
}
|
||||||
|
let tol = 0.05 * range2;
|
||||||
|
// Bullish: bar1 black, bar2 a long white opening marubozu (no lower wick).
|
||||||
|
if bar1.close < bar1.open && body2 > 0.0 && candle.open - candle.low <= tol {
|
||||||
|
return Some(1.0);
|
||||||
|
}
|
||||||
|
// Bearish: bar1 white, bar2 a long black opening marubozu (no upper wick).
|
||||||
|
if bar1.close > bar1.open && body2 < 0.0 && candle.high - candle.open <= tol {
|
||||||
|
return Some(-1.0);
|
||||||
|
}
|
||||||
|
Some(0.0)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn reset(&mut self) {
|
||||||
|
self.prev = None;
|
||||||
|
self.has_emitted = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn warmup_period(&self) -> usize {
|
||||||
|
2
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_ready(&self) -> bool {
|
||||||
|
self.has_emitted
|
||||||
|
}
|
||||||
|
|
||||||
|
fn name(&self) -> &'static str {
|
||||||
|
"SeparatingLines"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::traits::BatchExt;
|
||||||
|
|
||||||
|
fn c(open: f64, high: f64, low: f64, close: f64, ts: i64) -> Candle {
|
||||||
|
Candle::new(open, high, low, close, 1.0, ts).unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn accessors_and_metadata() {
|
||||||
|
let t = SeparatingLines::new();
|
||||||
|
assert_eq!(t.name(), "SeparatingLines");
|
||||||
|
assert_eq!(t.warmup_period(), 2);
|
||||||
|
assert!(!t.is_ready());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn bullish_separating_lines_is_plus_one() {
|
||||||
|
let mut t = SeparatingLines::new();
|
||||||
|
assert_eq!(t.update(c(12.0, 12.1, 9.9, 10.0, 0)), Some(0.0));
|
||||||
|
assert_eq!(t.update(c(12.0, 14.1, 12.0, 14.0, 1)), Some(1.0));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn bearish_separating_lines_is_minus_one() {
|
||||||
|
let mut t = SeparatingLines::new();
|
||||||
|
assert_eq!(t.update(c(10.0, 12.1, 9.9, 12.0, 0)), Some(0.0));
|
||||||
|
assert_eq!(t.update(c(10.0, 10.0, 7.9, 8.0, 1)), Some(-1.0));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn same_color_yields_zero() {
|
||||||
|
let mut t = SeparatingLines::new();
|
||||||
|
// Both white -> not separating (need opposite colours).
|
||||||
|
t.update(c(12.0, 14.1, 11.9, 14.0, 0));
|
||||||
|
assert_eq!(t.update(c(12.0, 14.1, 12.0, 14.0, 1)), Some(0.0));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn different_open_yields_zero() {
|
||||||
|
let mut t = SeparatingLines::new();
|
||||||
|
t.update(c(12.0, 12.1, 9.9, 10.0, 0));
|
||||||
|
// bar2 opens far from bar1's open.
|
||||||
|
assert_eq!(t.update(c(13.0, 15.1, 13.0, 15.0, 1)), Some(0.0));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn opening_shadow_yields_zero() {
|
||||||
|
let mut t = SeparatingLines::new();
|
||||||
|
t.update(c(12.0, 12.1, 9.9, 10.0, 0));
|
||||||
|
// White bar2 but it has a lower shadow -> not an opening marubozu.
|
||||||
|
assert_eq!(t.update(c(12.0, 14.1, 11.0, 14.0, 1)), Some(0.0));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn first_bar_returns_zero() {
|
||||||
|
let mut t = SeparatingLines::new();
|
||||||
|
assert_eq!(t.update(c(12.0, 12.1, 9.9, 10.0, 0)), Some(0.0));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn batch_equals_streaming() {
|
||||||
|
let candles: Vec<Candle> = (0..40)
|
||||||
|
.map(|i| {
|
||||||
|
let base = 100.0 + i as f64;
|
||||||
|
c(base, base + 2.0, base, base + 1.9, i)
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
let mut a = SeparatingLines::new();
|
||||||
|
let mut b = SeparatingLines::new();
|
||||||
|
assert_eq!(
|
||||||
|
a.batch(&candles),
|
||||||
|
candles.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn reset_clears_state() {
|
||||||
|
let mut t = SeparatingLines::new();
|
||||||
|
t.update(c(12.0, 12.1, 9.9, 10.0, 0));
|
||||||
|
t.update(c(12.0, 14.1, 12.0, 14.0, 1));
|
||||||
|
assert!(t.is_ready());
|
||||||
|
t.reset();
|
||||||
|
assert!(!t.is_ready());
|
||||||
|
assert_eq!(t.update(c(12.0, 12.1, 9.9, 10.0, 0)), Some(0.0));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -75,36 +75,36 @@ pub use indicators::{
|
|||||||
HikkakeModified, HilbertDominantCycle, HistoricalVolatility, Hma, HomingPigeon, HurstChannel,
|
HikkakeModified, HilbertDominantCycle, HistoricalVolatility, Hma, HomingPigeon, HurstChannel,
|
||||||
HurstChannelOutput, HurstExponent, Ichimoku, IchimokuOutput, IdenticalThreeCrows, InNeck,
|
HurstChannelOutput, HurstExponent, Ichimoku, IchimokuOutput, IdenticalThreeCrows, InNeck,
|
||||||
Inertia, InformationRatio, InitialBalance, InitialBalanceOutput, InstantaneousTrendline,
|
Inertia, InformationRatio, InitialBalance, InitialBalanceOutput, InstantaneousTrendline,
|
||||||
InverseFisherTransform, InvertedHammer, Jma, Kama, KellyCriterion, Keltner, KeltnerOutput, Kst,
|
InverseFisherTransform, InvertedHammer, Jma, Kama, KellyCriterion, Keltner, KeltnerOutput,
|
||||||
KstOutput, Kurtosis, Kvo, KylesLambda, LaguerreRsi, LeadLagCrossCorrelation,
|
Kicking, KickingByLength, Kst, KstOutput, Kurtosis, Kvo, KylesLambda, LadderBottom,
|
||||||
LeadLagCrossCorrelationOutput, LinRegAngle, LinRegChannel, LinRegChannelOutput, LinRegSlope,
|
LaguerreRsi, LeadLagCrossCorrelation, LeadLagCrossCorrelationOutput, LinRegAngle,
|
||||||
LinearRegression, LiquidationFeatures, LiquidationFeaturesOutput, LongLeggedDoji,
|
LinRegChannel, LinRegChannelOutput, LinRegSlope, LinearRegression, LiquidationFeatures,
|
||||||
LongShortRatio, MaEnvelope, MaEnvelopeOutput, MacdIndicator, MacdOutput, Mama, MamaOutput,
|
LiquidationFeaturesOutput, LongLeggedDoji, LongShortRatio, MaEnvelope, MaEnvelopeOutput,
|
||||||
MarketFacilitationIndex, Marubozu, MassIndex, MaxDrawdown, McGinleyDynamic,
|
MacdIndicator, MacdOutput, Mama, MamaOutput, MarketFacilitationIndex, Marubozu, MassIndex,
|
||||||
MedianAbsoluteDeviation, MedianPrice, Mfi, Microprice, Mom, MorningDojiStar,
|
MatHold, MaxDrawdown, McGinleyDynamic, MedianAbsoluteDeviation, MedianPrice, Mfi, Microprice,
|
||||||
MorningEveningStar, Natr, Nvi, OIPriceDivergence, OIWeighted, Obv, OmegaRatio, OnNeck,
|
Mom, MorningDojiStar, MorningEveningStar, Natr, Nvi, OIPriceDivergence, OIWeighted, Obv,
|
||||||
OpenInterestDelta, OpeningRange, OpeningRangeOutput, OrderBookImbalanceFull,
|
OmegaRatio, OnNeck, OpenInterestDelta, OpeningRange, OpeningRangeOutput,
|
||||||
OrderBookImbalanceTop1, OrderBookImbalanceTopN, PainIndex, PairSpreadZScore, PairwiseBeta,
|
OrderBookImbalanceFull, OrderBookImbalanceTop1, OrderBookImbalanceTopN, PainIndex,
|
||||||
ParkinsonVolatility, PearsonCorrelation, PercentB, PercentageTrailingStop, Pgo,
|
PairSpreadZScore, PairwiseBeta, ParkinsonVolatility, PearsonCorrelation, PercentB,
|
||||||
PiercingDarkCloud, Pmo, Ppo, ProfitFactor, Psar, Pvi, QuotedSpread, RSquared, RealizedSpread,
|
PercentageTrailingStop, Pgo, PiercingDarkCloud, Pmo, Ppo, ProfitFactor, Psar, Pvi,
|
||||||
RecoveryFactor, RelativeStrengthAB, RelativeStrengthOutput, RenkoTrailingStop, RickshawMan,
|
QuotedSpread, RSquared, RealizedSpread, RecoveryFactor, RelativeStrengthAB,
|
||||||
Roc, RogersSatchellVolatility, RollingVwap, RoofingFilter, Rsi, Rvi, RviVolatility, Rwi,
|
RelativeStrengthOutput, RenkoTrailingStop, RickshawMan, Roc, RogersSatchellVolatility,
|
||||||
RwiOutput, SharpeRatio, ShootingStar, SignedVolume, SineWave, Skewness, Sma, Smi, Smma,
|
RollingVwap, RoofingFilter, Rsi, Rvi, RviVolatility, Rwi, RwiOutput, SeparatingLines,
|
||||||
SortinoRatio, SpearmanCorrelation, SpinningTop, StandardError, StandardErrorBands,
|
SharpeRatio, ShootingStar, SignedVolume, SineWave, Skewness, Sma, Smi, Smma, SortinoRatio,
|
||||||
StandardErrorBandsOutput, StarcBands, StarcBandsOutput, Stc, StdDev, StepTrailingStop,
|
SpearmanCorrelation, SpinningTop, StandardError, StandardErrorBands, StandardErrorBandsOutput,
|
||||||
StochRsi, Stochastic, StochasticOutput, SuperSmoother, SuperTrend, SuperTrendOutput,
|
StarcBands, StarcBandsOutput, Stc, StdDev, StepTrailingStop, StochRsi, Stochastic,
|
||||||
TakerBuySellRatio, TdCombo, TdCountdown, TdDeMarker, TdDifferential, TdLines, TdLinesOutput,
|
StochasticOutput, SuperSmoother, SuperTrend, SuperTrendOutput, TakerBuySellRatio, TdCombo,
|
||||||
TdOpen, TdPressure, TdRangeProjection, TdRangeProjectionOutput, TdRei, TdRiskLevel,
|
TdCountdown, TdDeMarker, TdDifferential, TdLines, TdLinesOutput, TdOpen, TdPressure,
|
||||||
TdRiskLevelOutput, TdSequential, TdSequentialOutput, TdSetup, Tema, TermStructureBasis,
|
TdRangeProjection, TdRangeProjectionOutput, TdRei, TdRiskLevel, TdRiskLevelOutput,
|
||||||
ThreeInside, ThreeLineStrike, ThreeOutside, ThreeSoldiersOrCrows, ThreeStarsInSouth, Thrusting,
|
TdSequential, TdSequentialOutput, TdSetup, Tema, TermStructureBasis, ThreeInside,
|
||||||
Tii, TradeImbalance, TreynorRatio, Trima, Trix, TrueRange, Tsi, Tsv, TtmSqueeze,
|
ThreeLineStrike, ThreeOutside, ThreeSoldiersOrCrows, ThreeStarsInSouth, Thrusting, Tii,
|
||||||
TtmSqueezeOutput, Tweezer, TwoCrows, TypicalPrice, UlcerIndex, UltimateOscillator,
|
TradeImbalance, TreynorRatio, Trima, Trix, TrueRange, Tsi, Tsv, TtmSqueeze, TtmSqueezeOutput,
|
||||||
UpsideGapTwoCrows, ValueArea, ValueAreaOutput, ValueAtRisk, Variance, VerticalHorizontalFilter,
|
Tweezer, TwoCrows, TypicalPrice, UlcerIndex, UltimateOscillator, UpsideGapTwoCrows, ValueArea,
|
||||||
Vidya, VoltyStop, VolumeOscillator, VolumePriceTrend, Vortex, VortexOutput, Vwap,
|
ValueAreaOutput, ValueAtRisk, Variance, VerticalHorizontalFilter, Vidya, VoltyStop,
|
||||||
VwapStdDevBands, VwapStdDevBandsOutput, Vwma, Vzo, WaveTrend, WaveTrendOutput, WeightedClose,
|
VolumeOscillator, VolumePriceTrend, Vortex, VortexOutput, Vwap, VwapStdDevBands,
|
||||||
WilliamsFractals, WilliamsFractalsOutput, WilliamsR, Wma, WoodiePivots, WoodiePivotsOutput,
|
VwapStdDevBandsOutput, Vwma, Vzo, WaveTrend, WaveTrendOutput, WeightedClose, WilliamsFractals,
|
||||||
YangZhangVolatility, YoyoExit, ZScore, ZeroLagMacd, ZeroLagMacdOutput, ZigZag, ZigZagOutput,
|
WilliamsFractalsOutput, WilliamsR, Wma, WoodiePivots, WoodiePivotsOutput, YangZhangVolatility,
|
||||||
Zlema, FAMILIES, T3,
|
YoyoExit, ZScore, ZeroLagMacd, ZeroLagMacdOutput, ZigZag, ZigZagOutput, Zlema, FAMILIES, T3,
|
||||||
};
|
};
|
||||||
// `FootprintLevel` is a row element of `FootprintOutput`, re-exported on its own
|
// `FootprintLevel` is a row element of `FootprintOutput`, re-exported on its own
|
||||||
// line so the indicator-count tooling (which scans the braced block above and
|
// line so the indicator-count tooling (which scans the braced block above and
|
||||||
|
|||||||
+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 **269 indicators** across
|
- A per-indicator deep dive for every one of the **274 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 &
|
||||||
|
|||||||
@@ -23,7 +23,7 @@
|
|||||||
|
|
||||||
use libfuzzer_sys::fuzz_target;
|
use libfuzzer_sys::fuzz_target;
|
||||||
use wickra_core::{
|
use wickra_core::{
|
||||||
AbandonedBaby, AccelerationBands, AcceleratorOscillator, AdOscillator, Adl, AdvanceBlock, Adx, Adxr, Alligator, AnchoredVwap, Aroon, AroonOscillator, Atr, AtrBands, AtrTrailingStop, AwesomeOscillator, AwesomeOscillatorHistogram, BalanceOfPower, BatchExt, BeltHold, Breakaway, Camarilla, Candle, Cci, ChaikinMoneyFlow, ChaikinOscillator, ChaikinVolatility, ChandeKrollStop, ChandelierExit, ChoppinessIndex, ClassicPivots, Counterattack, DemandIndex, DemarkPivots, Doji, DojiStar, Donchian, DonchianStop, DragonflyDoji, EaseOfMovement, Engulfing, EveningDojiStar, Evwma, FibonacciPivots, ForceIndex, FractalChaosBands, GapSideBySideWhite, GarmanKlassVolatility, GravestoneDoji, Hammer, HangingMan, Harami, HeikinAshi, HiLoActivator, HighWave, Hikkake, HikkakeModified, HomingPigeon, HurstChannel, Ichimoku, IdenticalThreeCrows, InNeck, Indicator, Inertia, InitialBalance, InvertedHammer, Keltner, Kvo, LongLeggedDoji, MarketFacilitationIndex, Marubozu, MassIndex, MedianPrice, Mfi, MorningDojiStar, MorningEveningStar, Natr, Nvi, Obv, OnNeck, OpeningRange, ParkinsonVolatility, Pgo, PiercingDarkCloud, Psar, Pvi, RickshawMan, RogersSatchellVolatility, RollingVwap, Rvi, Rwi, ShootingStar, Smi, SpinningTop, StarcBands, Stochastic, SuperTrend, TdCombo, TdCountdown, TdDeMarker, TdDifferential, TdLines, TdOpen, TdPressure, TdRangeProjection, TdRei, TdRiskLevel, TdSequential, TdSetup, ThreeInside, ThreeLineStrike, ThreeOutside, ThreeSoldiersOrCrows, ThreeStarsInSouth, Thrusting, TrueRange, Tsv, TtmSqueeze, Tweezer, TwoCrows, TypicalPrice, UltimateOscillator, UpsideGapTwoCrows, ValueArea, VoltyStop, VolumeOscillator, VolumePriceTrend, Vortex, Vwap, VwapStdDevBands, Vwma, Vzo, WaveTrend, WeightedClose, WilliamsFractals, WilliamsR, WoodiePivots, YangZhangVolatility, YoyoExit, ZigZag
|
AbandonedBaby, AccelerationBands, AcceleratorOscillator, AdOscillator, Adl, AdvanceBlock, Adx, Adxr, Alligator, AnchoredVwap, Aroon, AroonOscillator, Atr, AtrBands, AtrTrailingStop, AwesomeOscillator, AwesomeOscillatorHistogram, BalanceOfPower, BatchExt, BeltHold, Breakaway, Camarilla, Candle, Cci, ChaikinMoneyFlow, ChaikinOscillator, ChaikinVolatility, ChandeKrollStop, ChandelierExit, ChoppinessIndex, ClassicPivots, Counterattack, DemandIndex, DemarkPivots, Doji, DojiStar, Donchian, DonchianStop, DragonflyDoji, EaseOfMovement, Engulfing, EveningDojiStar, Evwma, FibonacciPivots, ForceIndex, FractalChaosBands, GapSideBySideWhite, GarmanKlassVolatility, GravestoneDoji, Hammer, HangingMan, Harami, HeikinAshi, HiLoActivator, HighWave, Hikkake, HikkakeModified, HomingPigeon, HurstChannel, Ichimoku, IdenticalThreeCrows, InNeck, Indicator, Inertia, InitialBalance, InvertedHammer, Keltner, Kicking, KickingByLength, Kvo, LadderBottom, LongLeggedDoji, MarketFacilitationIndex, Marubozu, MassIndex, MatHold, MedianPrice, Mfi, MorningDojiStar, MorningEveningStar, Natr, Nvi, Obv, OnNeck, OpeningRange, ParkinsonVolatility, Pgo, PiercingDarkCloud, Psar, Pvi, RickshawMan, RogersSatchellVolatility, RollingVwap, Rvi, Rwi, SeparatingLines, ShootingStar, Smi, SpinningTop, StarcBands, Stochastic, SuperTrend, TdCombo, TdCountdown, TdDeMarker, TdDifferential, TdLines, TdOpen, TdPressure, TdRangeProjection, TdRei, TdRiskLevel, TdSequential, TdSetup, ThreeInside, ThreeLineStrike, ThreeOutside, ThreeSoldiersOrCrows, ThreeStarsInSouth, Thrusting, TrueRange, Tsv, TtmSqueeze, Tweezer, TwoCrows, TypicalPrice, UltimateOscillator, UpsideGapTwoCrows, ValueArea, VoltyStop, VolumeOscillator, VolumePriceTrend, Vortex, Vwap, VwapStdDevBands, Vwma, Vzo, WaveTrend, WeightedClose, WilliamsFractals, WilliamsR, WoodiePivots, YangZhangVolatility, YoyoExit, ZigZag
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Convert a flat `f64` stream into a `Vec<Candle>` by chunking it into
|
/// Convert a flat `f64` stream into a `Vec<Candle>` by chunking it into
|
||||||
@@ -278,6 +278,11 @@ fuzz_target!(|data: Vec<f64>| {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// --- Candlestick Patterns (family 14) ---
|
// --- Candlestick Patterns (family 14) ---
|
||||||
|
drive(MatHold::new, &candles);
|
||||||
|
drive(LadderBottom::new, &candles);
|
||||||
|
drive(KickingByLength::new, &candles);
|
||||||
|
drive(Kicking::new, &candles);
|
||||||
|
drive(SeparatingLines::new, &candles);
|
||||||
drive(Thrusting::new, &candles);
|
drive(Thrusting::new, &candles);
|
||||||
drive(InNeck::new, &candles);
|
drive(InNeck::new, &candles);
|
||||||
drive(OnNeck::new, &candles);
|
drive(OnNeck::new, &candles);
|
||||||
|
|||||||
Reference in New Issue
Block a user