//! C ABI for Wickra — the hub every C-capable language (C, C++, Go, C#, Java, R)
//! links against. Each indicator is exposed as `extern "C"` functions over an
//! opaque handle:
//!
//! - `wickra_<ind>_new(...)` — construct; returns `NULL` on invalid parameters.
//! - `wickra_<ind>_update(h, ...)` — feed one point; returns the output, or `NaN`
//!   while warming up / if `h` is `NULL` / if the inputs are invalid.
//! - `wickra_<ind>_batch(h, ..., out, n)` — write one output per input into the
//!   caller-owned `out` buffer (length `n`), `NaN` at warmup positions.
//! - `wickra_<ind>_warmup_period(h)` — updates needed before a non-`NaN` output
//!   (`0` on a `NULL` handle).
//! - `wickra_<ind>_is_ready(h)` — whether enough input has been consumed to emit
//!   a value (`false` on a `NULL` handle). Both are absent for the alt-chart bar
//!   builders, which have no warmup.
//! - `wickra_<ind>_name(h)` — canonical indicator name as a NUL-terminated C
//!   string with `'static` lifetime (`NULL` on a `NULL` handle). Present for every
//!   indicator and bar builder.
//! - `wickra_<ind>_reset(h)` — clear all state.
//! - `wickra_<ind>_free(h)` — destroy the handle. Every `_new` must be paired
//!   with exactly one `_free`; there is no RAII across the C boundary.
//!
//! GENERATED from the Wickra core. Do not edit by hand — regenerate from the
//! core and commit this file together with `include/wickra.h`.

use core::ffi::c_char;
use core::ptr;
use core::slice;
use std::ffi::CString;
use std::sync::OnceLock;

use wickra_core::{
    AbandonedBaby, Abcd, AbsoluteBreadthIndex, AccelerationBands, AcceleratorOscillator,
    AdOscillator, AdVolumeLine, AdaptiveCci, AdaptiveCycle, AdaptiveLaguerreFilter, AdaptiveRsi,
    Adl, AdvanceBlock, AdvanceDecline, AdvanceDeclineRatio, Adx, Adxr, Alligator, Alma, Alpha,
    AmihudIlliquidity, AnchoredRsi, AnchoredVwap, AndrewsPitchfork, Apo, Aroon, AroonOscillator,
    Atr, AtrBands, AtrRatchet, AtrTrailingStop, AutoFib, Autocorrelation,
    AutocorrelationPeriodogram, AverageDailyRange, AverageDrawdown, AvgPrice, AwesomeOscillator,
    AwesomeOscillatorHistogram, BalanceOfPower, BandpassFilter, BarBuilder, Bat, BeltHold, Beta,
    BetaNeutralSpread, BetterVolume, BipowerVariation, BodySizePct, BollingerBands,
    BollingerBandwidth, BomarBands, BreadthThrust, Breakaway, BullishPercentIndex, BurkeRatio,
    Butterfly, CalendarSpread, CalmarRatio, Camarilla, Candle, CandleVolume, Cci, CenterOfGravity,
    CentralPivotRange, Cfo, ChaikinMoneyFlow, ChaikinOscillator, ChaikinVolatility,
    ChandeKrollStop, ChandelierExit, ChoppinessIndex, ClassicPivots, CloseVsOpen, ClosingMarubozu,
    Cmo, CoefficientOfVariation, Cointegration, CommonSenseRatio, CompositeProfile,
    ConcealingBabySwallow, ConditionalValueAtRisk, ConnorsRsi, Coppock, CorrelationTrendIndicator,
    Counterattack, Crab, CrossSection, CumulativeVolumeDelta, CumulativeVolumeIndex, CupAndHandle,
    CyberneticCycle, Cypher, DayOfWeekProfile, Decycler, DecyclerOscillator, Dema, DemandIndex,
    DemarkPivots, DepthSlope, DerivativeOscillator, DerivativesTick, DetrendedStdDev,
    DisparityIndex, DistanceSsd, Doji, DojiStar, DollarBars, Donchian, DonchianStop,
    DoubleBollinger, DoubleTopBottom, DownsideGapThreeMethods, Dpo, DragonflyDoji,
    DrawdownDuration, DumplingTop, Dx, DynamicMomentumIndex, EaseOfMovement, EffectiveSpread,
    EhlersStochastic, Ehma, ElderImpulse, ElderRay, ElderSafeZone, Ema, EmpiricalModeDecomposition,
    Engulfing, Equivolume, EstimatedLeverageRatio, EvenBetterSinewave, EveningDojiStar, Evwma,
    EwmaVolatility, Expectancy, FallingThreeMethods, Fama, FibArcs, FibChannel, FibConfluence,
    FibExtension, FibFan, FibProjection, FibRetracement, FibTimeZones, FibonacciPivots, FisherRsi,
    FisherTransform, FlagPennant, Footprint, ForceIndex, FractalChaosBands, Frama, FryPanBottom,
    FundingBasis, FundingImpliedApr, FundingRate, FundingRateMean, FundingRateZScore,
    GainLossRatio, GainToPainRatio, GapSideBySideWhite, Garch11, GarmanKlassVolatility, Gartley,
    GatorOscillator, GeneralizedDema, GeometricMa, GoldenPocket, GrangerCausality, GravestoneDoji,
    Hammer, HangingMan, Harami, HaramiCross, HasbrouckInformationShare, HeadAndShoulders,
    HeikinAshi, HeikinAshiOscillator, HiLoActivator, HighLowIndex, HighLowRange,
    HighLowVolumeNodes, HighWave, HighpassFilter, Hikkake, HikkakeModified, HilbertDominantCycle,
    HistoricalVolatility, Hma, HoltWinters, HomingPigeon, HtDcPhase, HtPhasor, HtTrendMode,
    HurstChannel, HurstExponent, Ichimoku, IdenticalThreeCrows, ImbalanceBars, InNeck, Indicator,
    Inertia, InformationRatio, InitialBalance, InstantaneousTrendline, IntradayIntensity,
    IntradayMomentumIndex, IntradayVolatilityProfile, InverseFisherTransform, InvertedHammer,
    JarqueBera, Jma, JumpIndicator, KRatio, KagiBars, KalmanHedgeRatio, Kama, KaseDevStop,
    KasePermissionStochastic, KellyCriterion, Keltner, KendallTau, Kicking, KickingByLength, Kst,
    Kurtosis, Kvo, KylesLambda, LadderBottom, LaguerreRsi, LeadLagCrossCorrelation, Level,
    LinRegAngle, LinRegChannel, LinRegIntercept, LinRegSlope, LinearRegression,
    LiquidationFeatures, LogReturn, LongLeggedDoji, LongLine, LongShortRatio, M2Measure,
    MaEnvelope, MaType, MacdExt, MacdFix, MacdHistogram, MacdIndicator, Mama,
    MarketFacilitationIndex, MartinRatio, Marubozu, MassIndex, MatHold, MatchingLow, MaxDrawdown,
    McClellanOscillator, McClellanSummationIndex, McGinleyDynamic, MedianAbsoluteDeviation,
    MedianChannel, MedianMa, MedianPrice, Member, Mfi, Microprice, MidPoint, MidPrice, MinusDi,
    MinusDm, ModifiedMaStop, Mom, MorningDojiStar, MorningEveningStar, MurreyMathLines, NakedPoc,
    Natr, NewHighsNewLows, NewPriceLines, Nrtr, Nvi, OIPriceDivergence, OIWeighted, Obv,
    OiToVolumeRatio, OmegaRatio, OnNeck, OpenInterestDelta, OpenInterestMomentum, OpeningMarubozu,
    OpeningRange, OrderBook, OrderBookImbalanceFull, OrderBookImbalanceTop1,
    OrderBookImbalanceTopN, OrderFlowImbalance, OuHalfLife, OvernightGap, OvernightIntradayReturn,
    PainIndex, PairSpreadZScore, PairwiseBeta, ParkinsonVolatility, PearsonCorrelation,
    PercentAboveMa, PercentB, PercentageTrailingStop, PerpetualPremiumIndex, Pgo,
    PiercingDarkCloud, Pin, PivotReversal, PlusDi, PlusDm, Pmo, PointAndFigureBars,
    PolarizedFractalEfficiency, Ppo, PpoHistogram, ProfileShape, ProfitFactor, ProjectionBands,
    ProjectionOscillator, Psar, Pvi, Qqe, Qstick, QuartileBands, QuotedSpread, RSquared, RangeBars,
    RealizedSpread, RealizedVolatility, RecoveryFactor, RectangleRange, Reflex, RegimeLabel,
    RelativeStrengthAB, RenkoBars, RenkoTrailingStop, RickshawMan, RisingThreeMethods, Rmi, Roc,
    Rocp, Rocr, Rocr100, RogersSatchellVolatility, RollMeasure, RollingCorrelation,
    RollingCovariance, RollingIqr, RollingMinMaxScaler, RollingPercentileRank, RollingQuantile,
    RollingVwap, RoofingFilter, Rsi, Rsx, RunBars, Rvi, RviVolatility, Rwi, SampleEntropy, SarExt,
    SeasonalZScore, SeparatingLines, SessionHighLow, SessionRange, SessionVwap, ShannonEntropy,
    Shark, SharpeRatio, ShootingStar, ShortLine, Side, SignedVolume, SineWave, SineWeightedMa,
    SinglePrints, Skewness, Sma, Smi, Smma, SmoothedHeikinAshi, SortinoRatio, SpearmanCorrelation,
    SpinningTop, SpreadAr1Coefficient, SpreadBollingerBands, SpreadHurst, StalledPattern,
    StandardError, StandardErrorBands, StarcBands, Stc, StdDev, StepTrailingStop, SterlingRatio,
    StickSandwich, StochRsi, Stochastic, StochasticCci, SuperSmoother, SuperTrend, TailRatio,
    TakerBuySellRatio, Takuri, TasukiGap, TdCamouflage, TdClop, TdClopwin, TdCombo, TdCountdown,
    TdDWave, TdDeMarker, TdDifferential, TdLines, TdMovingAverage, TdOpen, TdPressure,
    TdPropulsion, TdRangeProjection, TdRei, TdRiskLevel, TdSequential, TdSetup, TdTrap, Tema,
    TermStructureBasis, ThreeDrives, ThreeInside, ThreeLineBreak, ThreeLineBreakBars,
    ThreeLineStrike, ThreeOutside, ThreeSoldiersOrCrows, ThreeStarsInSouth, Thrusting, Tick,
    TickBars, TickIndex, Tii, TimeBasedStop, TimeOfDayReturnProfile, TowerTopBottom, TpoProfile,
    Trade, TradeImbalance, TradeQuote, TradeSignAutocorrelation, TradeVolumeIndex, TrendLabel,
    TrendStrengthIndex, Trendflex, TreynorRatio, Triangle, Trima, Trin, TripleTopBottom, Tristar,
    Trix, TrueRange, Tsf, TsfOscillator, Tsi, Tsv, TtmSqueeze, TtmTrend, TurnOfMonth, Tweezer,
    TwiggsMoneyFlow, TwoCrows, TypicalPrice, UlcerIndex, UltimateOscillator, UniqueThreeRiver,
    UniversalOscillator, UpDownVolumeRatio, UpsideGapThreeMethods, UpsideGapTwoCrows,
    UpsidePotentialRatio, ValueArea, ValueAtRisk, Variance, VarianceRatio,
    VerticalHorizontalFilter, Vidya, VolatilityCone, VolatilityOfVolatility, VolatilityRatio,
    VoltyStop, VolumeBars, VolumeByTimeProfile, VolumeOscillator, VolumePriceTrend, VolumeProfile,
    VolumeRsi, VolumeWeightedMacd, VolumeWeightedSr, Vortex, Vpin, Vwap, VwapStdDevBands, Vwma,
    Vzo, Wad, WavePm, WaveTrend, Wedge, WeightedClose, WickRatio, WilliamsFractals, WilliamsR,
    WinRate, Wma, WoodiePivots, YangZhangVolatility, YoyoExit, ZScore, ZeroLagMacd, ZigZag, Zlema,
    T3,
};

use wickra_data::aggregator::{TickAggregator as DataTickAggregator, Timeframe};

use wickra_data::resample::Resampler;

// ===== Scalar indicators (f64 -> f64) =====

/// Create a `AdaptiveCycle` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_adaptive_cycle_free`.
#[no_mangle]
pub extern "C" fn wickra_adaptive_cycle_new() -> *mut AdaptiveCycle {
    Box::into_raw(Box::new(AdaptiveCycle::new()))
}

/// Feed one value; returns the output, or `NaN` during warmup / on a `NULL` handle.
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_adaptive_cycle_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_adaptive_cycle_update(
    handle: *mut AdaptiveCycle,
    value: f64,
) -> f64 {
    match handle.as_mut() {
        Some(ind) => ind.update(value).unwrap_or(f64::NAN),
        None => f64::NAN,
    }
}

/// Run over `input[0..n]`, writing one output per input into `out[0..n]` (`NaN` at warmup).
///
/// # Safety
/// `handle` valid (from `wickra_adaptive_cycle_new`, not freed); `input`/`out` cover `n` `double`s.
#[no_mangle]
pub unsafe extern "C" fn wickra_adaptive_cycle_batch(
    handle: *mut AdaptiveCycle,
    input: *const f64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null() || input.is_null() || out.is_null() {
        return;
    }
    let ind = &mut *handle;
    let inputs = slice::from_raw_parts(input, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (slot, &value) in outputs.iter_mut().zip(inputs) {
        *slot = ind.update(value).unwrap_or(f64::NAN);
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_adaptive_cycle_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_adaptive_cycle_warmup_period(handle: *mut AdaptiveCycle) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_adaptive_cycle_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_adaptive_cycle_is_ready(handle: *mut AdaptiveCycle) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_adaptive_cycle_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_adaptive_cycle_name(handle: *mut AdaptiveCycle) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_adaptive_cycle_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_adaptive_cycle_reset(handle: *mut AdaptiveCycle) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_adaptive_cycle_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_adaptive_cycle_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_adaptive_cycle_free(handle: *mut AdaptiveCycle) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `AdaptiveLaguerreFilter` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_adaptive_laguerre_filter_free`.
#[no_mangle]
pub extern "C" fn wickra_adaptive_laguerre_filter_new(
    period: usize,
) -> *mut AdaptiveLaguerreFilter {
    match AdaptiveLaguerreFilter::new(period) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one value; returns the output, or `NaN` during warmup / on a `NULL` handle.
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_adaptive_laguerre_filter_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_adaptive_laguerre_filter_update(
    handle: *mut AdaptiveLaguerreFilter,
    value: f64,
) -> f64 {
    match handle.as_mut() {
        Some(ind) => ind.update(value).unwrap_or(f64::NAN),
        None => f64::NAN,
    }
}

/// Run over `input[0..n]`, writing one output per input into `out[0..n]` (`NaN` at warmup).
///
/// # Safety
/// `handle` valid (from `wickra_adaptive_laguerre_filter_new`, not freed); `input`/`out` cover `n` `double`s.
#[no_mangle]
pub unsafe extern "C" fn wickra_adaptive_laguerre_filter_batch(
    handle: *mut AdaptiveLaguerreFilter,
    input: *const f64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null() || input.is_null() || out.is_null() {
        return;
    }
    let ind = &mut *handle;
    let inputs = slice::from_raw_parts(input, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (slot, &value) in outputs.iter_mut().zip(inputs) {
        *slot = ind.update(value).unwrap_or(f64::NAN);
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_adaptive_laguerre_filter_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_adaptive_laguerre_filter_warmup_period(
    handle: *mut AdaptiveLaguerreFilter,
) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_adaptive_laguerre_filter_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_adaptive_laguerre_filter_is_ready(
    handle: *mut AdaptiveLaguerreFilter,
) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_adaptive_laguerre_filter_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_adaptive_laguerre_filter_name(
    handle: *mut AdaptiveLaguerreFilter,
) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_adaptive_laguerre_filter_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_adaptive_laguerre_filter_reset(
    handle: *mut AdaptiveLaguerreFilter,
) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_adaptive_laguerre_filter_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_adaptive_laguerre_filter_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_adaptive_laguerre_filter_free(handle: *mut AdaptiveLaguerreFilter) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `AdaptiveRsi` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_adaptive_rsi_free`.
#[no_mangle]
pub extern "C" fn wickra_adaptive_rsi_new(period: usize) -> *mut AdaptiveRsi {
    match AdaptiveRsi::new(period) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one value; returns the output, or `NaN` during warmup / on a `NULL` handle.
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_adaptive_rsi_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_adaptive_rsi_update(handle: *mut AdaptiveRsi, value: f64) -> f64 {
    match handle.as_mut() {
        Some(ind) => ind.update(value).unwrap_or(f64::NAN),
        None => f64::NAN,
    }
}

/// Run over `input[0..n]`, writing one output per input into `out[0..n]` (`NaN` at warmup).
///
/// # Safety
/// `handle` valid (from `wickra_adaptive_rsi_new`, not freed); `input`/`out` cover `n` `double`s.
#[no_mangle]
pub unsafe extern "C" fn wickra_adaptive_rsi_batch(
    handle: *mut AdaptiveRsi,
    input: *const f64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null() || input.is_null() || out.is_null() {
        return;
    }
    let ind = &mut *handle;
    let inputs = slice::from_raw_parts(input, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (slot, &value) in outputs.iter_mut().zip(inputs) {
        *slot = ind.update(value).unwrap_or(f64::NAN);
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_adaptive_rsi_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_adaptive_rsi_warmup_period(handle: *mut AdaptiveRsi) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_adaptive_rsi_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_adaptive_rsi_is_ready(handle: *mut AdaptiveRsi) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_adaptive_rsi_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_adaptive_rsi_name(handle: *mut AdaptiveRsi) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_adaptive_rsi_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_adaptive_rsi_reset(handle: *mut AdaptiveRsi) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_adaptive_rsi_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_adaptive_rsi_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_adaptive_rsi_free(handle: *mut AdaptiveRsi) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `Alma` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_alma_free`.
#[no_mangle]
pub extern "C" fn wickra_alma_new(period: usize, offset: f64, sigma: f64) -> *mut Alma {
    match Alma::new(period, offset, sigma) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one value; returns the output, or `NaN` during warmup / on a `NULL` handle.
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_alma_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_alma_update(handle: *mut Alma, value: f64) -> f64 {
    match handle.as_mut() {
        Some(ind) => ind.update(value).unwrap_or(f64::NAN),
        None => f64::NAN,
    }
}

/// Run over `input[0..n]`, writing one output per input into `out[0..n]` (`NaN` at warmup).
///
/// # Safety
/// `handle` valid (from `wickra_alma_new`, not freed); `input`/`out` cover `n` `double`s.
#[no_mangle]
pub unsafe extern "C" fn wickra_alma_batch(
    handle: *mut Alma,
    input: *const f64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null() || input.is_null() || out.is_null() {
        return;
    }
    let ind = &mut *handle;
    let inputs = slice::from_raw_parts(input, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (slot, &value) in outputs.iter_mut().zip(inputs) {
        *slot = ind.update(value).unwrap_or(f64::NAN);
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_alma_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_alma_warmup_period(handle: *mut Alma) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_alma_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_alma_is_ready(handle: *mut Alma) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_alma_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_alma_name(handle: *mut Alma) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_alma_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_alma_reset(handle: *mut Alma) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_alma_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_alma_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_alma_free(handle: *mut Alma) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `AnchoredRsi` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_anchored_rsi_free`.
#[no_mangle]
pub extern "C" fn wickra_anchored_rsi_new() -> *mut AnchoredRsi {
    Box::into_raw(Box::new(AnchoredRsi::new()))
}

/// Feed one value; returns the output, or `NaN` during warmup / on a `NULL` handle.
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_anchored_rsi_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_anchored_rsi_update(handle: *mut AnchoredRsi, value: f64) -> f64 {
    match handle.as_mut() {
        Some(ind) => ind.update(value).unwrap_or(f64::NAN),
        None => f64::NAN,
    }
}

/// Run over `input[0..n]`, writing one output per input into `out[0..n]` (`NaN` at warmup).
///
/// # Safety
/// `handle` valid (from `wickra_anchored_rsi_new`, not freed); `input`/`out` cover `n` `double`s.
#[no_mangle]
pub unsafe extern "C" fn wickra_anchored_rsi_batch(
    handle: *mut AnchoredRsi,
    input: *const f64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null() || input.is_null() || out.is_null() {
        return;
    }
    let ind = &mut *handle;
    let inputs = slice::from_raw_parts(input, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (slot, &value) in outputs.iter_mut().zip(inputs) {
        *slot = ind.update(value).unwrap_or(f64::NAN);
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_anchored_rsi_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_anchored_rsi_warmup_period(handle: *mut AnchoredRsi) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_anchored_rsi_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_anchored_rsi_is_ready(handle: *mut AnchoredRsi) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_anchored_rsi_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_anchored_rsi_name(handle: *mut AnchoredRsi) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_anchored_rsi_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_anchored_rsi_reset(handle: *mut AnchoredRsi) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_anchored_rsi_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_anchored_rsi_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_anchored_rsi_free(handle: *mut AnchoredRsi) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `Apo` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_apo_free`.
#[no_mangle]
pub extern "C" fn wickra_apo_new(fast: usize, slow: usize) -> *mut Apo {
    match Apo::new(fast, slow) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one value; returns the output, or `NaN` during warmup / on a `NULL` handle.
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_apo_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_apo_update(handle: *mut Apo, value: f64) -> f64 {
    match handle.as_mut() {
        Some(ind) => ind.update(value).unwrap_or(f64::NAN),
        None => f64::NAN,
    }
}

/// Run over `input[0..n]`, writing one output per input into `out[0..n]` (`NaN` at warmup).
///
/// # Safety
/// `handle` valid (from `wickra_apo_new`, not freed); `input`/`out` cover `n` `double`s.
#[no_mangle]
pub unsafe extern "C" fn wickra_apo_batch(
    handle: *mut Apo,
    input: *const f64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null() || input.is_null() || out.is_null() {
        return;
    }
    let ind = &mut *handle;
    let inputs = slice::from_raw_parts(input, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (slot, &value) in outputs.iter_mut().zip(inputs) {
        *slot = ind.update(value).unwrap_or(f64::NAN);
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_apo_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_apo_warmup_period(handle: *mut Apo) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_apo_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_apo_is_ready(handle: *mut Apo) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_apo_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_apo_name(handle: *mut Apo) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_apo_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_apo_reset(handle: *mut Apo) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_apo_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_apo_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_apo_free(handle: *mut Apo) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `Autocorrelation` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_autocorrelation_free`.
#[no_mangle]
pub extern "C" fn wickra_autocorrelation_new(period: usize, lag: usize) -> *mut Autocorrelation {
    match Autocorrelation::new(period, lag) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one value; returns the output, or `NaN` during warmup / on a `NULL` handle.
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_autocorrelation_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_autocorrelation_update(
    handle: *mut Autocorrelation,
    value: f64,
) -> f64 {
    match handle.as_mut() {
        Some(ind) => ind.update(value).unwrap_or(f64::NAN),
        None => f64::NAN,
    }
}

/// Run over `input[0..n]`, writing one output per input into `out[0..n]` (`NaN` at warmup).
///
/// # Safety
/// `handle` valid (from `wickra_autocorrelation_new`, not freed); `input`/`out` cover `n` `double`s.
#[no_mangle]
pub unsafe extern "C" fn wickra_autocorrelation_batch(
    handle: *mut Autocorrelation,
    input: *const f64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null() || input.is_null() || out.is_null() {
        return;
    }
    let ind = &mut *handle;
    let inputs = slice::from_raw_parts(input, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (slot, &value) in outputs.iter_mut().zip(inputs) {
        *slot = ind.update(value).unwrap_or(f64::NAN);
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_autocorrelation_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_autocorrelation_warmup_period(
    handle: *mut Autocorrelation,
) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_autocorrelation_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_autocorrelation_is_ready(handle: *mut Autocorrelation) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_autocorrelation_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_autocorrelation_name(
    handle: *mut Autocorrelation,
) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_autocorrelation_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_autocorrelation_reset(handle: *mut Autocorrelation) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_autocorrelation_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_autocorrelation_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_autocorrelation_free(handle: *mut Autocorrelation) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `AutocorrelationPeriodogram` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_autocorrelation_periodogram_free`.
#[no_mangle]
pub extern "C" fn wickra_autocorrelation_periodogram_new(
    min_period: usize,
    max_period: usize,
) -> *mut AutocorrelationPeriodogram {
    match AutocorrelationPeriodogram::new(min_period, max_period) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one value; returns the output, or `NaN` during warmup / on a `NULL` handle.
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_autocorrelation_periodogram_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_autocorrelation_periodogram_update(
    handle: *mut AutocorrelationPeriodogram,
    value: f64,
) -> f64 {
    match handle.as_mut() {
        Some(ind) => ind.update(value).unwrap_or(f64::NAN),
        None => f64::NAN,
    }
}

/// Run over `input[0..n]`, writing one output per input into `out[0..n]` (`NaN` at warmup).
///
/// # Safety
/// `handle` valid (from `wickra_autocorrelation_periodogram_new`, not freed); `input`/`out` cover `n` `double`s.
#[no_mangle]
pub unsafe extern "C" fn wickra_autocorrelation_periodogram_batch(
    handle: *mut AutocorrelationPeriodogram,
    input: *const f64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null() || input.is_null() || out.is_null() {
        return;
    }
    let ind = &mut *handle;
    let inputs = slice::from_raw_parts(input, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (slot, &value) in outputs.iter_mut().zip(inputs) {
        *slot = ind.update(value).unwrap_or(f64::NAN);
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_autocorrelation_periodogram_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_autocorrelation_periodogram_warmup_period(
    handle: *mut AutocorrelationPeriodogram,
) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_autocorrelation_periodogram_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_autocorrelation_periodogram_is_ready(
    handle: *mut AutocorrelationPeriodogram,
) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_autocorrelation_periodogram_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_autocorrelation_periodogram_name(
    handle: *mut AutocorrelationPeriodogram,
) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_autocorrelation_periodogram_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_autocorrelation_periodogram_reset(
    handle: *mut AutocorrelationPeriodogram,
) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_autocorrelation_periodogram_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_autocorrelation_periodogram_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_autocorrelation_periodogram_free(
    handle: *mut AutocorrelationPeriodogram,
) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `AverageDrawdown` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_average_drawdown_free`.
#[no_mangle]
pub extern "C" fn wickra_average_drawdown_new(period: usize) -> *mut AverageDrawdown {
    match AverageDrawdown::new(period) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one value; returns the output, or `NaN` during warmup / on a `NULL` handle.
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_average_drawdown_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_average_drawdown_update(
    handle: *mut AverageDrawdown,
    value: f64,
) -> f64 {
    match handle.as_mut() {
        Some(ind) => ind.update(value).unwrap_or(f64::NAN),
        None => f64::NAN,
    }
}

/// Run over `input[0..n]`, writing one output per input into `out[0..n]` (`NaN` at warmup).
///
/// # Safety
/// `handle` valid (from `wickra_average_drawdown_new`, not freed); `input`/`out` cover `n` `double`s.
#[no_mangle]
pub unsafe extern "C" fn wickra_average_drawdown_batch(
    handle: *mut AverageDrawdown,
    input: *const f64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null() || input.is_null() || out.is_null() {
        return;
    }
    let ind = &mut *handle;
    let inputs = slice::from_raw_parts(input, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (slot, &value) in outputs.iter_mut().zip(inputs) {
        *slot = ind.update(value).unwrap_or(f64::NAN);
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_average_drawdown_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_average_drawdown_warmup_period(
    handle: *mut AverageDrawdown,
) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_average_drawdown_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_average_drawdown_is_ready(handle: *mut AverageDrawdown) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_average_drawdown_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_average_drawdown_name(
    handle: *mut AverageDrawdown,
) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_average_drawdown_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_average_drawdown_reset(handle: *mut AverageDrawdown) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_average_drawdown_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_average_drawdown_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_average_drawdown_free(handle: *mut AverageDrawdown) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `BandpassFilter` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_bandpass_filter_free`.
#[no_mangle]
pub extern "C" fn wickra_bandpass_filter_new(period: usize, bandwidth: f64) -> *mut BandpassFilter {
    match BandpassFilter::new(period, bandwidth) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one value; returns the output, or `NaN` during warmup / on a `NULL` handle.
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_bandpass_filter_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_bandpass_filter_update(
    handle: *mut BandpassFilter,
    value: f64,
) -> f64 {
    match handle.as_mut() {
        Some(ind) => ind.update(value).unwrap_or(f64::NAN),
        None => f64::NAN,
    }
}

/// Run over `input[0..n]`, writing one output per input into `out[0..n]` (`NaN` at warmup).
///
/// # Safety
/// `handle` valid (from `wickra_bandpass_filter_new`, not freed); `input`/`out` cover `n` `double`s.
#[no_mangle]
pub unsafe extern "C" fn wickra_bandpass_filter_batch(
    handle: *mut BandpassFilter,
    input: *const f64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null() || input.is_null() || out.is_null() {
        return;
    }
    let ind = &mut *handle;
    let inputs = slice::from_raw_parts(input, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (slot, &value) in outputs.iter_mut().zip(inputs) {
        *slot = ind.update(value).unwrap_or(f64::NAN);
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_bandpass_filter_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_bandpass_filter_warmup_period(
    handle: *mut BandpassFilter,
) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_bandpass_filter_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_bandpass_filter_is_ready(handle: *mut BandpassFilter) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_bandpass_filter_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_bandpass_filter_name(handle: *mut BandpassFilter) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_bandpass_filter_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_bandpass_filter_reset(handle: *mut BandpassFilter) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_bandpass_filter_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_bandpass_filter_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_bandpass_filter_free(handle: *mut BandpassFilter) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `BipowerVariation` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_bipower_variation_free`.
#[no_mangle]
pub extern "C" fn wickra_bipower_variation_new(period: usize) -> *mut BipowerVariation {
    match BipowerVariation::new(period) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one value; returns the output, or `NaN` during warmup / on a `NULL` handle.
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_bipower_variation_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_bipower_variation_update(
    handle: *mut BipowerVariation,
    value: f64,
) -> f64 {
    match handle.as_mut() {
        Some(ind) => ind.update(value).unwrap_or(f64::NAN),
        None => f64::NAN,
    }
}

/// Run over `input[0..n]`, writing one output per input into `out[0..n]` (`NaN` at warmup).
///
/// # Safety
/// `handle` valid (from `wickra_bipower_variation_new`, not freed); `input`/`out` cover `n` `double`s.
#[no_mangle]
pub unsafe extern "C" fn wickra_bipower_variation_batch(
    handle: *mut BipowerVariation,
    input: *const f64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null() || input.is_null() || out.is_null() {
        return;
    }
    let ind = &mut *handle;
    let inputs = slice::from_raw_parts(input, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (slot, &value) in outputs.iter_mut().zip(inputs) {
        *slot = ind.update(value).unwrap_or(f64::NAN);
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_bipower_variation_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_bipower_variation_warmup_period(
    handle: *mut BipowerVariation,
) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_bipower_variation_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_bipower_variation_is_ready(handle: *mut BipowerVariation) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_bipower_variation_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_bipower_variation_name(
    handle: *mut BipowerVariation,
) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_bipower_variation_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_bipower_variation_reset(handle: *mut BipowerVariation) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_bipower_variation_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_bipower_variation_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_bipower_variation_free(handle: *mut BipowerVariation) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `BollingerBandwidth` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_bollinger_bandwidth_free`.
#[no_mangle]
pub extern "C" fn wickra_bollinger_bandwidth_new(
    period: usize,
    multiplier: f64,
) -> *mut BollingerBandwidth {
    match BollingerBandwidth::new(period, multiplier) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one value; returns the output, or `NaN` during warmup / on a `NULL` handle.
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_bollinger_bandwidth_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_bollinger_bandwidth_update(
    handle: *mut BollingerBandwidth,
    value: f64,
) -> f64 {
    match handle.as_mut() {
        Some(ind) => ind.update(value).unwrap_or(f64::NAN),
        None => f64::NAN,
    }
}

/// Run over `input[0..n]`, writing one output per input into `out[0..n]` (`NaN` at warmup).
///
/// # Safety
/// `handle` valid (from `wickra_bollinger_bandwidth_new`, not freed); `input`/`out` cover `n` `double`s.
#[no_mangle]
pub unsafe extern "C" fn wickra_bollinger_bandwidth_batch(
    handle: *mut BollingerBandwidth,
    input: *const f64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null() || input.is_null() || out.is_null() {
        return;
    }
    let ind = &mut *handle;
    let inputs = slice::from_raw_parts(input, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (slot, &value) in outputs.iter_mut().zip(inputs) {
        *slot = ind.update(value).unwrap_or(f64::NAN);
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_bollinger_bandwidth_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_bollinger_bandwidth_warmup_period(
    handle: *mut BollingerBandwidth,
) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_bollinger_bandwidth_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_bollinger_bandwidth_is_ready(
    handle: *mut BollingerBandwidth,
) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_bollinger_bandwidth_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_bollinger_bandwidth_name(
    handle: *mut BollingerBandwidth,
) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_bollinger_bandwidth_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_bollinger_bandwidth_reset(handle: *mut BollingerBandwidth) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_bollinger_bandwidth_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_bollinger_bandwidth_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_bollinger_bandwidth_free(handle: *mut BollingerBandwidth) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `BurkeRatio` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_burke_ratio_free`.
#[no_mangle]
pub extern "C" fn wickra_burke_ratio_new(period: usize) -> *mut BurkeRatio {
    match BurkeRatio::new(period) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one value; returns the output, or `NaN` during warmup / on a `NULL` handle.
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_burke_ratio_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_burke_ratio_update(handle: *mut BurkeRatio, value: f64) -> f64 {
    match handle.as_mut() {
        Some(ind) => ind.update(value).unwrap_or(f64::NAN),
        None => f64::NAN,
    }
}

/// Run over `input[0..n]`, writing one output per input into `out[0..n]` (`NaN` at warmup).
///
/// # Safety
/// `handle` valid (from `wickra_burke_ratio_new`, not freed); `input`/`out` cover `n` `double`s.
#[no_mangle]
pub unsafe extern "C" fn wickra_burke_ratio_batch(
    handle: *mut BurkeRatio,
    input: *const f64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null() || input.is_null() || out.is_null() {
        return;
    }
    let ind = &mut *handle;
    let inputs = slice::from_raw_parts(input, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (slot, &value) in outputs.iter_mut().zip(inputs) {
        *slot = ind.update(value).unwrap_or(f64::NAN);
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_burke_ratio_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_burke_ratio_warmup_period(handle: *mut BurkeRatio) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_burke_ratio_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_burke_ratio_is_ready(handle: *mut BurkeRatio) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_burke_ratio_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_burke_ratio_name(handle: *mut BurkeRatio) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_burke_ratio_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_burke_ratio_reset(handle: *mut BurkeRatio) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_burke_ratio_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_burke_ratio_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_burke_ratio_free(handle: *mut BurkeRatio) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `CalmarRatio` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_calmar_ratio_free`.
#[no_mangle]
pub extern "C" fn wickra_calmar_ratio_new(period: usize) -> *mut CalmarRatio {
    match CalmarRatio::new(period) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one value; returns the output, or `NaN` during warmup / on a `NULL` handle.
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_calmar_ratio_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_calmar_ratio_update(handle: *mut CalmarRatio, value: f64) -> f64 {
    match handle.as_mut() {
        Some(ind) => ind.update(value).unwrap_or(f64::NAN),
        None => f64::NAN,
    }
}

/// Run over `input[0..n]`, writing one output per input into `out[0..n]` (`NaN` at warmup).
///
/// # Safety
/// `handle` valid (from `wickra_calmar_ratio_new`, not freed); `input`/`out` cover `n` `double`s.
#[no_mangle]
pub unsafe extern "C" fn wickra_calmar_ratio_batch(
    handle: *mut CalmarRatio,
    input: *const f64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null() || input.is_null() || out.is_null() {
        return;
    }
    let ind = &mut *handle;
    let inputs = slice::from_raw_parts(input, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (slot, &value) in outputs.iter_mut().zip(inputs) {
        *slot = ind.update(value).unwrap_or(f64::NAN);
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_calmar_ratio_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_calmar_ratio_warmup_period(handle: *mut CalmarRatio) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_calmar_ratio_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_calmar_ratio_is_ready(handle: *mut CalmarRatio) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_calmar_ratio_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_calmar_ratio_name(handle: *mut CalmarRatio) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_calmar_ratio_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_calmar_ratio_reset(handle: *mut CalmarRatio) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_calmar_ratio_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_calmar_ratio_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_calmar_ratio_free(handle: *mut CalmarRatio) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `CenterOfGravity` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_center_of_gravity_free`.
#[no_mangle]
pub extern "C" fn wickra_center_of_gravity_new(period: usize) -> *mut CenterOfGravity {
    match CenterOfGravity::new(period) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one value; returns the output, or `NaN` during warmup / on a `NULL` handle.
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_center_of_gravity_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_center_of_gravity_update(
    handle: *mut CenterOfGravity,
    value: f64,
) -> f64 {
    match handle.as_mut() {
        Some(ind) => ind.update(value).unwrap_or(f64::NAN),
        None => f64::NAN,
    }
}

/// Run over `input[0..n]`, writing one output per input into `out[0..n]` (`NaN` at warmup).
///
/// # Safety
/// `handle` valid (from `wickra_center_of_gravity_new`, not freed); `input`/`out` cover `n` `double`s.
#[no_mangle]
pub unsafe extern "C" fn wickra_center_of_gravity_batch(
    handle: *mut CenterOfGravity,
    input: *const f64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null() || input.is_null() || out.is_null() {
        return;
    }
    let ind = &mut *handle;
    let inputs = slice::from_raw_parts(input, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (slot, &value) in outputs.iter_mut().zip(inputs) {
        *slot = ind.update(value).unwrap_or(f64::NAN);
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_center_of_gravity_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_center_of_gravity_warmup_period(
    handle: *mut CenterOfGravity,
) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_center_of_gravity_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_center_of_gravity_is_ready(handle: *mut CenterOfGravity) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_center_of_gravity_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_center_of_gravity_name(
    handle: *mut CenterOfGravity,
) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_center_of_gravity_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_center_of_gravity_reset(handle: *mut CenterOfGravity) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_center_of_gravity_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_center_of_gravity_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_center_of_gravity_free(handle: *mut CenterOfGravity) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `Cfo` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_cfo_free`.
#[no_mangle]
pub extern "C" fn wickra_cfo_new(period: usize) -> *mut Cfo {
    match Cfo::new(period) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one value; returns the output, or `NaN` during warmup / on a `NULL` handle.
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_cfo_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_cfo_update(handle: *mut Cfo, value: f64) -> f64 {
    match handle.as_mut() {
        Some(ind) => ind.update(value).unwrap_or(f64::NAN),
        None => f64::NAN,
    }
}

/// Run over `input[0..n]`, writing one output per input into `out[0..n]` (`NaN` at warmup).
///
/// # Safety
/// `handle` valid (from `wickra_cfo_new`, not freed); `input`/`out` cover `n` `double`s.
#[no_mangle]
pub unsafe extern "C" fn wickra_cfo_batch(
    handle: *mut Cfo,
    input: *const f64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null() || input.is_null() || out.is_null() {
        return;
    }
    let ind = &mut *handle;
    let inputs = slice::from_raw_parts(input, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (slot, &value) in outputs.iter_mut().zip(inputs) {
        *slot = ind.update(value).unwrap_or(f64::NAN);
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_cfo_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_cfo_warmup_period(handle: *mut Cfo) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_cfo_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_cfo_is_ready(handle: *mut Cfo) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_cfo_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_cfo_name(handle: *mut Cfo) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_cfo_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_cfo_reset(handle: *mut Cfo) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_cfo_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_cfo_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_cfo_free(handle: *mut Cfo) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `Cmo` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_cmo_free`.
#[no_mangle]
pub extern "C" fn wickra_cmo_new(period: usize) -> *mut Cmo {
    match Cmo::new(period) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one value; returns the output, or `NaN` during warmup / on a `NULL` handle.
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_cmo_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_cmo_update(handle: *mut Cmo, value: f64) -> f64 {
    match handle.as_mut() {
        Some(ind) => ind.update(value).unwrap_or(f64::NAN),
        None => f64::NAN,
    }
}

/// Run over `input[0..n]`, writing one output per input into `out[0..n]` (`NaN` at warmup).
///
/// # Safety
/// `handle` valid (from `wickra_cmo_new`, not freed); `input`/`out` cover `n` `double`s.
#[no_mangle]
pub unsafe extern "C" fn wickra_cmo_batch(
    handle: *mut Cmo,
    input: *const f64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null() || input.is_null() || out.is_null() {
        return;
    }
    let ind = &mut *handle;
    let inputs = slice::from_raw_parts(input, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (slot, &value) in outputs.iter_mut().zip(inputs) {
        *slot = ind.update(value).unwrap_or(f64::NAN);
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_cmo_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_cmo_warmup_period(handle: *mut Cmo) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_cmo_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_cmo_is_ready(handle: *mut Cmo) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_cmo_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_cmo_name(handle: *mut Cmo) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_cmo_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_cmo_reset(handle: *mut Cmo) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_cmo_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_cmo_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_cmo_free(handle: *mut Cmo) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `CoefficientOfVariation` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_coefficient_of_variation_free`.
#[no_mangle]
pub extern "C" fn wickra_coefficient_of_variation_new(
    period: usize,
) -> *mut CoefficientOfVariation {
    match CoefficientOfVariation::new(period) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one value; returns the output, or `NaN` during warmup / on a `NULL` handle.
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_coefficient_of_variation_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_coefficient_of_variation_update(
    handle: *mut CoefficientOfVariation,
    value: f64,
) -> f64 {
    match handle.as_mut() {
        Some(ind) => ind.update(value).unwrap_or(f64::NAN),
        None => f64::NAN,
    }
}

/// Run over `input[0..n]`, writing one output per input into `out[0..n]` (`NaN` at warmup).
///
/// # Safety
/// `handle` valid (from `wickra_coefficient_of_variation_new`, not freed); `input`/`out` cover `n` `double`s.
#[no_mangle]
pub unsafe extern "C" fn wickra_coefficient_of_variation_batch(
    handle: *mut CoefficientOfVariation,
    input: *const f64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null() || input.is_null() || out.is_null() {
        return;
    }
    let ind = &mut *handle;
    let inputs = slice::from_raw_parts(input, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (slot, &value) in outputs.iter_mut().zip(inputs) {
        *slot = ind.update(value).unwrap_or(f64::NAN);
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_coefficient_of_variation_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_coefficient_of_variation_warmup_period(
    handle: *mut CoefficientOfVariation,
) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_coefficient_of_variation_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_coefficient_of_variation_is_ready(
    handle: *mut CoefficientOfVariation,
) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_coefficient_of_variation_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_coefficient_of_variation_name(
    handle: *mut CoefficientOfVariation,
) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_coefficient_of_variation_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_coefficient_of_variation_reset(
    handle: *mut CoefficientOfVariation,
) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_coefficient_of_variation_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_coefficient_of_variation_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_coefficient_of_variation_free(handle: *mut CoefficientOfVariation) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `CommonSenseRatio` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_common_sense_ratio_free`.
#[no_mangle]
pub extern "C" fn wickra_common_sense_ratio_new(period: usize) -> *mut CommonSenseRatio {
    match CommonSenseRatio::new(period) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one value; returns the output, or `NaN` during warmup / on a `NULL` handle.
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_common_sense_ratio_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_common_sense_ratio_update(
    handle: *mut CommonSenseRatio,
    value: f64,
) -> f64 {
    match handle.as_mut() {
        Some(ind) => ind.update(value).unwrap_or(f64::NAN),
        None => f64::NAN,
    }
}

/// Run over `input[0..n]`, writing one output per input into `out[0..n]` (`NaN` at warmup).
///
/// # Safety
/// `handle` valid (from `wickra_common_sense_ratio_new`, not freed); `input`/`out` cover `n` `double`s.
#[no_mangle]
pub unsafe extern "C" fn wickra_common_sense_ratio_batch(
    handle: *mut CommonSenseRatio,
    input: *const f64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null() || input.is_null() || out.is_null() {
        return;
    }
    let ind = &mut *handle;
    let inputs = slice::from_raw_parts(input, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (slot, &value) in outputs.iter_mut().zip(inputs) {
        *slot = ind.update(value).unwrap_or(f64::NAN);
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_common_sense_ratio_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_common_sense_ratio_warmup_period(
    handle: *mut CommonSenseRatio,
) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_common_sense_ratio_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_common_sense_ratio_is_ready(handle: *mut CommonSenseRatio) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_common_sense_ratio_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_common_sense_ratio_name(
    handle: *mut CommonSenseRatio,
) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_common_sense_ratio_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_common_sense_ratio_reset(handle: *mut CommonSenseRatio) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_common_sense_ratio_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_common_sense_ratio_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_common_sense_ratio_free(handle: *mut CommonSenseRatio) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `ConditionalValueAtRisk` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_conditional_value_at_risk_free`.
#[no_mangle]
pub extern "C" fn wickra_conditional_value_at_risk_new(
    period: usize,
    confidence: f64,
) -> *mut ConditionalValueAtRisk {
    match ConditionalValueAtRisk::new(period, confidence) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one value; returns the output, or `NaN` during warmup / on a `NULL` handle.
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_conditional_value_at_risk_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_conditional_value_at_risk_update(
    handle: *mut ConditionalValueAtRisk,
    value: f64,
) -> f64 {
    match handle.as_mut() {
        Some(ind) => ind.update(value).unwrap_or(f64::NAN),
        None => f64::NAN,
    }
}

/// Run over `input[0..n]`, writing one output per input into `out[0..n]` (`NaN` at warmup).
///
/// # Safety
/// `handle` valid (from `wickra_conditional_value_at_risk_new`, not freed); `input`/`out` cover `n` `double`s.
#[no_mangle]
pub unsafe extern "C" fn wickra_conditional_value_at_risk_batch(
    handle: *mut ConditionalValueAtRisk,
    input: *const f64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null() || input.is_null() || out.is_null() {
        return;
    }
    let ind = &mut *handle;
    let inputs = slice::from_raw_parts(input, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (slot, &value) in outputs.iter_mut().zip(inputs) {
        *slot = ind.update(value).unwrap_or(f64::NAN);
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_conditional_value_at_risk_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_conditional_value_at_risk_warmup_period(
    handle: *mut ConditionalValueAtRisk,
) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_conditional_value_at_risk_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_conditional_value_at_risk_is_ready(
    handle: *mut ConditionalValueAtRisk,
) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_conditional_value_at_risk_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_conditional_value_at_risk_name(
    handle: *mut ConditionalValueAtRisk,
) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_conditional_value_at_risk_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_conditional_value_at_risk_reset(
    handle: *mut ConditionalValueAtRisk,
) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_conditional_value_at_risk_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_conditional_value_at_risk_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_conditional_value_at_risk_free(
    handle: *mut ConditionalValueAtRisk,
) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `ConnorsRsi` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_connors_rsi_free`.
#[no_mangle]
pub extern "C" fn wickra_connors_rsi_new(
    period_rsi: usize,
    period_streak: usize,
    period_rank: usize,
) -> *mut ConnorsRsi {
    match ConnorsRsi::new(period_rsi, period_streak, period_rank) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one value; returns the output, or `NaN` during warmup / on a `NULL` handle.
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_connors_rsi_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_connors_rsi_update(handle: *mut ConnorsRsi, value: f64) -> f64 {
    match handle.as_mut() {
        Some(ind) => ind.update(value).unwrap_or(f64::NAN),
        None => f64::NAN,
    }
}

/// Run over `input[0..n]`, writing one output per input into `out[0..n]` (`NaN` at warmup).
///
/// # Safety
/// `handle` valid (from `wickra_connors_rsi_new`, not freed); `input`/`out` cover `n` `double`s.
#[no_mangle]
pub unsafe extern "C" fn wickra_connors_rsi_batch(
    handle: *mut ConnorsRsi,
    input: *const f64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null() || input.is_null() || out.is_null() {
        return;
    }
    let ind = &mut *handle;
    let inputs = slice::from_raw_parts(input, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (slot, &value) in outputs.iter_mut().zip(inputs) {
        *slot = ind.update(value).unwrap_or(f64::NAN);
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_connors_rsi_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_connors_rsi_warmup_period(handle: *mut ConnorsRsi) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_connors_rsi_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_connors_rsi_is_ready(handle: *mut ConnorsRsi) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_connors_rsi_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_connors_rsi_name(handle: *mut ConnorsRsi) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_connors_rsi_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_connors_rsi_reset(handle: *mut ConnorsRsi) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_connors_rsi_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_connors_rsi_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_connors_rsi_free(handle: *mut ConnorsRsi) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `Coppock` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_coppock_free`.
#[no_mangle]
pub extern "C" fn wickra_coppock_new(
    roc_long_period: usize,
    roc_short_period: usize,
    wma_period: usize,
) -> *mut Coppock {
    match Coppock::new(roc_long_period, roc_short_period, wma_period) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one value; returns the output, or `NaN` during warmup / on a `NULL` handle.
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_coppock_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_coppock_update(handle: *mut Coppock, value: f64) -> f64 {
    match handle.as_mut() {
        Some(ind) => ind.update(value).unwrap_or(f64::NAN),
        None => f64::NAN,
    }
}

/// Run over `input[0..n]`, writing one output per input into `out[0..n]` (`NaN` at warmup).
///
/// # Safety
/// `handle` valid (from `wickra_coppock_new`, not freed); `input`/`out` cover `n` `double`s.
#[no_mangle]
pub unsafe extern "C" fn wickra_coppock_batch(
    handle: *mut Coppock,
    input: *const f64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null() || input.is_null() || out.is_null() {
        return;
    }
    let ind = &mut *handle;
    let inputs = slice::from_raw_parts(input, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (slot, &value) in outputs.iter_mut().zip(inputs) {
        *slot = ind.update(value).unwrap_or(f64::NAN);
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_coppock_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_coppock_warmup_period(handle: *mut Coppock) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_coppock_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_coppock_is_ready(handle: *mut Coppock) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_coppock_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_coppock_name(handle: *mut Coppock) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_coppock_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_coppock_reset(handle: *mut Coppock) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_coppock_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_coppock_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_coppock_free(handle: *mut Coppock) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `CorrelationTrendIndicator` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_correlation_trend_indicator_free`.
#[no_mangle]
pub extern "C" fn wickra_correlation_trend_indicator_new(
    period: usize,
) -> *mut CorrelationTrendIndicator {
    match CorrelationTrendIndicator::new(period) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one value; returns the output, or `NaN` during warmup / on a `NULL` handle.
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_correlation_trend_indicator_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_correlation_trend_indicator_update(
    handle: *mut CorrelationTrendIndicator,
    value: f64,
) -> f64 {
    match handle.as_mut() {
        Some(ind) => ind.update(value).unwrap_or(f64::NAN),
        None => f64::NAN,
    }
}

/// Run over `input[0..n]`, writing one output per input into `out[0..n]` (`NaN` at warmup).
///
/// # Safety
/// `handle` valid (from `wickra_correlation_trend_indicator_new`, not freed); `input`/`out` cover `n` `double`s.
#[no_mangle]
pub unsafe extern "C" fn wickra_correlation_trend_indicator_batch(
    handle: *mut CorrelationTrendIndicator,
    input: *const f64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null() || input.is_null() || out.is_null() {
        return;
    }
    let ind = &mut *handle;
    let inputs = slice::from_raw_parts(input, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (slot, &value) in outputs.iter_mut().zip(inputs) {
        *slot = ind.update(value).unwrap_or(f64::NAN);
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_correlation_trend_indicator_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_correlation_trend_indicator_warmup_period(
    handle: *mut CorrelationTrendIndicator,
) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_correlation_trend_indicator_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_correlation_trend_indicator_is_ready(
    handle: *mut CorrelationTrendIndicator,
) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_correlation_trend_indicator_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_correlation_trend_indicator_name(
    handle: *mut CorrelationTrendIndicator,
) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_correlation_trend_indicator_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_correlation_trend_indicator_reset(
    handle: *mut CorrelationTrendIndicator,
) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_correlation_trend_indicator_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_correlation_trend_indicator_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_correlation_trend_indicator_free(
    handle: *mut CorrelationTrendIndicator,
) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `CyberneticCycle` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_cybernetic_cycle_free`.
#[no_mangle]
pub extern "C" fn wickra_cybernetic_cycle_new(period: usize) -> *mut CyberneticCycle {
    match CyberneticCycle::new(period) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one value; returns the output, or `NaN` during warmup / on a `NULL` handle.
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_cybernetic_cycle_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_cybernetic_cycle_update(
    handle: *mut CyberneticCycle,
    value: f64,
) -> f64 {
    match handle.as_mut() {
        Some(ind) => ind.update(value).unwrap_or(f64::NAN),
        None => f64::NAN,
    }
}

/// Run over `input[0..n]`, writing one output per input into `out[0..n]` (`NaN` at warmup).
///
/// # Safety
/// `handle` valid (from `wickra_cybernetic_cycle_new`, not freed); `input`/`out` cover `n` `double`s.
#[no_mangle]
pub unsafe extern "C" fn wickra_cybernetic_cycle_batch(
    handle: *mut CyberneticCycle,
    input: *const f64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null() || input.is_null() || out.is_null() {
        return;
    }
    let ind = &mut *handle;
    let inputs = slice::from_raw_parts(input, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (slot, &value) in outputs.iter_mut().zip(inputs) {
        *slot = ind.update(value).unwrap_or(f64::NAN);
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_cybernetic_cycle_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_cybernetic_cycle_warmup_period(
    handle: *mut CyberneticCycle,
) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_cybernetic_cycle_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_cybernetic_cycle_is_ready(handle: *mut CyberneticCycle) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_cybernetic_cycle_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_cybernetic_cycle_name(
    handle: *mut CyberneticCycle,
) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_cybernetic_cycle_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_cybernetic_cycle_reset(handle: *mut CyberneticCycle) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_cybernetic_cycle_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_cybernetic_cycle_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_cybernetic_cycle_free(handle: *mut CyberneticCycle) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `Decycler` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_decycler_free`.
#[no_mangle]
pub extern "C" fn wickra_decycler_new(period: usize) -> *mut Decycler {
    match Decycler::new(period) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one value; returns the output, or `NaN` during warmup / on a `NULL` handle.
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_decycler_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_decycler_update(handle: *mut Decycler, value: f64) -> f64 {
    match handle.as_mut() {
        Some(ind) => ind.update(value).unwrap_or(f64::NAN),
        None => f64::NAN,
    }
}

/// Run over `input[0..n]`, writing one output per input into `out[0..n]` (`NaN` at warmup).
///
/// # Safety
/// `handle` valid (from `wickra_decycler_new`, not freed); `input`/`out` cover `n` `double`s.
#[no_mangle]
pub unsafe extern "C" fn wickra_decycler_batch(
    handle: *mut Decycler,
    input: *const f64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null() || input.is_null() || out.is_null() {
        return;
    }
    let ind = &mut *handle;
    let inputs = slice::from_raw_parts(input, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (slot, &value) in outputs.iter_mut().zip(inputs) {
        *slot = ind.update(value).unwrap_or(f64::NAN);
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_decycler_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_decycler_warmup_period(handle: *mut Decycler) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_decycler_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_decycler_is_ready(handle: *mut Decycler) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_decycler_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_decycler_name(handle: *mut Decycler) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_decycler_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_decycler_reset(handle: *mut Decycler) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_decycler_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_decycler_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_decycler_free(handle: *mut Decycler) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `DecyclerOscillator` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_decycler_oscillator_free`.
#[no_mangle]
pub extern "C" fn wickra_decycler_oscillator_new(
    fast: usize,
    slow: usize,
) -> *mut DecyclerOscillator {
    match DecyclerOscillator::new(fast, slow) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one value; returns the output, or `NaN` during warmup / on a `NULL` handle.
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_decycler_oscillator_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_decycler_oscillator_update(
    handle: *mut DecyclerOscillator,
    value: f64,
) -> f64 {
    match handle.as_mut() {
        Some(ind) => ind.update(value).unwrap_or(f64::NAN),
        None => f64::NAN,
    }
}

/// Run over `input[0..n]`, writing one output per input into `out[0..n]` (`NaN` at warmup).
///
/// # Safety
/// `handle` valid (from `wickra_decycler_oscillator_new`, not freed); `input`/`out` cover `n` `double`s.
#[no_mangle]
pub unsafe extern "C" fn wickra_decycler_oscillator_batch(
    handle: *mut DecyclerOscillator,
    input: *const f64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null() || input.is_null() || out.is_null() {
        return;
    }
    let ind = &mut *handle;
    let inputs = slice::from_raw_parts(input, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (slot, &value) in outputs.iter_mut().zip(inputs) {
        *slot = ind.update(value).unwrap_or(f64::NAN);
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_decycler_oscillator_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_decycler_oscillator_warmup_period(
    handle: *mut DecyclerOscillator,
) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_decycler_oscillator_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_decycler_oscillator_is_ready(
    handle: *mut DecyclerOscillator,
) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_decycler_oscillator_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_decycler_oscillator_name(
    handle: *mut DecyclerOscillator,
) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_decycler_oscillator_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_decycler_oscillator_reset(handle: *mut DecyclerOscillator) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_decycler_oscillator_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_decycler_oscillator_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_decycler_oscillator_free(handle: *mut DecyclerOscillator) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `Dema` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_dema_free`.
#[no_mangle]
pub extern "C" fn wickra_dema_new(period: usize) -> *mut Dema {
    match Dema::new(period) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one value; returns the output, or `NaN` during warmup / on a `NULL` handle.
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_dema_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_dema_update(handle: *mut Dema, value: f64) -> f64 {
    match handle.as_mut() {
        Some(ind) => ind.update(value).unwrap_or(f64::NAN),
        None => f64::NAN,
    }
}

/// Run over `input[0..n]`, writing one output per input into `out[0..n]` (`NaN` at warmup).
///
/// # Safety
/// `handle` valid (from `wickra_dema_new`, not freed); `input`/`out` cover `n` `double`s.
#[no_mangle]
pub unsafe extern "C" fn wickra_dema_batch(
    handle: *mut Dema,
    input: *const f64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null() || input.is_null() || out.is_null() {
        return;
    }
    let ind = &mut *handle;
    let inputs = slice::from_raw_parts(input, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (slot, &value) in outputs.iter_mut().zip(inputs) {
        *slot = ind.update(value).unwrap_or(f64::NAN);
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_dema_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_dema_warmup_period(handle: *mut Dema) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_dema_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_dema_is_ready(handle: *mut Dema) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_dema_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_dema_name(handle: *mut Dema) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_dema_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_dema_reset(handle: *mut Dema) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_dema_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_dema_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_dema_free(handle: *mut Dema) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `DerivativeOscillator` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_derivative_oscillator_free`.
#[no_mangle]
pub extern "C" fn wickra_derivative_oscillator_new(
    rsi_period: usize,
    smooth1: usize,
    smooth2: usize,
    signal_period: usize,
) -> *mut DerivativeOscillator {
    match DerivativeOscillator::new(rsi_period, smooth1, smooth2, signal_period) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one value; returns the output, or `NaN` during warmup / on a `NULL` handle.
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_derivative_oscillator_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_derivative_oscillator_update(
    handle: *mut DerivativeOscillator,
    value: f64,
) -> f64 {
    match handle.as_mut() {
        Some(ind) => ind.update(value).unwrap_or(f64::NAN),
        None => f64::NAN,
    }
}

/// Run over `input[0..n]`, writing one output per input into `out[0..n]` (`NaN` at warmup).
///
/// # Safety
/// `handle` valid (from `wickra_derivative_oscillator_new`, not freed); `input`/`out` cover `n` `double`s.
#[no_mangle]
pub unsafe extern "C" fn wickra_derivative_oscillator_batch(
    handle: *mut DerivativeOscillator,
    input: *const f64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null() || input.is_null() || out.is_null() {
        return;
    }
    let ind = &mut *handle;
    let inputs = slice::from_raw_parts(input, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (slot, &value) in outputs.iter_mut().zip(inputs) {
        *slot = ind.update(value).unwrap_or(f64::NAN);
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_derivative_oscillator_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_derivative_oscillator_warmup_period(
    handle: *mut DerivativeOscillator,
) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_derivative_oscillator_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_derivative_oscillator_is_ready(
    handle: *mut DerivativeOscillator,
) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_derivative_oscillator_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_derivative_oscillator_name(
    handle: *mut DerivativeOscillator,
) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_derivative_oscillator_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_derivative_oscillator_reset(handle: *mut DerivativeOscillator) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_derivative_oscillator_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_derivative_oscillator_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_derivative_oscillator_free(handle: *mut DerivativeOscillator) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `DetrendedStdDev` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_detrended_std_dev_free`.
#[no_mangle]
pub extern "C" fn wickra_detrended_std_dev_new(period: usize) -> *mut DetrendedStdDev {
    match DetrendedStdDev::new(period) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one value; returns the output, or `NaN` during warmup / on a `NULL` handle.
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_detrended_std_dev_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_detrended_std_dev_update(
    handle: *mut DetrendedStdDev,
    value: f64,
) -> f64 {
    match handle.as_mut() {
        Some(ind) => ind.update(value).unwrap_or(f64::NAN),
        None => f64::NAN,
    }
}

/// Run over `input[0..n]`, writing one output per input into `out[0..n]` (`NaN` at warmup).
///
/// # Safety
/// `handle` valid (from `wickra_detrended_std_dev_new`, not freed); `input`/`out` cover `n` `double`s.
#[no_mangle]
pub unsafe extern "C" fn wickra_detrended_std_dev_batch(
    handle: *mut DetrendedStdDev,
    input: *const f64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null() || input.is_null() || out.is_null() {
        return;
    }
    let ind = &mut *handle;
    let inputs = slice::from_raw_parts(input, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (slot, &value) in outputs.iter_mut().zip(inputs) {
        *slot = ind.update(value).unwrap_or(f64::NAN);
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_detrended_std_dev_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_detrended_std_dev_warmup_period(
    handle: *mut DetrendedStdDev,
) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_detrended_std_dev_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_detrended_std_dev_is_ready(handle: *mut DetrendedStdDev) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_detrended_std_dev_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_detrended_std_dev_name(
    handle: *mut DetrendedStdDev,
) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_detrended_std_dev_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_detrended_std_dev_reset(handle: *mut DetrendedStdDev) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_detrended_std_dev_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_detrended_std_dev_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_detrended_std_dev_free(handle: *mut DetrendedStdDev) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `DisparityIndex` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_disparity_index_free`.
#[no_mangle]
pub extern "C" fn wickra_disparity_index_new(period: usize) -> *mut DisparityIndex {
    match DisparityIndex::new(period) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one value; returns the output, or `NaN` during warmup / on a `NULL` handle.
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_disparity_index_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_disparity_index_update(
    handle: *mut DisparityIndex,
    value: f64,
) -> f64 {
    match handle.as_mut() {
        Some(ind) => ind.update(value).unwrap_or(f64::NAN),
        None => f64::NAN,
    }
}

/// Run over `input[0..n]`, writing one output per input into `out[0..n]` (`NaN` at warmup).
///
/// # Safety
/// `handle` valid (from `wickra_disparity_index_new`, not freed); `input`/`out` cover `n` `double`s.
#[no_mangle]
pub unsafe extern "C" fn wickra_disparity_index_batch(
    handle: *mut DisparityIndex,
    input: *const f64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null() || input.is_null() || out.is_null() {
        return;
    }
    let ind = &mut *handle;
    let inputs = slice::from_raw_parts(input, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (slot, &value) in outputs.iter_mut().zip(inputs) {
        *slot = ind.update(value).unwrap_or(f64::NAN);
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_disparity_index_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_disparity_index_warmup_period(
    handle: *mut DisparityIndex,
) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_disparity_index_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_disparity_index_is_ready(handle: *mut DisparityIndex) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_disparity_index_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_disparity_index_name(handle: *mut DisparityIndex) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_disparity_index_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_disparity_index_reset(handle: *mut DisparityIndex) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_disparity_index_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_disparity_index_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_disparity_index_free(handle: *mut DisparityIndex) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `Dpo` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_dpo_free`.
#[no_mangle]
pub extern "C" fn wickra_dpo_new(period: usize) -> *mut Dpo {
    match Dpo::new(period) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one value; returns the output, or `NaN` during warmup / on a `NULL` handle.
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_dpo_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_dpo_update(handle: *mut Dpo, value: f64) -> f64 {
    match handle.as_mut() {
        Some(ind) => ind.update(value).unwrap_or(f64::NAN),
        None => f64::NAN,
    }
}

/// Run over `input[0..n]`, writing one output per input into `out[0..n]` (`NaN` at warmup).
///
/// # Safety
/// `handle` valid (from `wickra_dpo_new`, not freed); `input`/`out` cover `n` `double`s.
#[no_mangle]
pub unsafe extern "C" fn wickra_dpo_batch(
    handle: *mut Dpo,
    input: *const f64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null() || input.is_null() || out.is_null() {
        return;
    }
    let ind = &mut *handle;
    let inputs = slice::from_raw_parts(input, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (slot, &value) in outputs.iter_mut().zip(inputs) {
        *slot = ind.update(value).unwrap_or(f64::NAN);
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_dpo_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_dpo_warmup_period(handle: *mut Dpo) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_dpo_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_dpo_is_ready(handle: *mut Dpo) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_dpo_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_dpo_name(handle: *mut Dpo) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_dpo_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_dpo_reset(handle: *mut Dpo) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_dpo_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_dpo_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_dpo_free(handle: *mut Dpo) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `DrawdownDuration` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_drawdown_duration_free`.
#[no_mangle]
pub extern "C" fn wickra_drawdown_duration_new() -> *mut DrawdownDuration {
    Box::into_raw(Box::new(DrawdownDuration::new()))
}

/// Feed one value; returns the output, or `NaN` during warmup / on a `NULL` handle.
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_drawdown_duration_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_drawdown_duration_update(
    handle: *mut DrawdownDuration,
    value: f64,
) -> f64 {
    match handle.as_mut() {
        Some(ind) => ind.update(value).map_or(f64::NAN, f64::from),
        None => f64::NAN,
    }
}

/// Run over `input[0..n]`, writing one output per input into `out[0..n]` (`NaN` at warmup).
///
/// # Safety
/// `handle` valid (from `wickra_drawdown_duration_new`, not freed); `input`/`out` cover `n` `double`s.
#[no_mangle]
pub unsafe extern "C" fn wickra_drawdown_duration_batch(
    handle: *mut DrawdownDuration,
    input: *const f64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null() || input.is_null() || out.is_null() {
        return;
    }
    let ind = &mut *handle;
    let inputs = slice::from_raw_parts(input, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (slot, &value) in outputs.iter_mut().zip(inputs) {
        *slot = ind.update(value).map_or(f64::NAN, f64::from);
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_drawdown_duration_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_drawdown_duration_warmup_period(
    handle: *mut DrawdownDuration,
) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_drawdown_duration_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_drawdown_duration_is_ready(handle: *mut DrawdownDuration) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_drawdown_duration_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_drawdown_duration_name(
    handle: *mut DrawdownDuration,
) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_drawdown_duration_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_drawdown_duration_reset(handle: *mut DrawdownDuration) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_drawdown_duration_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_drawdown_duration_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_drawdown_duration_free(handle: *mut DrawdownDuration) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `DynamicMomentumIndex` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_dynamic_momentum_index_free`.
#[no_mangle]
pub extern "C" fn wickra_dynamic_momentum_index_new(period: usize) -> *mut DynamicMomentumIndex {
    match DynamicMomentumIndex::new(period) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one value; returns the output, or `NaN` during warmup / on a `NULL` handle.
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_dynamic_momentum_index_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_dynamic_momentum_index_update(
    handle: *mut DynamicMomentumIndex,
    value: f64,
) -> f64 {
    match handle.as_mut() {
        Some(ind) => ind.update(value).unwrap_or(f64::NAN),
        None => f64::NAN,
    }
}

/// Run over `input[0..n]`, writing one output per input into `out[0..n]` (`NaN` at warmup).
///
/// # Safety
/// `handle` valid (from `wickra_dynamic_momentum_index_new`, not freed); `input`/`out` cover `n` `double`s.
#[no_mangle]
pub unsafe extern "C" fn wickra_dynamic_momentum_index_batch(
    handle: *mut DynamicMomentumIndex,
    input: *const f64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null() || input.is_null() || out.is_null() {
        return;
    }
    let ind = &mut *handle;
    let inputs = slice::from_raw_parts(input, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (slot, &value) in outputs.iter_mut().zip(inputs) {
        *slot = ind.update(value).unwrap_or(f64::NAN);
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_dynamic_momentum_index_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_dynamic_momentum_index_warmup_period(
    handle: *mut DynamicMomentumIndex,
) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_dynamic_momentum_index_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_dynamic_momentum_index_is_ready(
    handle: *mut DynamicMomentumIndex,
) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_dynamic_momentum_index_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_dynamic_momentum_index_name(
    handle: *mut DynamicMomentumIndex,
) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_dynamic_momentum_index_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_dynamic_momentum_index_reset(handle: *mut DynamicMomentumIndex) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_dynamic_momentum_index_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_dynamic_momentum_index_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_dynamic_momentum_index_free(handle: *mut DynamicMomentumIndex) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `EhlersStochastic` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_ehlers_stochastic_free`.
#[no_mangle]
pub extern "C" fn wickra_ehlers_stochastic_new(period: usize) -> *mut EhlersStochastic {
    match EhlersStochastic::new(period) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one value; returns the output, or `NaN` during warmup / on a `NULL` handle.
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_ehlers_stochastic_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_ehlers_stochastic_update(
    handle: *mut EhlersStochastic,
    value: f64,
) -> f64 {
    match handle.as_mut() {
        Some(ind) => ind.update(value).unwrap_or(f64::NAN),
        None => f64::NAN,
    }
}

/// Run over `input[0..n]`, writing one output per input into `out[0..n]` (`NaN` at warmup).
///
/// # Safety
/// `handle` valid (from `wickra_ehlers_stochastic_new`, not freed); `input`/`out` cover `n` `double`s.
#[no_mangle]
pub unsafe extern "C" fn wickra_ehlers_stochastic_batch(
    handle: *mut EhlersStochastic,
    input: *const f64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null() || input.is_null() || out.is_null() {
        return;
    }
    let ind = &mut *handle;
    let inputs = slice::from_raw_parts(input, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (slot, &value) in outputs.iter_mut().zip(inputs) {
        *slot = ind.update(value).unwrap_or(f64::NAN);
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_ehlers_stochastic_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_ehlers_stochastic_warmup_period(
    handle: *mut EhlersStochastic,
) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_ehlers_stochastic_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_ehlers_stochastic_is_ready(handle: *mut EhlersStochastic) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_ehlers_stochastic_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_ehlers_stochastic_name(
    handle: *mut EhlersStochastic,
) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_ehlers_stochastic_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_ehlers_stochastic_reset(handle: *mut EhlersStochastic) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_ehlers_stochastic_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_ehlers_stochastic_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_ehlers_stochastic_free(handle: *mut EhlersStochastic) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `Ehma` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_ehma_free`.
#[no_mangle]
pub extern "C" fn wickra_ehma_new(period: usize) -> *mut Ehma {
    match Ehma::new(period) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one value; returns the output, or `NaN` during warmup / on a `NULL` handle.
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_ehma_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_ehma_update(handle: *mut Ehma, value: f64) -> f64 {
    match handle.as_mut() {
        Some(ind) => ind.update(value).unwrap_or(f64::NAN),
        None => f64::NAN,
    }
}

/// Run over `input[0..n]`, writing one output per input into `out[0..n]` (`NaN` at warmup).
///
/// # Safety
/// `handle` valid (from `wickra_ehma_new`, not freed); `input`/`out` cover `n` `double`s.
#[no_mangle]
pub unsafe extern "C" fn wickra_ehma_batch(
    handle: *mut Ehma,
    input: *const f64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null() || input.is_null() || out.is_null() {
        return;
    }
    let ind = &mut *handle;
    let inputs = slice::from_raw_parts(input, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (slot, &value) in outputs.iter_mut().zip(inputs) {
        *slot = ind.update(value).unwrap_or(f64::NAN);
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_ehma_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_ehma_warmup_period(handle: *mut Ehma) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_ehma_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_ehma_is_ready(handle: *mut Ehma) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_ehma_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_ehma_name(handle: *mut Ehma) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_ehma_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_ehma_reset(handle: *mut Ehma) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_ehma_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_ehma_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_ehma_free(handle: *mut Ehma) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `ElderImpulse` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_elder_impulse_free`.
#[no_mangle]
pub extern "C" fn wickra_elder_impulse_new(
    ema_period: usize,
    macd_fast: usize,
    macd_slow: usize,
    macd_signal: usize,
) -> *mut ElderImpulse {
    match ElderImpulse::new(ema_period, macd_fast, macd_slow, macd_signal) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one value; returns the output, or `NaN` during warmup / on a `NULL` handle.
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_elder_impulse_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_elder_impulse_update(handle: *mut ElderImpulse, value: f64) -> f64 {
    match handle.as_mut() {
        Some(ind) => ind.update(value).unwrap_or(f64::NAN),
        None => f64::NAN,
    }
}

/// Run over `input[0..n]`, writing one output per input into `out[0..n]` (`NaN` at warmup).
///
/// # Safety
/// `handle` valid (from `wickra_elder_impulse_new`, not freed); `input`/`out` cover `n` `double`s.
#[no_mangle]
pub unsafe extern "C" fn wickra_elder_impulse_batch(
    handle: *mut ElderImpulse,
    input: *const f64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null() || input.is_null() || out.is_null() {
        return;
    }
    let ind = &mut *handle;
    let inputs = slice::from_raw_parts(input, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (slot, &value) in outputs.iter_mut().zip(inputs) {
        *slot = ind.update(value).unwrap_or(f64::NAN);
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_elder_impulse_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_elder_impulse_warmup_period(handle: *mut ElderImpulse) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_elder_impulse_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_elder_impulse_is_ready(handle: *mut ElderImpulse) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_elder_impulse_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_elder_impulse_name(handle: *mut ElderImpulse) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_elder_impulse_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_elder_impulse_reset(handle: *mut ElderImpulse) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_elder_impulse_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_elder_impulse_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_elder_impulse_free(handle: *mut ElderImpulse) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `Ema` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_ema_free`.
#[no_mangle]
pub extern "C" fn wickra_ema_new(period: usize) -> *mut Ema {
    match Ema::new(period) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one value; returns the output, or `NaN` during warmup / on a `NULL` handle.
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_ema_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_ema_update(handle: *mut Ema, value: f64) -> f64 {
    match handle.as_mut() {
        Some(ind) => ind.update(value).unwrap_or(f64::NAN),
        None => f64::NAN,
    }
}

/// Run over `input[0..n]`, writing one output per input into `out[0..n]` (`NaN` at warmup).
///
/// # Safety
/// `handle` valid (from `wickra_ema_new`, not freed); `input`/`out` cover `n` `double`s.
#[no_mangle]
pub unsafe extern "C" fn wickra_ema_batch(
    handle: *mut Ema,
    input: *const f64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null() || input.is_null() || out.is_null() {
        return;
    }
    let ind = &mut *handle;
    let inputs = slice::from_raw_parts(input, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (slot, &value) in outputs.iter_mut().zip(inputs) {
        *slot = ind.update(value).unwrap_or(f64::NAN);
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_ema_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_ema_warmup_period(handle: *mut Ema) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_ema_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_ema_is_ready(handle: *mut Ema) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_ema_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_ema_name(handle: *mut Ema) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_ema_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_ema_reset(handle: *mut Ema) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_ema_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_ema_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_ema_free(handle: *mut Ema) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `EmpiricalModeDecomposition` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_empirical_mode_decomposition_free`.
#[no_mangle]
pub extern "C" fn wickra_empirical_mode_decomposition_new(
    period: usize,
    fraction: f64,
) -> *mut EmpiricalModeDecomposition {
    match EmpiricalModeDecomposition::new(period, fraction) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one value; returns the output, or `NaN` during warmup / on a `NULL` handle.
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_empirical_mode_decomposition_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_empirical_mode_decomposition_update(
    handle: *mut EmpiricalModeDecomposition,
    value: f64,
) -> f64 {
    match handle.as_mut() {
        Some(ind) => ind.update(value).unwrap_or(f64::NAN),
        None => f64::NAN,
    }
}

/// Run over `input[0..n]`, writing one output per input into `out[0..n]` (`NaN` at warmup).
///
/// # Safety
/// `handle` valid (from `wickra_empirical_mode_decomposition_new`, not freed); `input`/`out` cover `n` `double`s.
#[no_mangle]
pub unsafe extern "C" fn wickra_empirical_mode_decomposition_batch(
    handle: *mut EmpiricalModeDecomposition,
    input: *const f64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null() || input.is_null() || out.is_null() {
        return;
    }
    let ind = &mut *handle;
    let inputs = slice::from_raw_parts(input, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (slot, &value) in outputs.iter_mut().zip(inputs) {
        *slot = ind.update(value).unwrap_or(f64::NAN);
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_empirical_mode_decomposition_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_empirical_mode_decomposition_warmup_period(
    handle: *mut EmpiricalModeDecomposition,
) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_empirical_mode_decomposition_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_empirical_mode_decomposition_is_ready(
    handle: *mut EmpiricalModeDecomposition,
) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_empirical_mode_decomposition_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_empirical_mode_decomposition_name(
    handle: *mut EmpiricalModeDecomposition,
) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_empirical_mode_decomposition_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_empirical_mode_decomposition_reset(
    handle: *mut EmpiricalModeDecomposition,
) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_empirical_mode_decomposition_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_empirical_mode_decomposition_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_empirical_mode_decomposition_free(
    handle: *mut EmpiricalModeDecomposition,
) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `EvenBetterSinewave` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_even_better_sinewave_free`.
#[no_mangle]
pub extern "C" fn wickra_even_better_sinewave_new(
    hp_period: usize,
    ssf_length: usize,
) -> *mut EvenBetterSinewave {
    match EvenBetterSinewave::new(hp_period, ssf_length) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one value; returns the output, or `NaN` during warmup / on a `NULL` handle.
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_even_better_sinewave_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_even_better_sinewave_update(
    handle: *mut EvenBetterSinewave,
    value: f64,
) -> f64 {
    match handle.as_mut() {
        Some(ind) => ind.update(value).unwrap_or(f64::NAN),
        None => f64::NAN,
    }
}

/// Run over `input[0..n]`, writing one output per input into `out[0..n]` (`NaN` at warmup).
///
/// # Safety
/// `handle` valid (from `wickra_even_better_sinewave_new`, not freed); `input`/`out` cover `n` `double`s.
#[no_mangle]
pub unsafe extern "C" fn wickra_even_better_sinewave_batch(
    handle: *mut EvenBetterSinewave,
    input: *const f64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null() || input.is_null() || out.is_null() {
        return;
    }
    let ind = &mut *handle;
    let inputs = slice::from_raw_parts(input, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (slot, &value) in outputs.iter_mut().zip(inputs) {
        *slot = ind.update(value).unwrap_or(f64::NAN);
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_even_better_sinewave_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_even_better_sinewave_warmup_period(
    handle: *mut EvenBetterSinewave,
) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_even_better_sinewave_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_even_better_sinewave_is_ready(
    handle: *mut EvenBetterSinewave,
) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_even_better_sinewave_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_even_better_sinewave_name(
    handle: *mut EvenBetterSinewave,
) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_even_better_sinewave_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_even_better_sinewave_reset(handle: *mut EvenBetterSinewave) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_even_better_sinewave_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_even_better_sinewave_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_even_better_sinewave_free(handle: *mut EvenBetterSinewave) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `EwmaVolatility` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_ewma_volatility_free`.
#[no_mangle]
pub extern "C" fn wickra_ewma_volatility_new(lambda: f64) -> *mut EwmaVolatility {
    match EwmaVolatility::new(lambda) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one value; returns the output, or `NaN` during warmup / on a `NULL` handle.
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_ewma_volatility_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_ewma_volatility_update(
    handle: *mut EwmaVolatility,
    value: f64,
) -> f64 {
    match handle.as_mut() {
        Some(ind) => ind.update(value).unwrap_or(f64::NAN),
        None => f64::NAN,
    }
}

/// Run over `input[0..n]`, writing one output per input into `out[0..n]` (`NaN` at warmup).
///
/// # Safety
/// `handle` valid (from `wickra_ewma_volatility_new`, not freed); `input`/`out` cover `n` `double`s.
#[no_mangle]
pub unsafe extern "C" fn wickra_ewma_volatility_batch(
    handle: *mut EwmaVolatility,
    input: *const f64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null() || input.is_null() || out.is_null() {
        return;
    }
    let ind = &mut *handle;
    let inputs = slice::from_raw_parts(input, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (slot, &value) in outputs.iter_mut().zip(inputs) {
        *slot = ind.update(value).unwrap_or(f64::NAN);
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_ewma_volatility_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_ewma_volatility_warmup_period(
    handle: *mut EwmaVolatility,
) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_ewma_volatility_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_ewma_volatility_is_ready(handle: *mut EwmaVolatility) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_ewma_volatility_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_ewma_volatility_name(handle: *mut EwmaVolatility) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_ewma_volatility_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_ewma_volatility_reset(handle: *mut EwmaVolatility) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_ewma_volatility_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_ewma_volatility_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_ewma_volatility_free(handle: *mut EwmaVolatility) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `Expectancy` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_expectancy_free`.
#[no_mangle]
pub extern "C" fn wickra_expectancy_new(period: usize) -> *mut Expectancy {
    match Expectancy::new(period) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one value; returns the output, or `NaN` during warmup / on a `NULL` handle.
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_expectancy_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_expectancy_update(handle: *mut Expectancy, value: f64) -> f64 {
    match handle.as_mut() {
        Some(ind) => ind.update(value).unwrap_or(f64::NAN),
        None => f64::NAN,
    }
}

/// Run over `input[0..n]`, writing one output per input into `out[0..n]` (`NaN` at warmup).
///
/// # Safety
/// `handle` valid (from `wickra_expectancy_new`, not freed); `input`/`out` cover `n` `double`s.
#[no_mangle]
pub unsafe extern "C" fn wickra_expectancy_batch(
    handle: *mut Expectancy,
    input: *const f64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null() || input.is_null() || out.is_null() {
        return;
    }
    let ind = &mut *handle;
    let inputs = slice::from_raw_parts(input, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (slot, &value) in outputs.iter_mut().zip(inputs) {
        *slot = ind.update(value).unwrap_or(f64::NAN);
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_expectancy_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_expectancy_warmup_period(handle: *mut Expectancy) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_expectancy_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_expectancy_is_ready(handle: *mut Expectancy) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_expectancy_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_expectancy_name(handle: *mut Expectancy) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_expectancy_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_expectancy_reset(handle: *mut Expectancy) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_expectancy_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_expectancy_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_expectancy_free(handle: *mut Expectancy) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `Fama` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_fama_free`.
#[no_mangle]
pub extern "C" fn wickra_fama_new(fast_limit: f64, slow_limit: f64) -> *mut Fama {
    match Fama::new(fast_limit, slow_limit) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one value; returns the output, or `NaN` during warmup / on a `NULL` handle.
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_fama_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_fama_update(handle: *mut Fama, value: f64) -> f64 {
    match handle.as_mut() {
        Some(ind) => ind.update(value).unwrap_or(f64::NAN),
        None => f64::NAN,
    }
}

/// Run over `input[0..n]`, writing one output per input into `out[0..n]` (`NaN` at warmup).
///
/// # Safety
/// `handle` valid (from `wickra_fama_new`, not freed); `input`/`out` cover `n` `double`s.
#[no_mangle]
pub unsafe extern "C" fn wickra_fama_batch(
    handle: *mut Fama,
    input: *const f64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null() || input.is_null() || out.is_null() {
        return;
    }
    let ind = &mut *handle;
    let inputs = slice::from_raw_parts(input, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (slot, &value) in outputs.iter_mut().zip(inputs) {
        *slot = ind.update(value).unwrap_or(f64::NAN);
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_fama_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_fama_warmup_period(handle: *mut Fama) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_fama_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_fama_is_ready(handle: *mut Fama) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_fama_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_fama_name(handle: *mut Fama) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_fama_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_fama_reset(handle: *mut Fama) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_fama_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_fama_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_fama_free(handle: *mut Fama) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `FisherRsi` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_fisher_rsi_free`.
#[no_mangle]
pub extern "C" fn wickra_fisher_rsi_new(period: usize) -> *mut FisherRsi {
    match FisherRsi::new(period) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one value; returns the output, or `NaN` during warmup / on a `NULL` handle.
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_fisher_rsi_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_fisher_rsi_update(handle: *mut FisherRsi, value: f64) -> f64 {
    match handle.as_mut() {
        Some(ind) => ind.update(value).unwrap_or(f64::NAN),
        None => f64::NAN,
    }
}

/// Run over `input[0..n]`, writing one output per input into `out[0..n]` (`NaN` at warmup).
///
/// # Safety
/// `handle` valid (from `wickra_fisher_rsi_new`, not freed); `input`/`out` cover `n` `double`s.
#[no_mangle]
pub unsafe extern "C" fn wickra_fisher_rsi_batch(
    handle: *mut FisherRsi,
    input: *const f64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null() || input.is_null() || out.is_null() {
        return;
    }
    let ind = &mut *handle;
    let inputs = slice::from_raw_parts(input, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (slot, &value) in outputs.iter_mut().zip(inputs) {
        *slot = ind.update(value).unwrap_or(f64::NAN);
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_fisher_rsi_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_fisher_rsi_warmup_period(handle: *mut FisherRsi) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_fisher_rsi_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_fisher_rsi_is_ready(handle: *mut FisherRsi) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_fisher_rsi_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_fisher_rsi_name(handle: *mut FisherRsi) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_fisher_rsi_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_fisher_rsi_reset(handle: *mut FisherRsi) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_fisher_rsi_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_fisher_rsi_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_fisher_rsi_free(handle: *mut FisherRsi) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `FisherTransform` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_fisher_transform_free`.
#[no_mangle]
pub extern "C" fn wickra_fisher_transform_new(period: usize) -> *mut FisherTransform {
    match FisherTransform::new(period) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one value; returns the output, or `NaN` during warmup / on a `NULL` handle.
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_fisher_transform_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_fisher_transform_update(
    handle: *mut FisherTransform,
    value: f64,
) -> f64 {
    match handle.as_mut() {
        Some(ind) => ind.update(value).unwrap_or(f64::NAN),
        None => f64::NAN,
    }
}

/// Run over `input[0..n]`, writing one output per input into `out[0..n]` (`NaN` at warmup).
///
/// # Safety
/// `handle` valid (from `wickra_fisher_transform_new`, not freed); `input`/`out` cover `n` `double`s.
#[no_mangle]
pub unsafe extern "C" fn wickra_fisher_transform_batch(
    handle: *mut FisherTransform,
    input: *const f64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null() || input.is_null() || out.is_null() {
        return;
    }
    let ind = &mut *handle;
    let inputs = slice::from_raw_parts(input, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (slot, &value) in outputs.iter_mut().zip(inputs) {
        *slot = ind.update(value).unwrap_or(f64::NAN);
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_fisher_transform_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_fisher_transform_warmup_period(
    handle: *mut FisherTransform,
) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_fisher_transform_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_fisher_transform_is_ready(handle: *mut FisherTransform) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_fisher_transform_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_fisher_transform_name(
    handle: *mut FisherTransform,
) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_fisher_transform_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_fisher_transform_reset(handle: *mut FisherTransform) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_fisher_transform_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_fisher_transform_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_fisher_transform_free(handle: *mut FisherTransform) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `Frama` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_frama_free`.
#[no_mangle]
pub extern "C" fn wickra_frama_new(period: usize) -> *mut Frama {
    match Frama::new(period) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one value; returns the output, or `NaN` during warmup / on a `NULL` handle.
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_frama_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_frama_update(handle: *mut Frama, value: f64) -> f64 {
    match handle.as_mut() {
        Some(ind) => ind.update(value).unwrap_or(f64::NAN),
        None => f64::NAN,
    }
}

/// Run over `input[0..n]`, writing one output per input into `out[0..n]` (`NaN` at warmup).
///
/// # Safety
/// `handle` valid (from `wickra_frama_new`, not freed); `input`/`out` cover `n` `double`s.
#[no_mangle]
pub unsafe extern "C" fn wickra_frama_batch(
    handle: *mut Frama,
    input: *const f64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null() || input.is_null() || out.is_null() {
        return;
    }
    let ind = &mut *handle;
    let inputs = slice::from_raw_parts(input, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (slot, &value) in outputs.iter_mut().zip(inputs) {
        *slot = ind.update(value).unwrap_or(f64::NAN);
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_frama_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_frama_warmup_period(handle: *mut Frama) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_frama_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_frama_is_ready(handle: *mut Frama) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_frama_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_frama_name(handle: *mut Frama) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_frama_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_frama_reset(handle: *mut Frama) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_frama_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_frama_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_frama_free(handle: *mut Frama) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `GainLossRatio` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_gain_loss_ratio_free`.
#[no_mangle]
pub extern "C" fn wickra_gain_loss_ratio_new(period: usize) -> *mut GainLossRatio {
    match GainLossRatio::new(period) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one value; returns the output, or `NaN` during warmup / on a `NULL` handle.
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_gain_loss_ratio_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_gain_loss_ratio_update(
    handle: *mut GainLossRatio,
    value: f64,
) -> f64 {
    match handle.as_mut() {
        Some(ind) => ind.update(value).unwrap_or(f64::NAN),
        None => f64::NAN,
    }
}

/// Run over `input[0..n]`, writing one output per input into `out[0..n]` (`NaN` at warmup).
///
/// # Safety
/// `handle` valid (from `wickra_gain_loss_ratio_new`, not freed); `input`/`out` cover `n` `double`s.
#[no_mangle]
pub unsafe extern "C" fn wickra_gain_loss_ratio_batch(
    handle: *mut GainLossRatio,
    input: *const f64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null() || input.is_null() || out.is_null() {
        return;
    }
    let ind = &mut *handle;
    let inputs = slice::from_raw_parts(input, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (slot, &value) in outputs.iter_mut().zip(inputs) {
        *slot = ind.update(value).unwrap_or(f64::NAN);
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_gain_loss_ratio_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_gain_loss_ratio_warmup_period(handle: *mut GainLossRatio) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_gain_loss_ratio_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_gain_loss_ratio_is_ready(handle: *mut GainLossRatio) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_gain_loss_ratio_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_gain_loss_ratio_name(handle: *mut GainLossRatio) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_gain_loss_ratio_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_gain_loss_ratio_reset(handle: *mut GainLossRatio) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_gain_loss_ratio_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_gain_loss_ratio_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_gain_loss_ratio_free(handle: *mut GainLossRatio) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `GainToPainRatio` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_gain_to_pain_ratio_free`.
#[no_mangle]
pub extern "C" fn wickra_gain_to_pain_ratio_new(period: usize) -> *mut GainToPainRatio {
    match GainToPainRatio::new(period) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one value; returns the output, or `NaN` during warmup / on a `NULL` handle.
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_gain_to_pain_ratio_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_gain_to_pain_ratio_update(
    handle: *mut GainToPainRatio,
    value: f64,
) -> f64 {
    match handle.as_mut() {
        Some(ind) => ind.update(value).unwrap_or(f64::NAN),
        None => f64::NAN,
    }
}

/// Run over `input[0..n]`, writing one output per input into `out[0..n]` (`NaN` at warmup).
///
/// # Safety
/// `handle` valid (from `wickra_gain_to_pain_ratio_new`, not freed); `input`/`out` cover `n` `double`s.
#[no_mangle]
pub unsafe extern "C" fn wickra_gain_to_pain_ratio_batch(
    handle: *mut GainToPainRatio,
    input: *const f64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null() || input.is_null() || out.is_null() {
        return;
    }
    let ind = &mut *handle;
    let inputs = slice::from_raw_parts(input, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (slot, &value) in outputs.iter_mut().zip(inputs) {
        *slot = ind.update(value).unwrap_or(f64::NAN);
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_gain_to_pain_ratio_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_gain_to_pain_ratio_warmup_period(
    handle: *mut GainToPainRatio,
) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_gain_to_pain_ratio_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_gain_to_pain_ratio_is_ready(handle: *mut GainToPainRatio) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_gain_to_pain_ratio_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_gain_to_pain_ratio_name(
    handle: *mut GainToPainRatio,
) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_gain_to_pain_ratio_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_gain_to_pain_ratio_reset(handle: *mut GainToPainRatio) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_gain_to_pain_ratio_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_gain_to_pain_ratio_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_gain_to_pain_ratio_free(handle: *mut GainToPainRatio) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `Garch11` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_garch11_free`.
#[no_mangle]
pub extern "C" fn wickra_garch11_new(omega: f64, alpha: f64, beta: f64) -> *mut Garch11 {
    match Garch11::new(omega, alpha, beta) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one value; returns the output, or `NaN` during warmup / on a `NULL` handle.
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_garch11_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_garch11_update(handle: *mut Garch11, value: f64) -> f64 {
    match handle.as_mut() {
        Some(ind) => ind.update(value).unwrap_or(f64::NAN),
        None => f64::NAN,
    }
}

/// Run over `input[0..n]`, writing one output per input into `out[0..n]` (`NaN` at warmup).
///
/// # Safety
/// `handle` valid (from `wickra_garch11_new`, not freed); `input`/`out` cover `n` `double`s.
#[no_mangle]
pub unsafe extern "C" fn wickra_garch11_batch(
    handle: *mut Garch11,
    input: *const f64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null() || input.is_null() || out.is_null() {
        return;
    }
    let ind = &mut *handle;
    let inputs = slice::from_raw_parts(input, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (slot, &value) in outputs.iter_mut().zip(inputs) {
        *slot = ind.update(value).unwrap_or(f64::NAN);
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_garch11_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_garch11_warmup_period(handle: *mut Garch11) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_garch11_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_garch11_is_ready(handle: *mut Garch11) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_garch11_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_garch11_name(handle: *mut Garch11) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_garch11_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_garch11_reset(handle: *mut Garch11) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_garch11_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_garch11_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_garch11_free(handle: *mut Garch11) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `GeneralizedDema` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_generalized_dema_free`.
#[no_mangle]
pub extern "C" fn wickra_generalized_dema_new(period: usize, v: f64) -> *mut GeneralizedDema {
    match GeneralizedDema::new(period, v) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one value; returns the output, or `NaN` during warmup / on a `NULL` handle.
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_generalized_dema_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_generalized_dema_update(
    handle: *mut GeneralizedDema,
    value: f64,
) -> f64 {
    match handle.as_mut() {
        Some(ind) => ind.update(value).unwrap_or(f64::NAN),
        None => f64::NAN,
    }
}

/// Run over `input[0..n]`, writing one output per input into `out[0..n]` (`NaN` at warmup).
///
/// # Safety
/// `handle` valid (from `wickra_generalized_dema_new`, not freed); `input`/`out` cover `n` `double`s.
#[no_mangle]
pub unsafe extern "C" fn wickra_generalized_dema_batch(
    handle: *mut GeneralizedDema,
    input: *const f64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null() || input.is_null() || out.is_null() {
        return;
    }
    let ind = &mut *handle;
    let inputs = slice::from_raw_parts(input, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (slot, &value) in outputs.iter_mut().zip(inputs) {
        *slot = ind.update(value).unwrap_or(f64::NAN);
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_generalized_dema_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_generalized_dema_warmup_period(
    handle: *mut GeneralizedDema,
) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_generalized_dema_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_generalized_dema_is_ready(handle: *mut GeneralizedDema) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_generalized_dema_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_generalized_dema_name(
    handle: *mut GeneralizedDema,
) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_generalized_dema_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_generalized_dema_reset(handle: *mut GeneralizedDema) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_generalized_dema_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_generalized_dema_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_generalized_dema_free(handle: *mut GeneralizedDema) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `GeometricMa` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_geometric_ma_free`.
#[no_mangle]
pub extern "C" fn wickra_geometric_ma_new(period: usize) -> *mut GeometricMa {
    match GeometricMa::new(period) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one value; returns the output, or `NaN` during warmup / on a `NULL` handle.
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_geometric_ma_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_geometric_ma_update(handle: *mut GeometricMa, value: f64) -> f64 {
    match handle.as_mut() {
        Some(ind) => ind.update(value).unwrap_or(f64::NAN),
        None => f64::NAN,
    }
}

/// Run over `input[0..n]`, writing one output per input into `out[0..n]` (`NaN` at warmup).
///
/// # Safety
/// `handle` valid (from `wickra_geometric_ma_new`, not freed); `input`/`out` cover `n` `double`s.
#[no_mangle]
pub unsafe extern "C" fn wickra_geometric_ma_batch(
    handle: *mut GeometricMa,
    input: *const f64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null() || input.is_null() || out.is_null() {
        return;
    }
    let ind = &mut *handle;
    let inputs = slice::from_raw_parts(input, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (slot, &value) in outputs.iter_mut().zip(inputs) {
        *slot = ind.update(value).unwrap_or(f64::NAN);
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_geometric_ma_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_geometric_ma_warmup_period(handle: *mut GeometricMa) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_geometric_ma_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_geometric_ma_is_ready(handle: *mut GeometricMa) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_geometric_ma_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_geometric_ma_name(handle: *mut GeometricMa) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_geometric_ma_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_geometric_ma_reset(handle: *mut GeometricMa) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_geometric_ma_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_geometric_ma_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_geometric_ma_free(handle: *mut GeometricMa) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `HighpassFilter` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_highpass_filter_free`.
#[no_mangle]
pub extern "C" fn wickra_highpass_filter_new(period: usize) -> *mut HighpassFilter {
    match HighpassFilter::new(period) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one value; returns the output, or `NaN` during warmup / on a `NULL` handle.
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_highpass_filter_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_highpass_filter_update(
    handle: *mut HighpassFilter,
    value: f64,
) -> f64 {
    match handle.as_mut() {
        Some(ind) => ind.update(value).unwrap_or(f64::NAN),
        None => f64::NAN,
    }
}

/// Run over `input[0..n]`, writing one output per input into `out[0..n]` (`NaN` at warmup).
///
/// # Safety
/// `handle` valid (from `wickra_highpass_filter_new`, not freed); `input`/`out` cover `n` `double`s.
#[no_mangle]
pub unsafe extern "C" fn wickra_highpass_filter_batch(
    handle: *mut HighpassFilter,
    input: *const f64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null() || input.is_null() || out.is_null() {
        return;
    }
    let ind = &mut *handle;
    let inputs = slice::from_raw_parts(input, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (slot, &value) in outputs.iter_mut().zip(inputs) {
        *slot = ind.update(value).unwrap_or(f64::NAN);
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_highpass_filter_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_highpass_filter_warmup_period(
    handle: *mut HighpassFilter,
) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_highpass_filter_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_highpass_filter_is_ready(handle: *mut HighpassFilter) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_highpass_filter_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_highpass_filter_name(handle: *mut HighpassFilter) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_highpass_filter_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_highpass_filter_reset(handle: *mut HighpassFilter) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_highpass_filter_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_highpass_filter_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_highpass_filter_free(handle: *mut HighpassFilter) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `HilbertDominantCycle` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_hilbert_dominant_cycle_free`.
#[no_mangle]
pub extern "C" fn wickra_hilbert_dominant_cycle_new() -> *mut HilbertDominantCycle {
    Box::into_raw(Box::new(HilbertDominantCycle::new()))
}

/// Feed one value; returns the output, or `NaN` during warmup / on a `NULL` handle.
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_hilbert_dominant_cycle_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_hilbert_dominant_cycle_update(
    handle: *mut HilbertDominantCycle,
    value: f64,
) -> f64 {
    match handle.as_mut() {
        Some(ind) => ind.update(value).unwrap_or(f64::NAN),
        None => f64::NAN,
    }
}

/// Run over `input[0..n]`, writing one output per input into `out[0..n]` (`NaN` at warmup).
///
/// # Safety
/// `handle` valid (from `wickra_hilbert_dominant_cycle_new`, not freed); `input`/`out` cover `n` `double`s.
#[no_mangle]
pub unsafe extern "C" fn wickra_hilbert_dominant_cycle_batch(
    handle: *mut HilbertDominantCycle,
    input: *const f64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null() || input.is_null() || out.is_null() {
        return;
    }
    let ind = &mut *handle;
    let inputs = slice::from_raw_parts(input, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (slot, &value) in outputs.iter_mut().zip(inputs) {
        *slot = ind.update(value).unwrap_or(f64::NAN);
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_hilbert_dominant_cycle_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_hilbert_dominant_cycle_warmup_period(
    handle: *mut HilbertDominantCycle,
) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_hilbert_dominant_cycle_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_hilbert_dominant_cycle_is_ready(
    handle: *mut HilbertDominantCycle,
) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_hilbert_dominant_cycle_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_hilbert_dominant_cycle_name(
    handle: *mut HilbertDominantCycle,
) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_hilbert_dominant_cycle_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_hilbert_dominant_cycle_reset(handle: *mut HilbertDominantCycle) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_hilbert_dominant_cycle_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_hilbert_dominant_cycle_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_hilbert_dominant_cycle_free(handle: *mut HilbertDominantCycle) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `HistoricalVolatility` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_historical_volatility_free`.
#[no_mangle]
pub extern "C" fn wickra_historical_volatility_new(
    period: usize,
    trading_periods: usize,
) -> *mut HistoricalVolatility {
    match HistoricalVolatility::new(period, trading_periods) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one value; returns the output, or `NaN` during warmup / on a `NULL` handle.
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_historical_volatility_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_historical_volatility_update(
    handle: *mut HistoricalVolatility,
    value: f64,
) -> f64 {
    match handle.as_mut() {
        Some(ind) => ind.update(value).unwrap_or(f64::NAN),
        None => f64::NAN,
    }
}

/// Run over `input[0..n]`, writing one output per input into `out[0..n]` (`NaN` at warmup).
///
/// # Safety
/// `handle` valid (from `wickra_historical_volatility_new`, not freed); `input`/`out` cover `n` `double`s.
#[no_mangle]
pub unsafe extern "C" fn wickra_historical_volatility_batch(
    handle: *mut HistoricalVolatility,
    input: *const f64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null() || input.is_null() || out.is_null() {
        return;
    }
    let ind = &mut *handle;
    let inputs = slice::from_raw_parts(input, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (slot, &value) in outputs.iter_mut().zip(inputs) {
        *slot = ind.update(value).unwrap_or(f64::NAN);
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_historical_volatility_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_historical_volatility_warmup_period(
    handle: *mut HistoricalVolatility,
) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_historical_volatility_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_historical_volatility_is_ready(
    handle: *mut HistoricalVolatility,
) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_historical_volatility_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_historical_volatility_name(
    handle: *mut HistoricalVolatility,
) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_historical_volatility_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_historical_volatility_reset(handle: *mut HistoricalVolatility) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_historical_volatility_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_historical_volatility_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_historical_volatility_free(handle: *mut HistoricalVolatility) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `Hma` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_hma_free`.
#[no_mangle]
pub extern "C" fn wickra_hma_new(period: usize) -> *mut Hma {
    match Hma::new(period) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one value; returns the output, or `NaN` during warmup / on a `NULL` handle.
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_hma_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_hma_update(handle: *mut Hma, value: f64) -> f64 {
    match handle.as_mut() {
        Some(ind) => ind.update(value).unwrap_or(f64::NAN),
        None => f64::NAN,
    }
}

/// Run over `input[0..n]`, writing one output per input into `out[0..n]` (`NaN` at warmup).
///
/// # Safety
/// `handle` valid (from `wickra_hma_new`, not freed); `input`/`out` cover `n` `double`s.
#[no_mangle]
pub unsafe extern "C" fn wickra_hma_batch(
    handle: *mut Hma,
    input: *const f64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null() || input.is_null() || out.is_null() {
        return;
    }
    let ind = &mut *handle;
    let inputs = slice::from_raw_parts(input, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (slot, &value) in outputs.iter_mut().zip(inputs) {
        *slot = ind.update(value).unwrap_or(f64::NAN);
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_hma_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_hma_warmup_period(handle: *mut Hma) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_hma_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_hma_is_ready(handle: *mut Hma) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_hma_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_hma_name(handle: *mut Hma) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_hma_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_hma_reset(handle: *mut Hma) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_hma_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_hma_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_hma_free(handle: *mut Hma) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `HoltWinters` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_holt_winters_free`.
#[no_mangle]
pub extern "C" fn wickra_holt_winters_new(alpha: f64, beta: f64) -> *mut HoltWinters {
    match HoltWinters::new(alpha, beta) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one value; returns the output, or `NaN` during warmup / on a `NULL` handle.
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_holt_winters_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_holt_winters_update(handle: *mut HoltWinters, value: f64) -> f64 {
    match handle.as_mut() {
        Some(ind) => ind.update(value).unwrap_or(f64::NAN),
        None => f64::NAN,
    }
}

/// Run over `input[0..n]`, writing one output per input into `out[0..n]` (`NaN` at warmup).
///
/// # Safety
/// `handle` valid (from `wickra_holt_winters_new`, not freed); `input`/`out` cover `n` `double`s.
#[no_mangle]
pub unsafe extern "C" fn wickra_holt_winters_batch(
    handle: *mut HoltWinters,
    input: *const f64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null() || input.is_null() || out.is_null() {
        return;
    }
    let ind = &mut *handle;
    let inputs = slice::from_raw_parts(input, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (slot, &value) in outputs.iter_mut().zip(inputs) {
        *slot = ind.update(value).unwrap_or(f64::NAN);
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_holt_winters_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_holt_winters_warmup_period(handle: *mut HoltWinters) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_holt_winters_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_holt_winters_is_ready(handle: *mut HoltWinters) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_holt_winters_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_holt_winters_name(handle: *mut HoltWinters) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_holt_winters_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_holt_winters_reset(handle: *mut HoltWinters) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_holt_winters_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_holt_winters_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_holt_winters_free(handle: *mut HoltWinters) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `HtDcPhase` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_ht_dc_phase_free`.
#[no_mangle]
pub extern "C" fn wickra_ht_dc_phase_new() -> *mut HtDcPhase {
    Box::into_raw(Box::new(HtDcPhase::new()))
}

/// Feed one value; returns the output, or `NaN` during warmup / on a `NULL` handle.
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_ht_dc_phase_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_ht_dc_phase_update(handle: *mut HtDcPhase, value: f64) -> f64 {
    match handle.as_mut() {
        Some(ind) => ind.update(value).unwrap_or(f64::NAN),
        None => f64::NAN,
    }
}

/// Run over `input[0..n]`, writing one output per input into `out[0..n]` (`NaN` at warmup).
///
/// # Safety
/// `handle` valid (from `wickra_ht_dc_phase_new`, not freed); `input`/`out` cover `n` `double`s.
#[no_mangle]
pub unsafe extern "C" fn wickra_ht_dc_phase_batch(
    handle: *mut HtDcPhase,
    input: *const f64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null() || input.is_null() || out.is_null() {
        return;
    }
    let ind = &mut *handle;
    let inputs = slice::from_raw_parts(input, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (slot, &value) in outputs.iter_mut().zip(inputs) {
        *slot = ind.update(value).unwrap_or(f64::NAN);
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_ht_dc_phase_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_ht_dc_phase_warmup_period(handle: *mut HtDcPhase) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_ht_dc_phase_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_ht_dc_phase_is_ready(handle: *mut HtDcPhase) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_ht_dc_phase_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_ht_dc_phase_name(handle: *mut HtDcPhase) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_ht_dc_phase_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_ht_dc_phase_reset(handle: *mut HtDcPhase) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_ht_dc_phase_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_ht_dc_phase_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_ht_dc_phase_free(handle: *mut HtDcPhase) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `HtTrendMode` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_ht_trend_mode_free`.
#[no_mangle]
pub extern "C" fn wickra_ht_trend_mode_new() -> *mut HtTrendMode {
    Box::into_raw(Box::new(HtTrendMode::new()))
}

/// Feed one value; returns the output, or `NaN` during warmup / on a `NULL` handle.
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_ht_trend_mode_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_ht_trend_mode_update(handle: *mut HtTrendMode, value: f64) -> f64 {
    match handle.as_mut() {
        Some(ind) => ind.update(value).unwrap_or(f64::NAN),
        None => f64::NAN,
    }
}

/// Run over `input[0..n]`, writing one output per input into `out[0..n]` (`NaN` at warmup).
///
/// # Safety
/// `handle` valid (from `wickra_ht_trend_mode_new`, not freed); `input`/`out` cover `n` `double`s.
#[no_mangle]
pub unsafe extern "C" fn wickra_ht_trend_mode_batch(
    handle: *mut HtTrendMode,
    input: *const f64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null() || input.is_null() || out.is_null() {
        return;
    }
    let ind = &mut *handle;
    let inputs = slice::from_raw_parts(input, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (slot, &value) in outputs.iter_mut().zip(inputs) {
        *slot = ind.update(value).unwrap_or(f64::NAN);
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_ht_trend_mode_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_ht_trend_mode_warmup_period(handle: *mut HtTrendMode) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_ht_trend_mode_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_ht_trend_mode_is_ready(handle: *mut HtTrendMode) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_ht_trend_mode_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_ht_trend_mode_name(handle: *mut HtTrendMode) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_ht_trend_mode_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_ht_trend_mode_reset(handle: *mut HtTrendMode) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_ht_trend_mode_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_ht_trend_mode_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_ht_trend_mode_free(handle: *mut HtTrendMode) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `HurstExponent` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_hurst_exponent_free`.
#[no_mangle]
pub extern "C" fn wickra_hurst_exponent_new(period: usize, chunks: usize) -> *mut HurstExponent {
    match HurstExponent::new(period, chunks) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one value; returns the output, or `NaN` during warmup / on a `NULL` handle.
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_hurst_exponent_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_hurst_exponent_update(
    handle: *mut HurstExponent,
    value: f64,
) -> f64 {
    match handle.as_mut() {
        Some(ind) => ind.update(value).unwrap_or(f64::NAN),
        None => f64::NAN,
    }
}

/// Run over `input[0..n]`, writing one output per input into `out[0..n]` (`NaN` at warmup).
///
/// # Safety
/// `handle` valid (from `wickra_hurst_exponent_new`, not freed); `input`/`out` cover `n` `double`s.
#[no_mangle]
pub unsafe extern "C" fn wickra_hurst_exponent_batch(
    handle: *mut HurstExponent,
    input: *const f64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null() || input.is_null() || out.is_null() {
        return;
    }
    let ind = &mut *handle;
    let inputs = slice::from_raw_parts(input, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (slot, &value) in outputs.iter_mut().zip(inputs) {
        *slot = ind.update(value).unwrap_or(f64::NAN);
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_hurst_exponent_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_hurst_exponent_warmup_period(handle: *mut HurstExponent) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_hurst_exponent_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_hurst_exponent_is_ready(handle: *mut HurstExponent) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_hurst_exponent_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_hurst_exponent_name(handle: *mut HurstExponent) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_hurst_exponent_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_hurst_exponent_reset(handle: *mut HurstExponent) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_hurst_exponent_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_hurst_exponent_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_hurst_exponent_free(handle: *mut HurstExponent) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `InstantaneousTrendline` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_instantaneous_trendline_free`.
#[no_mangle]
pub extern "C" fn wickra_instantaneous_trendline_new(period: usize) -> *mut InstantaneousTrendline {
    match InstantaneousTrendline::new(period) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one value; returns the output, or `NaN` during warmup / on a `NULL` handle.
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_instantaneous_trendline_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_instantaneous_trendline_update(
    handle: *mut InstantaneousTrendline,
    value: f64,
) -> f64 {
    match handle.as_mut() {
        Some(ind) => ind.update(value).unwrap_or(f64::NAN),
        None => f64::NAN,
    }
}

/// Run over `input[0..n]`, writing one output per input into `out[0..n]` (`NaN` at warmup).
///
/// # Safety
/// `handle` valid (from `wickra_instantaneous_trendline_new`, not freed); `input`/`out` cover `n` `double`s.
#[no_mangle]
pub unsafe extern "C" fn wickra_instantaneous_trendline_batch(
    handle: *mut InstantaneousTrendline,
    input: *const f64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null() || input.is_null() || out.is_null() {
        return;
    }
    let ind = &mut *handle;
    let inputs = slice::from_raw_parts(input, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (slot, &value) in outputs.iter_mut().zip(inputs) {
        *slot = ind.update(value).unwrap_or(f64::NAN);
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_instantaneous_trendline_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_instantaneous_trendline_warmup_period(
    handle: *mut InstantaneousTrendline,
) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_instantaneous_trendline_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_instantaneous_trendline_is_ready(
    handle: *mut InstantaneousTrendline,
) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_instantaneous_trendline_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_instantaneous_trendline_name(
    handle: *mut InstantaneousTrendline,
) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_instantaneous_trendline_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_instantaneous_trendline_reset(handle: *mut InstantaneousTrendline) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_instantaneous_trendline_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_instantaneous_trendline_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_instantaneous_trendline_free(handle: *mut InstantaneousTrendline) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `InverseFisherTransform` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_inverse_fisher_transform_free`.
#[no_mangle]
pub extern "C" fn wickra_inverse_fisher_transform_new(scale: f64) -> *mut InverseFisherTransform {
    match InverseFisherTransform::new(scale) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one value; returns the output, or `NaN` during warmup / on a `NULL` handle.
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_inverse_fisher_transform_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_inverse_fisher_transform_update(
    handle: *mut InverseFisherTransform,
    value: f64,
) -> f64 {
    match handle.as_mut() {
        Some(ind) => ind.update(value).unwrap_or(f64::NAN),
        None => f64::NAN,
    }
}

/// Run over `input[0..n]`, writing one output per input into `out[0..n]` (`NaN` at warmup).
///
/// # Safety
/// `handle` valid (from `wickra_inverse_fisher_transform_new`, not freed); `input`/`out` cover `n` `double`s.
#[no_mangle]
pub unsafe extern "C" fn wickra_inverse_fisher_transform_batch(
    handle: *mut InverseFisherTransform,
    input: *const f64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null() || input.is_null() || out.is_null() {
        return;
    }
    let ind = &mut *handle;
    let inputs = slice::from_raw_parts(input, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (slot, &value) in outputs.iter_mut().zip(inputs) {
        *slot = ind.update(value).unwrap_or(f64::NAN);
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_inverse_fisher_transform_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_inverse_fisher_transform_warmup_period(
    handle: *mut InverseFisherTransform,
) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_inverse_fisher_transform_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_inverse_fisher_transform_is_ready(
    handle: *mut InverseFisherTransform,
) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_inverse_fisher_transform_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_inverse_fisher_transform_name(
    handle: *mut InverseFisherTransform,
) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_inverse_fisher_transform_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_inverse_fisher_transform_reset(
    handle: *mut InverseFisherTransform,
) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_inverse_fisher_transform_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_inverse_fisher_transform_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_inverse_fisher_transform_free(handle: *mut InverseFisherTransform) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `JarqueBera` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_jarque_bera_free`.
#[no_mangle]
pub extern "C" fn wickra_jarque_bera_new(period: usize) -> *mut JarqueBera {
    match JarqueBera::new(period) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one value; returns the output, or `NaN` during warmup / on a `NULL` handle.
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_jarque_bera_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_jarque_bera_update(handle: *mut JarqueBera, value: f64) -> f64 {
    match handle.as_mut() {
        Some(ind) => ind.update(value).unwrap_or(f64::NAN),
        None => f64::NAN,
    }
}

/// Run over `input[0..n]`, writing one output per input into `out[0..n]` (`NaN` at warmup).
///
/// # Safety
/// `handle` valid (from `wickra_jarque_bera_new`, not freed); `input`/`out` cover `n` `double`s.
#[no_mangle]
pub unsafe extern "C" fn wickra_jarque_bera_batch(
    handle: *mut JarqueBera,
    input: *const f64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null() || input.is_null() || out.is_null() {
        return;
    }
    let ind = &mut *handle;
    let inputs = slice::from_raw_parts(input, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (slot, &value) in outputs.iter_mut().zip(inputs) {
        *slot = ind.update(value).unwrap_or(f64::NAN);
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_jarque_bera_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_jarque_bera_warmup_period(handle: *mut JarqueBera) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_jarque_bera_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_jarque_bera_is_ready(handle: *mut JarqueBera) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_jarque_bera_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_jarque_bera_name(handle: *mut JarqueBera) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_jarque_bera_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_jarque_bera_reset(handle: *mut JarqueBera) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_jarque_bera_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_jarque_bera_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_jarque_bera_free(handle: *mut JarqueBera) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `Jma` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_jma_free`.
#[no_mangle]
pub extern "C" fn wickra_jma_new(period: usize, phase: f64, power: u32) -> *mut Jma {
    match Jma::new(period, phase, power) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one value; returns the output, or `NaN` during warmup / on a `NULL` handle.
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_jma_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_jma_update(handle: *mut Jma, value: f64) -> f64 {
    match handle.as_mut() {
        Some(ind) => ind.update(value).unwrap_or(f64::NAN),
        None => f64::NAN,
    }
}

/// Run over `input[0..n]`, writing one output per input into `out[0..n]` (`NaN` at warmup).
///
/// # Safety
/// `handle` valid (from `wickra_jma_new`, not freed); `input`/`out` cover `n` `double`s.
#[no_mangle]
pub unsafe extern "C" fn wickra_jma_batch(
    handle: *mut Jma,
    input: *const f64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null() || input.is_null() || out.is_null() {
        return;
    }
    let ind = &mut *handle;
    let inputs = slice::from_raw_parts(input, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (slot, &value) in outputs.iter_mut().zip(inputs) {
        *slot = ind.update(value).unwrap_or(f64::NAN);
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_jma_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_jma_warmup_period(handle: *mut Jma) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_jma_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_jma_is_ready(handle: *mut Jma) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_jma_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_jma_name(handle: *mut Jma) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_jma_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_jma_reset(handle: *mut Jma) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_jma_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_jma_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_jma_free(handle: *mut Jma) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `JumpIndicator` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_jump_indicator_free`.
#[no_mangle]
pub extern "C" fn wickra_jump_indicator_new(period: usize, threshold: f64) -> *mut JumpIndicator {
    match JumpIndicator::new(period, threshold) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one value; returns the output, or `NaN` during warmup / on a `NULL` handle.
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_jump_indicator_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_jump_indicator_update(
    handle: *mut JumpIndicator,
    value: f64,
) -> f64 {
    match handle.as_mut() {
        Some(ind) => ind.update(value).unwrap_or(f64::NAN),
        None => f64::NAN,
    }
}

/// Run over `input[0..n]`, writing one output per input into `out[0..n]` (`NaN` at warmup).
///
/// # Safety
/// `handle` valid (from `wickra_jump_indicator_new`, not freed); `input`/`out` cover `n` `double`s.
#[no_mangle]
pub unsafe extern "C" fn wickra_jump_indicator_batch(
    handle: *mut JumpIndicator,
    input: *const f64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null() || input.is_null() || out.is_null() {
        return;
    }
    let ind = &mut *handle;
    let inputs = slice::from_raw_parts(input, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (slot, &value) in outputs.iter_mut().zip(inputs) {
        *slot = ind.update(value).unwrap_or(f64::NAN);
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_jump_indicator_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_jump_indicator_warmup_period(handle: *mut JumpIndicator) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_jump_indicator_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_jump_indicator_is_ready(handle: *mut JumpIndicator) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_jump_indicator_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_jump_indicator_name(handle: *mut JumpIndicator) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_jump_indicator_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_jump_indicator_reset(handle: *mut JumpIndicator) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_jump_indicator_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_jump_indicator_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_jump_indicator_free(handle: *mut JumpIndicator) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `KRatio` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_k_ratio_free`.
#[no_mangle]
pub extern "C" fn wickra_k_ratio_new(period: usize) -> *mut KRatio {
    match KRatio::new(period) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one value; returns the output, or `NaN` during warmup / on a `NULL` handle.
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_k_ratio_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_k_ratio_update(handle: *mut KRatio, value: f64) -> f64 {
    match handle.as_mut() {
        Some(ind) => ind.update(value).unwrap_or(f64::NAN),
        None => f64::NAN,
    }
}

/// Run over `input[0..n]`, writing one output per input into `out[0..n]` (`NaN` at warmup).
///
/// # Safety
/// `handle` valid (from `wickra_k_ratio_new`, not freed); `input`/`out` cover `n` `double`s.
#[no_mangle]
pub unsafe extern "C" fn wickra_k_ratio_batch(
    handle: *mut KRatio,
    input: *const f64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null() || input.is_null() || out.is_null() {
        return;
    }
    let ind = &mut *handle;
    let inputs = slice::from_raw_parts(input, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (slot, &value) in outputs.iter_mut().zip(inputs) {
        *slot = ind.update(value).unwrap_or(f64::NAN);
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_k_ratio_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_k_ratio_warmup_period(handle: *mut KRatio) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_k_ratio_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_k_ratio_is_ready(handle: *mut KRatio) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_k_ratio_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_k_ratio_name(handle: *mut KRatio) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_k_ratio_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_k_ratio_reset(handle: *mut KRatio) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_k_ratio_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_k_ratio_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_k_ratio_free(handle: *mut KRatio) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `Kama` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_kama_free`.
#[no_mangle]
pub extern "C" fn wickra_kama_new(er_period: usize, fast: usize, slow: usize) -> *mut Kama {
    match Kama::new(er_period, fast, slow) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one value; returns the output, or `NaN` during warmup / on a `NULL` handle.
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_kama_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_kama_update(handle: *mut Kama, value: f64) -> f64 {
    match handle.as_mut() {
        Some(ind) => ind.update(value).unwrap_or(f64::NAN),
        None => f64::NAN,
    }
}

/// Run over `input[0..n]`, writing one output per input into `out[0..n]` (`NaN` at warmup).
///
/// # Safety
/// `handle` valid (from `wickra_kama_new`, not freed); `input`/`out` cover `n` `double`s.
#[no_mangle]
pub unsafe extern "C" fn wickra_kama_batch(
    handle: *mut Kama,
    input: *const f64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null() || input.is_null() || out.is_null() {
        return;
    }
    let ind = &mut *handle;
    let inputs = slice::from_raw_parts(input, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (slot, &value) in outputs.iter_mut().zip(inputs) {
        *slot = ind.update(value).unwrap_or(f64::NAN);
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_kama_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_kama_warmup_period(handle: *mut Kama) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_kama_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_kama_is_ready(handle: *mut Kama) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_kama_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_kama_name(handle: *mut Kama) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_kama_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_kama_reset(handle: *mut Kama) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_kama_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_kama_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_kama_free(handle: *mut Kama) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `KellyCriterion` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_kelly_criterion_free`.
#[no_mangle]
pub extern "C" fn wickra_kelly_criterion_new(period: usize) -> *mut KellyCriterion {
    match KellyCriterion::new(period) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one value; returns the output, or `NaN` during warmup / on a `NULL` handle.
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_kelly_criterion_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_kelly_criterion_update(
    handle: *mut KellyCriterion,
    value: f64,
) -> f64 {
    match handle.as_mut() {
        Some(ind) => ind.update(value).unwrap_or(f64::NAN),
        None => f64::NAN,
    }
}

/// Run over `input[0..n]`, writing one output per input into `out[0..n]` (`NaN` at warmup).
///
/// # Safety
/// `handle` valid (from `wickra_kelly_criterion_new`, not freed); `input`/`out` cover `n` `double`s.
#[no_mangle]
pub unsafe extern "C" fn wickra_kelly_criterion_batch(
    handle: *mut KellyCriterion,
    input: *const f64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null() || input.is_null() || out.is_null() {
        return;
    }
    let ind = &mut *handle;
    let inputs = slice::from_raw_parts(input, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (slot, &value) in outputs.iter_mut().zip(inputs) {
        *slot = ind.update(value).unwrap_or(f64::NAN);
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_kelly_criterion_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_kelly_criterion_warmup_period(
    handle: *mut KellyCriterion,
) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_kelly_criterion_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_kelly_criterion_is_ready(handle: *mut KellyCriterion) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_kelly_criterion_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_kelly_criterion_name(handle: *mut KellyCriterion) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_kelly_criterion_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_kelly_criterion_reset(handle: *mut KellyCriterion) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_kelly_criterion_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_kelly_criterion_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_kelly_criterion_free(handle: *mut KellyCriterion) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `Kurtosis` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_kurtosis_free`.
#[no_mangle]
pub extern "C" fn wickra_kurtosis_new(period: usize) -> *mut Kurtosis {
    match Kurtosis::new(period) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one value; returns the output, or `NaN` during warmup / on a `NULL` handle.
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_kurtosis_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_kurtosis_update(handle: *mut Kurtosis, value: f64) -> f64 {
    match handle.as_mut() {
        Some(ind) => ind.update(value).unwrap_or(f64::NAN),
        None => f64::NAN,
    }
}

/// Run over `input[0..n]`, writing one output per input into `out[0..n]` (`NaN` at warmup).
///
/// # Safety
/// `handle` valid (from `wickra_kurtosis_new`, not freed); `input`/`out` cover `n` `double`s.
#[no_mangle]
pub unsafe extern "C" fn wickra_kurtosis_batch(
    handle: *mut Kurtosis,
    input: *const f64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null() || input.is_null() || out.is_null() {
        return;
    }
    let ind = &mut *handle;
    let inputs = slice::from_raw_parts(input, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (slot, &value) in outputs.iter_mut().zip(inputs) {
        *slot = ind.update(value).unwrap_or(f64::NAN);
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_kurtosis_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_kurtosis_warmup_period(handle: *mut Kurtosis) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_kurtosis_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_kurtosis_is_ready(handle: *mut Kurtosis) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_kurtosis_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_kurtosis_name(handle: *mut Kurtosis) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_kurtosis_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_kurtosis_reset(handle: *mut Kurtosis) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_kurtosis_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_kurtosis_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_kurtosis_free(handle: *mut Kurtosis) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `LaguerreRsi` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_laguerre_rsi_free`.
#[no_mangle]
pub extern "C" fn wickra_laguerre_rsi_new(gamma: f64) -> *mut LaguerreRsi {
    match LaguerreRsi::new(gamma) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one value; returns the output, or `NaN` during warmup / on a `NULL` handle.
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_laguerre_rsi_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_laguerre_rsi_update(handle: *mut LaguerreRsi, value: f64) -> f64 {
    match handle.as_mut() {
        Some(ind) => ind.update(value).unwrap_or(f64::NAN),
        None => f64::NAN,
    }
}

/// Run over `input[0..n]`, writing one output per input into `out[0..n]` (`NaN` at warmup).
///
/// # Safety
/// `handle` valid (from `wickra_laguerre_rsi_new`, not freed); `input`/`out` cover `n` `double`s.
#[no_mangle]
pub unsafe extern "C" fn wickra_laguerre_rsi_batch(
    handle: *mut LaguerreRsi,
    input: *const f64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null() || input.is_null() || out.is_null() {
        return;
    }
    let ind = &mut *handle;
    let inputs = slice::from_raw_parts(input, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (slot, &value) in outputs.iter_mut().zip(inputs) {
        *slot = ind.update(value).unwrap_or(f64::NAN);
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_laguerre_rsi_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_laguerre_rsi_warmup_period(handle: *mut LaguerreRsi) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_laguerre_rsi_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_laguerre_rsi_is_ready(handle: *mut LaguerreRsi) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_laguerre_rsi_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_laguerre_rsi_name(handle: *mut LaguerreRsi) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_laguerre_rsi_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_laguerre_rsi_reset(handle: *mut LaguerreRsi) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_laguerre_rsi_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_laguerre_rsi_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_laguerre_rsi_free(handle: *mut LaguerreRsi) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `LinearRegression` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_linear_regression_free`.
#[no_mangle]
pub extern "C" fn wickra_linear_regression_new(period: usize) -> *mut LinearRegression {
    match LinearRegression::new(period) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one value; returns the output, or `NaN` during warmup / on a `NULL` handle.
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_linear_regression_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_linear_regression_update(
    handle: *mut LinearRegression,
    value: f64,
) -> f64 {
    match handle.as_mut() {
        Some(ind) => ind.update(value).unwrap_or(f64::NAN),
        None => f64::NAN,
    }
}

/// Run over `input[0..n]`, writing one output per input into `out[0..n]` (`NaN` at warmup).
///
/// # Safety
/// `handle` valid (from `wickra_linear_regression_new`, not freed); `input`/`out` cover `n` `double`s.
#[no_mangle]
pub unsafe extern "C" fn wickra_linear_regression_batch(
    handle: *mut LinearRegression,
    input: *const f64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null() || input.is_null() || out.is_null() {
        return;
    }
    let ind = &mut *handle;
    let inputs = slice::from_raw_parts(input, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (slot, &value) in outputs.iter_mut().zip(inputs) {
        *slot = ind.update(value).unwrap_or(f64::NAN);
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_linear_regression_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_linear_regression_warmup_period(
    handle: *mut LinearRegression,
) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_linear_regression_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_linear_regression_is_ready(handle: *mut LinearRegression) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_linear_regression_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_linear_regression_name(
    handle: *mut LinearRegression,
) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_linear_regression_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_linear_regression_reset(handle: *mut LinearRegression) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_linear_regression_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_linear_regression_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_linear_regression_free(handle: *mut LinearRegression) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `LinRegAngle` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_lin_reg_angle_free`.
#[no_mangle]
pub extern "C" fn wickra_lin_reg_angle_new(period: usize) -> *mut LinRegAngle {
    match LinRegAngle::new(period) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one value; returns the output, or `NaN` during warmup / on a `NULL` handle.
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_lin_reg_angle_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_lin_reg_angle_update(handle: *mut LinRegAngle, value: f64) -> f64 {
    match handle.as_mut() {
        Some(ind) => ind.update(value).unwrap_or(f64::NAN),
        None => f64::NAN,
    }
}

/// Run over `input[0..n]`, writing one output per input into `out[0..n]` (`NaN` at warmup).
///
/// # Safety
/// `handle` valid (from `wickra_lin_reg_angle_new`, not freed); `input`/`out` cover `n` `double`s.
#[no_mangle]
pub unsafe extern "C" fn wickra_lin_reg_angle_batch(
    handle: *mut LinRegAngle,
    input: *const f64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null() || input.is_null() || out.is_null() {
        return;
    }
    let ind = &mut *handle;
    let inputs = slice::from_raw_parts(input, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (slot, &value) in outputs.iter_mut().zip(inputs) {
        *slot = ind.update(value).unwrap_or(f64::NAN);
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_lin_reg_angle_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_lin_reg_angle_warmup_period(handle: *mut LinRegAngle) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_lin_reg_angle_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_lin_reg_angle_is_ready(handle: *mut LinRegAngle) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_lin_reg_angle_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_lin_reg_angle_name(handle: *mut LinRegAngle) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_lin_reg_angle_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_lin_reg_angle_reset(handle: *mut LinRegAngle) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_lin_reg_angle_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_lin_reg_angle_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_lin_reg_angle_free(handle: *mut LinRegAngle) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `LinRegIntercept` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_lin_reg_intercept_free`.
#[no_mangle]
pub extern "C" fn wickra_lin_reg_intercept_new(period: usize) -> *mut LinRegIntercept {
    match LinRegIntercept::new(period) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one value; returns the output, or `NaN` during warmup / on a `NULL` handle.
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_lin_reg_intercept_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_lin_reg_intercept_update(
    handle: *mut LinRegIntercept,
    value: f64,
) -> f64 {
    match handle.as_mut() {
        Some(ind) => ind.update(value).unwrap_or(f64::NAN),
        None => f64::NAN,
    }
}

/// Run over `input[0..n]`, writing one output per input into `out[0..n]` (`NaN` at warmup).
///
/// # Safety
/// `handle` valid (from `wickra_lin_reg_intercept_new`, not freed); `input`/`out` cover `n` `double`s.
#[no_mangle]
pub unsafe extern "C" fn wickra_lin_reg_intercept_batch(
    handle: *mut LinRegIntercept,
    input: *const f64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null() || input.is_null() || out.is_null() {
        return;
    }
    let ind = &mut *handle;
    let inputs = slice::from_raw_parts(input, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (slot, &value) in outputs.iter_mut().zip(inputs) {
        *slot = ind.update(value).unwrap_or(f64::NAN);
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_lin_reg_intercept_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_lin_reg_intercept_warmup_period(
    handle: *mut LinRegIntercept,
) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_lin_reg_intercept_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_lin_reg_intercept_is_ready(handle: *mut LinRegIntercept) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_lin_reg_intercept_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_lin_reg_intercept_name(
    handle: *mut LinRegIntercept,
) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_lin_reg_intercept_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_lin_reg_intercept_reset(handle: *mut LinRegIntercept) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_lin_reg_intercept_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_lin_reg_intercept_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_lin_reg_intercept_free(handle: *mut LinRegIntercept) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `LinRegSlope` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_lin_reg_slope_free`.
#[no_mangle]
pub extern "C" fn wickra_lin_reg_slope_new(period: usize) -> *mut LinRegSlope {
    match LinRegSlope::new(period) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one value; returns the output, or `NaN` during warmup / on a `NULL` handle.
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_lin_reg_slope_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_lin_reg_slope_update(handle: *mut LinRegSlope, value: f64) -> f64 {
    match handle.as_mut() {
        Some(ind) => ind.update(value).unwrap_or(f64::NAN),
        None => f64::NAN,
    }
}

/// Run over `input[0..n]`, writing one output per input into `out[0..n]` (`NaN` at warmup).
///
/// # Safety
/// `handle` valid (from `wickra_lin_reg_slope_new`, not freed); `input`/`out` cover `n` `double`s.
#[no_mangle]
pub unsafe extern "C" fn wickra_lin_reg_slope_batch(
    handle: *mut LinRegSlope,
    input: *const f64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null() || input.is_null() || out.is_null() {
        return;
    }
    let ind = &mut *handle;
    let inputs = slice::from_raw_parts(input, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (slot, &value) in outputs.iter_mut().zip(inputs) {
        *slot = ind.update(value).unwrap_or(f64::NAN);
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_lin_reg_slope_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_lin_reg_slope_warmup_period(handle: *mut LinRegSlope) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_lin_reg_slope_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_lin_reg_slope_is_ready(handle: *mut LinRegSlope) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_lin_reg_slope_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_lin_reg_slope_name(handle: *mut LinRegSlope) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_lin_reg_slope_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_lin_reg_slope_reset(handle: *mut LinRegSlope) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_lin_reg_slope_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_lin_reg_slope_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_lin_reg_slope_free(handle: *mut LinRegSlope) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `LogReturn` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_log_return_free`.
#[no_mangle]
pub extern "C" fn wickra_log_return_new(period: usize) -> *mut LogReturn {
    match LogReturn::new(period) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one value; returns the output, or `NaN` during warmup / on a `NULL` handle.
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_log_return_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_log_return_update(handle: *mut LogReturn, value: f64) -> f64 {
    match handle.as_mut() {
        Some(ind) => ind.update(value).unwrap_or(f64::NAN),
        None => f64::NAN,
    }
}

/// Run over `input[0..n]`, writing one output per input into `out[0..n]` (`NaN` at warmup).
///
/// # Safety
/// `handle` valid (from `wickra_log_return_new`, not freed); `input`/`out` cover `n` `double`s.
#[no_mangle]
pub unsafe extern "C" fn wickra_log_return_batch(
    handle: *mut LogReturn,
    input: *const f64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null() || input.is_null() || out.is_null() {
        return;
    }
    let ind = &mut *handle;
    let inputs = slice::from_raw_parts(input, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (slot, &value) in outputs.iter_mut().zip(inputs) {
        *slot = ind.update(value).unwrap_or(f64::NAN);
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_log_return_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_log_return_warmup_period(handle: *mut LogReturn) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_log_return_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_log_return_is_ready(handle: *mut LogReturn) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_log_return_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_log_return_name(handle: *mut LogReturn) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_log_return_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_log_return_reset(handle: *mut LogReturn) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_log_return_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_log_return_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_log_return_free(handle: *mut LogReturn) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `M2Measure` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_m2_measure_free`.
#[no_mangle]
pub extern "C" fn wickra_m2_measure_new(
    period: usize,
    risk_free: f64,
    benchmark_stddev: f64,
) -> *mut M2Measure {
    match M2Measure::new(period, risk_free, benchmark_stddev) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one value; returns the output, or `NaN` during warmup / on a `NULL` handle.
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_m2_measure_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_m2_measure_update(handle: *mut M2Measure, value: f64) -> f64 {
    match handle.as_mut() {
        Some(ind) => ind.update(value).unwrap_or(f64::NAN),
        None => f64::NAN,
    }
}

/// Run over `input[0..n]`, writing one output per input into `out[0..n]` (`NaN` at warmup).
///
/// # Safety
/// `handle` valid (from `wickra_m2_measure_new`, not freed); `input`/`out` cover `n` `double`s.
#[no_mangle]
pub unsafe extern "C" fn wickra_m2_measure_batch(
    handle: *mut M2Measure,
    input: *const f64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null() || input.is_null() || out.is_null() {
        return;
    }
    let ind = &mut *handle;
    let inputs = slice::from_raw_parts(input, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (slot, &value) in outputs.iter_mut().zip(inputs) {
        *slot = ind.update(value).unwrap_or(f64::NAN);
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_m2_measure_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_m2_measure_warmup_period(handle: *mut M2Measure) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_m2_measure_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_m2_measure_is_ready(handle: *mut M2Measure) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_m2_measure_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_m2_measure_name(handle: *mut M2Measure) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_m2_measure_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_m2_measure_reset(handle: *mut M2Measure) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_m2_measure_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_m2_measure_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_m2_measure_free(handle: *mut M2Measure) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `MacdHistogram` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_macd_histogram_free`.
#[no_mangle]
pub extern "C" fn wickra_macd_histogram_new(
    fast: usize,
    slow: usize,
    signal: usize,
) -> *mut MacdHistogram {
    match MacdHistogram::new(fast, slow, signal) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one value; returns the output, or `NaN` during warmup / on a `NULL` handle.
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_macd_histogram_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_macd_histogram_update(
    handle: *mut MacdHistogram,
    value: f64,
) -> f64 {
    match handle.as_mut() {
        Some(ind) => ind.update(value).unwrap_or(f64::NAN),
        None => f64::NAN,
    }
}

/// Run over `input[0..n]`, writing one output per input into `out[0..n]` (`NaN` at warmup).
///
/// # Safety
/// `handle` valid (from `wickra_macd_histogram_new`, not freed); `input`/`out` cover `n` `double`s.
#[no_mangle]
pub unsafe extern "C" fn wickra_macd_histogram_batch(
    handle: *mut MacdHistogram,
    input: *const f64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null() || input.is_null() || out.is_null() {
        return;
    }
    let ind = &mut *handle;
    let inputs = slice::from_raw_parts(input, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (slot, &value) in outputs.iter_mut().zip(inputs) {
        *slot = ind.update(value).unwrap_or(f64::NAN);
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_macd_histogram_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_macd_histogram_warmup_period(handle: *mut MacdHistogram) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_macd_histogram_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_macd_histogram_is_ready(handle: *mut MacdHistogram) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_macd_histogram_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_macd_histogram_name(handle: *mut MacdHistogram) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_macd_histogram_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_macd_histogram_reset(handle: *mut MacdHistogram) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_macd_histogram_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_macd_histogram_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_macd_histogram_free(handle: *mut MacdHistogram) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `MartinRatio` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_martin_ratio_free`.
#[no_mangle]
pub extern "C" fn wickra_martin_ratio_new(period: usize) -> *mut MartinRatio {
    match MartinRatio::new(period) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one value; returns the output, or `NaN` during warmup / on a `NULL` handle.
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_martin_ratio_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_martin_ratio_update(handle: *mut MartinRatio, value: f64) -> f64 {
    match handle.as_mut() {
        Some(ind) => ind.update(value).unwrap_or(f64::NAN),
        None => f64::NAN,
    }
}

/// Run over `input[0..n]`, writing one output per input into `out[0..n]` (`NaN` at warmup).
///
/// # Safety
/// `handle` valid (from `wickra_martin_ratio_new`, not freed); `input`/`out` cover `n` `double`s.
#[no_mangle]
pub unsafe extern "C" fn wickra_martin_ratio_batch(
    handle: *mut MartinRatio,
    input: *const f64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null() || input.is_null() || out.is_null() {
        return;
    }
    let ind = &mut *handle;
    let inputs = slice::from_raw_parts(input, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (slot, &value) in outputs.iter_mut().zip(inputs) {
        *slot = ind.update(value).unwrap_or(f64::NAN);
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_martin_ratio_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_martin_ratio_warmup_period(handle: *mut MartinRatio) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_martin_ratio_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_martin_ratio_is_ready(handle: *mut MartinRatio) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_martin_ratio_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_martin_ratio_name(handle: *mut MartinRatio) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_martin_ratio_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_martin_ratio_reset(handle: *mut MartinRatio) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_martin_ratio_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_martin_ratio_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_martin_ratio_free(handle: *mut MartinRatio) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `MaxDrawdown` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_max_drawdown_free`.
#[no_mangle]
pub extern "C" fn wickra_max_drawdown_new(period: usize) -> *mut MaxDrawdown {
    match MaxDrawdown::new(period) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one value; returns the output, or `NaN` during warmup / on a `NULL` handle.
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_max_drawdown_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_max_drawdown_update(handle: *mut MaxDrawdown, value: f64) -> f64 {
    match handle.as_mut() {
        Some(ind) => ind.update(value).unwrap_or(f64::NAN),
        None => f64::NAN,
    }
}

/// Run over `input[0..n]`, writing one output per input into `out[0..n]` (`NaN` at warmup).
///
/// # Safety
/// `handle` valid (from `wickra_max_drawdown_new`, not freed); `input`/`out` cover `n` `double`s.
#[no_mangle]
pub unsafe extern "C" fn wickra_max_drawdown_batch(
    handle: *mut MaxDrawdown,
    input: *const f64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null() || input.is_null() || out.is_null() {
        return;
    }
    let ind = &mut *handle;
    let inputs = slice::from_raw_parts(input, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (slot, &value) in outputs.iter_mut().zip(inputs) {
        *slot = ind.update(value).unwrap_or(f64::NAN);
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_max_drawdown_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_max_drawdown_warmup_period(handle: *mut MaxDrawdown) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_max_drawdown_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_max_drawdown_is_ready(handle: *mut MaxDrawdown) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_max_drawdown_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_max_drawdown_name(handle: *mut MaxDrawdown) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_max_drawdown_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_max_drawdown_reset(handle: *mut MaxDrawdown) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_max_drawdown_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_max_drawdown_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_max_drawdown_free(handle: *mut MaxDrawdown) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `McGinleyDynamic` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_mc_ginley_dynamic_free`.
#[no_mangle]
pub extern "C" fn wickra_mc_ginley_dynamic_new(period: usize) -> *mut McGinleyDynamic {
    match McGinleyDynamic::new(period) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one value; returns the output, or `NaN` during warmup / on a `NULL` handle.
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_mc_ginley_dynamic_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_mc_ginley_dynamic_update(
    handle: *mut McGinleyDynamic,
    value: f64,
) -> f64 {
    match handle.as_mut() {
        Some(ind) => ind.update(value).unwrap_or(f64::NAN),
        None => f64::NAN,
    }
}

/// Run over `input[0..n]`, writing one output per input into `out[0..n]` (`NaN` at warmup).
///
/// # Safety
/// `handle` valid (from `wickra_mc_ginley_dynamic_new`, not freed); `input`/`out` cover `n` `double`s.
#[no_mangle]
pub unsafe extern "C" fn wickra_mc_ginley_dynamic_batch(
    handle: *mut McGinleyDynamic,
    input: *const f64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null() || input.is_null() || out.is_null() {
        return;
    }
    let ind = &mut *handle;
    let inputs = slice::from_raw_parts(input, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (slot, &value) in outputs.iter_mut().zip(inputs) {
        *slot = ind.update(value).unwrap_or(f64::NAN);
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_mc_ginley_dynamic_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_mc_ginley_dynamic_warmup_period(
    handle: *mut McGinleyDynamic,
) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_mc_ginley_dynamic_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_mc_ginley_dynamic_is_ready(handle: *mut McGinleyDynamic) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_mc_ginley_dynamic_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_mc_ginley_dynamic_name(
    handle: *mut McGinleyDynamic,
) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_mc_ginley_dynamic_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_mc_ginley_dynamic_reset(handle: *mut McGinleyDynamic) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_mc_ginley_dynamic_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_mc_ginley_dynamic_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_mc_ginley_dynamic_free(handle: *mut McGinleyDynamic) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `MedianAbsoluteDeviation` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_median_absolute_deviation_free`.
#[no_mangle]
pub extern "C" fn wickra_median_absolute_deviation_new(
    period: usize,
) -> *mut MedianAbsoluteDeviation {
    match MedianAbsoluteDeviation::new(period) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one value; returns the output, or `NaN` during warmup / on a `NULL` handle.
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_median_absolute_deviation_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_median_absolute_deviation_update(
    handle: *mut MedianAbsoluteDeviation,
    value: f64,
) -> f64 {
    match handle.as_mut() {
        Some(ind) => ind.update(value).unwrap_or(f64::NAN),
        None => f64::NAN,
    }
}

/// Run over `input[0..n]`, writing one output per input into `out[0..n]` (`NaN` at warmup).
///
/// # Safety
/// `handle` valid (from `wickra_median_absolute_deviation_new`, not freed); `input`/`out` cover `n` `double`s.
#[no_mangle]
pub unsafe extern "C" fn wickra_median_absolute_deviation_batch(
    handle: *mut MedianAbsoluteDeviation,
    input: *const f64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null() || input.is_null() || out.is_null() {
        return;
    }
    let ind = &mut *handle;
    let inputs = slice::from_raw_parts(input, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (slot, &value) in outputs.iter_mut().zip(inputs) {
        *slot = ind.update(value).unwrap_or(f64::NAN);
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_median_absolute_deviation_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_median_absolute_deviation_warmup_period(
    handle: *mut MedianAbsoluteDeviation,
) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_median_absolute_deviation_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_median_absolute_deviation_is_ready(
    handle: *mut MedianAbsoluteDeviation,
) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_median_absolute_deviation_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_median_absolute_deviation_name(
    handle: *mut MedianAbsoluteDeviation,
) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_median_absolute_deviation_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_median_absolute_deviation_reset(
    handle: *mut MedianAbsoluteDeviation,
) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_median_absolute_deviation_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_median_absolute_deviation_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_median_absolute_deviation_free(
    handle: *mut MedianAbsoluteDeviation,
) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `MedianMa` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_median_ma_free`.
#[no_mangle]
pub extern "C" fn wickra_median_ma_new(period: usize) -> *mut MedianMa {
    match MedianMa::new(period) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one value; returns the output, or `NaN` during warmup / on a `NULL` handle.
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_median_ma_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_median_ma_update(handle: *mut MedianMa, value: f64) -> f64 {
    match handle.as_mut() {
        Some(ind) => ind.update(value).unwrap_or(f64::NAN),
        None => f64::NAN,
    }
}

/// Run over `input[0..n]`, writing one output per input into `out[0..n]` (`NaN` at warmup).
///
/// # Safety
/// `handle` valid (from `wickra_median_ma_new`, not freed); `input`/`out` cover `n` `double`s.
#[no_mangle]
pub unsafe extern "C" fn wickra_median_ma_batch(
    handle: *mut MedianMa,
    input: *const f64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null() || input.is_null() || out.is_null() {
        return;
    }
    let ind = &mut *handle;
    let inputs = slice::from_raw_parts(input, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (slot, &value) in outputs.iter_mut().zip(inputs) {
        *slot = ind.update(value).unwrap_or(f64::NAN);
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_median_ma_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_median_ma_warmup_period(handle: *mut MedianMa) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_median_ma_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_median_ma_is_ready(handle: *mut MedianMa) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_median_ma_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_median_ma_name(handle: *mut MedianMa) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_median_ma_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_median_ma_reset(handle: *mut MedianMa) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_median_ma_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_median_ma_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_median_ma_free(handle: *mut MedianMa) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `MidPoint` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_mid_point_free`.
#[no_mangle]
pub extern "C" fn wickra_mid_point_new(period: usize) -> *mut MidPoint {
    match MidPoint::new(period) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one value; returns the output, or `NaN` during warmup / on a `NULL` handle.
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_mid_point_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_mid_point_update(handle: *mut MidPoint, value: f64) -> f64 {
    match handle.as_mut() {
        Some(ind) => ind.update(value).unwrap_or(f64::NAN),
        None => f64::NAN,
    }
}

/// Run over `input[0..n]`, writing one output per input into `out[0..n]` (`NaN` at warmup).
///
/// # Safety
/// `handle` valid (from `wickra_mid_point_new`, not freed); `input`/`out` cover `n` `double`s.
#[no_mangle]
pub unsafe extern "C" fn wickra_mid_point_batch(
    handle: *mut MidPoint,
    input: *const f64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null() || input.is_null() || out.is_null() {
        return;
    }
    let ind = &mut *handle;
    let inputs = slice::from_raw_parts(input, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (slot, &value) in outputs.iter_mut().zip(inputs) {
        *slot = ind.update(value).unwrap_or(f64::NAN);
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_mid_point_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_mid_point_warmup_period(handle: *mut MidPoint) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_mid_point_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_mid_point_is_ready(handle: *mut MidPoint) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_mid_point_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_mid_point_name(handle: *mut MidPoint) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_mid_point_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_mid_point_reset(handle: *mut MidPoint) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_mid_point_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_mid_point_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_mid_point_free(handle: *mut MidPoint) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `Mom` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_mom_free`.
#[no_mangle]
pub extern "C" fn wickra_mom_new(period: usize) -> *mut Mom {
    match Mom::new(period) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one value; returns the output, or `NaN` during warmup / on a `NULL` handle.
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_mom_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_mom_update(handle: *mut Mom, value: f64) -> f64 {
    match handle.as_mut() {
        Some(ind) => ind.update(value).unwrap_or(f64::NAN),
        None => f64::NAN,
    }
}

/// Run over `input[0..n]`, writing one output per input into `out[0..n]` (`NaN` at warmup).
///
/// # Safety
/// `handle` valid (from `wickra_mom_new`, not freed); `input`/`out` cover `n` `double`s.
#[no_mangle]
pub unsafe extern "C" fn wickra_mom_batch(
    handle: *mut Mom,
    input: *const f64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null() || input.is_null() || out.is_null() {
        return;
    }
    let ind = &mut *handle;
    let inputs = slice::from_raw_parts(input, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (slot, &value) in outputs.iter_mut().zip(inputs) {
        *slot = ind.update(value).unwrap_or(f64::NAN);
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_mom_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_mom_warmup_period(handle: *mut Mom) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_mom_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_mom_is_ready(handle: *mut Mom) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_mom_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_mom_name(handle: *mut Mom) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_mom_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_mom_reset(handle: *mut Mom) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_mom_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_mom_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_mom_free(handle: *mut Mom) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `OmegaRatio` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_omega_ratio_free`.
#[no_mangle]
pub extern "C" fn wickra_omega_ratio_new(period: usize, threshold: f64) -> *mut OmegaRatio {
    match OmegaRatio::new(period, threshold) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one value; returns the output, or `NaN` during warmup / on a `NULL` handle.
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_omega_ratio_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_omega_ratio_update(handle: *mut OmegaRatio, value: f64) -> f64 {
    match handle.as_mut() {
        Some(ind) => ind.update(value).unwrap_or(f64::NAN),
        None => f64::NAN,
    }
}

/// Run over `input[0..n]`, writing one output per input into `out[0..n]` (`NaN` at warmup).
///
/// # Safety
/// `handle` valid (from `wickra_omega_ratio_new`, not freed); `input`/`out` cover `n` `double`s.
#[no_mangle]
pub unsafe extern "C" fn wickra_omega_ratio_batch(
    handle: *mut OmegaRatio,
    input: *const f64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null() || input.is_null() || out.is_null() {
        return;
    }
    let ind = &mut *handle;
    let inputs = slice::from_raw_parts(input, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (slot, &value) in outputs.iter_mut().zip(inputs) {
        *slot = ind.update(value).unwrap_or(f64::NAN);
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_omega_ratio_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_omega_ratio_warmup_period(handle: *mut OmegaRatio) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_omega_ratio_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_omega_ratio_is_ready(handle: *mut OmegaRatio) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_omega_ratio_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_omega_ratio_name(handle: *mut OmegaRatio) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_omega_ratio_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_omega_ratio_reset(handle: *mut OmegaRatio) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_omega_ratio_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_omega_ratio_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_omega_ratio_free(handle: *mut OmegaRatio) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `PainIndex` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_pain_index_free`.
#[no_mangle]
pub extern "C" fn wickra_pain_index_new(period: usize) -> *mut PainIndex {
    match PainIndex::new(period) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one value; returns the output, or `NaN` during warmup / on a `NULL` handle.
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_pain_index_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_pain_index_update(handle: *mut PainIndex, value: f64) -> f64 {
    match handle.as_mut() {
        Some(ind) => ind.update(value).unwrap_or(f64::NAN),
        None => f64::NAN,
    }
}

/// Run over `input[0..n]`, writing one output per input into `out[0..n]` (`NaN` at warmup).
///
/// # Safety
/// `handle` valid (from `wickra_pain_index_new`, not freed); `input`/`out` cover `n` `double`s.
#[no_mangle]
pub unsafe extern "C" fn wickra_pain_index_batch(
    handle: *mut PainIndex,
    input: *const f64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null() || input.is_null() || out.is_null() {
        return;
    }
    let ind = &mut *handle;
    let inputs = slice::from_raw_parts(input, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (slot, &value) in outputs.iter_mut().zip(inputs) {
        *slot = ind.update(value).unwrap_or(f64::NAN);
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_pain_index_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_pain_index_warmup_period(handle: *mut PainIndex) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_pain_index_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_pain_index_is_ready(handle: *mut PainIndex) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_pain_index_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_pain_index_name(handle: *mut PainIndex) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_pain_index_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_pain_index_reset(handle: *mut PainIndex) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_pain_index_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_pain_index_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_pain_index_free(handle: *mut PainIndex) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `PercentB` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_percent_b_free`.
#[no_mangle]
pub extern "C" fn wickra_percent_b_new(period: usize, multiplier: f64) -> *mut PercentB {
    match PercentB::new(period, multiplier) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one value; returns the output, or `NaN` during warmup / on a `NULL` handle.
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_percent_b_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_percent_b_update(handle: *mut PercentB, value: f64) -> f64 {
    match handle.as_mut() {
        Some(ind) => ind.update(value).unwrap_or(f64::NAN),
        None => f64::NAN,
    }
}

/// Run over `input[0..n]`, writing one output per input into `out[0..n]` (`NaN` at warmup).
///
/// # Safety
/// `handle` valid (from `wickra_percent_b_new`, not freed); `input`/`out` cover `n` `double`s.
#[no_mangle]
pub unsafe extern "C" fn wickra_percent_b_batch(
    handle: *mut PercentB,
    input: *const f64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null() || input.is_null() || out.is_null() {
        return;
    }
    let ind = &mut *handle;
    let inputs = slice::from_raw_parts(input, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (slot, &value) in outputs.iter_mut().zip(inputs) {
        *slot = ind.update(value).unwrap_or(f64::NAN);
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_percent_b_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_percent_b_warmup_period(handle: *mut PercentB) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_percent_b_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_percent_b_is_ready(handle: *mut PercentB) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_percent_b_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_percent_b_name(handle: *mut PercentB) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_percent_b_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_percent_b_reset(handle: *mut PercentB) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_percent_b_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_percent_b_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_percent_b_free(handle: *mut PercentB) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `PercentageTrailingStop` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_percentage_trailing_stop_free`.
#[no_mangle]
pub extern "C" fn wickra_percentage_trailing_stop_new(percent: f64) -> *mut PercentageTrailingStop {
    match PercentageTrailingStop::new(percent) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one value; returns the output, or `NaN` during warmup / on a `NULL` handle.
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_percentage_trailing_stop_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_percentage_trailing_stop_update(
    handle: *mut PercentageTrailingStop,
    value: f64,
) -> f64 {
    match handle.as_mut() {
        Some(ind) => ind.update(value).unwrap_or(f64::NAN),
        None => f64::NAN,
    }
}

/// Run over `input[0..n]`, writing one output per input into `out[0..n]` (`NaN` at warmup).
///
/// # Safety
/// `handle` valid (from `wickra_percentage_trailing_stop_new`, not freed); `input`/`out` cover `n` `double`s.
#[no_mangle]
pub unsafe extern "C" fn wickra_percentage_trailing_stop_batch(
    handle: *mut PercentageTrailingStop,
    input: *const f64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null() || input.is_null() || out.is_null() {
        return;
    }
    let ind = &mut *handle;
    let inputs = slice::from_raw_parts(input, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (slot, &value) in outputs.iter_mut().zip(inputs) {
        *slot = ind.update(value).unwrap_or(f64::NAN);
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_percentage_trailing_stop_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_percentage_trailing_stop_warmup_period(
    handle: *mut PercentageTrailingStop,
) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_percentage_trailing_stop_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_percentage_trailing_stop_is_ready(
    handle: *mut PercentageTrailingStop,
) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_percentage_trailing_stop_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_percentage_trailing_stop_name(
    handle: *mut PercentageTrailingStop,
) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_percentage_trailing_stop_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_percentage_trailing_stop_reset(
    handle: *mut PercentageTrailingStop,
) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_percentage_trailing_stop_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_percentage_trailing_stop_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_percentage_trailing_stop_free(handle: *mut PercentageTrailingStop) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `Pmo` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_pmo_free`.
#[no_mangle]
pub extern "C" fn wickra_pmo_new(smoothing1: usize, smoothing2: usize) -> *mut Pmo {
    match Pmo::new(smoothing1, smoothing2) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one value; returns the output, or `NaN` during warmup / on a `NULL` handle.
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_pmo_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_pmo_update(handle: *mut Pmo, value: f64) -> f64 {
    match handle.as_mut() {
        Some(ind) => ind.update(value).unwrap_or(f64::NAN),
        None => f64::NAN,
    }
}

/// Run over `input[0..n]`, writing one output per input into `out[0..n]` (`NaN` at warmup).
///
/// # Safety
/// `handle` valid (from `wickra_pmo_new`, not freed); `input`/`out` cover `n` `double`s.
#[no_mangle]
pub unsafe extern "C" fn wickra_pmo_batch(
    handle: *mut Pmo,
    input: *const f64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null() || input.is_null() || out.is_null() {
        return;
    }
    let ind = &mut *handle;
    let inputs = slice::from_raw_parts(input, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (slot, &value) in outputs.iter_mut().zip(inputs) {
        *slot = ind.update(value).unwrap_or(f64::NAN);
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_pmo_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_pmo_warmup_period(handle: *mut Pmo) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_pmo_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_pmo_is_ready(handle: *mut Pmo) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_pmo_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_pmo_name(handle: *mut Pmo) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_pmo_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_pmo_reset(handle: *mut Pmo) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_pmo_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_pmo_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_pmo_free(handle: *mut Pmo) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `PolarizedFractalEfficiency` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_polarized_fractal_efficiency_free`.
#[no_mangle]
pub extern "C" fn wickra_polarized_fractal_efficiency_new(
    period: usize,
    smoothing: usize,
) -> *mut PolarizedFractalEfficiency {
    match PolarizedFractalEfficiency::new(period, smoothing) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one value; returns the output, or `NaN` during warmup / on a `NULL` handle.
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_polarized_fractal_efficiency_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_polarized_fractal_efficiency_update(
    handle: *mut PolarizedFractalEfficiency,
    value: f64,
) -> f64 {
    match handle.as_mut() {
        Some(ind) => ind.update(value).unwrap_or(f64::NAN),
        None => f64::NAN,
    }
}

/// Run over `input[0..n]`, writing one output per input into `out[0..n]` (`NaN` at warmup).
///
/// # Safety
/// `handle` valid (from `wickra_polarized_fractal_efficiency_new`, not freed); `input`/`out` cover `n` `double`s.
#[no_mangle]
pub unsafe extern "C" fn wickra_polarized_fractal_efficiency_batch(
    handle: *mut PolarizedFractalEfficiency,
    input: *const f64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null() || input.is_null() || out.is_null() {
        return;
    }
    let ind = &mut *handle;
    let inputs = slice::from_raw_parts(input, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (slot, &value) in outputs.iter_mut().zip(inputs) {
        *slot = ind.update(value).unwrap_or(f64::NAN);
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_polarized_fractal_efficiency_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_polarized_fractal_efficiency_warmup_period(
    handle: *mut PolarizedFractalEfficiency,
) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_polarized_fractal_efficiency_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_polarized_fractal_efficiency_is_ready(
    handle: *mut PolarizedFractalEfficiency,
) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_polarized_fractal_efficiency_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_polarized_fractal_efficiency_name(
    handle: *mut PolarizedFractalEfficiency,
) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_polarized_fractal_efficiency_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_polarized_fractal_efficiency_reset(
    handle: *mut PolarizedFractalEfficiency,
) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_polarized_fractal_efficiency_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_polarized_fractal_efficiency_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_polarized_fractal_efficiency_free(
    handle: *mut PolarizedFractalEfficiency,
) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `Ppo` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_ppo_free`.
#[no_mangle]
pub extern "C" fn wickra_ppo_new(fast: usize, slow: usize) -> *mut Ppo {
    match Ppo::new(fast, slow) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one value; returns the output, or `NaN` during warmup / on a `NULL` handle.
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_ppo_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_ppo_update(handle: *mut Ppo, value: f64) -> f64 {
    match handle.as_mut() {
        Some(ind) => ind.update(value).unwrap_or(f64::NAN),
        None => f64::NAN,
    }
}

/// Run over `input[0..n]`, writing one output per input into `out[0..n]` (`NaN` at warmup).
///
/// # Safety
/// `handle` valid (from `wickra_ppo_new`, not freed); `input`/`out` cover `n` `double`s.
#[no_mangle]
pub unsafe extern "C" fn wickra_ppo_batch(
    handle: *mut Ppo,
    input: *const f64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null() || input.is_null() || out.is_null() {
        return;
    }
    let ind = &mut *handle;
    let inputs = slice::from_raw_parts(input, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (slot, &value) in outputs.iter_mut().zip(inputs) {
        *slot = ind.update(value).unwrap_or(f64::NAN);
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_ppo_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_ppo_warmup_period(handle: *mut Ppo) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_ppo_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_ppo_is_ready(handle: *mut Ppo) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_ppo_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_ppo_name(handle: *mut Ppo) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_ppo_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_ppo_reset(handle: *mut Ppo) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_ppo_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_ppo_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_ppo_free(handle: *mut Ppo) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `PpoHistogram` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_ppo_histogram_free`.
#[no_mangle]
pub extern "C" fn wickra_ppo_histogram_new(
    fast: usize,
    slow: usize,
    signal: usize,
) -> *mut PpoHistogram {
    match PpoHistogram::new(fast, slow, signal) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one value; returns the output, or `NaN` during warmup / on a `NULL` handle.
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_ppo_histogram_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_ppo_histogram_update(handle: *mut PpoHistogram, value: f64) -> f64 {
    match handle.as_mut() {
        Some(ind) => ind.update(value).unwrap_or(f64::NAN),
        None => f64::NAN,
    }
}

/// Run over `input[0..n]`, writing one output per input into `out[0..n]` (`NaN` at warmup).
///
/// # Safety
/// `handle` valid (from `wickra_ppo_histogram_new`, not freed); `input`/`out` cover `n` `double`s.
#[no_mangle]
pub unsafe extern "C" fn wickra_ppo_histogram_batch(
    handle: *mut PpoHistogram,
    input: *const f64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null() || input.is_null() || out.is_null() {
        return;
    }
    let ind = &mut *handle;
    let inputs = slice::from_raw_parts(input, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (slot, &value) in outputs.iter_mut().zip(inputs) {
        *slot = ind.update(value).unwrap_or(f64::NAN);
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_ppo_histogram_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_ppo_histogram_warmup_period(handle: *mut PpoHistogram) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_ppo_histogram_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_ppo_histogram_is_ready(handle: *mut PpoHistogram) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_ppo_histogram_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_ppo_histogram_name(handle: *mut PpoHistogram) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_ppo_histogram_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_ppo_histogram_reset(handle: *mut PpoHistogram) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_ppo_histogram_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_ppo_histogram_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_ppo_histogram_free(handle: *mut PpoHistogram) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `ProfitFactor` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_profit_factor_free`.
#[no_mangle]
pub extern "C" fn wickra_profit_factor_new(period: usize) -> *mut ProfitFactor {
    match ProfitFactor::new(period) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one value; returns the output, or `NaN` during warmup / on a `NULL` handle.
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_profit_factor_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_profit_factor_update(handle: *mut ProfitFactor, value: f64) -> f64 {
    match handle.as_mut() {
        Some(ind) => ind.update(value).unwrap_or(f64::NAN),
        None => f64::NAN,
    }
}

/// Run over `input[0..n]`, writing one output per input into `out[0..n]` (`NaN` at warmup).
///
/// # Safety
/// `handle` valid (from `wickra_profit_factor_new`, not freed); `input`/`out` cover `n` `double`s.
#[no_mangle]
pub unsafe extern "C" fn wickra_profit_factor_batch(
    handle: *mut ProfitFactor,
    input: *const f64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null() || input.is_null() || out.is_null() {
        return;
    }
    let ind = &mut *handle;
    let inputs = slice::from_raw_parts(input, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (slot, &value) in outputs.iter_mut().zip(inputs) {
        *slot = ind.update(value).unwrap_or(f64::NAN);
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_profit_factor_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_profit_factor_warmup_period(handle: *mut ProfitFactor) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_profit_factor_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_profit_factor_is_ready(handle: *mut ProfitFactor) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_profit_factor_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_profit_factor_name(handle: *mut ProfitFactor) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_profit_factor_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_profit_factor_reset(handle: *mut ProfitFactor) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_profit_factor_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_profit_factor_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_profit_factor_free(handle: *mut ProfitFactor) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `RSquared` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_r_squared_free`.
#[no_mangle]
pub extern "C" fn wickra_r_squared_new(period: usize) -> *mut RSquared {
    match RSquared::new(period) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one value; returns the output, or `NaN` during warmup / on a `NULL` handle.
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_r_squared_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_r_squared_update(handle: *mut RSquared, value: f64) -> f64 {
    match handle.as_mut() {
        Some(ind) => ind.update(value).unwrap_or(f64::NAN),
        None => f64::NAN,
    }
}

/// Run over `input[0..n]`, writing one output per input into `out[0..n]` (`NaN` at warmup).
///
/// # Safety
/// `handle` valid (from `wickra_r_squared_new`, not freed); `input`/`out` cover `n` `double`s.
#[no_mangle]
pub unsafe extern "C" fn wickra_r_squared_batch(
    handle: *mut RSquared,
    input: *const f64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null() || input.is_null() || out.is_null() {
        return;
    }
    let ind = &mut *handle;
    let inputs = slice::from_raw_parts(input, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (slot, &value) in outputs.iter_mut().zip(inputs) {
        *slot = ind.update(value).unwrap_or(f64::NAN);
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_r_squared_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_r_squared_warmup_period(handle: *mut RSquared) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_r_squared_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_r_squared_is_ready(handle: *mut RSquared) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_r_squared_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_r_squared_name(handle: *mut RSquared) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_r_squared_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_r_squared_reset(handle: *mut RSquared) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_r_squared_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_r_squared_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_r_squared_free(handle: *mut RSquared) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `RealizedVolatility` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_realized_volatility_free`.
#[no_mangle]
pub extern "C" fn wickra_realized_volatility_new(period: usize) -> *mut RealizedVolatility {
    match RealizedVolatility::new(period) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one value; returns the output, or `NaN` during warmup / on a `NULL` handle.
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_realized_volatility_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_realized_volatility_update(
    handle: *mut RealizedVolatility,
    value: f64,
) -> f64 {
    match handle.as_mut() {
        Some(ind) => ind.update(value).unwrap_or(f64::NAN),
        None => f64::NAN,
    }
}

/// Run over `input[0..n]`, writing one output per input into `out[0..n]` (`NaN` at warmup).
///
/// # Safety
/// `handle` valid (from `wickra_realized_volatility_new`, not freed); `input`/`out` cover `n` `double`s.
#[no_mangle]
pub unsafe extern "C" fn wickra_realized_volatility_batch(
    handle: *mut RealizedVolatility,
    input: *const f64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null() || input.is_null() || out.is_null() {
        return;
    }
    let ind = &mut *handle;
    let inputs = slice::from_raw_parts(input, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (slot, &value) in outputs.iter_mut().zip(inputs) {
        *slot = ind.update(value).unwrap_or(f64::NAN);
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_realized_volatility_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_realized_volatility_warmup_period(
    handle: *mut RealizedVolatility,
) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_realized_volatility_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_realized_volatility_is_ready(
    handle: *mut RealizedVolatility,
) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_realized_volatility_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_realized_volatility_name(
    handle: *mut RealizedVolatility,
) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_realized_volatility_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_realized_volatility_reset(handle: *mut RealizedVolatility) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_realized_volatility_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_realized_volatility_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_realized_volatility_free(handle: *mut RealizedVolatility) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `RecoveryFactor` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_recovery_factor_free`.
#[no_mangle]
pub extern "C" fn wickra_recovery_factor_new() -> *mut RecoveryFactor {
    Box::into_raw(Box::new(RecoveryFactor::new()))
}

/// Feed one value; returns the output, or `NaN` during warmup / on a `NULL` handle.
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_recovery_factor_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_recovery_factor_update(
    handle: *mut RecoveryFactor,
    value: f64,
) -> f64 {
    match handle.as_mut() {
        Some(ind) => ind.update(value).unwrap_or(f64::NAN),
        None => f64::NAN,
    }
}

/// Run over `input[0..n]`, writing one output per input into `out[0..n]` (`NaN` at warmup).
///
/// # Safety
/// `handle` valid (from `wickra_recovery_factor_new`, not freed); `input`/`out` cover `n` `double`s.
#[no_mangle]
pub unsafe extern "C" fn wickra_recovery_factor_batch(
    handle: *mut RecoveryFactor,
    input: *const f64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null() || input.is_null() || out.is_null() {
        return;
    }
    let ind = &mut *handle;
    let inputs = slice::from_raw_parts(input, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (slot, &value) in outputs.iter_mut().zip(inputs) {
        *slot = ind.update(value).unwrap_or(f64::NAN);
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_recovery_factor_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_recovery_factor_warmup_period(
    handle: *mut RecoveryFactor,
) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_recovery_factor_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_recovery_factor_is_ready(handle: *mut RecoveryFactor) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_recovery_factor_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_recovery_factor_name(handle: *mut RecoveryFactor) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_recovery_factor_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_recovery_factor_reset(handle: *mut RecoveryFactor) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_recovery_factor_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_recovery_factor_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_recovery_factor_free(handle: *mut RecoveryFactor) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `Reflex` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_reflex_free`.
#[no_mangle]
pub extern "C" fn wickra_reflex_new(period: usize) -> *mut Reflex {
    match Reflex::new(period) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one value; returns the output, or `NaN` during warmup / on a `NULL` handle.
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_reflex_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_reflex_update(handle: *mut Reflex, value: f64) -> f64 {
    match handle.as_mut() {
        Some(ind) => ind.update(value).unwrap_or(f64::NAN),
        None => f64::NAN,
    }
}

/// Run over `input[0..n]`, writing one output per input into `out[0..n]` (`NaN` at warmup).
///
/// # Safety
/// `handle` valid (from `wickra_reflex_new`, not freed); `input`/`out` cover `n` `double`s.
#[no_mangle]
pub unsafe extern "C" fn wickra_reflex_batch(
    handle: *mut Reflex,
    input: *const f64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null() || input.is_null() || out.is_null() {
        return;
    }
    let ind = &mut *handle;
    let inputs = slice::from_raw_parts(input, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (slot, &value) in outputs.iter_mut().zip(inputs) {
        *slot = ind.update(value).unwrap_or(f64::NAN);
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_reflex_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_reflex_warmup_period(handle: *mut Reflex) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_reflex_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_reflex_is_ready(handle: *mut Reflex) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_reflex_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_reflex_name(handle: *mut Reflex) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_reflex_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_reflex_reset(handle: *mut Reflex) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_reflex_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_reflex_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_reflex_free(handle: *mut Reflex) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `RegimeLabel` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_regime_label_free`.
#[no_mangle]
pub extern "C" fn wickra_regime_label_new(vol_period: usize, lookback: usize) -> *mut RegimeLabel {
    match RegimeLabel::new(vol_period, lookback) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one value; returns the output, or `NaN` during warmup / on a `NULL` handle.
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_regime_label_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_regime_label_update(handle: *mut RegimeLabel, value: f64) -> f64 {
    match handle.as_mut() {
        Some(ind) => ind.update(value).unwrap_or(f64::NAN),
        None => f64::NAN,
    }
}

/// Run over `input[0..n]`, writing one output per input into `out[0..n]` (`NaN` at warmup).
///
/// # Safety
/// `handle` valid (from `wickra_regime_label_new`, not freed); `input`/`out` cover `n` `double`s.
#[no_mangle]
pub unsafe extern "C" fn wickra_regime_label_batch(
    handle: *mut RegimeLabel,
    input: *const f64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null() || input.is_null() || out.is_null() {
        return;
    }
    let ind = &mut *handle;
    let inputs = slice::from_raw_parts(input, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (slot, &value) in outputs.iter_mut().zip(inputs) {
        *slot = ind.update(value).unwrap_or(f64::NAN);
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_regime_label_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_regime_label_warmup_period(handle: *mut RegimeLabel) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_regime_label_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_regime_label_is_ready(handle: *mut RegimeLabel) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_regime_label_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_regime_label_name(handle: *mut RegimeLabel) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_regime_label_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_regime_label_reset(handle: *mut RegimeLabel) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_regime_label_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_regime_label_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_regime_label_free(handle: *mut RegimeLabel) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `RenkoTrailingStop` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_renko_trailing_stop_free`.
#[no_mangle]
pub extern "C" fn wickra_renko_trailing_stop_new(block_size: f64) -> *mut RenkoTrailingStop {
    match RenkoTrailingStop::new(block_size) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one value; returns the output, or `NaN` during warmup / on a `NULL` handle.
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_renko_trailing_stop_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_renko_trailing_stop_update(
    handle: *mut RenkoTrailingStop,
    value: f64,
) -> f64 {
    match handle.as_mut() {
        Some(ind) => ind.update(value).unwrap_or(f64::NAN),
        None => f64::NAN,
    }
}

/// Run over `input[0..n]`, writing one output per input into `out[0..n]` (`NaN` at warmup).
///
/// # Safety
/// `handle` valid (from `wickra_renko_trailing_stop_new`, not freed); `input`/`out` cover `n` `double`s.
#[no_mangle]
pub unsafe extern "C" fn wickra_renko_trailing_stop_batch(
    handle: *mut RenkoTrailingStop,
    input: *const f64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null() || input.is_null() || out.is_null() {
        return;
    }
    let ind = &mut *handle;
    let inputs = slice::from_raw_parts(input, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (slot, &value) in outputs.iter_mut().zip(inputs) {
        *slot = ind.update(value).unwrap_or(f64::NAN);
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_renko_trailing_stop_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_renko_trailing_stop_warmup_period(
    handle: *mut RenkoTrailingStop,
) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_renko_trailing_stop_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_renko_trailing_stop_is_ready(
    handle: *mut RenkoTrailingStop,
) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_renko_trailing_stop_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_renko_trailing_stop_name(
    handle: *mut RenkoTrailingStop,
) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_renko_trailing_stop_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_renko_trailing_stop_reset(handle: *mut RenkoTrailingStop) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_renko_trailing_stop_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_renko_trailing_stop_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_renko_trailing_stop_free(handle: *mut RenkoTrailingStop) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `Rmi` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_rmi_free`.
#[no_mangle]
pub extern "C" fn wickra_rmi_new(period: usize, momentum: usize) -> *mut Rmi {
    match Rmi::new(period, momentum) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one value; returns the output, or `NaN` during warmup / on a `NULL` handle.
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_rmi_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_rmi_update(handle: *mut Rmi, value: f64) -> f64 {
    match handle.as_mut() {
        Some(ind) => ind.update(value).unwrap_or(f64::NAN),
        None => f64::NAN,
    }
}

/// Run over `input[0..n]`, writing one output per input into `out[0..n]` (`NaN` at warmup).
///
/// # Safety
/// `handle` valid (from `wickra_rmi_new`, not freed); `input`/`out` cover `n` `double`s.
#[no_mangle]
pub unsafe extern "C" fn wickra_rmi_batch(
    handle: *mut Rmi,
    input: *const f64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null() || input.is_null() || out.is_null() {
        return;
    }
    let ind = &mut *handle;
    let inputs = slice::from_raw_parts(input, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (slot, &value) in outputs.iter_mut().zip(inputs) {
        *slot = ind.update(value).unwrap_or(f64::NAN);
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_rmi_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_rmi_warmup_period(handle: *mut Rmi) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_rmi_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_rmi_is_ready(handle: *mut Rmi) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_rmi_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_rmi_name(handle: *mut Rmi) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_rmi_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_rmi_reset(handle: *mut Rmi) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_rmi_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_rmi_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_rmi_free(handle: *mut Rmi) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `Roc` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_roc_free`.
#[no_mangle]
pub extern "C" fn wickra_roc_new(period: usize) -> *mut Roc {
    match Roc::new(period) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one value; returns the output, or `NaN` during warmup / on a `NULL` handle.
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_roc_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_roc_update(handle: *mut Roc, value: f64) -> f64 {
    match handle.as_mut() {
        Some(ind) => ind.update(value).unwrap_or(f64::NAN),
        None => f64::NAN,
    }
}

/// Run over `input[0..n]`, writing one output per input into `out[0..n]` (`NaN` at warmup).
///
/// # Safety
/// `handle` valid (from `wickra_roc_new`, not freed); `input`/`out` cover `n` `double`s.
#[no_mangle]
pub unsafe extern "C" fn wickra_roc_batch(
    handle: *mut Roc,
    input: *const f64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null() || input.is_null() || out.is_null() {
        return;
    }
    let ind = &mut *handle;
    let inputs = slice::from_raw_parts(input, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (slot, &value) in outputs.iter_mut().zip(inputs) {
        *slot = ind.update(value).unwrap_or(f64::NAN);
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_roc_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_roc_warmup_period(handle: *mut Roc) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_roc_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_roc_is_ready(handle: *mut Roc) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_roc_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_roc_name(handle: *mut Roc) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_roc_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_roc_reset(handle: *mut Roc) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_roc_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_roc_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_roc_free(handle: *mut Roc) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `Rocp` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_rocp_free`.
#[no_mangle]
pub extern "C" fn wickra_rocp_new(period: usize) -> *mut Rocp {
    match Rocp::new(period) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one value; returns the output, or `NaN` during warmup / on a `NULL` handle.
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_rocp_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_rocp_update(handle: *mut Rocp, value: f64) -> f64 {
    match handle.as_mut() {
        Some(ind) => ind.update(value).unwrap_or(f64::NAN),
        None => f64::NAN,
    }
}

/// Run over `input[0..n]`, writing one output per input into `out[0..n]` (`NaN` at warmup).
///
/// # Safety
/// `handle` valid (from `wickra_rocp_new`, not freed); `input`/`out` cover `n` `double`s.
#[no_mangle]
pub unsafe extern "C" fn wickra_rocp_batch(
    handle: *mut Rocp,
    input: *const f64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null() || input.is_null() || out.is_null() {
        return;
    }
    let ind = &mut *handle;
    let inputs = slice::from_raw_parts(input, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (slot, &value) in outputs.iter_mut().zip(inputs) {
        *slot = ind.update(value).unwrap_or(f64::NAN);
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_rocp_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_rocp_warmup_period(handle: *mut Rocp) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_rocp_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_rocp_is_ready(handle: *mut Rocp) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_rocp_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_rocp_name(handle: *mut Rocp) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_rocp_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_rocp_reset(handle: *mut Rocp) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_rocp_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_rocp_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_rocp_free(handle: *mut Rocp) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `Rocr` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_rocr_free`.
#[no_mangle]
pub extern "C" fn wickra_rocr_new(period: usize) -> *mut Rocr {
    match Rocr::new(period) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one value; returns the output, or `NaN` during warmup / on a `NULL` handle.
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_rocr_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_rocr_update(handle: *mut Rocr, value: f64) -> f64 {
    match handle.as_mut() {
        Some(ind) => ind.update(value).unwrap_or(f64::NAN),
        None => f64::NAN,
    }
}

/// Run over `input[0..n]`, writing one output per input into `out[0..n]` (`NaN` at warmup).
///
/// # Safety
/// `handle` valid (from `wickra_rocr_new`, not freed); `input`/`out` cover `n` `double`s.
#[no_mangle]
pub unsafe extern "C" fn wickra_rocr_batch(
    handle: *mut Rocr,
    input: *const f64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null() || input.is_null() || out.is_null() {
        return;
    }
    let ind = &mut *handle;
    let inputs = slice::from_raw_parts(input, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (slot, &value) in outputs.iter_mut().zip(inputs) {
        *slot = ind.update(value).unwrap_or(f64::NAN);
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_rocr_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_rocr_warmup_period(handle: *mut Rocr) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_rocr_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_rocr_is_ready(handle: *mut Rocr) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_rocr_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_rocr_name(handle: *mut Rocr) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_rocr_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_rocr_reset(handle: *mut Rocr) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_rocr_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_rocr_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_rocr_free(handle: *mut Rocr) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `Rocr100` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_rocr100_free`.
#[no_mangle]
pub extern "C" fn wickra_rocr100_new(period: usize) -> *mut Rocr100 {
    match Rocr100::new(period) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one value; returns the output, or `NaN` during warmup / on a `NULL` handle.
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_rocr100_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_rocr100_update(handle: *mut Rocr100, value: f64) -> f64 {
    match handle.as_mut() {
        Some(ind) => ind.update(value).unwrap_or(f64::NAN),
        None => f64::NAN,
    }
}

/// Run over `input[0..n]`, writing one output per input into `out[0..n]` (`NaN` at warmup).
///
/// # Safety
/// `handle` valid (from `wickra_rocr100_new`, not freed); `input`/`out` cover `n` `double`s.
#[no_mangle]
pub unsafe extern "C" fn wickra_rocr100_batch(
    handle: *mut Rocr100,
    input: *const f64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null() || input.is_null() || out.is_null() {
        return;
    }
    let ind = &mut *handle;
    let inputs = slice::from_raw_parts(input, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (slot, &value) in outputs.iter_mut().zip(inputs) {
        *slot = ind.update(value).unwrap_or(f64::NAN);
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_rocr100_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_rocr100_warmup_period(handle: *mut Rocr100) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_rocr100_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_rocr100_is_ready(handle: *mut Rocr100) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_rocr100_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_rocr100_name(handle: *mut Rocr100) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_rocr100_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_rocr100_reset(handle: *mut Rocr100) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_rocr100_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_rocr100_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_rocr100_free(handle: *mut Rocr100) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `RollingIqr` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_rolling_iqr_free`.
#[no_mangle]
pub extern "C" fn wickra_rolling_iqr_new(period: usize) -> *mut RollingIqr {
    match RollingIqr::new(period) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one value; returns the output, or `NaN` during warmup / on a `NULL` handle.
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_rolling_iqr_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_rolling_iqr_update(handle: *mut RollingIqr, value: f64) -> f64 {
    match handle.as_mut() {
        Some(ind) => ind.update(value).unwrap_or(f64::NAN),
        None => f64::NAN,
    }
}

/// Run over `input[0..n]`, writing one output per input into `out[0..n]` (`NaN` at warmup).
///
/// # Safety
/// `handle` valid (from `wickra_rolling_iqr_new`, not freed); `input`/`out` cover `n` `double`s.
#[no_mangle]
pub unsafe extern "C" fn wickra_rolling_iqr_batch(
    handle: *mut RollingIqr,
    input: *const f64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null() || input.is_null() || out.is_null() {
        return;
    }
    let ind = &mut *handle;
    let inputs = slice::from_raw_parts(input, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (slot, &value) in outputs.iter_mut().zip(inputs) {
        *slot = ind.update(value).unwrap_or(f64::NAN);
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_rolling_iqr_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_rolling_iqr_warmup_period(handle: *mut RollingIqr) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_rolling_iqr_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_rolling_iqr_is_ready(handle: *mut RollingIqr) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_rolling_iqr_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_rolling_iqr_name(handle: *mut RollingIqr) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_rolling_iqr_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_rolling_iqr_reset(handle: *mut RollingIqr) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_rolling_iqr_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_rolling_iqr_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_rolling_iqr_free(handle: *mut RollingIqr) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `RollingMinMaxScaler` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_rolling_min_max_scaler_free`.
#[no_mangle]
pub extern "C" fn wickra_rolling_min_max_scaler_new(period: usize) -> *mut RollingMinMaxScaler {
    match RollingMinMaxScaler::new(period) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one value; returns the output, or `NaN` during warmup / on a `NULL` handle.
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_rolling_min_max_scaler_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_rolling_min_max_scaler_update(
    handle: *mut RollingMinMaxScaler,
    value: f64,
) -> f64 {
    match handle.as_mut() {
        Some(ind) => ind.update(value).unwrap_or(f64::NAN),
        None => f64::NAN,
    }
}

/// Run over `input[0..n]`, writing one output per input into `out[0..n]` (`NaN` at warmup).
///
/// # Safety
/// `handle` valid (from `wickra_rolling_min_max_scaler_new`, not freed); `input`/`out` cover `n` `double`s.
#[no_mangle]
pub unsafe extern "C" fn wickra_rolling_min_max_scaler_batch(
    handle: *mut RollingMinMaxScaler,
    input: *const f64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null() || input.is_null() || out.is_null() {
        return;
    }
    let ind = &mut *handle;
    let inputs = slice::from_raw_parts(input, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (slot, &value) in outputs.iter_mut().zip(inputs) {
        *slot = ind.update(value).unwrap_or(f64::NAN);
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_rolling_min_max_scaler_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_rolling_min_max_scaler_warmup_period(
    handle: *mut RollingMinMaxScaler,
) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_rolling_min_max_scaler_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_rolling_min_max_scaler_is_ready(
    handle: *mut RollingMinMaxScaler,
) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_rolling_min_max_scaler_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_rolling_min_max_scaler_name(
    handle: *mut RollingMinMaxScaler,
) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_rolling_min_max_scaler_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_rolling_min_max_scaler_reset(handle: *mut RollingMinMaxScaler) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_rolling_min_max_scaler_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_rolling_min_max_scaler_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_rolling_min_max_scaler_free(handle: *mut RollingMinMaxScaler) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `RollingPercentileRank` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_rolling_percentile_rank_free`.
#[no_mangle]
pub extern "C" fn wickra_rolling_percentile_rank_new(period: usize) -> *mut RollingPercentileRank {
    match RollingPercentileRank::new(period) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one value; returns the output, or `NaN` during warmup / on a `NULL` handle.
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_rolling_percentile_rank_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_rolling_percentile_rank_update(
    handle: *mut RollingPercentileRank,
    value: f64,
) -> f64 {
    match handle.as_mut() {
        Some(ind) => ind.update(value).unwrap_or(f64::NAN),
        None => f64::NAN,
    }
}

/// Run over `input[0..n]`, writing one output per input into `out[0..n]` (`NaN` at warmup).
///
/// # Safety
/// `handle` valid (from `wickra_rolling_percentile_rank_new`, not freed); `input`/`out` cover `n` `double`s.
#[no_mangle]
pub unsafe extern "C" fn wickra_rolling_percentile_rank_batch(
    handle: *mut RollingPercentileRank,
    input: *const f64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null() || input.is_null() || out.is_null() {
        return;
    }
    let ind = &mut *handle;
    let inputs = slice::from_raw_parts(input, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (slot, &value) in outputs.iter_mut().zip(inputs) {
        *slot = ind.update(value).unwrap_or(f64::NAN);
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_rolling_percentile_rank_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_rolling_percentile_rank_warmup_period(
    handle: *mut RollingPercentileRank,
) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_rolling_percentile_rank_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_rolling_percentile_rank_is_ready(
    handle: *mut RollingPercentileRank,
) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_rolling_percentile_rank_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_rolling_percentile_rank_name(
    handle: *mut RollingPercentileRank,
) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_rolling_percentile_rank_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_rolling_percentile_rank_reset(handle: *mut RollingPercentileRank) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_rolling_percentile_rank_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_rolling_percentile_rank_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_rolling_percentile_rank_free(handle: *mut RollingPercentileRank) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `RollingQuantile` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_rolling_quantile_free`.
#[no_mangle]
pub extern "C" fn wickra_rolling_quantile_new(
    period: usize,
    quantile: f64,
) -> *mut RollingQuantile {
    match RollingQuantile::new(period, quantile) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one value; returns the output, or `NaN` during warmup / on a `NULL` handle.
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_rolling_quantile_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_rolling_quantile_update(
    handle: *mut RollingQuantile,
    value: f64,
) -> f64 {
    match handle.as_mut() {
        Some(ind) => ind.update(value).unwrap_or(f64::NAN),
        None => f64::NAN,
    }
}

/// Run over `input[0..n]`, writing one output per input into `out[0..n]` (`NaN` at warmup).
///
/// # Safety
/// `handle` valid (from `wickra_rolling_quantile_new`, not freed); `input`/`out` cover `n` `double`s.
#[no_mangle]
pub unsafe extern "C" fn wickra_rolling_quantile_batch(
    handle: *mut RollingQuantile,
    input: *const f64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null() || input.is_null() || out.is_null() {
        return;
    }
    let ind = &mut *handle;
    let inputs = slice::from_raw_parts(input, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (slot, &value) in outputs.iter_mut().zip(inputs) {
        *slot = ind.update(value).unwrap_or(f64::NAN);
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_rolling_quantile_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_rolling_quantile_warmup_period(
    handle: *mut RollingQuantile,
) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_rolling_quantile_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_rolling_quantile_is_ready(handle: *mut RollingQuantile) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_rolling_quantile_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_rolling_quantile_name(
    handle: *mut RollingQuantile,
) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_rolling_quantile_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_rolling_quantile_reset(handle: *mut RollingQuantile) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_rolling_quantile_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_rolling_quantile_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_rolling_quantile_free(handle: *mut RollingQuantile) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `RoofingFilter` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_roofing_filter_free`.
#[no_mangle]
pub extern "C" fn wickra_roofing_filter_new(
    lp_period: usize,
    hp_period: usize,
) -> *mut RoofingFilter {
    match RoofingFilter::new(lp_period, hp_period) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one value; returns the output, or `NaN` during warmup / on a `NULL` handle.
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_roofing_filter_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_roofing_filter_update(
    handle: *mut RoofingFilter,
    value: f64,
) -> f64 {
    match handle.as_mut() {
        Some(ind) => ind.update(value).unwrap_or(f64::NAN),
        None => f64::NAN,
    }
}

/// Run over `input[0..n]`, writing one output per input into `out[0..n]` (`NaN` at warmup).
///
/// # Safety
/// `handle` valid (from `wickra_roofing_filter_new`, not freed); `input`/`out` cover `n` `double`s.
#[no_mangle]
pub unsafe extern "C" fn wickra_roofing_filter_batch(
    handle: *mut RoofingFilter,
    input: *const f64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null() || input.is_null() || out.is_null() {
        return;
    }
    let ind = &mut *handle;
    let inputs = slice::from_raw_parts(input, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (slot, &value) in outputs.iter_mut().zip(inputs) {
        *slot = ind.update(value).unwrap_or(f64::NAN);
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_roofing_filter_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_roofing_filter_warmup_period(handle: *mut RoofingFilter) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_roofing_filter_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_roofing_filter_is_ready(handle: *mut RoofingFilter) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_roofing_filter_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_roofing_filter_name(handle: *mut RoofingFilter) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_roofing_filter_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_roofing_filter_reset(handle: *mut RoofingFilter) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_roofing_filter_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_roofing_filter_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_roofing_filter_free(handle: *mut RoofingFilter) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `Rsi` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_rsi_free`.
#[no_mangle]
pub extern "C" fn wickra_rsi_new(period: usize) -> *mut Rsi {
    match Rsi::new(period) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one value; returns the output, or `NaN` during warmup / on a `NULL` handle.
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_rsi_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_rsi_update(handle: *mut Rsi, value: f64) -> f64 {
    match handle.as_mut() {
        Some(ind) => ind.update(value).unwrap_or(f64::NAN),
        None => f64::NAN,
    }
}

/// Run over `input[0..n]`, writing one output per input into `out[0..n]` (`NaN` at warmup).
///
/// # Safety
/// `handle` valid (from `wickra_rsi_new`, not freed); `input`/`out` cover `n` `double`s.
#[no_mangle]
pub unsafe extern "C" fn wickra_rsi_batch(
    handle: *mut Rsi,
    input: *const f64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null() || input.is_null() || out.is_null() {
        return;
    }
    let ind = &mut *handle;
    let inputs = slice::from_raw_parts(input, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (slot, &value) in outputs.iter_mut().zip(inputs) {
        *slot = ind.update(value).unwrap_or(f64::NAN);
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_rsi_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_rsi_warmup_period(handle: *mut Rsi) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_rsi_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_rsi_is_ready(handle: *mut Rsi) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_rsi_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_rsi_name(handle: *mut Rsi) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_rsi_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_rsi_reset(handle: *mut Rsi) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_rsi_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_rsi_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_rsi_free(handle: *mut Rsi) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `Rsx` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_rsx_free`.
#[no_mangle]
pub extern "C" fn wickra_rsx_new(length: usize) -> *mut Rsx {
    match Rsx::new(length) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one value; returns the output, or `NaN` during warmup / on a `NULL` handle.
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_rsx_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_rsx_update(handle: *mut Rsx, value: f64) -> f64 {
    match handle.as_mut() {
        Some(ind) => ind.update(value).unwrap_or(f64::NAN),
        None => f64::NAN,
    }
}

/// Run over `input[0..n]`, writing one output per input into `out[0..n]` (`NaN` at warmup).
///
/// # Safety
/// `handle` valid (from `wickra_rsx_new`, not freed); `input`/`out` cover `n` `double`s.
#[no_mangle]
pub unsafe extern "C" fn wickra_rsx_batch(
    handle: *mut Rsx,
    input: *const f64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null() || input.is_null() || out.is_null() {
        return;
    }
    let ind = &mut *handle;
    let inputs = slice::from_raw_parts(input, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (slot, &value) in outputs.iter_mut().zip(inputs) {
        *slot = ind.update(value).unwrap_or(f64::NAN);
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_rsx_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_rsx_warmup_period(handle: *mut Rsx) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_rsx_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_rsx_is_ready(handle: *mut Rsx) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_rsx_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_rsx_name(handle: *mut Rsx) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_rsx_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_rsx_reset(handle: *mut Rsx) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_rsx_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_rsx_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_rsx_free(handle: *mut Rsx) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `RviVolatility` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_rvi_volatility_free`.
#[no_mangle]
pub extern "C" fn wickra_rvi_volatility_new(period: usize) -> *mut RviVolatility {
    match RviVolatility::new(period) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one value; returns the output, or `NaN` during warmup / on a `NULL` handle.
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_rvi_volatility_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_rvi_volatility_update(
    handle: *mut RviVolatility,
    value: f64,
) -> f64 {
    match handle.as_mut() {
        Some(ind) => ind.update(value).unwrap_or(f64::NAN),
        None => f64::NAN,
    }
}

/// Run over `input[0..n]`, writing one output per input into `out[0..n]` (`NaN` at warmup).
///
/// # Safety
/// `handle` valid (from `wickra_rvi_volatility_new`, not freed); `input`/`out` cover `n` `double`s.
#[no_mangle]
pub unsafe extern "C" fn wickra_rvi_volatility_batch(
    handle: *mut RviVolatility,
    input: *const f64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null() || input.is_null() || out.is_null() {
        return;
    }
    let ind = &mut *handle;
    let inputs = slice::from_raw_parts(input, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (slot, &value) in outputs.iter_mut().zip(inputs) {
        *slot = ind.update(value).unwrap_or(f64::NAN);
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_rvi_volatility_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_rvi_volatility_warmup_period(handle: *mut RviVolatility) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_rvi_volatility_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_rvi_volatility_is_ready(handle: *mut RviVolatility) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_rvi_volatility_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_rvi_volatility_name(handle: *mut RviVolatility) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_rvi_volatility_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_rvi_volatility_reset(handle: *mut RviVolatility) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_rvi_volatility_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_rvi_volatility_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_rvi_volatility_free(handle: *mut RviVolatility) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `SampleEntropy` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_sample_entropy_free`.
#[no_mangle]
pub extern "C" fn wickra_sample_entropy_new(
    period: usize,
    m: usize,
    r_factor: f64,
) -> *mut SampleEntropy {
    match SampleEntropy::new(period, m, r_factor) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one value; returns the output, or `NaN` during warmup / on a `NULL` handle.
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_sample_entropy_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_sample_entropy_update(
    handle: *mut SampleEntropy,
    value: f64,
) -> f64 {
    match handle.as_mut() {
        Some(ind) => ind.update(value).unwrap_or(f64::NAN),
        None => f64::NAN,
    }
}

/// Run over `input[0..n]`, writing one output per input into `out[0..n]` (`NaN` at warmup).
///
/// # Safety
/// `handle` valid (from `wickra_sample_entropy_new`, not freed); `input`/`out` cover `n` `double`s.
#[no_mangle]
pub unsafe extern "C" fn wickra_sample_entropy_batch(
    handle: *mut SampleEntropy,
    input: *const f64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null() || input.is_null() || out.is_null() {
        return;
    }
    let ind = &mut *handle;
    let inputs = slice::from_raw_parts(input, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (slot, &value) in outputs.iter_mut().zip(inputs) {
        *slot = ind.update(value).unwrap_or(f64::NAN);
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_sample_entropy_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_sample_entropy_warmup_period(handle: *mut SampleEntropy) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_sample_entropy_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_sample_entropy_is_ready(handle: *mut SampleEntropy) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_sample_entropy_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_sample_entropy_name(handle: *mut SampleEntropy) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_sample_entropy_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_sample_entropy_reset(handle: *mut SampleEntropy) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_sample_entropy_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_sample_entropy_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_sample_entropy_free(handle: *mut SampleEntropy) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `ShannonEntropy` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_shannon_entropy_free`.
#[no_mangle]
pub extern "C" fn wickra_shannon_entropy_new(period: usize, bins: usize) -> *mut ShannonEntropy {
    match ShannonEntropy::new(period, bins) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one value; returns the output, or `NaN` during warmup / on a `NULL` handle.
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_shannon_entropy_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_shannon_entropy_update(
    handle: *mut ShannonEntropy,
    value: f64,
) -> f64 {
    match handle.as_mut() {
        Some(ind) => ind.update(value).unwrap_or(f64::NAN),
        None => f64::NAN,
    }
}

/// Run over `input[0..n]`, writing one output per input into `out[0..n]` (`NaN` at warmup).
///
/// # Safety
/// `handle` valid (from `wickra_shannon_entropy_new`, not freed); `input`/`out` cover `n` `double`s.
#[no_mangle]
pub unsafe extern "C" fn wickra_shannon_entropy_batch(
    handle: *mut ShannonEntropy,
    input: *const f64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null() || input.is_null() || out.is_null() {
        return;
    }
    let ind = &mut *handle;
    let inputs = slice::from_raw_parts(input, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (slot, &value) in outputs.iter_mut().zip(inputs) {
        *slot = ind.update(value).unwrap_or(f64::NAN);
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_shannon_entropy_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_shannon_entropy_warmup_period(
    handle: *mut ShannonEntropy,
) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_shannon_entropy_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_shannon_entropy_is_ready(handle: *mut ShannonEntropy) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_shannon_entropy_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_shannon_entropy_name(handle: *mut ShannonEntropy) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_shannon_entropy_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_shannon_entropy_reset(handle: *mut ShannonEntropy) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_shannon_entropy_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_shannon_entropy_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_shannon_entropy_free(handle: *mut ShannonEntropy) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `SharpeRatio` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_sharpe_ratio_free`.
#[no_mangle]
pub extern "C" fn wickra_sharpe_ratio_new(period: usize, risk_free: f64) -> *mut SharpeRatio {
    match SharpeRatio::new(period, risk_free) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one value; returns the output, or `NaN` during warmup / on a `NULL` handle.
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_sharpe_ratio_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_sharpe_ratio_update(handle: *mut SharpeRatio, value: f64) -> f64 {
    match handle.as_mut() {
        Some(ind) => ind.update(value).unwrap_or(f64::NAN),
        None => f64::NAN,
    }
}

/// Run over `input[0..n]`, writing one output per input into `out[0..n]` (`NaN` at warmup).
///
/// # Safety
/// `handle` valid (from `wickra_sharpe_ratio_new`, not freed); `input`/`out` cover `n` `double`s.
#[no_mangle]
pub unsafe extern "C" fn wickra_sharpe_ratio_batch(
    handle: *mut SharpeRatio,
    input: *const f64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null() || input.is_null() || out.is_null() {
        return;
    }
    let ind = &mut *handle;
    let inputs = slice::from_raw_parts(input, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (slot, &value) in outputs.iter_mut().zip(inputs) {
        *slot = ind.update(value).unwrap_or(f64::NAN);
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_sharpe_ratio_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_sharpe_ratio_warmup_period(handle: *mut SharpeRatio) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_sharpe_ratio_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_sharpe_ratio_is_ready(handle: *mut SharpeRatio) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_sharpe_ratio_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_sharpe_ratio_name(handle: *mut SharpeRatio) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_sharpe_ratio_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_sharpe_ratio_reset(handle: *mut SharpeRatio) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_sharpe_ratio_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_sharpe_ratio_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_sharpe_ratio_free(handle: *mut SharpeRatio) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `SineWave` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_sine_wave_free`.
#[no_mangle]
pub extern "C" fn wickra_sine_wave_new() -> *mut SineWave {
    Box::into_raw(Box::new(SineWave::new()))
}

/// Feed one value; returns the output, or `NaN` during warmup / on a `NULL` handle.
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_sine_wave_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_sine_wave_update(handle: *mut SineWave, value: f64) -> f64 {
    match handle.as_mut() {
        Some(ind) => ind.update(value).unwrap_or(f64::NAN),
        None => f64::NAN,
    }
}

/// Run over `input[0..n]`, writing one output per input into `out[0..n]` (`NaN` at warmup).
///
/// # Safety
/// `handle` valid (from `wickra_sine_wave_new`, not freed); `input`/`out` cover `n` `double`s.
#[no_mangle]
pub unsafe extern "C" fn wickra_sine_wave_batch(
    handle: *mut SineWave,
    input: *const f64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null() || input.is_null() || out.is_null() {
        return;
    }
    let ind = &mut *handle;
    let inputs = slice::from_raw_parts(input, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (slot, &value) in outputs.iter_mut().zip(inputs) {
        *slot = ind.update(value).unwrap_or(f64::NAN);
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_sine_wave_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_sine_wave_warmup_period(handle: *mut SineWave) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_sine_wave_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_sine_wave_is_ready(handle: *mut SineWave) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_sine_wave_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_sine_wave_name(handle: *mut SineWave) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_sine_wave_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_sine_wave_reset(handle: *mut SineWave) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_sine_wave_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_sine_wave_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_sine_wave_free(handle: *mut SineWave) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `SineWeightedMa` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_sine_weighted_ma_free`.
#[no_mangle]
pub extern "C" fn wickra_sine_weighted_ma_new(period: usize) -> *mut SineWeightedMa {
    match SineWeightedMa::new(period) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one value; returns the output, or `NaN` during warmup / on a `NULL` handle.
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_sine_weighted_ma_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_sine_weighted_ma_update(
    handle: *mut SineWeightedMa,
    value: f64,
) -> f64 {
    match handle.as_mut() {
        Some(ind) => ind.update(value).unwrap_or(f64::NAN),
        None => f64::NAN,
    }
}

/// Run over `input[0..n]`, writing one output per input into `out[0..n]` (`NaN` at warmup).
///
/// # Safety
/// `handle` valid (from `wickra_sine_weighted_ma_new`, not freed); `input`/`out` cover `n` `double`s.
#[no_mangle]
pub unsafe extern "C" fn wickra_sine_weighted_ma_batch(
    handle: *mut SineWeightedMa,
    input: *const f64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null() || input.is_null() || out.is_null() {
        return;
    }
    let ind = &mut *handle;
    let inputs = slice::from_raw_parts(input, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (slot, &value) in outputs.iter_mut().zip(inputs) {
        *slot = ind.update(value).unwrap_or(f64::NAN);
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_sine_weighted_ma_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_sine_weighted_ma_warmup_period(
    handle: *mut SineWeightedMa,
) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_sine_weighted_ma_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_sine_weighted_ma_is_ready(handle: *mut SineWeightedMa) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_sine_weighted_ma_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_sine_weighted_ma_name(
    handle: *mut SineWeightedMa,
) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_sine_weighted_ma_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_sine_weighted_ma_reset(handle: *mut SineWeightedMa) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_sine_weighted_ma_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_sine_weighted_ma_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_sine_weighted_ma_free(handle: *mut SineWeightedMa) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `Skewness` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_skewness_free`.
#[no_mangle]
pub extern "C" fn wickra_skewness_new(period: usize) -> *mut Skewness {
    match Skewness::new(period) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one value; returns the output, or `NaN` during warmup / on a `NULL` handle.
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_skewness_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_skewness_update(handle: *mut Skewness, value: f64) -> f64 {
    match handle.as_mut() {
        Some(ind) => ind.update(value).unwrap_or(f64::NAN),
        None => f64::NAN,
    }
}

/// Run over `input[0..n]`, writing one output per input into `out[0..n]` (`NaN` at warmup).
///
/// # Safety
/// `handle` valid (from `wickra_skewness_new`, not freed); `input`/`out` cover `n` `double`s.
#[no_mangle]
pub unsafe extern "C" fn wickra_skewness_batch(
    handle: *mut Skewness,
    input: *const f64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null() || input.is_null() || out.is_null() {
        return;
    }
    let ind = &mut *handle;
    let inputs = slice::from_raw_parts(input, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (slot, &value) in outputs.iter_mut().zip(inputs) {
        *slot = ind.update(value).unwrap_or(f64::NAN);
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_skewness_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_skewness_warmup_period(handle: *mut Skewness) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_skewness_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_skewness_is_ready(handle: *mut Skewness) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_skewness_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_skewness_name(handle: *mut Skewness) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_skewness_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_skewness_reset(handle: *mut Skewness) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_skewness_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_skewness_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_skewness_free(handle: *mut Skewness) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `Sma` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_sma_free`.
#[no_mangle]
pub extern "C" fn wickra_sma_new(period: usize) -> *mut Sma {
    match Sma::new(period) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one value; returns the output, or `NaN` during warmup / on a `NULL` handle.
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_sma_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_sma_update(handle: *mut Sma, value: f64) -> f64 {
    match handle.as_mut() {
        Some(ind) => ind.update(value).unwrap_or(f64::NAN),
        None => f64::NAN,
    }
}

/// Run over `input[0..n]`, writing one output per input into `out[0..n]` (`NaN` at warmup).
///
/// # Safety
/// `handle` valid (from `wickra_sma_new`, not freed); `input`/`out` cover `n` `double`s.
#[no_mangle]
pub unsafe extern "C" fn wickra_sma_batch(
    handle: *mut Sma,
    input: *const f64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null() || input.is_null() || out.is_null() {
        return;
    }
    let ind = &mut *handle;
    let inputs = slice::from_raw_parts(input, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (slot, &value) in outputs.iter_mut().zip(inputs) {
        *slot = ind.update(value).unwrap_or(f64::NAN);
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_sma_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_sma_warmup_period(handle: *mut Sma) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_sma_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_sma_is_ready(handle: *mut Sma) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_sma_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_sma_name(handle: *mut Sma) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_sma_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_sma_reset(handle: *mut Sma) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_sma_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_sma_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_sma_free(handle: *mut Sma) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `Smma` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_smma_free`.
#[no_mangle]
pub extern "C" fn wickra_smma_new(period: usize) -> *mut Smma {
    match Smma::new(period) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one value; returns the output, or `NaN` during warmup / on a `NULL` handle.
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_smma_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_smma_update(handle: *mut Smma, value: f64) -> f64 {
    match handle.as_mut() {
        Some(ind) => ind.update(value).unwrap_or(f64::NAN),
        None => f64::NAN,
    }
}

/// Run over `input[0..n]`, writing one output per input into `out[0..n]` (`NaN` at warmup).
///
/// # Safety
/// `handle` valid (from `wickra_smma_new`, not freed); `input`/`out` cover `n` `double`s.
#[no_mangle]
pub unsafe extern "C" fn wickra_smma_batch(
    handle: *mut Smma,
    input: *const f64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null() || input.is_null() || out.is_null() {
        return;
    }
    let ind = &mut *handle;
    let inputs = slice::from_raw_parts(input, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (slot, &value) in outputs.iter_mut().zip(inputs) {
        *slot = ind.update(value).unwrap_or(f64::NAN);
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_smma_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_smma_warmup_period(handle: *mut Smma) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_smma_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_smma_is_ready(handle: *mut Smma) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_smma_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_smma_name(handle: *mut Smma) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_smma_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_smma_reset(handle: *mut Smma) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_smma_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_smma_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_smma_free(handle: *mut Smma) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `SortinoRatio` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_sortino_ratio_free`.
#[no_mangle]
pub extern "C" fn wickra_sortino_ratio_new(period: usize, mar: f64) -> *mut SortinoRatio {
    match SortinoRatio::new(period, mar) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one value; returns the output, or `NaN` during warmup / on a `NULL` handle.
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_sortino_ratio_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_sortino_ratio_update(handle: *mut SortinoRatio, value: f64) -> f64 {
    match handle.as_mut() {
        Some(ind) => ind.update(value).unwrap_or(f64::NAN),
        None => f64::NAN,
    }
}

/// Run over `input[0..n]`, writing one output per input into `out[0..n]` (`NaN` at warmup).
///
/// # Safety
/// `handle` valid (from `wickra_sortino_ratio_new`, not freed); `input`/`out` cover `n` `double`s.
#[no_mangle]
pub unsafe extern "C" fn wickra_sortino_ratio_batch(
    handle: *mut SortinoRatio,
    input: *const f64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null() || input.is_null() || out.is_null() {
        return;
    }
    let ind = &mut *handle;
    let inputs = slice::from_raw_parts(input, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (slot, &value) in outputs.iter_mut().zip(inputs) {
        *slot = ind.update(value).unwrap_or(f64::NAN);
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_sortino_ratio_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_sortino_ratio_warmup_period(handle: *mut SortinoRatio) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_sortino_ratio_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_sortino_ratio_is_ready(handle: *mut SortinoRatio) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_sortino_ratio_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_sortino_ratio_name(handle: *mut SortinoRatio) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_sortino_ratio_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_sortino_ratio_reset(handle: *mut SortinoRatio) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_sortino_ratio_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_sortino_ratio_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_sortino_ratio_free(handle: *mut SortinoRatio) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `StandardError` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_standard_error_free`.
#[no_mangle]
pub extern "C" fn wickra_standard_error_new(period: usize) -> *mut StandardError {
    match StandardError::new(period) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one value; returns the output, or `NaN` during warmup / on a `NULL` handle.
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_standard_error_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_standard_error_update(
    handle: *mut StandardError,
    value: f64,
) -> f64 {
    match handle.as_mut() {
        Some(ind) => ind.update(value).unwrap_or(f64::NAN),
        None => f64::NAN,
    }
}

/// Run over `input[0..n]`, writing one output per input into `out[0..n]` (`NaN` at warmup).
///
/// # Safety
/// `handle` valid (from `wickra_standard_error_new`, not freed); `input`/`out` cover `n` `double`s.
#[no_mangle]
pub unsafe extern "C" fn wickra_standard_error_batch(
    handle: *mut StandardError,
    input: *const f64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null() || input.is_null() || out.is_null() {
        return;
    }
    let ind = &mut *handle;
    let inputs = slice::from_raw_parts(input, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (slot, &value) in outputs.iter_mut().zip(inputs) {
        *slot = ind.update(value).unwrap_or(f64::NAN);
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_standard_error_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_standard_error_warmup_period(handle: *mut StandardError) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_standard_error_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_standard_error_is_ready(handle: *mut StandardError) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_standard_error_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_standard_error_name(handle: *mut StandardError) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_standard_error_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_standard_error_reset(handle: *mut StandardError) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_standard_error_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_standard_error_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_standard_error_free(handle: *mut StandardError) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `Stc` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_stc_free`.
#[no_mangle]
pub extern "C" fn wickra_stc_new(
    fast: usize,
    slow: usize,
    schaff_period: usize,
    factor: f64,
) -> *mut Stc {
    match Stc::new(fast, slow, schaff_period, factor) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one value; returns the output, or `NaN` during warmup / on a `NULL` handle.
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_stc_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_stc_update(handle: *mut Stc, value: f64) -> f64 {
    match handle.as_mut() {
        Some(ind) => ind.update(value).unwrap_or(f64::NAN),
        None => f64::NAN,
    }
}

/// Run over `input[0..n]`, writing one output per input into `out[0..n]` (`NaN` at warmup).
///
/// # Safety
/// `handle` valid (from `wickra_stc_new`, not freed); `input`/`out` cover `n` `double`s.
#[no_mangle]
pub unsafe extern "C" fn wickra_stc_batch(
    handle: *mut Stc,
    input: *const f64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null() || input.is_null() || out.is_null() {
        return;
    }
    let ind = &mut *handle;
    let inputs = slice::from_raw_parts(input, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (slot, &value) in outputs.iter_mut().zip(inputs) {
        *slot = ind.update(value).unwrap_or(f64::NAN);
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_stc_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_stc_warmup_period(handle: *mut Stc) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_stc_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_stc_is_ready(handle: *mut Stc) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_stc_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_stc_name(handle: *mut Stc) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_stc_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_stc_reset(handle: *mut Stc) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_stc_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_stc_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_stc_free(handle: *mut Stc) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `StdDev` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_std_dev_free`.
#[no_mangle]
pub extern "C" fn wickra_std_dev_new(period: usize) -> *mut StdDev {
    match StdDev::new(period) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one value; returns the output, or `NaN` during warmup / on a `NULL` handle.
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_std_dev_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_std_dev_update(handle: *mut StdDev, value: f64) -> f64 {
    match handle.as_mut() {
        Some(ind) => ind.update(value).unwrap_or(f64::NAN),
        None => f64::NAN,
    }
}

/// Run over `input[0..n]`, writing one output per input into `out[0..n]` (`NaN` at warmup).
///
/// # Safety
/// `handle` valid (from `wickra_std_dev_new`, not freed); `input`/`out` cover `n` `double`s.
#[no_mangle]
pub unsafe extern "C" fn wickra_std_dev_batch(
    handle: *mut StdDev,
    input: *const f64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null() || input.is_null() || out.is_null() {
        return;
    }
    let ind = &mut *handle;
    let inputs = slice::from_raw_parts(input, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (slot, &value) in outputs.iter_mut().zip(inputs) {
        *slot = ind.update(value).unwrap_or(f64::NAN);
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_std_dev_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_std_dev_warmup_period(handle: *mut StdDev) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_std_dev_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_std_dev_is_ready(handle: *mut StdDev) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_std_dev_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_std_dev_name(handle: *mut StdDev) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_std_dev_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_std_dev_reset(handle: *mut StdDev) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_std_dev_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_std_dev_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_std_dev_free(handle: *mut StdDev) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `StepTrailingStop` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_step_trailing_stop_free`.
#[no_mangle]
pub extern "C" fn wickra_step_trailing_stop_new(step_size: f64) -> *mut StepTrailingStop {
    match StepTrailingStop::new(step_size) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one value; returns the output, or `NaN` during warmup / on a `NULL` handle.
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_step_trailing_stop_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_step_trailing_stop_update(
    handle: *mut StepTrailingStop,
    value: f64,
) -> f64 {
    match handle.as_mut() {
        Some(ind) => ind.update(value).unwrap_or(f64::NAN),
        None => f64::NAN,
    }
}

/// Run over `input[0..n]`, writing one output per input into `out[0..n]` (`NaN` at warmup).
///
/// # Safety
/// `handle` valid (from `wickra_step_trailing_stop_new`, not freed); `input`/`out` cover `n` `double`s.
#[no_mangle]
pub unsafe extern "C" fn wickra_step_trailing_stop_batch(
    handle: *mut StepTrailingStop,
    input: *const f64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null() || input.is_null() || out.is_null() {
        return;
    }
    let ind = &mut *handle;
    let inputs = slice::from_raw_parts(input, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (slot, &value) in outputs.iter_mut().zip(inputs) {
        *slot = ind.update(value).unwrap_or(f64::NAN);
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_step_trailing_stop_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_step_trailing_stop_warmup_period(
    handle: *mut StepTrailingStop,
) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_step_trailing_stop_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_step_trailing_stop_is_ready(handle: *mut StepTrailingStop) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_step_trailing_stop_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_step_trailing_stop_name(
    handle: *mut StepTrailingStop,
) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_step_trailing_stop_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_step_trailing_stop_reset(handle: *mut StepTrailingStop) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_step_trailing_stop_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_step_trailing_stop_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_step_trailing_stop_free(handle: *mut StepTrailingStop) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `SterlingRatio` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_sterling_ratio_free`.
#[no_mangle]
pub extern "C" fn wickra_sterling_ratio_new(period: usize) -> *mut SterlingRatio {
    match SterlingRatio::new(period) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one value; returns the output, or `NaN` during warmup / on a `NULL` handle.
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_sterling_ratio_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_sterling_ratio_update(
    handle: *mut SterlingRatio,
    value: f64,
) -> f64 {
    match handle.as_mut() {
        Some(ind) => ind.update(value).unwrap_or(f64::NAN),
        None => f64::NAN,
    }
}

/// Run over `input[0..n]`, writing one output per input into `out[0..n]` (`NaN` at warmup).
///
/// # Safety
/// `handle` valid (from `wickra_sterling_ratio_new`, not freed); `input`/`out` cover `n` `double`s.
#[no_mangle]
pub unsafe extern "C" fn wickra_sterling_ratio_batch(
    handle: *mut SterlingRatio,
    input: *const f64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null() || input.is_null() || out.is_null() {
        return;
    }
    let ind = &mut *handle;
    let inputs = slice::from_raw_parts(input, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (slot, &value) in outputs.iter_mut().zip(inputs) {
        *slot = ind.update(value).unwrap_or(f64::NAN);
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_sterling_ratio_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_sterling_ratio_warmup_period(handle: *mut SterlingRatio) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_sterling_ratio_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_sterling_ratio_is_ready(handle: *mut SterlingRatio) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_sterling_ratio_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_sterling_ratio_name(handle: *mut SterlingRatio) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_sterling_ratio_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_sterling_ratio_reset(handle: *mut SterlingRatio) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_sterling_ratio_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_sterling_ratio_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_sterling_ratio_free(handle: *mut SterlingRatio) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `StochRsi` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_stoch_rsi_free`.
#[no_mangle]
pub extern "C" fn wickra_stoch_rsi_new(rsi_period: usize, stoch_period: usize) -> *mut StochRsi {
    match StochRsi::new(rsi_period, stoch_period) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one value; returns the output, or `NaN` during warmup / on a `NULL` handle.
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_stoch_rsi_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_stoch_rsi_update(handle: *mut StochRsi, value: f64) -> f64 {
    match handle.as_mut() {
        Some(ind) => ind.update(value).unwrap_or(f64::NAN),
        None => f64::NAN,
    }
}

/// Run over `input[0..n]`, writing one output per input into `out[0..n]` (`NaN` at warmup).
///
/// # Safety
/// `handle` valid (from `wickra_stoch_rsi_new`, not freed); `input`/`out` cover `n` `double`s.
#[no_mangle]
pub unsafe extern "C" fn wickra_stoch_rsi_batch(
    handle: *mut StochRsi,
    input: *const f64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null() || input.is_null() || out.is_null() {
        return;
    }
    let ind = &mut *handle;
    let inputs = slice::from_raw_parts(input, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (slot, &value) in outputs.iter_mut().zip(inputs) {
        *slot = ind.update(value).unwrap_or(f64::NAN);
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_stoch_rsi_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_stoch_rsi_warmup_period(handle: *mut StochRsi) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_stoch_rsi_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_stoch_rsi_is_ready(handle: *mut StochRsi) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_stoch_rsi_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_stoch_rsi_name(handle: *mut StochRsi) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_stoch_rsi_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_stoch_rsi_reset(handle: *mut StochRsi) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_stoch_rsi_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_stoch_rsi_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_stoch_rsi_free(handle: *mut StochRsi) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `SuperSmoother` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_super_smoother_free`.
#[no_mangle]
pub extern "C" fn wickra_super_smoother_new(period: usize) -> *mut SuperSmoother {
    match SuperSmoother::new(period) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one value; returns the output, or `NaN` during warmup / on a `NULL` handle.
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_super_smoother_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_super_smoother_update(
    handle: *mut SuperSmoother,
    value: f64,
) -> f64 {
    match handle.as_mut() {
        Some(ind) => ind.update(value).unwrap_or(f64::NAN),
        None => f64::NAN,
    }
}

/// Run over `input[0..n]`, writing one output per input into `out[0..n]` (`NaN` at warmup).
///
/// # Safety
/// `handle` valid (from `wickra_super_smoother_new`, not freed); `input`/`out` cover `n` `double`s.
#[no_mangle]
pub unsafe extern "C" fn wickra_super_smoother_batch(
    handle: *mut SuperSmoother,
    input: *const f64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null() || input.is_null() || out.is_null() {
        return;
    }
    let ind = &mut *handle;
    let inputs = slice::from_raw_parts(input, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (slot, &value) in outputs.iter_mut().zip(inputs) {
        *slot = ind.update(value).unwrap_or(f64::NAN);
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_super_smoother_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_super_smoother_warmup_period(handle: *mut SuperSmoother) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_super_smoother_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_super_smoother_is_ready(handle: *mut SuperSmoother) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_super_smoother_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_super_smoother_name(handle: *mut SuperSmoother) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_super_smoother_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_super_smoother_reset(handle: *mut SuperSmoother) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_super_smoother_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_super_smoother_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_super_smoother_free(handle: *mut SuperSmoother) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `T3` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_t3_free`.
#[no_mangle]
pub extern "C" fn wickra_t3_new(period: usize, v: f64) -> *mut T3 {
    match T3::new(period, v) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one value; returns the output, or `NaN` during warmup / on a `NULL` handle.
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_t3_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_t3_update(handle: *mut T3, value: f64) -> f64 {
    match handle.as_mut() {
        Some(ind) => ind.update(value).unwrap_or(f64::NAN),
        None => f64::NAN,
    }
}

/// Run over `input[0..n]`, writing one output per input into `out[0..n]` (`NaN` at warmup).
///
/// # Safety
/// `handle` valid (from `wickra_t3_new`, not freed); `input`/`out` cover `n` `double`s.
#[no_mangle]
pub unsafe extern "C" fn wickra_t3_batch(
    handle: *mut T3,
    input: *const f64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null() || input.is_null() || out.is_null() {
        return;
    }
    let ind = &mut *handle;
    let inputs = slice::from_raw_parts(input, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (slot, &value) in outputs.iter_mut().zip(inputs) {
        *slot = ind.update(value).unwrap_or(f64::NAN);
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_t3_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_t3_warmup_period(handle: *mut T3) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_t3_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_t3_is_ready(handle: *mut T3) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_t3_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_t3_name(handle: *mut T3) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_t3_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_t3_reset(handle: *mut T3) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_t3_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_t3_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_t3_free(handle: *mut T3) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `TailRatio` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_tail_ratio_free`.
#[no_mangle]
pub extern "C" fn wickra_tail_ratio_new(period: usize) -> *mut TailRatio {
    match TailRatio::new(period) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one value; returns the output, or `NaN` during warmup / on a `NULL` handle.
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_tail_ratio_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_tail_ratio_update(handle: *mut TailRatio, value: f64) -> f64 {
    match handle.as_mut() {
        Some(ind) => ind.update(value).unwrap_or(f64::NAN),
        None => f64::NAN,
    }
}

/// Run over `input[0..n]`, writing one output per input into `out[0..n]` (`NaN` at warmup).
///
/// # Safety
/// `handle` valid (from `wickra_tail_ratio_new`, not freed); `input`/`out` cover `n` `double`s.
#[no_mangle]
pub unsafe extern "C" fn wickra_tail_ratio_batch(
    handle: *mut TailRatio,
    input: *const f64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null() || input.is_null() || out.is_null() {
        return;
    }
    let ind = &mut *handle;
    let inputs = slice::from_raw_parts(input, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (slot, &value) in outputs.iter_mut().zip(inputs) {
        *slot = ind.update(value).unwrap_or(f64::NAN);
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_tail_ratio_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_tail_ratio_warmup_period(handle: *mut TailRatio) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_tail_ratio_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_tail_ratio_is_ready(handle: *mut TailRatio) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_tail_ratio_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_tail_ratio_name(handle: *mut TailRatio) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_tail_ratio_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_tail_ratio_reset(handle: *mut TailRatio) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_tail_ratio_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_tail_ratio_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_tail_ratio_free(handle: *mut TailRatio) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `Tema` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_tema_free`.
#[no_mangle]
pub extern "C" fn wickra_tema_new(period: usize) -> *mut Tema {
    match Tema::new(period) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one value; returns the output, or `NaN` during warmup / on a `NULL` handle.
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_tema_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_tema_update(handle: *mut Tema, value: f64) -> f64 {
    match handle.as_mut() {
        Some(ind) => ind.update(value).unwrap_or(f64::NAN),
        None => f64::NAN,
    }
}

/// Run over `input[0..n]`, writing one output per input into `out[0..n]` (`NaN` at warmup).
///
/// # Safety
/// `handle` valid (from `wickra_tema_new`, not freed); `input`/`out` cover `n` `double`s.
#[no_mangle]
pub unsafe extern "C" fn wickra_tema_batch(
    handle: *mut Tema,
    input: *const f64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null() || input.is_null() || out.is_null() {
        return;
    }
    let ind = &mut *handle;
    let inputs = slice::from_raw_parts(input, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (slot, &value) in outputs.iter_mut().zip(inputs) {
        *slot = ind.update(value).unwrap_or(f64::NAN);
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_tema_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_tema_warmup_period(handle: *mut Tema) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_tema_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_tema_is_ready(handle: *mut Tema) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_tema_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_tema_name(handle: *mut Tema) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_tema_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_tema_reset(handle: *mut Tema) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_tema_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_tema_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_tema_free(handle: *mut Tema) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `Tii` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_tii_free`.
#[no_mangle]
pub extern "C" fn wickra_tii_new(sma_period: usize, dev_period: usize) -> *mut Tii {
    match Tii::new(sma_period, dev_period) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one value; returns the output, or `NaN` during warmup / on a `NULL` handle.
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_tii_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_tii_update(handle: *mut Tii, value: f64) -> f64 {
    match handle.as_mut() {
        Some(ind) => ind.update(value).unwrap_or(f64::NAN),
        None => f64::NAN,
    }
}

/// Run over `input[0..n]`, writing one output per input into `out[0..n]` (`NaN` at warmup).
///
/// # Safety
/// `handle` valid (from `wickra_tii_new`, not freed); `input`/`out` cover `n` `double`s.
#[no_mangle]
pub unsafe extern "C" fn wickra_tii_batch(
    handle: *mut Tii,
    input: *const f64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null() || input.is_null() || out.is_null() {
        return;
    }
    let ind = &mut *handle;
    let inputs = slice::from_raw_parts(input, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (slot, &value) in outputs.iter_mut().zip(inputs) {
        *slot = ind.update(value).unwrap_or(f64::NAN);
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_tii_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_tii_warmup_period(handle: *mut Tii) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_tii_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_tii_is_ready(handle: *mut Tii) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_tii_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_tii_name(handle: *mut Tii) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_tii_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_tii_reset(handle: *mut Tii) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_tii_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_tii_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_tii_free(handle: *mut Tii) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `TrendLabel` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_trend_label_free`.
#[no_mangle]
pub extern "C" fn wickra_trend_label_new(period: usize) -> *mut TrendLabel {
    match TrendLabel::new(period) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one value; returns the output, or `NaN` during warmup / on a `NULL` handle.
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_trend_label_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_trend_label_update(handle: *mut TrendLabel, value: f64) -> f64 {
    match handle.as_mut() {
        Some(ind) => ind.update(value).unwrap_or(f64::NAN),
        None => f64::NAN,
    }
}

/// Run over `input[0..n]`, writing one output per input into `out[0..n]` (`NaN` at warmup).
///
/// # Safety
/// `handle` valid (from `wickra_trend_label_new`, not freed); `input`/`out` cover `n` `double`s.
#[no_mangle]
pub unsafe extern "C" fn wickra_trend_label_batch(
    handle: *mut TrendLabel,
    input: *const f64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null() || input.is_null() || out.is_null() {
        return;
    }
    let ind = &mut *handle;
    let inputs = slice::from_raw_parts(input, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (slot, &value) in outputs.iter_mut().zip(inputs) {
        *slot = ind.update(value).unwrap_or(f64::NAN);
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_trend_label_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_trend_label_warmup_period(handle: *mut TrendLabel) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_trend_label_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_trend_label_is_ready(handle: *mut TrendLabel) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_trend_label_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_trend_label_name(handle: *mut TrendLabel) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_trend_label_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_trend_label_reset(handle: *mut TrendLabel) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_trend_label_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_trend_label_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_trend_label_free(handle: *mut TrendLabel) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `TrendStrengthIndex` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_trend_strength_index_free`.
#[no_mangle]
pub extern "C" fn wickra_trend_strength_index_new(period: usize) -> *mut TrendStrengthIndex {
    match TrendStrengthIndex::new(period) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one value; returns the output, or `NaN` during warmup / on a `NULL` handle.
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_trend_strength_index_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_trend_strength_index_update(
    handle: *mut TrendStrengthIndex,
    value: f64,
) -> f64 {
    match handle.as_mut() {
        Some(ind) => ind.update(value).unwrap_or(f64::NAN),
        None => f64::NAN,
    }
}

/// Run over `input[0..n]`, writing one output per input into `out[0..n]` (`NaN` at warmup).
///
/// # Safety
/// `handle` valid (from `wickra_trend_strength_index_new`, not freed); `input`/`out` cover `n` `double`s.
#[no_mangle]
pub unsafe extern "C" fn wickra_trend_strength_index_batch(
    handle: *mut TrendStrengthIndex,
    input: *const f64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null() || input.is_null() || out.is_null() {
        return;
    }
    let ind = &mut *handle;
    let inputs = slice::from_raw_parts(input, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (slot, &value) in outputs.iter_mut().zip(inputs) {
        *slot = ind.update(value).unwrap_or(f64::NAN);
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_trend_strength_index_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_trend_strength_index_warmup_period(
    handle: *mut TrendStrengthIndex,
) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_trend_strength_index_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_trend_strength_index_is_ready(
    handle: *mut TrendStrengthIndex,
) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_trend_strength_index_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_trend_strength_index_name(
    handle: *mut TrendStrengthIndex,
) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_trend_strength_index_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_trend_strength_index_reset(handle: *mut TrendStrengthIndex) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_trend_strength_index_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_trend_strength_index_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_trend_strength_index_free(handle: *mut TrendStrengthIndex) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `Trendflex` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_trendflex_free`.
#[no_mangle]
pub extern "C" fn wickra_trendflex_new(period: usize) -> *mut Trendflex {
    match Trendflex::new(period) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one value; returns the output, or `NaN` during warmup / on a `NULL` handle.
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_trendflex_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_trendflex_update(handle: *mut Trendflex, value: f64) -> f64 {
    match handle.as_mut() {
        Some(ind) => ind.update(value).unwrap_or(f64::NAN),
        None => f64::NAN,
    }
}

/// Run over `input[0..n]`, writing one output per input into `out[0..n]` (`NaN` at warmup).
///
/// # Safety
/// `handle` valid (from `wickra_trendflex_new`, not freed); `input`/`out` cover `n` `double`s.
#[no_mangle]
pub unsafe extern "C" fn wickra_trendflex_batch(
    handle: *mut Trendflex,
    input: *const f64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null() || input.is_null() || out.is_null() {
        return;
    }
    let ind = &mut *handle;
    let inputs = slice::from_raw_parts(input, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (slot, &value) in outputs.iter_mut().zip(inputs) {
        *slot = ind.update(value).unwrap_or(f64::NAN);
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_trendflex_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_trendflex_warmup_period(handle: *mut Trendflex) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_trendflex_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_trendflex_is_ready(handle: *mut Trendflex) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_trendflex_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_trendflex_name(handle: *mut Trendflex) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_trendflex_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_trendflex_reset(handle: *mut Trendflex) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_trendflex_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_trendflex_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_trendflex_free(handle: *mut Trendflex) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `Trima` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_trima_free`.
#[no_mangle]
pub extern "C" fn wickra_trima_new(period: usize) -> *mut Trima {
    match Trima::new(period) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one value; returns the output, or `NaN` during warmup / on a `NULL` handle.
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_trima_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_trima_update(handle: *mut Trima, value: f64) -> f64 {
    match handle.as_mut() {
        Some(ind) => ind.update(value).unwrap_or(f64::NAN),
        None => f64::NAN,
    }
}

/// Run over `input[0..n]`, writing one output per input into `out[0..n]` (`NaN` at warmup).
///
/// # Safety
/// `handle` valid (from `wickra_trima_new`, not freed); `input`/`out` cover `n` `double`s.
#[no_mangle]
pub unsafe extern "C" fn wickra_trima_batch(
    handle: *mut Trima,
    input: *const f64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null() || input.is_null() || out.is_null() {
        return;
    }
    let ind = &mut *handle;
    let inputs = slice::from_raw_parts(input, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (slot, &value) in outputs.iter_mut().zip(inputs) {
        *slot = ind.update(value).unwrap_or(f64::NAN);
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_trima_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_trima_warmup_period(handle: *mut Trima) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_trima_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_trima_is_ready(handle: *mut Trima) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_trima_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_trima_name(handle: *mut Trima) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_trima_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_trima_reset(handle: *mut Trima) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_trima_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_trima_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_trima_free(handle: *mut Trima) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `Trix` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_trix_free`.
#[no_mangle]
pub extern "C" fn wickra_trix_new(period: usize) -> *mut Trix {
    match Trix::new(period) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one value; returns the output, or `NaN` during warmup / on a `NULL` handle.
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_trix_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_trix_update(handle: *mut Trix, value: f64) -> f64 {
    match handle.as_mut() {
        Some(ind) => ind.update(value).unwrap_or(f64::NAN),
        None => f64::NAN,
    }
}

/// Run over `input[0..n]`, writing one output per input into `out[0..n]` (`NaN` at warmup).
///
/// # Safety
/// `handle` valid (from `wickra_trix_new`, not freed); `input`/`out` cover `n` `double`s.
#[no_mangle]
pub unsafe extern "C" fn wickra_trix_batch(
    handle: *mut Trix,
    input: *const f64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null() || input.is_null() || out.is_null() {
        return;
    }
    let ind = &mut *handle;
    let inputs = slice::from_raw_parts(input, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (slot, &value) in outputs.iter_mut().zip(inputs) {
        *slot = ind.update(value).unwrap_or(f64::NAN);
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_trix_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_trix_warmup_period(handle: *mut Trix) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_trix_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_trix_is_ready(handle: *mut Trix) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_trix_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_trix_name(handle: *mut Trix) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_trix_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_trix_reset(handle: *mut Trix) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_trix_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_trix_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_trix_free(handle: *mut Trix) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `Tsf` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_tsf_free`.
#[no_mangle]
pub extern "C" fn wickra_tsf_new(period: usize) -> *mut Tsf {
    match Tsf::new(period) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one value; returns the output, or `NaN` during warmup / on a `NULL` handle.
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_tsf_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_tsf_update(handle: *mut Tsf, value: f64) -> f64 {
    match handle.as_mut() {
        Some(ind) => ind.update(value).unwrap_or(f64::NAN),
        None => f64::NAN,
    }
}

/// Run over `input[0..n]`, writing one output per input into `out[0..n]` (`NaN` at warmup).
///
/// # Safety
/// `handle` valid (from `wickra_tsf_new`, not freed); `input`/`out` cover `n` `double`s.
#[no_mangle]
pub unsafe extern "C" fn wickra_tsf_batch(
    handle: *mut Tsf,
    input: *const f64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null() || input.is_null() || out.is_null() {
        return;
    }
    let ind = &mut *handle;
    let inputs = slice::from_raw_parts(input, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (slot, &value) in outputs.iter_mut().zip(inputs) {
        *slot = ind.update(value).unwrap_or(f64::NAN);
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_tsf_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_tsf_warmup_period(handle: *mut Tsf) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_tsf_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_tsf_is_ready(handle: *mut Tsf) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_tsf_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_tsf_name(handle: *mut Tsf) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_tsf_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_tsf_reset(handle: *mut Tsf) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_tsf_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_tsf_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_tsf_free(handle: *mut Tsf) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `TsfOscillator` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_tsf_oscillator_free`.
#[no_mangle]
pub extern "C" fn wickra_tsf_oscillator_new(period: usize) -> *mut TsfOscillator {
    match TsfOscillator::new(period) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one value; returns the output, or `NaN` during warmup / on a `NULL` handle.
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_tsf_oscillator_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_tsf_oscillator_update(
    handle: *mut TsfOscillator,
    value: f64,
) -> f64 {
    match handle.as_mut() {
        Some(ind) => ind.update(value).unwrap_or(f64::NAN),
        None => f64::NAN,
    }
}

/// Run over `input[0..n]`, writing one output per input into `out[0..n]` (`NaN` at warmup).
///
/// # Safety
/// `handle` valid (from `wickra_tsf_oscillator_new`, not freed); `input`/`out` cover `n` `double`s.
#[no_mangle]
pub unsafe extern "C" fn wickra_tsf_oscillator_batch(
    handle: *mut TsfOscillator,
    input: *const f64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null() || input.is_null() || out.is_null() {
        return;
    }
    let ind = &mut *handle;
    let inputs = slice::from_raw_parts(input, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (slot, &value) in outputs.iter_mut().zip(inputs) {
        *slot = ind.update(value).unwrap_or(f64::NAN);
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_tsf_oscillator_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_tsf_oscillator_warmup_period(handle: *mut TsfOscillator) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_tsf_oscillator_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_tsf_oscillator_is_ready(handle: *mut TsfOscillator) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_tsf_oscillator_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_tsf_oscillator_name(handle: *mut TsfOscillator) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_tsf_oscillator_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_tsf_oscillator_reset(handle: *mut TsfOscillator) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_tsf_oscillator_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_tsf_oscillator_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_tsf_oscillator_free(handle: *mut TsfOscillator) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `Tsi` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_tsi_free`.
#[no_mangle]
pub extern "C" fn wickra_tsi_new(long: usize, short: usize) -> *mut Tsi {
    match Tsi::new(long, short) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one value; returns the output, or `NaN` during warmup / on a `NULL` handle.
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_tsi_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_tsi_update(handle: *mut Tsi, value: f64) -> f64 {
    match handle.as_mut() {
        Some(ind) => ind.update(value).unwrap_or(f64::NAN),
        None => f64::NAN,
    }
}

/// Run over `input[0..n]`, writing one output per input into `out[0..n]` (`NaN` at warmup).
///
/// # Safety
/// `handle` valid (from `wickra_tsi_new`, not freed); `input`/`out` cover `n` `double`s.
#[no_mangle]
pub unsafe extern "C" fn wickra_tsi_batch(
    handle: *mut Tsi,
    input: *const f64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null() || input.is_null() || out.is_null() {
        return;
    }
    let ind = &mut *handle;
    let inputs = slice::from_raw_parts(input, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (slot, &value) in outputs.iter_mut().zip(inputs) {
        *slot = ind.update(value).unwrap_or(f64::NAN);
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_tsi_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_tsi_warmup_period(handle: *mut Tsi) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_tsi_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_tsi_is_ready(handle: *mut Tsi) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_tsi_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_tsi_name(handle: *mut Tsi) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_tsi_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_tsi_reset(handle: *mut Tsi) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_tsi_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_tsi_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_tsi_free(handle: *mut Tsi) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `UlcerIndex` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_ulcer_index_free`.
#[no_mangle]
pub extern "C" fn wickra_ulcer_index_new(period: usize) -> *mut UlcerIndex {
    match UlcerIndex::new(period) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one value; returns the output, or `NaN` during warmup / on a `NULL` handle.
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_ulcer_index_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_ulcer_index_update(handle: *mut UlcerIndex, value: f64) -> f64 {
    match handle.as_mut() {
        Some(ind) => ind.update(value).unwrap_or(f64::NAN),
        None => f64::NAN,
    }
}

/// Run over `input[0..n]`, writing one output per input into `out[0..n]` (`NaN` at warmup).
///
/// # Safety
/// `handle` valid (from `wickra_ulcer_index_new`, not freed); `input`/`out` cover `n` `double`s.
#[no_mangle]
pub unsafe extern "C" fn wickra_ulcer_index_batch(
    handle: *mut UlcerIndex,
    input: *const f64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null() || input.is_null() || out.is_null() {
        return;
    }
    let ind = &mut *handle;
    let inputs = slice::from_raw_parts(input, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (slot, &value) in outputs.iter_mut().zip(inputs) {
        *slot = ind.update(value).unwrap_or(f64::NAN);
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_ulcer_index_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_ulcer_index_warmup_period(handle: *mut UlcerIndex) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_ulcer_index_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_ulcer_index_is_ready(handle: *mut UlcerIndex) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_ulcer_index_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_ulcer_index_name(handle: *mut UlcerIndex) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_ulcer_index_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_ulcer_index_reset(handle: *mut UlcerIndex) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_ulcer_index_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_ulcer_index_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_ulcer_index_free(handle: *mut UlcerIndex) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `UniversalOscillator` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_universal_oscillator_free`.
#[no_mangle]
pub extern "C" fn wickra_universal_oscillator_new(period: usize) -> *mut UniversalOscillator {
    match UniversalOscillator::new(period) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one value; returns the output, or `NaN` during warmup / on a `NULL` handle.
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_universal_oscillator_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_universal_oscillator_update(
    handle: *mut UniversalOscillator,
    value: f64,
) -> f64 {
    match handle.as_mut() {
        Some(ind) => ind.update(value).unwrap_or(f64::NAN),
        None => f64::NAN,
    }
}

/// Run over `input[0..n]`, writing one output per input into `out[0..n]` (`NaN` at warmup).
///
/// # Safety
/// `handle` valid (from `wickra_universal_oscillator_new`, not freed); `input`/`out` cover `n` `double`s.
#[no_mangle]
pub unsafe extern "C" fn wickra_universal_oscillator_batch(
    handle: *mut UniversalOscillator,
    input: *const f64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null() || input.is_null() || out.is_null() {
        return;
    }
    let ind = &mut *handle;
    let inputs = slice::from_raw_parts(input, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (slot, &value) in outputs.iter_mut().zip(inputs) {
        *slot = ind.update(value).unwrap_or(f64::NAN);
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_universal_oscillator_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_universal_oscillator_warmup_period(
    handle: *mut UniversalOscillator,
) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_universal_oscillator_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_universal_oscillator_is_ready(
    handle: *mut UniversalOscillator,
) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_universal_oscillator_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_universal_oscillator_name(
    handle: *mut UniversalOscillator,
) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_universal_oscillator_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_universal_oscillator_reset(handle: *mut UniversalOscillator) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_universal_oscillator_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_universal_oscillator_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_universal_oscillator_free(handle: *mut UniversalOscillator) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `UpsidePotentialRatio` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_upside_potential_ratio_free`.
#[no_mangle]
pub extern "C" fn wickra_upside_potential_ratio_new(
    period: usize,
    mar: f64,
) -> *mut UpsidePotentialRatio {
    match UpsidePotentialRatio::new(period, mar) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one value; returns the output, or `NaN` during warmup / on a `NULL` handle.
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_upside_potential_ratio_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_upside_potential_ratio_update(
    handle: *mut UpsidePotentialRatio,
    value: f64,
) -> f64 {
    match handle.as_mut() {
        Some(ind) => ind.update(value).unwrap_or(f64::NAN),
        None => f64::NAN,
    }
}

/// Run over `input[0..n]`, writing one output per input into `out[0..n]` (`NaN` at warmup).
///
/// # Safety
/// `handle` valid (from `wickra_upside_potential_ratio_new`, not freed); `input`/`out` cover `n` `double`s.
#[no_mangle]
pub unsafe extern "C" fn wickra_upside_potential_ratio_batch(
    handle: *mut UpsidePotentialRatio,
    input: *const f64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null() || input.is_null() || out.is_null() {
        return;
    }
    let ind = &mut *handle;
    let inputs = slice::from_raw_parts(input, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (slot, &value) in outputs.iter_mut().zip(inputs) {
        *slot = ind.update(value).unwrap_or(f64::NAN);
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_upside_potential_ratio_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_upside_potential_ratio_warmup_period(
    handle: *mut UpsidePotentialRatio,
) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_upside_potential_ratio_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_upside_potential_ratio_is_ready(
    handle: *mut UpsidePotentialRatio,
) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_upside_potential_ratio_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_upside_potential_ratio_name(
    handle: *mut UpsidePotentialRatio,
) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_upside_potential_ratio_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_upside_potential_ratio_reset(handle: *mut UpsidePotentialRatio) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_upside_potential_ratio_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_upside_potential_ratio_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_upside_potential_ratio_free(handle: *mut UpsidePotentialRatio) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `ValueAtRisk` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_value_at_risk_free`.
#[no_mangle]
pub extern "C" fn wickra_value_at_risk_new(period: usize, confidence: f64) -> *mut ValueAtRisk {
    match ValueAtRisk::new(period, confidence) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one value; returns the output, or `NaN` during warmup / on a `NULL` handle.
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_value_at_risk_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_value_at_risk_update(handle: *mut ValueAtRisk, value: f64) -> f64 {
    match handle.as_mut() {
        Some(ind) => ind.update(value).unwrap_or(f64::NAN),
        None => f64::NAN,
    }
}

/// Run over `input[0..n]`, writing one output per input into `out[0..n]` (`NaN` at warmup).
///
/// # Safety
/// `handle` valid (from `wickra_value_at_risk_new`, not freed); `input`/`out` cover `n` `double`s.
#[no_mangle]
pub unsafe extern "C" fn wickra_value_at_risk_batch(
    handle: *mut ValueAtRisk,
    input: *const f64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null() || input.is_null() || out.is_null() {
        return;
    }
    let ind = &mut *handle;
    let inputs = slice::from_raw_parts(input, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (slot, &value) in outputs.iter_mut().zip(inputs) {
        *slot = ind.update(value).unwrap_or(f64::NAN);
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_value_at_risk_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_value_at_risk_warmup_period(handle: *mut ValueAtRisk) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_value_at_risk_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_value_at_risk_is_ready(handle: *mut ValueAtRisk) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_value_at_risk_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_value_at_risk_name(handle: *mut ValueAtRisk) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_value_at_risk_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_value_at_risk_reset(handle: *mut ValueAtRisk) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_value_at_risk_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_value_at_risk_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_value_at_risk_free(handle: *mut ValueAtRisk) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `Variance` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_variance_free`.
#[no_mangle]
pub extern "C" fn wickra_variance_new(period: usize) -> *mut Variance {
    match Variance::new(period) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one value; returns the output, or `NaN` during warmup / on a `NULL` handle.
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_variance_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_variance_update(handle: *mut Variance, value: f64) -> f64 {
    match handle.as_mut() {
        Some(ind) => ind.update(value).unwrap_or(f64::NAN),
        None => f64::NAN,
    }
}

/// Run over `input[0..n]`, writing one output per input into `out[0..n]` (`NaN` at warmup).
///
/// # Safety
/// `handle` valid (from `wickra_variance_new`, not freed); `input`/`out` cover `n` `double`s.
#[no_mangle]
pub unsafe extern "C" fn wickra_variance_batch(
    handle: *mut Variance,
    input: *const f64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null() || input.is_null() || out.is_null() {
        return;
    }
    let ind = &mut *handle;
    let inputs = slice::from_raw_parts(input, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (slot, &value) in outputs.iter_mut().zip(inputs) {
        *slot = ind.update(value).unwrap_or(f64::NAN);
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_variance_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_variance_warmup_period(handle: *mut Variance) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_variance_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_variance_is_ready(handle: *mut Variance) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_variance_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_variance_name(handle: *mut Variance) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_variance_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_variance_reset(handle: *mut Variance) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_variance_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_variance_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_variance_free(handle: *mut Variance) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `VerticalHorizontalFilter` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_vertical_horizontal_filter_free`.
#[no_mangle]
pub extern "C" fn wickra_vertical_horizontal_filter_new(
    period: usize,
) -> *mut VerticalHorizontalFilter {
    match VerticalHorizontalFilter::new(period) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one value; returns the output, or `NaN` during warmup / on a `NULL` handle.
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_vertical_horizontal_filter_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_vertical_horizontal_filter_update(
    handle: *mut VerticalHorizontalFilter,
    value: f64,
) -> f64 {
    match handle.as_mut() {
        Some(ind) => ind.update(value).unwrap_or(f64::NAN),
        None => f64::NAN,
    }
}

/// Run over `input[0..n]`, writing one output per input into `out[0..n]` (`NaN` at warmup).
///
/// # Safety
/// `handle` valid (from `wickra_vertical_horizontal_filter_new`, not freed); `input`/`out` cover `n` `double`s.
#[no_mangle]
pub unsafe extern "C" fn wickra_vertical_horizontal_filter_batch(
    handle: *mut VerticalHorizontalFilter,
    input: *const f64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null() || input.is_null() || out.is_null() {
        return;
    }
    let ind = &mut *handle;
    let inputs = slice::from_raw_parts(input, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (slot, &value) in outputs.iter_mut().zip(inputs) {
        *slot = ind.update(value).unwrap_or(f64::NAN);
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_vertical_horizontal_filter_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_vertical_horizontal_filter_warmup_period(
    handle: *mut VerticalHorizontalFilter,
) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_vertical_horizontal_filter_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_vertical_horizontal_filter_is_ready(
    handle: *mut VerticalHorizontalFilter,
) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_vertical_horizontal_filter_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_vertical_horizontal_filter_name(
    handle: *mut VerticalHorizontalFilter,
) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_vertical_horizontal_filter_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_vertical_horizontal_filter_reset(
    handle: *mut VerticalHorizontalFilter,
) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_vertical_horizontal_filter_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_vertical_horizontal_filter_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_vertical_horizontal_filter_free(
    handle: *mut VerticalHorizontalFilter,
) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `Vidya` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_vidya_free`.
#[no_mangle]
pub extern "C" fn wickra_vidya_new(period: usize, cmo_period: usize) -> *mut Vidya {
    match Vidya::new(period, cmo_period) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one value; returns the output, or `NaN` during warmup / on a `NULL` handle.
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_vidya_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_vidya_update(handle: *mut Vidya, value: f64) -> f64 {
    match handle.as_mut() {
        Some(ind) => ind.update(value).unwrap_or(f64::NAN),
        None => f64::NAN,
    }
}

/// Run over `input[0..n]`, writing one output per input into `out[0..n]` (`NaN` at warmup).
///
/// # Safety
/// `handle` valid (from `wickra_vidya_new`, not freed); `input`/`out` cover `n` `double`s.
#[no_mangle]
pub unsafe extern "C" fn wickra_vidya_batch(
    handle: *mut Vidya,
    input: *const f64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null() || input.is_null() || out.is_null() {
        return;
    }
    let ind = &mut *handle;
    let inputs = slice::from_raw_parts(input, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (slot, &value) in outputs.iter_mut().zip(inputs) {
        *slot = ind.update(value).unwrap_or(f64::NAN);
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_vidya_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_vidya_warmup_period(handle: *mut Vidya) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_vidya_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_vidya_is_ready(handle: *mut Vidya) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_vidya_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_vidya_name(handle: *mut Vidya) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_vidya_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_vidya_reset(handle: *mut Vidya) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_vidya_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_vidya_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_vidya_free(handle: *mut Vidya) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `VolatilityOfVolatility` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_volatility_of_volatility_free`.
#[no_mangle]
pub extern "C" fn wickra_volatility_of_volatility_new(
    vol_window: usize,
    vov_window: usize,
) -> *mut VolatilityOfVolatility {
    match VolatilityOfVolatility::new(vol_window, vov_window) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one value; returns the output, or `NaN` during warmup / on a `NULL` handle.
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_volatility_of_volatility_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_volatility_of_volatility_update(
    handle: *mut VolatilityOfVolatility,
    value: f64,
) -> f64 {
    match handle.as_mut() {
        Some(ind) => ind.update(value).unwrap_or(f64::NAN),
        None => f64::NAN,
    }
}

/// Run over `input[0..n]`, writing one output per input into `out[0..n]` (`NaN` at warmup).
///
/// # Safety
/// `handle` valid (from `wickra_volatility_of_volatility_new`, not freed); `input`/`out` cover `n` `double`s.
#[no_mangle]
pub unsafe extern "C" fn wickra_volatility_of_volatility_batch(
    handle: *mut VolatilityOfVolatility,
    input: *const f64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null() || input.is_null() || out.is_null() {
        return;
    }
    let ind = &mut *handle;
    let inputs = slice::from_raw_parts(input, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (slot, &value) in outputs.iter_mut().zip(inputs) {
        *slot = ind.update(value).unwrap_or(f64::NAN);
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_volatility_of_volatility_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_volatility_of_volatility_warmup_period(
    handle: *mut VolatilityOfVolatility,
) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_volatility_of_volatility_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_volatility_of_volatility_is_ready(
    handle: *mut VolatilityOfVolatility,
) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_volatility_of_volatility_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_volatility_of_volatility_name(
    handle: *mut VolatilityOfVolatility,
) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_volatility_of_volatility_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_volatility_of_volatility_reset(
    handle: *mut VolatilityOfVolatility,
) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_volatility_of_volatility_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_volatility_of_volatility_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_volatility_of_volatility_free(handle: *mut VolatilityOfVolatility) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `WavePm` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_wave_pm_free`.
#[no_mangle]
pub extern "C" fn wickra_wave_pm_new(length: usize, smoothing: usize) -> *mut WavePm {
    match WavePm::new(length, smoothing) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one value; returns the output, or `NaN` during warmup / on a `NULL` handle.
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_wave_pm_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_wave_pm_update(handle: *mut WavePm, value: f64) -> f64 {
    match handle.as_mut() {
        Some(ind) => ind.update(value).unwrap_or(f64::NAN),
        None => f64::NAN,
    }
}

/// Run over `input[0..n]`, writing one output per input into `out[0..n]` (`NaN` at warmup).
///
/// # Safety
/// `handle` valid (from `wickra_wave_pm_new`, not freed); `input`/`out` cover `n` `double`s.
#[no_mangle]
pub unsafe extern "C" fn wickra_wave_pm_batch(
    handle: *mut WavePm,
    input: *const f64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null() || input.is_null() || out.is_null() {
        return;
    }
    let ind = &mut *handle;
    let inputs = slice::from_raw_parts(input, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (slot, &value) in outputs.iter_mut().zip(inputs) {
        *slot = ind.update(value).unwrap_or(f64::NAN);
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_wave_pm_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_wave_pm_warmup_period(handle: *mut WavePm) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_wave_pm_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_wave_pm_is_ready(handle: *mut WavePm) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_wave_pm_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_wave_pm_name(handle: *mut WavePm) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_wave_pm_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_wave_pm_reset(handle: *mut WavePm) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_wave_pm_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_wave_pm_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_wave_pm_free(handle: *mut WavePm) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `WinRate` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_win_rate_free`.
#[no_mangle]
pub extern "C" fn wickra_win_rate_new(period: usize) -> *mut WinRate {
    match WinRate::new(period) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one value; returns the output, or `NaN` during warmup / on a `NULL` handle.
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_win_rate_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_win_rate_update(handle: *mut WinRate, value: f64) -> f64 {
    match handle.as_mut() {
        Some(ind) => ind.update(value).unwrap_or(f64::NAN),
        None => f64::NAN,
    }
}

/// Run over `input[0..n]`, writing one output per input into `out[0..n]` (`NaN` at warmup).
///
/// # Safety
/// `handle` valid (from `wickra_win_rate_new`, not freed); `input`/`out` cover `n` `double`s.
#[no_mangle]
pub unsafe extern "C" fn wickra_win_rate_batch(
    handle: *mut WinRate,
    input: *const f64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null() || input.is_null() || out.is_null() {
        return;
    }
    let ind = &mut *handle;
    let inputs = slice::from_raw_parts(input, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (slot, &value) in outputs.iter_mut().zip(inputs) {
        *slot = ind.update(value).unwrap_or(f64::NAN);
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_win_rate_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_win_rate_warmup_period(handle: *mut WinRate) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_win_rate_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_win_rate_is_ready(handle: *mut WinRate) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_win_rate_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_win_rate_name(handle: *mut WinRate) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_win_rate_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_win_rate_reset(handle: *mut WinRate) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_win_rate_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_win_rate_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_win_rate_free(handle: *mut WinRate) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `Wma` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_wma_free`.
#[no_mangle]
pub extern "C" fn wickra_wma_new(period: usize) -> *mut Wma {
    match Wma::new(period) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one value; returns the output, or `NaN` during warmup / on a `NULL` handle.
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_wma_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_wma_update(handle: *mut Wma, value: f64) -> f64 {
    match handle.as_mut() {
        Some(ind) => ind.update(value).unwrap_or(f64::NAN),
        None => f64::NAN,
    }
}

/// Run over `input[0..n]`, writing one output per input into `out[0..n]` (`NaN` at warmup).
///
/// # Safety
/// `handle` valid (from `wickra_wma_new`, not freed); `input`/`out` cover `n` `double`s.
#[no_mangle]
pub unsafe extern "C" fn wickra_wma_batch(
    handle: *mut Wma,
    input: *const f64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null() || input.is_null() || out.is_null() {
        return;
    }
    let ind = &mut *handle;
    let inputs = slice::from_raw_parts(input, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (slot, &value) in outputs.iter_mut().zip(inputs) {
        *slot = ind.update(value).unwrap_or(f64::NAN);
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_wma_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_wma_warmup_period(handle: *mut Wma) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_wma_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_wma_is_ready(handle: *mut Wma) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_wma_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_wma_name(handle: *mut Wma) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_wma_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_wma_reset(handle: *mut Wma) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_wma_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_wma_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_wma_free(handle: *mut Wma) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `ZScore` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_z_score_free`.
#[no_mangle]
pub extern "C" fn wickra_z_score_new(period: usize) -> *mut ZScore {
    match ZScore::new(period) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one value; returns the output, or `NaN` during warmup / on a `NULL` handle.
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_z_score_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_z_score_update(handle: *mut ZScore, value: f64) -> f64 {
    match handle.as_mut() {
        Some(ind) => ind.update(value).unwrap_or(f64::NAN),
        None => f64::NAN,
    }
}

/// Run over `input[0..n]`, writing one output per input into `out[0..n]` (`NaN` at warmup).
///
/// # Safety
/// `handle` valid (from `wickra_z_score_new`, not freed); `input`/`out` cover `n` `double`s.
#[no_mangle]
pub unsafe extern "C" fn wickra_z_score_batch(
    handle: *mut ZScore,
    input: *const f64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null() || input.is_null() || out.is_null() {
        return;
    }
    let ind = &mut *handle;
    let inputs = slice::from_raw_parts(input, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (slot, &value) in outputs.iter_mut().zip(inputs) {
        *slot = ind.update(value).unwrap_or(f64::NAN);
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_z_score_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_z_score_warmup_period(handle: *mut ZScore) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_z_score_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_z_score_is_ready(handle: *mut ZScore) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_z_score_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_z_score_name(handle: *mut ZScore) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_z_score_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_z_score_reset(handle: *mut ZScore) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_z_score_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_z_score_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_z_score_free(handle: *mut ZScore) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `Zlema` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_zlema_free`.
#[no_mangle]
pub extern "C" fn wickra_zlema_new(period: usize) -> *mut Zlema {
    match Zlema::new(period) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one value; returns the output, or `NaN` during warmup / on a `NULL` handle.
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_zlema_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_zlema_update(handle: *mut Zlema, value: f64) -> f64 {
    match handle.as_mut() {
        Some(ind) => ind.update(value).unwrap_or(f64::NAN),
        None => f64::NAN,
    }
}

/// Run over `input[0..n]`, writing one output per input into `out[0..n]` (`NaN` at warmup).
///
/// # Safety
/// `handle` valid (from `wickra_zlema_new`, not freed); `input`/`out` cover `n` `double`s.
#[no_mangle]
pub unsafe extern "C" fn wickra_zlema_batch(
    handle: *mut Zlema,
    input: *const f64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null() || input.is_null() || out.is_null() {
        return;
    }
    let ind = &mut *handle;
    let inputs = slice::from_raw_parts(input, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (slot, &value) in outputs.iter_mut().zip(inputs) {
        *slot = ind.update(value).unwrap_or(f64::NAN);
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_zlema_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_zlema_warmup_period(handle: *mut Zlema) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_zlema_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_zlema_is_ready(handle: *mut Zlema) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_zlema_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_zlema_name(handle: *mut Zlema) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_zlema_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_zlema_reset(handle: *mut Zlema) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_zlema_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_zlema_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_zlema_free(handle: *mut Zlema) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

// ===== Pairwise indicators ((f64, f64) -> f64) =====

/// Create a `Alpha` pairwise indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_alpha_free`.
#[no_mangle]
pub extern "C" fn wickra_alpha_new(period: usize, risk_free: f64) -> *mut Alpha {
    match Alpha::new(period, risk_free) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one `(x, y)` pair; returns the output, or `NaN` during warmup / on a `NULL` handle.
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_alpha_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_alpha_update(handle: *mut Alpha, x: f64, y: f64) -> f64 {
    match handle.as_mut() {
        Some(ind) => ind.update((x, y)).unwrap_or(f64::NAN),
        None => f64::NAN,
    }
}

/// Run over the paired series `x[0..n]` / `y[0..n]` into `out[0..n]` (`NaN` at warmup).
///
/// # Safety
/// `handle` valid (from `wickra_alpha_new`, not freed); `x`/`y`/`out` cover `n` `double`s.
#[no_mangle]
pub unsafe extern "C" fn wickra_alpha_batch(
    handle: *mut Alpha,
    x: *const f64,
    y: *const f64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null() || x.is_null() || y.is_null() || out.is_null() {
        return;
    }
    let ind = &mut *handle;
    let xs = slice::from_raw_parts(x, n);
    let ys = slice::from_raw_parts(y, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for ((slot, &xv), &yv) in outputs.iter_mut().zip(xs).zip(ys) {
        *slot = ind.update((xv, yv)).unwrap_or(f64::NAN);
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_alpha_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_alpha_warmup_period(handle: *mut Alpha) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_alpha_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_alpha_is_ready(handle: *mut Alpha) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_alpha_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_alpha_name(handle: *mut Alpha) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_alpha_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_alpha_reset(handle: *mut Alpha) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_alpha_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_alpha_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_alpha_free(handle: *mut Alpha) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `Beta` pairwise indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_beta_free`.
#[no_mangle]
pub extern "C" fn wickra_beta_new(period: usize) -> *mut Beta {
    match Beta::new(period) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one `(x, y)` pair; returns the output, or `NaN` during warmup / on a `NULL` handle.
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_beta_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_beta_update(handle: *mut Beta, x: f64, y: f64) -> f64 {
    match handle.as_mut() {
        Some(ind) => ind.update((x, y)).unwrap_or(f64::NAN),
        None => f64::NAN,
    }
}

/// Run over the paired series `x[0..n]` / `y[0..n]` into `out[0..n]` (`NaN` at warmup).
///
/// # Safety
/// `handle` valid (from `wickra_beta_new`, not freed); `x`/`y`/`out` cover `n` `double`s.
#[no_mangle]
pub unsafe extern "C" fn wickra_beta_batch(
    handle: *mut Beta,
    x: *const f64,
    y: *const f64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null() || x.is_null() || y.is_null() || out.is_null() {
        return;
    }
    let ind = &mut *handle;
    let xs = slice::from_raw_parts(x, n);
    let ys = slice::from_raw_parts(y, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for ((slot, &xv), &yv) in outputs.iter_mut().zip(xs).zip(ys) {
        *slot = ind.update((xv, yv)).unwrap_or(f64::NAN);
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_beta_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_beta_warmup_period(handle: *mut Beta) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_beta_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_beta_is_ready(handle: *mut Beta) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_beta_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_beta_name(handle: *mut Beta) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_beta_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_beta_reset(handle: *mut Beta) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_beta_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_beta_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_beta_free(handle: *mut Beta) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `BetaNeutralSpread` pairwise indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_beta_neutral_spread_free`.
#[no_mangle]
pub extern "C" fn wickra_beta_neutral_spread_new(period: usize) -> *mut BetaNeutralSpread {
    match BetaNeutralSpread::new(period) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one `(x, y)` pair; returns the output, or `NaN` during warmup / on a `NULL` handle.
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_beta_neutral_spread_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_beta_neutral_spread_update(
    handle: *mut BetaNeutralSpread,
    x: f64,
    y: f64,
) -> f64 {
    match handle.as_mut() {
        Some(ind) => ind.update((x, y)).unwrap_or(f64::NAN),
        None => f64::NAN,
    }
}

/// Run over the paired series `x[0..n]` / `y[0..n]` into `out[0..n]` (`NaN` at warmup).
///
/// # Safety
/// `handle` valid (from `wickra_beta_neutral_spread_new`, not freed); `x`/`y`/`out` cover `n` `double`s.
#[no_mangle]
pub unsafe extern "C" fn wickra_beta_neutral_spread_batch(
    handle: *mut BetaNeutralSpread,
    x: *const f64,
    y: *const f64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null() || x.is_null() || y.is_null() || out.is_null() {
        return;
    }
    let ind = &mut *handle;
    let xs = slice::from_raw_parts(x, n);
    let ys = slice::from_raw_parts(y, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for ((slot, &xv), &yv) in outputs.iter_mut().zip(xs).zip(ys) {
        *slot = ind.update((xv, yv)).unwrap_or(f64::NAN);
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_beta_neutral_spread_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_beta_neutral_spread_warmup_period(
    handle: *mut BetaNeutralSpread,
) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_beta_neutral_spread_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_beta_neutral_spread_is_ready(
    handle: *mut BetaNeutralSpread,
) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_beta_neutral_spread_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_beta_neutral_spread_name(
    handle: *mut BetaNeutralSpread,
) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_beta_neutral_spread_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_beta_neutral_spread_reset(handle: *mut BetaNeutralSpread) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_beta_neutral_spread_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_beta_neutral_spread_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_beta_neutral_spread_free(handle: *mut BetaNeutralSpread) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `DistanceSsd` pairwise indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_distance_ssd_free`.
#[no_mangle]
pub extern "C" fn wickra_distance_ssd_new(period: usize) -> *mut DistanceSsd {
    match DistanceSsd::new(period) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one `(x, y)` pair; returns the output, or `NaN` during warmup / on a `NULL` handle.
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_distance_ssd_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_distance_ssd_update(
    handle: *mut DistanceSsd,
    x: f64,
    y: f64,
) -> f64 {
    match handle.as_mut() {
        Some(ind) => ind.update((x, y)).unwrap_or(f64::NAN),
        None => f64::NAN,
    }
}

/// Run over the paired series `x[0..n]` / `y[0..n]` into `out[0..n]` (`NaN` at warmup).
///
/// # Safety
/// `handle` valid (from `wickra_distance_ssd_new`, not freed); `x`/`y`/`out` cover `n` `double`s.
#[no_mangle]
pub unsafe extern "C" fn wickra_distance_ssd_batch(
    handle: *mut DistanceSsd,
    x: *const f64,
    y: *const f64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null() || x.is_null() || y.is_null() || out.is_null() {
        return;
    }
    let ind = &mut *handle;
    let xs = slice::from_raw_parts(x, n);
    let ys = slice::from_raw_parts(y, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for ((slot, &xv), &yv) in outputs.iter_mut().zip(xs).zip(ys) {
        *slot = ind.update((xv, yv)).unwrap_or(f64::NAN);
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_distance_ssd_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_distance_ssd_warmup_period(handle: *mut DistanceSsd) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_distance_ssd_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_distance_ssd_is_ready(handle: *mut DistanceSsd) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_distance_ssd_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_distance_ssd_name(handle: *mut DistanceSsd) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_distance_ssd_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_distance_ssd_reset(handle: *mut DistanceSsd) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_distance_ssd_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_distance_ssd_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_distance_ssd_free(handle: *mut DistanceSsd) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `GrangerCausality` pairwise indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_granger_causality_free`.
#[no_mangle]
pub extern "C" fn wickra_granger_causality_new(period: usize, lag: usize) -> *mut GrangerCausality {
    match GrangerCausality::new(period, lag) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one `(x, y)` pair; returns the output, or `NaN` during warmup / on a `NULL` handle.
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_granger_causality_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_granger_causality_update(
    handle: *mut GrangerCausality,
    x: f64,
    y: f64,
) -> f64 {
    match handle.as_mut() {
        Some(ind) => ind.update((x, y)).unwrap_or(f64::NAN),
        None => f64::NAN,
    }
}

/// Run over the paired series `x[0..n]` / `y[0..n]` into `out[0..n]` (`NaN` at warmup).
///
/// # Safety
/// `handle` valid (from `wickra_granger_causality_new`, not freed); `x`/`y`/`out` cover `n` `double`s.
#[no_mangle]
pub unsafe extern "C" fn wickra_granger_causality_batch(
    handle: *mut GrangerCausality,
    x: *const f64,
    y: *const f64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null() || x.is_null() || y.is_null() || out.is_null() {
        return;
    }
    let ind = &mut *handle;
    let xs = slice::from_raw_parts(x, n);
    let ys = slice::from_raw_parts(y, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for ((slot, &xv), &yv) in outputs.iter_mut().zip(xs).zip(ys) {
        *slot = ind.update((xv, yv)).unwrap_or(f64::NAN);
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_granger_causality_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_granger_causality_warmup_period(
    handle: *mut GrangerCausality,
) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_granger_causality_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_granger_causality_is_ready(handle: *mut GrangerCausality) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_granger_causality_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_granger_causality_name(
    handle: *mut GrangerCausality,
) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_granger_causality_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_granger_causality_reset(handle: *mut GrangerCausality) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_granger_causality_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_granger_causality_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_granger_causality_free(handle: *mut GrangerCausality) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `HasbrouckInformationShare` pairwise indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_hasbrouck_information_share_free`.
#[no_mangle]
pub extern "C" fn wickra_hasbrouck_information_share_new(
    period: usize,
) -> *mut HasbrouckInformationShare {
    match HasbrouckInformationShare::new(period) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one `(x, y)` pair; returns the output, or `NaN` during warmup / on a `NULL` handle.
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_hasbrouck_information_share_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_hasbrouck_information_share_update(
    handle: *mut HasbrouckInformationShare,
    x: f64,
    y: f64,
) -> f64 {
    match handle.as_mut() {
        Some(ind) => ind.update((x, y)).unwrap_or(f64::NAN),
        None => f64::NAN,
    }
}

/// Run over the paired series `x[0..n]` / `y[0..n]` into `out[0..n]` (`NaN` at warmup).
///
/// # Safety
/// `handle` valid (from `wickra_hasbrouck_information_share_new`, not freed); `x`/`y`/`out` cover `n` `double`s.
#[no_mangle]
pub unsafe extern "C" fn wickra_hasbrouck_information_share_batch(
    handle: *mut HasbrouckInformationShare,
    x: *const f64,
    y: *const f64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null() || x.is_null() || y.is_null() || out.is_null() {
        return;
    }
    let ind = &mut *handle;
    let xs = slice::from_raw_parts(x, n);
    let ys = slice::from_raw_parts(y, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for ((slot, &xv), &yv) in outputs.iter_mut().zip(xs).zip(ys) {
        *slot = ind.update((xv, yv)).unwrap_or(f64::NAN);
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_hasbrouck_information_share_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_hasbrouck_information_share_warmup_period(
    handle: *mut HasbrouckInformationShare,
) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_hasbrouck_information_share_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_hasbrouck_information_share_is_ready(
    handle: *mut HasbrouckInformationShare,
) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_hasbrouck_information_share_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_hasbrouck_information_share_name(
    handle: *mut HasbrouckInformationShare,
) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_hasbrouck_information_share_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_hasbrouck_information_share_reset(
    handle: *mut HasbrouckInformationShare,
) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_hasbrouck_information_share_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_hasbrouck_information_share_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_hasbrouck_information_share_free(
    handle: *mut HasbrouckInformationShare,
) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `InformationRatio` pairwise indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_information_ratio_free`.
#[no_mangle]
pub extern "C" fn wickra_information_ratio_new(period: usize) -> *mut InformationRatio {
    match InformationRatio::new(period) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one `(x, y)` pair; returns the output, or `NaN` during warmup / on a `NULL` handle.
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_information_ratio_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_information_ratio_update(
    handle: *mut InformationRatio,
    x: f64,
    y: f64,
) -> f64 {
    match handle.as_mut() {
        Some(ind) => ind.update((x, y)).unwrap_or(f64::NAN),
        None => f64::NAN,
    }
}

/// Run over the paired series `x[0..n]` / `y[0..n]` into `out[0..n]` (`NaN` at warmup).
///
/// # Safety
/// `handle` valid (from `wickra_information_ratio_new`, not freed); `x`/`y`/`out` cover `n` `double`s.
#[no_mangle]
pub unsafe extern "C" fn wickra_information_ratio_batch(
    handle: *mut InformationRatio,
    x: *const f64,
    y: *const f64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null() || x.is_null() || y.is_null() || out.is_null() {
        return;
    }
    let ind = &mut *handle;
    let xs = slice::from_raw_parts(x, n);
    let ys = slice::from_raw_parts(y, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for ((slot, &xv), &yv) in outputs.iter_mut().zip(xs).zip(ys) {
        *slot = ind.update((xv, yv)).unwrap_or(f64::NAN);
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_information_ratio_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_information_ratio_warmup_period(
    handle: *mut InformationRatio,
) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_information_ratio_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_information_ratio_is_ready(handle: *mut InformationRatio) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_information_ratio_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_information_ratio_name(
    handle: *mut InformationRatio,
) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_information_ratio_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_information_ratio_reset(handle: *mut InformationRatio) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_information_ratio_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_information_ratio_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_information_ratio_free(handle: *mut InformationRatio) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `KendallTau` pairwise indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_kendall_tau_free`.
#[no_mangle]
pub extern "C" fn wickra_kendall_tau_new(period: usize) -> *mut KendallTau {
    match KendallTau::new(period) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one `(x, y)` pair; returns the output, or `NaN` during warmup / on a `NULL` handle.
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_kendall_tau_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_kendall_tau_update(handle: *mut KendallTau, x: f64, y: f64) -> f64 {
    match handle.as_mut() {
        Some(ind) => ind.update((x, y)).unwrap_or(f64::NAN),
        None => f64::NAN,
    }
}

/// Run over the paired series `x[0..n]` / `y[0..n]` into `out[0..n]` (`NaN` at warmup).
///
/// # Safety
/// `handle` valid (from `wickra_kendall_tau_new`, not freed); `x`/`y`/`out` cover `n` `double`s.
#[no_mangle]
pub unsafe extern "C" fn wickra_kendall_tau_batch(
    handle: *mut KendallTau,
    x: *const f64,
    y: *const f64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null() || x.is_null() || y.is_null() || out.is_null() {
        return;
    }
    let ind = &mut *handle;
    let xs = slice::from_raw_parts(x, n);
    let ys = slice::from_raw_parts(y, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for ((slot, &xv), &yv) in outputs.iter_mut().zip(xs).zip(ys) {
        *slot = ind.update((xv, yv)).unwrap_or(f64::NAN);
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_kendall_tau_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_kendall_tau_warmup_period(handle: *mut KendallTau) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_kendall_tau_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_kendall_tau_is_ready(handle: *mut KendallTau) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_kendall_tau_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_kendall_tau_name(handle: *mut KendallTau) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_kendall_tau_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_kendall_tau_reset(handle: *mut KendallTau) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_kendall_tau_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_kendall_tau_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_kendall_tau_free(handle: *mut KendallTau) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `OuHalfLife` pairwise indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_ou_half_life_free`.
#[no_mangle]
pub extern "C" fn wickra_ou_half_life_new(period: usize) -> *mut OuHalfLife {
    match OuHalfLife::new(period) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one `(x, y)` pair; returns the output, or `NaN` during warmup / on a `NULL` handle.
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_ou_half_life_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_ou_half_life_update(
    handle: *mut OuHalfLife,
    x: f64,
    y: f64,
) -> f64 {
    match handle.as_mut() {
        Some(ind) => ind.update((x, y)).unwrap_or(f64::NAN),
        None => f64::NAN,
    }
}

/// Run over the paired series `x[0..n]` / `y[0..n]` into `out[0..n]` (`NaN` at warmup).
///
/// # Safety
/// `handle` valid (from `wickra_ou_half_life_new`, not freed); `x`/`y`/`out` cover `n` `double`s.
#[no_mangle]
pub unsafe extern "C" fn wickra_ou_half_life_batch(
    handle: *mut OuHalfLife,
    x: *const f64,
    y: *const f64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null() || x.is_null() || y.is_null() || out.is_null() {
        return;
    }
    let ind = &mut *handle;
    let xs = slice::from_raw_parts(x, n);
    let ys = slice::from_raw_parts(y, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for ((slot, &xv), &yv) in outputs.iter_mut().zip(xs).zip(ys) {
        *slot = ind.update((xv, yv)).unwrap_or(f64::NAN);
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_ou_half_life_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_ou_half_life_warmup_period(handle: *mut OuHalfLife) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_ou_half_life_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_ou_half_life_is_ready(handle: *mut OuHalfLife) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_ou_half_life_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_ou_half_life_name(handle: *mut OuHalfLife) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_ou_half_life_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_ou_half_life_reset(handle: *mut OuHalfLife) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_ou_half_life_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_ou_half_life_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_ou_half_life_free(handle: *mut OuHalfLife) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `PairSpreadZScore` pairwise indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_pair_spread_z_score_free`.
#[no_mangle]
pub extern "C" fn wickra_pair_spread_z_score_new(
    beta_period: usize,
    z_period: usize,
) -> *mut PairSpreadZScore {
    match PairSpreadZScore::new(beta_period, z_period) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one `(x, y)` pair; returns the output, or `NaN` during warmup / on a `NULL` handle.
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_pair_spread_z_score_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_pair_spread_z_score_update(
    handle: *mut PairSpreadZScore,
    x: f64,
    y: f64,
) -> f64 {
    match handle.as_mut() {
        Some(ind) => ind.update((x, y)).unwrap_or(f64::NAN),
        None => f64::NAN,
    }
}

/// Run over the paired series `x[0..n]` / `y[0..n]` into `out[0..n]` (`NaN` at warmup).
///
/// # Safety
/// `handle` valid (from `wickra_pair_spread_z_score_new`, not freed); `x`/`y`/`out` cover `n` `double`s.
#[no_mangle]
pub unsafe extern "C" fn wickra_pair_spread_z_score_batch(
    handle: *mut PairSpreadZScore,
    x: *const f64,
    y: *const f64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null() || x.is_null() || y.is_null() || out.is_null() {
        return;
    }
    let ind = &mut *handle;
    let xs = slice::from_raw_parts(x, n);
    let ys = slice::from_raw_parts(y, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for ((slot, &xv), &yv) in outputs.iter_mut().zip(xs).zip(ys) {
        *slot = ind.update((xv, yv)).unwrap_or(f64::NAN);
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_pair_spread_z_score_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_pair_spread_z_score_warmup_period(
    handle: *mut PairSpreadZScore,
) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_pair_spread_z_score_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_pair_spread_z_score_is_ready(
    handle: *mut PairSpreadZScore,
) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_pair_spread_z_score_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_pair_spread_z_score_name(
    handle: *mut PairSpreadZScore,
) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_pair_spread_z_score_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_pair_spread_z_score_reset(handle: *mut PairSpreadZScore) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_pair_spread_z_score_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_pair_spread_z_score_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_pair_spread_z_score_free(handle: *mut PairSpreadZScore) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `PairwiseBeta` pairwise indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_pairwise_beta_free`.
#[no_mangle]
pub extern "C" fn wickra_pairwise_beta_new(period: usize) -> *mut PairwiseBeta {
    match PairwiseBeta::new(period) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one `(x, y)` pair; returns the output, or `NaN` during warmup / on a `NULL` handle.
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_pairwise_beta_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_pairwise_beta_update(
    handle: *mut PairwiseBeta,
    x: f64,
    y: f64,
) -> f64 {
    match handle.as_mut() {
        Some(ind) => ind.update((x, y)).unwrap_or(f64::NAN),
        None => f64::NAN,
    }
}

/// Run over the paired series `x[0..n]` / `y[0..n]` into `out[0..n]` (`NaN` at warmup).
///
/// # Safety
/// `handle` valid (from `wickra_pairwise_beta_new`, not freed); `x`/`y`/`out` cover `n` `double`s.
#[no_mangle]
pub unsafe extern "C" fn wickra_pairwise_beta_batch(
    handle: *mut PairwiseBeta,
    x: *const f64,
    y: *const f64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null() || x.is_null() || y.is_null() || out.is_null() {
        return;
    }
    let ind = &mut *handle;
    let xs = slice::from_raw_parts(x, n);
    let ys = slice::from_raw_parts(y, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for ((slot, &xv), &yv) in outputs.iter_mut().zip(xs).zip(ys) {
        *slot = ind.update((xv, yv)).unwrap_or(f64::NAN);
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_pairwise_beta_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_pairwise_beta_warmup_period(handle: *mut PairwiseBeta) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_pairwise_beta_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_pairwise_beta_is_ready(handle: *mut PairwiseBeta) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_pairwise_beta_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_pairwise_beta_name(handle: *mut PairwiseBeta) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_pairwise_beta_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_pairwise_beta_reset(handle: *mut PairwiseBeta) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_pairwise_beta_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_pairwise_beta_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_pairwise_beta_free(handle: *mut PairwiseBeta) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `PearsonCorrelation` pairwise indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_pearson_correlation_free`.
#[no_mangle]
pub extern "C" fn wickra_pearson_correlation_new(period: usize) -> *mut PearsonCorrelation {
    match PearsonCorrelation::new(period) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one `(x, y)` pair; returns the output, or `NaN` during warmup / on a `NULL` handle.
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_pearson_correlation_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_pearson_correlation_update(
    handle: *mut PearsonCorrelation,
    x: f64,
    y: f64,
) -> f64 {
    match handle.as_mut() {
        Some(ind) => ind.update((x, y)).unwrap_or(f64::NAN),
        None => f64::NAN,
    }
}

/// Run over the paired series `x[0..n]` / `y[0..n]` into `out[0..n]` (`NaN` at warmup).
///
/// # Safety
/// `handle` valid (from `wickra_pearson_correlation_new`, not freed); `x`/`y`/`out` cover `n` `double`s.
#[no_mangle]
pub unsafe extern "C" fn wickra_pearson_correlation_batch(
    handle: *mut PearsonCorrelation,
    x: *const f64,
    y: *const f64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null() || x.is_null() || y.is_null() || out.is_null() {
        return;
    }
    let ind = &mut *handle;
    let xs = slice::from_raw_parts(x, n);
    let ys = slice::from_raw_parts(y, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for ((slot, &xv), &yv) in outputs.iter_mut().zip(xs).zip(ys) {
        *slot = ind.update((xv, yv)).unwrap_or(f64::NAN);
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_pearson_correlation_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_pearson_correlation_warmup_period(
    handle: *mut PearsonCorrelation,
) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_pearson_correlation_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_pearson_correlation_is_ready(
    handle: *mut PearsonCorrelation,
) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_pearson_correlation_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_pearson_correlation_name(
    handle: *mut PearsonCorrelation,
) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_pearson_correlation_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_pearson_correlation_reset(handle: *mut PearsonCorrelation) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_pearson_correlation_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_pearson_correlation_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_pearson_correlation_free(handle: *mut PearsonCorrelation) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `RollingCorrelation` pairwise indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_rolling_correlation_free`.
#[no_mangle]
pub extern "C" fn wickra_rolling_correlation_new(period: usize) -> *mut RollingCorrelation {
    match RollingCorrelation::new(period) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one `(x, y)` pair; returns the output, or `NaN` during warmup / on a `NULL` handle.
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_rolling_correlation_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_rolling_correlation_update(
    handle: *mut RollingCorrelation,
    x: f64,
    y: f64,
) -> f64 {
    match handle.as_mut() {
        Some(ind) => ind.update((x, y)).unwrap_or(f64::NAN),
        None => f64::NAN,
    }
}

/// Run over the paired series `x[0..n]` / `y[0..n]` into `out[0..n]` (`NaN` at warmup).
///
/// # Safety
/// `handle` valid (from `wickra_rolling_correlation_new`, not freed); `x`/`y`/`out` cover `n` `double`s.
#[no_mangle]
pub unsafe extern "C" fn wickra_rolling_correlation_batch(
    handle: *mut RollingCorrelation,
    x: *const f64,
    y: *const f64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null() || x.is_null() || y.is_null() || out.is_null() {
        return;
    }
    let ind = &mut *handle;
    let xs = slice::from_raw_parts(x, n);
    let ys = slice::from_raw_parts(y, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for ((slot, &xv), &yv) in outputs.iter_mut().zip(xs).zip(ys) {
        *slot = ind.update((xv, yv)).unwrap_or(f64::NAN);
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_rolling_correlation_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_rolling_correlation_warmup_period(
    handle: *mut RollingCorrelation,
) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_rolling_correlation_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_rolling_correlation_is_ready(
    handle: *mut RollingCorrelation,
) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_rolling_correlation_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_rolling_correlation_name(
    handle: *mut RollingCorrelation,
) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_rolling_correlation_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_rolling_correlation_reset(handle: *mut RollingCorrelation) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_rolling_correlation_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_rolling_correlation_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_rolling_correlation_free(handle: *mut RollingCorrelation) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `RollingCovariance` pairwise indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_rolling_covariance_free`.
#[no_mangle]
pub extern "C" fn wickra_rolling_covariance_new(period: usize) -> *mut RollingCovariance {
    match RollingCovariance::new(period) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one `(x, y)` pair; returns the output, or `NaN` during warmup / on a `NULL` handle.
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_rolling_covariance_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_rolling_covariance_update(
    handle: *mut RollingCovariance,
    x: f64,
    y: f64,
) -> f64 {
    match handle.as_mut() {
        Some(ind) => ind.update((x, y)).unwrap_or(f64::NAN),
        None => f64::NAN,
    }
}

/// Run over the paired series `x[0..n]` / `y[0..n]` into `out[0..n]` (`NaN` at warmup).
///
/// # Safety
/// `handle` valid (from `wickra_rolling_covariance_new`, not freed); `x`/`y`/`out` cover `n` `double`s.
#[no_mangle]
pub unsafe extern "C" fn wickra_rolling_covariance_batch(
    handle: *mut RollingCovariance,
    x: *const f64,
    y: *const f64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null() || x.is_null() || y.is_null() || out.is_null() {
        return;
    }
    let ind = &mut *handle;
    let xs = slice::from_raw_parts(x, n);
    let ys = slice::from_raw_parts(y, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for ((slot, &xv), &yv) in outputs.iter_mut().zip(xs).zip(ys) {
        *slot = ind.update((xv, yv)).unwrap_or(f64::NAN);
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_rolling_covariance_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_rolling_covariance_warmup_period(
    handle: *mut RollingCovariance,
) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_rolling_covariance_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_rolling_covariance_is_ready(
    handle: *mut RollingCovariance,
) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_rolling_covariance_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_rolling_covariance_name(
    handle: *mut RollingCovariance,
) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_rolling_covariance_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_rolling_covariance_reset(handle: *mut RollingCovariance) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_rolling_covariance_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_rolling_covariance_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_rolling_covariance_free(handle: *mut RollingCovariance) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `SpearmanCorrelation` pairwise indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_spearman_correlation_free`.
#[no_mangle]
pub extern "C" fn wickra_spearman_correlation_new(period: usize) -> *mut SpearmanCorrelation {
    match SpearmanCorrelation::new(period) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one `(x, y)` pair; returns the output, or `NaN` during warmup / on a `NULL` handle.
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_spearman_correlation_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_spearman_correlation_update(
    handle: *mut SpearmanCorrelation,
    x: f64,
    y: f64,
) -> f64 {
    match handle.as_mut() {
        Some(ind) => ind.update((x, y)).unwrap_or(f64::NAN),
        None => f64::NAN,
    }
}

/// Run over the paired series `x[0..n]` / `y[0..n]` into `out[0..n]` (`NaN` at warmup).
///
/// # Safety
/// `handle` valid (from `wickra_spearman_correlation_new`, not freed); `x`/`y`/`out` cover `n` `double`s.
#[no_mangle]
pub unsafe extern "C" fn wickra_spearman_correlation_batch(
    handle: *mut SpearmanCorrelation,
    x: *const f64,
    y: *const f64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null() || x.is_null() || y.is_null() || out.is_null() {
        return;
    }
    let ind = &mut *handle;
    let xs = slice::from_raw_parts(x, n);
    let ys = slice::from_raw_parts(y, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for ((slot, &xv), &yv) in outputs.iter_mut().zip(xs).zip(ys) {
        *slot = ind.update((xv, yv)).unwrap_or(f64::NAN);
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_spearman_correlation_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_spearman_correlation_warmup_period(
    handle: *mut SpearmanCorrelation,
) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_spearman_correlation_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_spearman_correlation_is_ready(
    handle: *mut SpearmanCorrelation,
) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_spearman_correlation_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_spearman_correlation_name(
    handle: *mut SpearmanCorrelation,
) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_spearman_correlation_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_spearman_correlation_reset(handle: *mut SpearmanCorrelation) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_spearman_correlation_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_spearman_correlation_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_spearman_correlation_free(handle: *mut SpearmanCorrelation) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `SpreadAr1Coefficient` pairwise indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_spread_ar1_coefficient_free`.
#[no_mangle]
pub extern "C" fn wickra_spread_ar1_coefficient_new(period: usize) -> *mut SpreadAr1Coefficient {
    match SpreadAr1Coefficient::new(period) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one `(x, y)` pair; returns the output, or `NaN` during warmup / on a `NULL` handle.
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_spread_ar1_coefficient_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_spread_ar1_coefficient_update(
    handle: *mut SpreadAr1Coefficient,
    x: f64,
    y: f64,
) -> f64 {
    match handle.as_mut() {
        Some(ind) => ind.update((x, y)).unwrap_or(f64::NAN),
        None => f64::NAN,
    }
}

/// Run over the paired series `x[0..n]` / `y[0..n]` into `out[0..n]` (`NaN` at warmup).
///
/// # Safety
/// `handle` valid (from `wickra_spread_ar1_coefficient_new`, not freed); `x`/`y`/`out` cover `n` `double`s.
#[no_mangle]
pub unsafe extern "C" fn wickra_spread_ar1_coefficient_batch(
    handle: *mut SpreadAr1Coefficient,
    x: *const f64,
    y: *const f64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null() || x.is_null() || y.is_null() || out.is_null() {
        return;
    }
    let ind = &mut *handle;
    let xs = slice::from_raw_parts(x, n);
    let ys = slice::from_raw_parts(y, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for ((slot, &xv), &yv) in outputs.iter_mut().zip(xs).zip(ys) {
        *slot = ind.update((xv, yv)).unwrap_or(f64::NAN);
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_spread_ar1_coefficient_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_spread_ar1_coefficient_warmup_period(
    handle: *mut SpreadAr1Coefficient,
) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_spread_ar1_coefficient_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_spread_ar1_coefficient_is_ready(
    handle: *mut SpreadAr1Coefficient,
) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_spread_ar1_coefficient_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_spread_ar1_coefficient_name(
    handle: *mut SpreadAr1Coefficient,
) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_spread_ar1_coefficient_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_spread_ar1_coefficient_reset(handle: *mut SpreadAr1Coefficient) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_spread_ar1_coefficient_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_spread_ar1_coefficient_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_spread_ar1_coefficient_free(handle: *mut SpreadAr1Coefficient) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `SpreadHurst` pairwise indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_spread_hurst_free`.
#[no_mangle]
pub extern "C" fn wickra_spread_hurst_new(period: usize) -> *mut SpreadHurst {
    match SpreadHurst::new(period) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one `(x, y)` pair; returns the output, or `NaN` during warmup / on a `NULL` handle.
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_spread_hurst_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_spread_hurst_update(
    handle: *mut SpreadHurst,
    x: f64,
    y: f64,
) -> f64 {
    match handle.as_mut() {
        Some(ind) => ind.update((x, y)).unwrap_or(f64::NAN),
        None => f64::NAN,
    }
}

/// Run over the paired series `x[0..n]` / `y[0..n]` into `out[0..n]` (`NaN` at warmup).
///
/// # Safety
/// `handle` valid (from `wickra_spread_hurst_new`, not freed); `x`/`y`/`out` cover `n` `double`s.
#[no_mangle]
pub unsafe extern "C" fn wickra_spread_hurst_batch(
    handle: *mut SpreadHurst,
    x: *const f64,
    y: *const f64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null() || x.is_null() || y.is_null() || out.is_null() {
        return;
    }
    let ind = &mut *handle;
    let xs = slice::from_raw_parts(x, n);
    let ys = slice::from_raw_parts(y, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for ((slot, &xv), &yv) in outputs.iter_mut().zip(xs).zip(ys) {
        *slot = ind.update((xv, yv)).unwrap_or(f64::NAN);
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_spread_hurst_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_spread_hurst_warmup_period(handle: *mut SpreadHurst) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_spread_hurst_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_spread_hurst_is_ready(handle: *mut SpreadHurst) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_spread_hurst_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_spread_hurst_name(handle: *mut SpreadHurst) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_spread_hurst_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_spread_hurst_reset(handle: *mut SpreadHurst) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_spread_hurst_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_spread_hurst_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_spread_hurst_free(handle: *mut SpreadHurst) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `TreynorRatio` pairwise indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_treynor_ratio_free`.
#[no_mangle]
pub extern "C" fn wickra_treynor_ratio_new(period: usize, risk_free: f64) -> *mut TreynorRatio {
    match TreynorRatio::new(period, risk_free) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one `(x, y)` pair; returns the output, or `NaN` during warmup / on a `NULL` handle.
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_treynor_ratio_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_treynor_ratio_update(
    handle: *mut TreynorRatio,
    x: f64,
    y: f64,
) -> f64 {
    match handle.as_mut() {
        Some(ind) => ind.update((x, y)).unwrap_or(f64::NAN),
        None => f64::NAN,
    }
}

/// Run over the paired series `x[0..n]` / `y[0..n]` into `out[0..n]` (`NaN` at warmup).
///
/// # Safety
/// `handle` valid (from `wickra_treynor_ratio_new`, not freed); `x`/`y`/`out` cover `n` `double`s.
#[no_mangle]
pub unsafe extern "C" fn wickra_treynor_ratio_batch(
    handle: *mut TreynorRatio,
    x: *const f64,
    y: *const f64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null() || x.is_null() || y.is_null() || out.is_null() {
        return;
    }
    let ind = &mut *handle;
    let xs = slice::from_raw_parts(x, n);
    let ys = slice::from_raw_parts(y, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for ((slot, &xv), &yv) in outputs.iter_mut().zip(xs).zip(ys) {
        *slot = ind.update((xv, yv)).unwrap_or(f64::NAN);
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_treynor_ratio_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_treynor_ratio_warmup_period(handle: *mut TreynorRatio) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_treynor_ratio_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_treynor_ratio_is_ready(handle: *mut TreynorRatio) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_treynor_ratio_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_treynor_ratio_name(handle: *mut TreynorRatio) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_treynor_ratio_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_treynor_ratio_reset(handle: *mut TreynorRatio) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_treynor_ratio_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_treynor_ratio_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_treynor_ratio_free(handle: *mut TreynorRatio) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `VarianceRatio` pairwise indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_variance_ratio_free`.
#[no_mangle]
pub extern "C" fn wickra_variance_ratio_new(period: usize, q: usize) -> *mut VarianceRatio {
    match VarianceRatio::new(period, q) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one `(x, y)` pair; returns the output, or `NaN` during warmup / on a `NULL` handle.
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_variance_ratio_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_variance_ratio_update(
    handle: *mut VarianceRatio,
    x: f64,
    y: f64,
) -> f64 {
    match handle.as_mut() {
        Some(ind) => ind.update((x, y)).unwrap_or(f64::NAN),
        None => f64::NAN,
    }
}

/// Run over the paired series `x[0..n]` / `y[0..n]` into `out[0..n]` (`NaN` at warmup).
///
/// # Safety
/// `handle` valid (from `wickra_variance_ratio_new`, not freed); `x`/`y`/`out` cover `n` `double`s.
#[no_mangle]
pub unsafe extern "C" fn wickra_variance_ratio_batch(
    handle: *mut VarianceRatio,
    x: *const f64,
    y: *const f64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null() || x.is_null() || y.is_null() || out.is_null() {
        return;
    }
    let ind = &mut *handle;
    let xs = slice::from_raw_parts(x, n);
    let ys = slice::from_raw_parts(y, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for ((slot, &xv), &yv) in outputs.iter_mut().zip(xs).zip(ys) {
        *slot = ind.update((xv, yv)).unwrap_or(f64::NAN);
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_variance_ratio_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_variance_ratio_warmup_period(handle: *mut VarianceRatio) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_variance_ratio_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_variance_ratio_is_ready(handle: *mut VarianceRatio) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_variance_ratio_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_variance_ratio_name(handle: *mut VarianceRatio) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_variance_ratio_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_variance_ratio_reset(handle: *mut VarianceRatio) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_variance_ratio_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_variance_ratio_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_variance_ratio_free(handle: *mut VarianceRatio) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

// ===== Candle indicators (OHLCV -> f64; includes candlestick patterns) =====

/// Create a `AbandonedBaby` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_abandoned_baby_free`.
#[no_mangle]
pub extern "C" fn wickra_abandoned_baby_new() -> *mut AbandonedBaby {
    Box::into_raw(Box::new(AbandonedBaby::new()))
}

/// Feed one OHLCV candle; returns the output, or `NaN` during warmup / on a `NULL`
/// handle / if the candle is invalid (e.g. `high < low`).
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_abandoned_baby_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_abandoned_baby_update(
    handle: *mut AbandonedBaby,
    open: f64,
    high: f64,
    low: f64,
    close: f64,
    volume: f64,
    timestamp: i64,
) -> f64 {
    let Some(ind) = handle.as_mut() else {
        return f64::NAN;
    };
    match Candle::new(open, high, low, close, volume, timestamp) {
        Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
        Err(_) => f64::NAN,
    }
}

/// Run over the OHLCV series (`open/high/low/close/volume/timestamp[0..n]`) into
/// `out[0..n]` (`NaN` at warmup or on an invalid candle).
///
/// # Safety
/// `handle` valid (from `wickra_abandoned_baby_new`, not freed); every input pointer and `out`
/// cover `n` elements.
#[no_mangle]
pub unsafe extern "C" fn wickra_abandoned_baby_batch(
    handle: *mut AbandonedBaby,
    open: *const f64,
    high: *const f64,
    low: *const f64,
    close: *const f64,
    volume: *const f64,
    timestamp: *const i64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null()
        || open.is_null()
        || high.is_null()
        || low.is_null()
        || close.is_null()
        || volume.is_null()
        || timestamp.is_null()
        || out.is_null()
    {
        return;
    }
    let ind = &mut *handle;
    let opens = slice::from_raw_parts(open, n);
    let highs = slice::from_raw_parts(high, n);
    let lows = slice::from_raw_parts(low, n);
    let closes = slice::from_raw_parts(close, n);
    let volumes = slice::from_raw_parts(volume, n);
    let timestamps = slice::from_raw_parts(timestamp, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (idx, slot) in outputs.iter_mut().enumerate() {
        *slot = match Candle::new(
            opens[idx],
            highs[idx],
            lows[idx],
            closes[idx],
            volumes[idx],
            timestamps[idx],
        ) {
            Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
            Err(_) => f64::NAN,
        };
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_abandoned_baby_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_abandoned_baby_warmup_period(handle: *mut AbandonedBaby) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_abandoned_baby_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_abandoned_baby_is_ready(handle: *mut AbandonedBaby) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_abandoned_baby_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_abandoned_baby_name(handle: *mut AbandonedBaby) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_abandoned_baby_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_abandoned_baby_reset(handle: *mut AbandonedBaby) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_abandoned_baby_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_abandoned_baby_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_abandoned_baby_free(handle: *mut AbandonedBaby) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `Abcd` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_abcd_free`.
#[no_mangle]
pub extern "C" fn wickra_abcd_new() -> *mut Abcd {
    Box::into_raw(Box::new(Abcd::new()))
}

/// Feed one OHLCV candle; returns the output, or `NaN` during warmup / on a `NULL`
/// handle / if the candle is invalid (e.g. `high < low`).
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_abcd_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_abcd_update(
    handle: *mut Abcd,
    open: f64,
    high: f64,
    low: f64,
    close: f64,
    volume: f64,
    timestamp: i64,
) -> f64 {
    let Some(ind) = handle.as_mut() else {
        return f64::NAN;
    };
    match Candle::new(open, high, low, close, volume, timestamp) {
        Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
        Err(_) => f64::NAN,
    }
}

/// Run over the OHLCV series (`open/high/low/close/volume/timestamp[0..n]`) into
/// `out[0..n]` (`NaN` at warmup or on an invalid candle).
///
/// # Safety
/// `handle` valid (from `wickra_abcd_new`, not freed); every input pointer and `out`
/// cover `n` elements.
#[no_mangle]
pub unsafe extern "C" fn wickra_abcd_batch(
    handle: *mut Abcd,
    open: *const f64,
    high: *const f64,
    low: *const f64,
    close: *const f64,
    volume: *const f64,
    timestamp: *const i64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null()
        || open.is_null()
        || high.is_null()
        || low.is_null()
        || close.is_null()
        || volume.is_null()
        || timestamp.is_null()
        || out.is_null()
    {
        return;
    }
    let ind = &mut *handle;
    let opens = slice::from_raw_parts(open, n);
    let highs = slice::from_raw_parts(high, n);
    let lows = slice::from_raw_parts(low, n);
    let closes = slice::from_raw_parts(close, n);
    let volumes = slice::from_raw_parts(volume, n);
    let timestamps = slice::from_raw_parts(timestamp, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (idx, slot) in outputs.iter_mut().enumerate() {
        *slot = match Candle::new(
            opens[idx],
            highs[idx],
            lows[idx],
            closes[idx],
            volumes[idx],
            timestamps[idx],
        ) {
            Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
            Err(_) => f64::NAN,
        };
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_abcd_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_abcd_warmup_period(handle: *mut Abcd) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_abcd_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_abcd_is_ready(handle: *mut Abcd) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_abcd_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_abcd_name(handle: *mut Abcd) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_abcd_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_abcd_reset(handle: *mut Abcd) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_abcd_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_abcd_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_abcd_free(handle: *mut Abcd) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `AcceleratorOscillator` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_accelerator_oscillator_free`.
#[no_mangle]
pub extern "C" fn wickra_accelerator_oscillator_new(
    ao_fast: usize,
    ao_slow: usize,
    signal_period: usize,
) -> *mut AcceleratorOscillator {
    match AcceleratorOscillator::new(ao_fast, ao_slow, signal_period) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one OHLCV candle; returns the output, or `NaN` during warmup / on a `NULL`
/// handle / if the candle is invalid (e.g. `high < low`).
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_accelerator_oscillator_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_accelerator_oscillator_update(
    handle: *mut AcceleratorOscillator,
    open: f64,
    high: f64,
    low: f64,
    close: f64,
    volume: f64,
    timestamp: i64,
) -> f64 {
    let Some(ind) = handle.as_mut() else {
        return f64::NAN;
    };
    match Candle::new(open, high, low, close, volume, timestamp) {
        Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
        Err(_) => f64::NAN,
    }
}

/// Run over the OHLCV series (`open/high/low/close/volume/timestamp[0..n]`) into
/// `out[0..n]` (`NaN` at warmup or on an invalid candle).
///
/// # Safety
/// `handle` valid (from `wickra_accelerator_oscillator_new`, not freed); every input pointer and `out`
/// cover `n` elements.
#[no_mangle]
pub unsafe extern "C" fn wickra_accelerator_oscillator_batch(
    handle: *mut AcceleratorOscillator,
    open: *const f64,
    high: *const f64,
    low: *const f64,
    close: *const f64,
    volume: *const f64,
    timestamp: *const i64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null()
        || open.is_null()
        || high.is_null()
        || low.is_null()
        || close.is_null()
        || volume.is_null()
        || timestamp.is_null()
        || out.is_null()
    {
        return;
    }
    let ind = &mut *handle;
    let opens = slice::from_raw_parts(open, n);
    let highs = slice::from_raw_parts(high, n);
    let lows = slice::from_raw_parts(low, n);
    let closes = slice::from_raw_parts(close, n);
    let volumes = slice::from_raw_parts(volume, n);
    let timestamps = slice::from_raw_parts(timestamp, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (idx, slot) in outputs.iter_mut().enumerate() {
        *slot = match Candle::new(
            opens[idx],
            highs[idx],
            lows[idx],
            closes[idx],
            volumes[idx],
            timestamps[idx],
        ) {
            Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
            Err(_) => f64::NAN,
        };
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_accelerator_oscillator_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_accelerator_oscillator_warmup_period(
    handle: *mut AcceleratorOscillator,
) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_accelerator_oscillator_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_accelerator_oscillator_is_ready(
    handle: *mut AcceleratorOscillator,
) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_accelerator_oscillator_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_accelerator_oscillator_name(
    handle: *mut AcceleratorOscillator,
) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_accelerator_oscillator_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_accelerator_oscillator_reset(handle: *mut AcceleratorOscillator) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_accelerator_oscillator_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_accelerator_oscillator_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_accelerator_oscillator_free(handle: *mut AcceleratorOscillator) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `AdOscillator` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_ad_oscillator_free`.
#[no_mangle]
pub extern "C" fn wickra_ad_oscillator_new() -> *mut AdOscillator {
    Box::into_raw(Box::new(AdOscillator::new()))
}

/// Feed one OHLCV candle; returns the output, or `NaN` during warmup / on a `NULL`
/// handle / if the candle is invalid (e.g. `high < low`).
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_ad_oscillator_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_ad_oscillator_update(
    handle: *mut AdOscillator,
    open: f64,
    high: f64,
    low: f64,
    close: f64,
    volume: f64,
    timestamp: i64,
) -> f64 {
    let Some(ind) = handle.as_mut() else {
        return f64::NAN;
    };
    match Candle::new(open, high, low, close, volume, timestamp) {
        Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
        Err(_) => f64::NAN,
    }
}

/// Run over the OHLCV series (`open/high/low/close/volume/timestamp[0..n]`) into
/// `out[0..n]` (`NaN` at warmup or on an invalid candle).
///
/// # Safety
/// `handle` valid (from `wickra_ad_oscillator_new`, not freed); every input pointer and `out`
/// cover `n` elements.
#[no_mangle]
pub unsafe extern "C" fn wickra_ad_oscillator_batch(
    handle: *mut AdOscillator,
    open: *const f64,
    high: *const f64,
    low: *const f64,
    close: *const f64,
    volume: *const f64,
    timestamp: *const i64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null()
        || open.is_null()
        || high.is_null()
        || low.is_null()
        || close.is_null()
        || volume.is_null()
        || timestamp.is_null()
        || out.is_null()
    {
        return;
    }
    let ind = &mut *handle;
    let opens = slice::from_raw_parts(open, n);
    let highs = slice::from_raw_parts(high, n);
    let lows = slice::from_raw_parts(low, n);
    let closes = slice::from_raw_parts(close, n);
    let volumes = slice::from_raw_parts(volume, n);
    let timestamps = slice::from_raw_parts(timestamp, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (idx, slot) in outputs.iter_mut().enumerate() {
        *slot = match Candle::new(
            opens[idx],
            highs[idx],
            lows[idx],
            closes[idx],
            volumes[idx],
            timestamps[idx],
        ) {
            Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
            Err(_) => f64::NAN,
        };
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_ad_oscillator_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_ad_oscillator_warmup_period(handle: *mut AdOscillator) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_ad_oscillator_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_ad_oscillator_is_ready(handle: *mut AdOscillator) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_ad_oscillator_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_ad_oscillator_name(handle: *mut AdOscillator) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_ad_oscillator_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_ad_oscillator_reset(handle: *mut AdOscillator) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_ad_oscillator_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_ad_oscillator_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_ad_oscillator_free(handle: *mut AdOscillator) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `AdaptiveCci` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_adaptive_cci_free`.
#[no_mangle]
pub extern "C" fn wickra_adaptive_cci_new(period: usize) -> *mut AdaptiveCci {
    match AdaptiveCci::new(period) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one OHLCV candle; returns the output, or `NaN` during warmup / on a `NULL`
/// handle / if the candle is invalid (e.g. `high < low`).
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_adaptive_cci_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_adaptive_cci_update(
    handle: *mut AdaptiveCci,
    open: f64,
    high: f64,
    low: f64,
    close: f64,
    volume: f64,
    timestamp: i64,
) -> f64 {
    let Some(ind) = handle.as_mut() else {
        return f64::NAN;
    };
    match Candle::new(open, high, low, close, volume, timestamp) {
        Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
        Err(_) => f64::NAN,
    }
}

/// Run over the OHLCV series (`open/high/low/close/volume/timestamp[0..n]`) into
/// `out[0..n]` (`NaN` at warmup or on an invalid candle).
///
/// # Safety
/// `handle` valid (from `wickra_adaptive_cci_new`, not freed); every input pointer and `out`
/// cover `n` elements.
#[no_mangle]
pub unsafe extern "C" fn wickra_adaptive_cci_batch(
    handle: *mut AdaptiveCci,
    open: *const f64,
    high: *const f64,
    low: *const f64,
    close: *const f64,
    volume: *const f64,
    timestamp: *const i64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null()
        || open.is_null()
        || high.is_null()
        || low.is_null()
        || close.is_null()
        || volume.is_null()
        || timestamp.is_null()
        || out.is_null()
    {
        return;
    }
    let ind = &mut *handle;
    let opens = slice::from_raw_parts(open, n);
    let highs = slice::from_raw_parts(high, n);
    let lows = slice::from_raw_parts(low, n);
    let closes = slice::from_raw_parts(close, n);
    let volumes = slice::from_raw_parts(volume, n);
    let timestamps = slice::from_raw_parts(timestamp, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (idx, slot) in outputs.iter_mut().enumerate() {
        *slot = match Candle::new(
            opens[idx],
            highs[idx],
            lows[idx],
            closes[idx],
            volumes[idx],
            timestamps[idx],
        ) {
            Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
            Err(_) => f64::NAN,
        };
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_adaptive_cci_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_adaptive_cci_warmup_period(handle: *mut AdaptiveCci) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_adaptive_cci_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_adaptive_cci_is_ready(handle: *mut AdaptiveCci) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_adaptive_cci_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_adaptive_cci_name(handle: *mut AdaptiveCci) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_adaptive_cci_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_adaptive_cci_reset(handle: *mut AdaptiveCci) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_adaptive_cci_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_adaptive_cci_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_adaptive_cci_free(handle: *mut AdaptiveCci) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `Adl` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_adl_free`.
#[no_mangle]
pub extern "C" fn wickra_adl_new() -> *mut Adl {
    Box::into_raw(Box::new(Adl::new()))
}

/// Feed one OHLCV candle; returns the output, or `NaN` during warmup / on a `NULL`
/// handle / if the candle is invalid (e.g. `high < low`).
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_adl_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_adl_update(
    handle: *mut Adl,
    open: f64,
    high: f64,
    low: f64,
    close: f64,
    volume: f64,
    timestamp: i64,
) -> f64 {
    let Some(ind) = handle.as_mut() else {
        return f64::NAN;
    };
    match Candle::new(open, high, low, close, volume, timestamp) {
        Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
        Err(_) => f64::NAN,
    }
}

/// Run over the OHLCV series (`open/high/low/close/volume/timestamp[0..n]`) into
/// `out[0..n]` (`NaN` at warmup or on an invalid candle).
///
/// # Safety
/// `handle` valid (from `wickra_adl_new`, not freed); every input pointer and `out`
/// cover `n` elements.
#[no_mangle]
pub unsafe extern "C" fn wickra_adl_batch(
    handle: *mut Adl,
    open: *const f64,
    high: *const f64,
    low: *const f64,
    close: *const f64,
    volume: *const f64,
    timestamp: *const i64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null()
        || open.is_null()
        || high.is_null()
        || low.is_null()
        || close.is_null()
        || volume.is_null()
        || timestamp.is_null()
        || out.is_null()
    {
        return;
    }
    let ind = &mut *handle;
    let opens = slice::from_raw_parts(open, n);
    let highs = slice::from_raw_parts(high, n);
    let lows = slice::from_raw_parts(low, n);
    let closes = slice::from_raw_parts(close, n);
    let volumes = slice::from_raw_parts(volume, n);
    let timestamps = slice::from_raw_parts(timestamp, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (idx, slot) in outputs.iter_mut().enumerate() {
        *slot = match Candle::new(
            opens[idx],
            highs[idx],
            lows[idx],
            closes[idx],
            volumes[idx],
            timestamps[idx],
        ) {
            Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
            Err(_) => f64::NAN,
        };
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_adl_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_adl_warmup_period(handle: *mut Adl) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_adl_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_adl_is_ready(handle: *mut Adl) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_adl_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_adl_name(handle: *mut Adl) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_adl_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_adl_reset(handle: *mut Adl) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_adl_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_adl_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_adl_free(handle: *mut Adl) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `AdvanceBlock` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_advance_block_free`.
#[no_mangle]
pub extern "C" fn wickra_advance_block_new() -> *mut AdvanceBlock {
    Box::into_raw(Box::new(AdvanceBlock::new()))
}

/// Feed one OHLCV candle; returns the output, or `NaN` during warmup / on a `NULL`
/// handle / if the candle is invalid (e.g. `high < low`).
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_advance_block_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_advance_block_update(
    handle: *mut AdvanceBlock,
    open: f64,
    high: f64,
    low: f64,
    close: f64,
    volume: f64,
    timestamp: i64,
) -> f64 {
    let Some(ind) = handle.as_mut() else {
        return f64::NAN;
    };
    match Candle::new(open, high, low, close, volume, timestamp) {
        Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
        Err(_) => f64::NAN,
    }
}

/// Run over the OHLCV series (`open/high/low/close/volume/timestamp[0..n]`) into
/// `out[0..n]` (`NaN` at warmup or on an invalid candle).
///
/// # Safety
/// `handle` valid (from `wickra_advance_block_new`, not freed); every input pointer and `out`
/// cover `n` elements.
#[no_mangle]
pub unsafe extern "C" fn wickra_advance_block_batch(
    handle: *mut AdvanceBlock,
    open: *const f64,
    high: *const f64,
    low: *const f64,
    close: *const f64,
    volume: *const f64,
    timestamp: *const i64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null()
        || open.is_null()
        || high.is_null()
        || low.is_null()
        || close.is_null()
        || volume.is_null()
        || timestamp.is_null()
        || out.is_null()
    {
        return;
    }
    let ind = &mut *handle;
    let opens = slice::from_raw_parts(open, n);
    let highs = slice::from_raw_parts(high, n);
    let lows = slice::from_raw_parts(low, n);
    let closes = slice::from_raw_parts(close, n);
    let volumes = slice::from_raw_parts(volume, n);
    let timestamps = slice::from_raw_parts(timestamp, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (idx, slot) in outputs.iter_mut().enumerate() {
        *slot = match Candle::new(
            opens[idx],
            highs[idx],
            lows[idx],
            closes[idx],
            volumes[idx],
            timestamps[idx],
        ) {
            Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
            Err(_) => f64::NAN,
        };
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_advance_block_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_advance_block_warmup_period(handle: *mut AdvanceBlock) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_advance_block_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_advance_block_is_ready(handle: *mut AdvanceBlock) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_advance_block_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_advance_block_name(handle: *mut AdvanceBlock) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_advance_block_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_advance_block_reset(handle: *mut AdvanceBlock) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_advance_block_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_advance_block_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_advance_block_free(handle: *mut AdvanceBlock) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `Adxr` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_adxr_free`.
#[no_mangle]
pub extern "C" fn wickra_adxr_new(period: usize) -> *mut Adxr {
    match Adxr::new(period) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one OHLCV candle; returns the output, or `NaN` during warmup / on a `NULL`
/// handle / if the candle is invalid (e.g. `high < low`).
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_adxr_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_adxr_update(
    handle: *mut Adxr,
    open: f64,
    high: f64,
    low: f64,
    close: f64,
    volume: f64,
    timestamp: i64,
) -> f64 {
    let Some(ind) = handle.as_mut() else {
        return f64::NAN;
    };
    match Candle::new(open, high, low, close, volume, timestamp) {
        Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
        Err(_) => f64::NAN,
    }
}

/// Run over the OHLCV series (`open/high/low/close/volume/timestamp[0..n]`) into
/// `out[0..n]` (`NaN` at warmup or on an invalid candle).
///
/// # Safety
/// `handle` valid (from `wickra_adxr_new`, not freed); every input pointer and `out`
/// cover `n` elements.
#[no_mangle]
pub unsafe extern "C" fn wickra_adxr_batch(
    handle: *mut Adxr,
    open: *const f64,
    high: *const f64,
    low: *const f64,
    close: *const f64,
    volume: *const f64,
    timestamp: *const i64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null()
        || open.is_null()
        || high.is_null()
        || low.is_null()
        || close.is_null()
        || volume.is_null()
        || timestamp.is_null()
        || out.is_null()
    {
        return;
    }
    let ind = &mut *handle;
    let opens = slice::from_raw_parts(open, n);
    let highs = slice::from_raw_parts(high, n);
    let lows = slice::from_raw_parts(low, n);
    let closes = slice::from_raw_parts(close, n);
    let volumes = slice::from_raw_parts(volume, n);
    let timestamps = slice::from_raw_parts(timestamp, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (idx, slot) in outputs.iter_mut().enumerate() {
        *slot = match Candle::new(
            opens[idx],
            highs[idx],
            lows[idx],
            closes[idx],
            volumes[idx],
            timestamps[idx],
        ) {
            Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
            Err(_) => f64::NAN,
        };
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_adxr_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_adxr_warmup_period(handle: *mut Adxr) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_adxr_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_adxr_is_ready(handle: *mut Adxr) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_adxr_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_adxr_name(handle: *mut Adxr) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_adxr_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_adxr_reset(handle: *mut Adxr) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_adxr_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_adxr_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_adxr_free(handle: *mut Adxr) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `AnchoredVwap` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_anchored_vwap_free`.
#[no_mangle]
pub extern "C" fn wickra_anchored_vwap_new() -> *mut AnchoredVwap {
    Box::into_raw(Box::new(AnchoredVwap::new()))
}

/// Feed one OHLCV candle; returns the output, or `NaN` during warmup / on a `NULL`
/// handle / if the candle is invalid (e.g. `high < low`).
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_anchored_vwap_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_anchored_vwap_update(
    handle: *mut AnchoredVwap,
    open: f64,
    high: f64,
    low: f64,
    close: f64,
    volume: f64,
    timestamp: i64,
) -> f64 {
    let Some(ind) = handle.as_mut() else {
        return f64::NAN;
    };
    match Candle::new(open, high, low, close, volume, timestamp) {
        Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
        Err(_) => f64::NAN,
    }
}

/// Run over the OHLCV series (`open/high/low/close/volume/timestamp[0..n]`) into
/// `out[0..n]` (`NaN` at warmup or on an invalid candle).
///
/// # Safety
/// `handle` valid (from `wickra_anchored_vwap_new`, not freed); every input pointer and `out`
/// cover `n` elements.
#[no_mangle]
pub unsafe extern "C" fn wickra_anchored_vwap_batch(
    handle: *mut AnchoredVwap,
    open: *const f64,
    high: *const f64,
    low: *const f64,
    close: *const f64,
    volume: *const f64,
    timestamp: *const i64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null()
        || open.is_null()
        || high.is_null()
        || low.is_null()
        || close.is_null()
        || volume.is_null()
        || timestamp.is_null()
        || out.is_null()
    {
        return;
    }
    let ind = &mut *handle;
    let opens = slice::from_raw_parts(open, n);
    let highs = slice::from_raw_parts(high, n);
    let lows = slice::from_raw_parts(low, n);
    let closes = slice::from_raw_parts(close, n);
    let volumes = slice::from_raw_parts(volume, n);
    let timestamps = slice::from_raw_parts(timestamp, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (idx, slot) in outputs.iter_mut().enumerate() {
        *slot = match Candle::new(
            opens[idx],
            highs[idx],
            lows[idx],
            closes[idx],
            volumes[idx],
            timestamps[idx],
        ) {
            Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
            Err(_) => f64::NAN,
        };
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_anchored_vwap_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_anchored_vwap_warmup_period(handle: *mut AnchoredVwap) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_anchored_vwap_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_anchored_vwap_is_ready(handle: *mut AnchoredVwap) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_anchored_vwap_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_anchored_vwap_name(handle: *mut AnchoredVwap) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_anchored_vwap_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_anchored_vwap_reset(handle: *mut AnchoredVwap) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_anchored_vwap_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_anchored_vwap_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_anchored_vwap_free(handle: *mut AnchoredVwap) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `AroonOscillator` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_aroon_oscillator_free`.
#[no_mangle]
pub extern "C" fn wickra_aroon_oscillator_new(period: usize) -> *mut AroonOscillator {
    match AroonOscillator::new(period) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one OHLCV candle; returns the output, or `NaN` during warmup / on a `NULL`
/// handle / if the candle is invalid (e.g. `high < low`).
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_aroon_oscillator_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_aroon_oscillator_update(
    handle: *mut AroonOscillator,
    open: f64,
    high: f64,
    low: f64,
    close: f64,
    volume: f64,
    timestamp: i64,
) -> f64 {
    let Some(ind) = handle.as_mut() else {
        return f64::NAN;
    };
    match Candle::new(open, high, low, close, volume, timestamp) {
        Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
        Err(_) => f64::NAN,
    }
}

/// Run over the OHLCV series (`open/high/low/close/volume/timestamp[0..n]`) into
/// `out[0..n]` (`NaN` at warmup or on an invalid candle).
///
/// # Safety
/// `handle` valid (from `wickra_aroon_oscillator_new`, not freed); every input pointer and `out`
/// cover `n` elements.
#[no_mangle]
pub unsafe extern "C" fn wickra_aroon_oscillator_batch(
    handle: *mut AroonOscillator,
    open: *const f64,
    high: *const f64,
    low: *const f64,
    close: *const f64,
    volume: *const f64,
    timestamp: *const i64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null()
        || open.is_null()
        || high.is_null()
        || low.is_null()
        || close.is_null()
        || volume.is_null()
        || timestamp.is_null()
        || out.is_null()
    {
        return;
    }
    let ind = &mut *handle;
    let opens = slice::from_raw_parts(open, n);
    let highs = slice::from_raw_parts(high, n);
    let lows = slice::from_raw_parts(low, n);
    let closes = slice::from_raw_parts(close, n);
    let volumes = slice::from_raw_parts(volume, n);
    let timestamps = slice::from_raw_parts(timestamp, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (idx, slot) in outputs.iter_mut().enumerate() {
        *slot = match Candle::new(
            opens[idx],
            highs[idx],
            lows[idx],
            closes[idx],
            volumes[idx],
            timestamps[idx],
        ) {
            Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
            Err(_) => f64::NAN,
        };
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_aroon_oscillator_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_aroon_oscillator_warmup_period(
    handle: *mut AroonOscillator,
) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_aroon_oscillator_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_aroon_oscillator_is_ready(handle: *mut AroonOscillator) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_aroon_oscillator_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_aroon_oscillator_name(
    handle: *mut AroonOscillator,
) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_aroon_oscillator_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_aroon_oscillator_reset(handle: *mut AroonOscillator) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_aroon_oscillator_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_aroon_oscillator_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_aroon_oscillator_free(handle: *mut AroonOscillator) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `Atr` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_atr_free`.
#[no_mangle]
pub extern "C" fn wickra_atr_new(period: usize) -> *mut Atr {
    match Atr::new(period) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one OHLCV candle; returns the output, or `NaN` during warmup / on a `NULL`
/// handle / if the candle is invalid (e.g. `high < low`).
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_atr_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_atr_update(
    handle: *mut Atr,
    open: f64,
    high: f64,
    low: f64,
    close: f64,
    volume: f64,
    timestamp: i64,
) -> f64 {
    let Some(ind) = handle.as_mut() else {
        return f64::NAN;
    };
    match Candle::new(open, high, low, close, volume, timestamp) {
        Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
        Err(_) => f64::NAN,
    }
}

/// Run over the OHLCV series (`open/high/low/close/volume/timestamp[0..n]`) into
/// `out[0..n]` (`NaN` at warmup or on an invalid candle).
///
/// # Safety
/// `handle` valid (from `wickra_atr_new`, not freed); every input pointer and `out`
/// cover `n` elements.
#[no_mangle]
pub unsafe extern "C" fn wickra_atr_batch(
    handle: *mut Atr,
    open: *const f64,
    high: *const f64,
    low: *const f64,
    close: *const f64,
    volume: *const f64,
    timestamp: *const i64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null()
        || open.is_null()
        || high.is_null()
        || low.is_null()
        || close.is_null()
        || volume.is_null()
        || timestamp.is_null()
        || out.is_null()
    {
        return;
    }
    let ind = &mut *handle;
    let opens = slice::from_raw_parts(open, n);
    let highs = slice::from_raw_parts(high, n);
    let lows = slice::from_raw_parts(low, n);
    let closes = slice::from_raw_parts(close, n);
    let volumes = slice::from_raw_parts(volume, n);
    let timestamps = slice::from_raw_parts(timestamp, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (idx, slot) in outputs.iter_mut().enumerate() {
        *slot = match Candle::new(
            opens[idx],
            highs[idx],
            lows[idx],
            closes[idx],
            volumes[idx],
            timestamps[idx],
        ) {
            Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
            Err(_) => f64::NAN,
        };
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_atr_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_atr_warmup_period(handle: *mut Atr) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_atr_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_atr_is_ready(handle: *mut Atr) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_atr_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_atr_name(handle: *mut Atr) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_atr_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_atr_reset(handle: *mut Atr) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_atr_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_atr_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_atr_free(handle: *mut Atr) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `AtrTrailingStop` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_atr_trailing_stop_free`.
#[no_mangle]
pub extern "C" fn wickra_atr_trailing_stop_new(
    atr_period: usize,
    multiplier: f64,
) -> *mut AtrTrailingStop {
    match AtrTrailingStop::new(atr_period, multiplier) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one OHLCV candle; returns the output, or `NaN` during warmup / on a `NULL`
/// handle / if the candle is invalid (e.g. `high < low`).
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_atr_trailing_stop_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_atr_trailing_stop_update(
    handle: *mut AtrTrailingStop,
    open: f64,
    high: f64,
    low: f64,
    close: f64,
    volume: f64,
    timestamp: i64,
) -> f64 {
    let Some(ind) = handle.as_mut() else {
        return f64::NAN;
    };
    match Candle::new(open, high, low, close, volume, timestamp) {
        Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
        Err(_) => f64::NAN,
    }
}

/// Run over the OHLCV series (`open/high/low/close/volume/timestamp[0..n]`) into
/// `out[0..n]` (`NaN` at warmup or on an invalid candle).
///
/// # Safety
/// `handle` valid (from `wickra_atr_trailing_stop_new`, not freed); every input pointer and `out`
/// cover `n` elements.
#[no_mangle]
pub unsafe extern "C" fn wickra_atr_trailing_stop_batch(
    handle: *mut AtrTrailingStop,
    open: *const f64,
    high: *const f64,
    low: *const f64,
    close: *const f64,
    volume: *const f64,
    timestamp: *const i64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null()
        || open.is_null()
        || high.is_null()
        || low.is_null()
        || close.is_null()
        || volume.is_null()
        || timestamp.is_null()
        || out.is_null()
    {
        return;
    }
    let ind = &mut *handle;
    let opens = slice::from_raw_parts(open, n);
    let highs = slice::from_raw_parts(high, n);
    let lows = slice::from_raw_parts(low, n);
    let closes = slice::from_raw_parts(close, n);
    let volumes = slice::from_raw_parts(volume, n);
    let timestamps = slice::from_raw_parts(timestamp, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (idx, slot) in outputs.iter_mut().enumerate() {
        *slot = match Candle::new(
            opens[idx],
            highs[idx],
            lows[idx],
            closes[idx],
            volumes[idx],
            timestamps[idx],
        ) {
            Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
            Err(_) => f64::NAN,
        };
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_atr_trailing_stop_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_atr_trailing_stop_warmup_period(
    handle: *mut AtrTrailingStop,
) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_atr_trailing_stop_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_atr_trailing_stop_is_ready(handle: *mut AtrTrailingStop) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_atr_trailing_stop_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_atr_trailing_stop_name(
    handle: *mut AtrTrailingStop,
) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_atr_trailing_stop_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_atr_trailing_stop_reset(handle: *mut AtrTrailingStop) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_atr_trailing_stop_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_atr_trailing_stop_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_atr_trailing_stop_free(handle: *mut AtrTrailingStop) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `AverageDailyRange` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_average_daily_range_free`.
#[no_mangle]
pub extern "C" fn wickra_average_daily_range_new(
    period: usize,
    utc_offset_minutes: i32,
) -> *mut AverageDailyRange {
    match AverageDailyRange::new(period, utc_offset_minutes) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one OHLCV candle; returns the output, or `NaN` during warmup / on a `NULL`
/// handle / if the candle is invalid (e.g. `high < low`).
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_average_daily_range_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_average_daily_range_update(
    handle: *mut AverageDailyRange,
    open: f64,
    high: f64,
    low: f64,
    close: f64,
    volume: f64,
    timestamp: i64,
) -> f64 {
    let Some(ind) = handle.as_mut() else {
        return f64::NAN;
    };
    match Candle::new(open, high, low, close, volume, timestamp) {
        Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
        Err(_) => f64::NAN,
    }
}

/// Run over the OHLCV series (`open/high/low/close/volume/timestamp[0..n]`) into
/// `out[0..n]` (`NaN` at warmup or on an invalid candle).
///
/// # Safety
/// `handle` valid (from `wickra_average_daily_range_new`, not freed); every input pointer and `out`
/// cover `n` elements.
#[no_mangle]
pub unsafe extern "C" fn wickra_average_daily_range_batch(
    handle: *mut AverageDailyRange,
    open: *const f64,
    high: *const f64,
    low: *const f64,
    close: *const f64,
    volume: *const f64,
    timestamp: *const i64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null()
        || open.is_null()
        || high.is_null()
        || low.is_null()
        || close.is_null()
        || volume.is_null()
        || timestamp.is_null()
        || out.is_null()
    {
        return;
    }
    let ind = &mut *handle;
    let opens = slice::from_raw_parts(open, n);
    let highs = slice::from_raw_parts(high, n);
    let lows = slice::from_raw_parts(low, n);
    let closes = slice::from_raw_parts(close, n);
    let volumes = slice::from_raw_parts(volume, n);
    let timestamps = slice::from_raw_parts(timestamp, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (idx, slot) in outputs.iter_mut().enumerate() {
        *slot = match Candle::new(
            opens[idx],
            highs[idx],
            lows[idx],
            closes[idx],
            volumes[idx],
            timestamps[idx],
        ) {
            Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
            Err(_) => f64::NAN,
        };
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_average_daily_range_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_average_daily_range_warmup_period(
    handle: *mut AverageDailyRange,
) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_average_daily_range_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_average_daily_range_is_ready(
    handle: *mut AverageDailyRange,
) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_average_daily_range_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_average_daily_range_name(
    handle: *mut AverageDailyRange,
) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_average_daily_range_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_average_daily_range_reset(handle: *mut AverageDailyRange) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_average_daily_range_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_average_daily_range_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_average_daily_range_free(handle: *mut AverageDailyRange) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `AvgPrice` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_avg_price_free`.
#[no_mangle]
pub extern "C" fn wickra_avg_price_new() -> *mut AvgPrice {
    Box::into_raw(Box::new(AvgPrice::new()))
}

/// Feed one OHLCV candle; returns the output, or `NaN` during warmup / on a `NULL`
/// handle / if the candle is invalid (e.g. `high < low`).
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_avg_price_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_avg_price_update(
    handle: *mut AvgPrice,
    open: f64,
    high: f64,
    low: f64,
    close: f64,
    volume: f64,
    timestamp: i64,
) -> f64 {
    let Some(ind) = handle.as_mut() else {
        return f64::NAN;
    };
    match Candle::new(open, high, low, close, volume, timestamp) {
        Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
        Err(_) => f64::NAN,
    }
}

/// Run over the OHLCV series (`open/high/low/close/volume/timestamp[0..n]`) into
/// `out[0..n]` (`NaN` at warmup or on an invalid candle).
///
/// # Safety
/// `handle` valid (from `wickra_avg_price_new`, not freed); every input pointer and `out`
/// cover `n` elements.
#[no_mangle]
pub unsafe extern "C" fn wickra_avg_price_batch(
    handle: *mut AvgPrice,
    open: *const f64,
    high: *const f64,
    low: *const f64,
    close: *const f64,
    volume: *const f64,
    timestamp: *const i64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null()
        || open.is_null()
        || high.is_null()
        || low.is_null()
        || close.is_null()
        || volume.is_null()
        || timestamp.is_null()
        || out.is_null()
    {
        return;
    }
    let ind = &mut *handle;
    let opens = slice::from_raw_parts(open, n);
    let highs = slice::from_raw_parts(high, n);
    let lows = slice::from_raw_parts(low, n);
    let closes = slice::from_raw_parts(close, n);
    let volumes = slice::from_raw_parts(volume, n);
    let timestamps = slice::from_raw_parts(timestamp, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (idx, slot) in outputs.iter_mut().enumerate() {
        *slot = match Candle::new(
            opens[idx],
            highs[idx],
            lows[idx],
            closes[idx],
            volumes[idx],
            timestamps[idx],
        ) {
            Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
            Err(_) => f64::NAN,
        };
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_avg_price_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_avg_price_warmup_period(handle: *mut AvgPrice) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_avg_price_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_avg_price_is_ready(handle: *mut AvgPrice) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_avg_price_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_avg_price_name(handle: *mut AvgPrice) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_avg_price_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_avg_price_reset(handle: *mut AvgPrice) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_avg_price_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_avg_price_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_avg_price_free(handle: *mut AvgPrice) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `AwesomeOscillator` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_awesome_oscillator_free`.
#[no_mangle]
pub extern "C" fn wickra_awesome_oscillator_new(
    fast: usize,
    slow: usize,
) -> *mut AwesomeOscillator {
    match AwesomeOscillator::new(fast, slow) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one OHLCV candle; returns the output, or `NaN` during warmup / on a `NULL`
/// handle / if the candle is invalid (e.g. `high < low`).
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_awesome_oscillator_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_awesome_oscillator_update(
    handle: *mut AwesomeOscillator,
    open: f64,
    high: f64,
    low: f64,
    close: f64,
    volume: f64,
    timestamp: i64,
) -> f64 {
    let Some(ind) = handle.as_mut() else {
        return f64::NAN;
    };
    match Candle::new(open, high, low, close, volume, timestamp) {
        Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
        Err(_) => f64::NAN,
    }
}

/// Run over the OHLCV series (`open/high/low/close/volume/timestamp[0..n]`) into
/// `out[0..n]` (`NaN` at warmup or on an invalid candle).
///
/// # Safety
/// `handle` valid (from `wickra_awesome_oscillator_new`, not freed); every input pointer and `out`
/// cover `n` elements.
#[no_mangle]
pub unsafe extern "C" fn wickra_awesome_oscillator_batch(
    handle: *mut AwesomeOscillator,
    open: *const f64,
    high: *const f64,
    low: *const f64,
    close: *const f64,
    volume: *const f64,
    timestamp: *const i64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null()
        || open.is_null()
        || high.is_null()
        || low.is_null()
        || close.is_null()
        || volume.is_null()
        || timestamp.is_null()
        || out.is_null()
    {
        return;
    }
    let ind = &mut *handle;
    let opens = slice::from_raw_parts(open, n);
    let highs = slice::from_raw_parts(high, n);
    let lows = slice::from_raw_parts(low, n);
    let closes = slice::from_raw_parts(close, n);
    let volumes = slice::from_raw_parts(volume, n);
    let timestamps = slice::from_raw_parts(timestamp, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (idx, slot) in outputs.iter_mut().enumerate() {
        *slot = match Candle::new(
            opens[idx],
            highs[idx],
            lows[idx],
            closes[idx],
            volumes[idx],
            timestamps[idx],
        ) {
            Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
            Err(_) => f64::NAN,
        };
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_awesome_oscillator_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_awesome_oscillator_warmup_period(
    handle: *mut AwesomeOscillator,
) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_awesome_oscillator_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_awesome_oscillator_is_ready(
    handle: *mut AwesomeOscillator,
) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_awesome_oscillator_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_awesome_oscillator_name(
    handle: *mut AwesomeOscillator,
) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_awesome_oscillator_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_awesome_oscillator_reset(handle: *mut AwesomeOscillator) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_awesome_oscillator_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_awesome_oscillator_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_awesome_oscillator_free(handle: *mut AwesomeOscillator) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `AwesomeOscillatorHistogram` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_awesome_oscillator_histogram_free`.
#[no_mangle]
pub extern "C" fn wickra_awesome_oscillator_histogram_new(
    fast: usize,
    slow: usize,
    lookback: usize,
) -> *mut AwesomeOscillatorHistogram {
    match AwesomeOscillatorHistogram::new(fast, slow, lookback) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one OHLCV candle; returns the output, or `NaN` during warmup / on a `NULL`
/// handle / if the candle is invalid (e.g. `high < low`).
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_awesome_oscillator_histogram_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_awesome_oscillator_histogram_update(
    handle: *mut AwesomeOscillatorHistogram,
    open: f64,
    high: f64,
    low: f64,
    close: f64,
    volume: f64,
    timestamp: i64,
) -> f64 {
    let Some(ind) = handle.as_mut() else {
        return f64::NAN;
    };
    match Candle::new(open, high, low, close, volume, timestamp) {
        Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
        Err(_) => f64::NAN,
    }
}

/// Run over the OHLCV series (`open/high/low/close/volume/timestamp[0..n]`) into
/// `out[0..n]` (`NaN` at warmup or on an invalid candle).
///
/// # Safety
/// `handle` valid (from `wickra_awesome_oscillator_histogram_new`, not freed); every input pointer and `out`
/// cover `n` elements.
#[no_mangle]
pub unsafe extern "C" fn wickra_awesome_oscillator_histogram_batch(
    handle: *mut AwesomeOscillatorHistogram,
    open: *const f64,
    high: *const f64,
    low: *const f64,
    close: *const f64,
    volume: *const f64,
    timestamp: *const i64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null()
        || open.is_null()
        || high.is_null()
        || low.is_null()
        || close.is_null()
        || volume.is_null()
        || timestamp.is_null()
        || out.is_null()
    {
        return;
    }
    let ind = &mut *handle;
    let opens = slice::from_raw_parts(open, n);
    let highs = slice::from_raw_parts(high, n);
    let lows = slice::from_raw_parts(low, n);
    let closes = slice::from_raw_parts(close, n);
    let volumes = slice::from_raw_parts(volume, n);
    let timestamps = slice::from_raw_parts(timestamp, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (idx, slot) in outputs.iter_mut().enumerate() {
        *slot = match Candle::new(
            opens[idx],
            highs[idx],
            lows[idx],
            closes[idx],
            volumes[idx],
            timestamps[idx],
        ) {
            Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
            Err(_) => f64::NAN,
        };
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_awesome_oscillator_histogram_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_awesome_oscillator_histogram_warmup_period(
    handle: *mut AwesomeOscillatorHistogram,
) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_awesome_oscillator_histogram_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_awesome_oscillator_histogram_is_ready(
    handle: *mut AwesomeOscillatorHistogram,
) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_awesome_oscillator_histogram_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_awesome_oscillator_histogram_name(
    handle: *mut AwesomeOscillatorHistogram,
) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_awesome_oscillator_histogram_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_awesome_oscillator_histogram_reset(
    handle: *mut AwesomeOscillatorHistogram,
) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_awesome_oscillator_histogram_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_awesome_oscillator_histogram_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_awesome_oscillator_histogram_free(
    handle: *mut AwesomeOscillatorHistogram,
) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `BalanceOfPower` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_balance_of_power_free`.
#[no_mangle]
pub extern "C" fn wickra_balance_of_power_new() -> *mut BalanceOfPower {
    Box::into_raw(Box::new(BalanceOfPower::new()))
}

/// Feed one OHLCV candle; returns the output, or `NaN` during warmup / on a `NULL`
/// handle / if the candle is invalid (e.g. `high < low`).
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_balance_of_power_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_balance_of_power_update(
    handle: *mut BalanceOfPower,
    open: f64,
    high: f64,
    low: f64,
    close: f64,
    volume: f64,
    timestamp: i64,
) -> f64 {
    let Some(ind) = handle.as_mut() else {
        return f64::NAN;
    };
    match Candle::new(open, high, low, close, volume, timestamp) {
        Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
        Err(_) => f64::NAN,
    }
}

/// Run over the OHLCV series (`open/high/low/close/volume/timestamp[0..n]`) into
/// `out[0..n]` (`NaN` at warmup or on an invalid candle).
///
/// # Safety
/// `handle` valid (from `wickra_balance_of_power_new`, not freed); every input pointer and `out`
/// cover `n` elements.
#[no_mangle]
pub unsafe extern "C" fn wickra_balance_of_power_batch(
    handle: *mut BalanceOfPower,
    open: *const f64,
    high: *const f64,
    low: *const f64,
    close: *const f64,
    volume: *const f64,
    timestamp: *const i64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null()
        || open.is_null()
        || high.is_null()
        || low.is_null()
        || close.is_null()
        || volume.is_null()
        || timestamp.is_null()
        || out.is_null()
    {
        return;
    }
    let ind = &mut *handle;
    let opens = slice::from_raw_parts(open, n);
    let highs = slice::from_raw_parts(high, n);
    let lows = slice::from_raw_parts(low, n);
    let closes = slice::from_raw_parts(close, n);
    let volumes = slice::from_raw_parts(volume, n);
    let timestamps = slice::from_raw_parts(timestamp, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (idx, slot) in outputs.iter_mut().enumerate() {
        *slot = match Candle::new(
            opens[idx],
            highs[idx],
            lows[idx],
            closes[idx],
            volumes[idx],
            timestamps[idx],
        ) {
            Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
            Err(_) => f64::NAN,
        };
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_balance_of_power_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_balance_of_power_warmup_period(
    handle: *mut BalanceOfPower,
) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_balance_of_power_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_balance_of_power_is_ready(handle: *mut BalanceOfPower) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_balance_of_power_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_balance_of_power_name(
    handle: *mut BalanceOfPower,
) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_balance_of_power_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_balance_of_power_reset(handle: *mut BalanceOfPower) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_balance_of_power_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_balance_of_power_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_balance_of_power_free(handle: *mut BalanceOfPower) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `Bat` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_bat_free`.
#[no_mangle]
pub extern "C" fn wickra_bat_new() -> *mut Bat {
    Box::into_raw(Box::new(Bat::new()))
}

/// Feed one OHLCV candle; returns the output, or `NaN` during warmup / on a `NULL`
/// handle / if the candle is invalid (e.g. `high < low`).
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_bat_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_bat_update(
    handle: *mut Bat,
    open: f64,
    high: f64,
    low: f64,
    close: f64,
    volume: f64,
    timestamp: i64,
) -> f64 {
    let Some(ind) = handle.as_mut() else {
        return f64::NAN;
    };
    match Candle::new(open, high, low, close, volume, timestamp) {
        Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
        Err(_) => f64::NAN,
    }
}

/// Run over the OHLCV series (`open/high/low/close/volume/timestamp[0..n]`) into
/// `out[0..n]` (`NaN` at warmup or on an invalid candle).
///
/// # Safety
/// `handle` valid (from `wickra_bat_new`, not freed); every input pointer and `out`
/// cover `n` elements.
#[no_mangle]
pub unsafe extern "C" fn wickra_bat_batch(
    handle: *mut Bat,
    open: *const f64,
    high: *const f64,
    low: *const f64,
    close: *const f64,
    volume: *const f64,
    timestamp: *const i64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null()
        || open.is_null()
        || high.is_null()
        || low.is_null()
        || close.is_null()
        || volume.is_null()
        || timestamp.is_null()
        || out.is_null()
    {
        return;
    }
    let ind = &mut *handle;
    let opens = slice::from_raw_parts(open, n);
    let highs = slice::from_raw_parts(high, n);
    let lows = slice::from_raw_parts(low, n);
    let closes = slice::from_raw_parts(close, n);
    let volumes = slice::from_raw_parts(volume, n);
    let timestamps = slice::from_raw_parts(timestamp, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (idx, slot) in outputs.iter_mut().enumerate() {
        *slot = match Candle::new(
            opens[idx],
            highs[idx],
            lows[idx],
            closes[idx],
            volumes[idx],
            timestamps[idx],
        ) {
            Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
            Err(_) => f64::NAN,
        };
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_bat_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_bat_warmup_period(handle: *mut Bat) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_bat_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_bat_is_ready(handle: *mut Bat) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_bat_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_bat_name(handle: *mut Bat) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_bat_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_bat_reset(handle: *mut Bat) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_bat_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_bat_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_bat_free(handle: *mut Bat) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `BeltHold` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_belt_hold_free`.
#[no_mangle]
pub extern "C" fn wickra_belt_hold_new() -> *mut BeltHold {
    Box::into_raw(Box::new(BeltHold::new()))
}

/// Feed one OHLCV candle; returns the output, or `NaN` during warmup / on a `NULL`
/// handle / if the candle is invalid (e.g. `high < low`).
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_belt_hold_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_belt_hold_update(
    handle: *mut BeltHold,
    open: f64,
    high: f64,
    low: f64,
    close: f64,
    volume: f64,
    timestamp: i64,
) -> f64 {
    let Some(ind) = handle.as_mut() else {
        return f64::NAN;
    };
    match Candle::new(open, high, low, close, volume, timestamp) {
        Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
        Err(_) => f64::NAN,
    }
}

/// Run over the OHLCV series (`open/high/low/close/volume/timestamp[0..n]`) into
/// `out[0..n]` (`NaN` at warmup or on an invalid candle).
///
/// # Safety
/// `handle` valid (from `wickra_belt_hold_new`, not freed); every input pointer and `out`
/// cover `n` elements.
#[no_mangle]
pub unsafe extern "C" fn wickra_belt_hold_batch(
    handle: *mut BeltHold,
    open: *const f64,
    high: *const f64,
    low: *const f64,
    close: *const f64,
    volume: *const f64,
    timestamp: *const i64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null()
        || open.is_null()
        || high.is_null()
        || low.is_null()
        || close.is_null()
        || volume.is_null()
        || timestamp.is_null()
        || out.is_null()
    {
        return;
    }
    let ind = &mut *handle;
    let opens = slice::from_raw_parts(open, n);
    let highs = slice::from_raw_parts(high, n);
    let lows = slice::from_raw_parts(low, n);
    let closes = slice::from_raw_parts(close, n);
    let volumes = slice::from_raw_parts(volume, n);
    let timestamps = slice::from_raw_parts(timestamp, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (idx, slot) in outputs.iter_mut().enumerate() {
        *slot = match Candle::new(
            opens[idx],
            highs[idx],
            lows[idx],
            closes[idx],
            volumes[idx],
            timestamps[idx],
        ) {
            Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
            Err(_) => f64::NAN,
        };
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_belt_hold_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_belt_hold_warmup_period(handle: *mut BeltHold) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_belt_hold_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_belt_hold_is_ready(handle: *mut BeltHold) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_belt_hold_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_belt_hold_name(handle: *mut BeltHold) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_belt_hold_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_belt_hold_reset(handle: *mut BeltHold) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_belt_hold_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_belt_hold_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_belt_hold_free(handle: *mut BeltHold) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `BetterVolume` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_better_volume_free`.
#[no_mangle]
pub extern "C" fn wickra_better_volume_new(period: usize) -> *mut BetterVolume {
    match BetterVolume::new(period) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one OHLCV candle; returns the output, or `NaN` during warmup / on a `NULL`
/// handle / if the candle is invalid (e.g. `high < low`).
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_better_volume_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_better_volume_update(
    handle: *mut BetterVolume,
    open: f64,
    high: f64,
    low: f64,
    close: f64,
    volume: f64,
    timestamp: i64,
) -> f64 {
    let Some(ind) = handle.as_mut() else {
        return f64::NAN;
    };
    match Candle::new(open, high, low, close, volume, timestamp) {
        Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
        Err(_) => f64::NAN,
    }
}

/// Run over the OHLCV series (`open/high/low/close/volume/timestamp[0..n]`) into
/// `out[0..n]` (`NaN` at warmup or on an invalid candle).
///
/// # Safety
/// `handle` valid (from `wickra_better_volume_new`, not freed); every input pointer and `out`
/// cover `n` elements.
#[no_mangle]
pub unsafe extern "C" fn wickra_better_volume_batch(
    handle: *mut BetterVolume,
    open: *const f64,
    high: *const f64,
    low: *const f64,
    close: *const f64,
    volume: *const f64,
    timestamp: *const i64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null()
        || open.is_null()
        || high.is_null()
        || low.is_null()
        || close.is_null()
        || volume.is_null()
        || timestamp.is_null()
        || out.is_null()
    {
        return;
    }
    let ind = &mut *handle;
    let opens = slice::from_raw_parts(open, n);
    let highs = slice::from_raw_parts(high, n);
    let lows = slice::from_raw_parts(low, n);
    let closes = slice::from_raw_parts(close, n);
    let volumes = slice::from_raw_parts(volume, n);
    let timestamps = slice::from_raw_parts(timestamp, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (idx, slot) in outputs.iter_mut().enumerate() {
        *slot = match Candle::new(
            opens[idx],
            highs[idx],
            lows[idx],
            closes[idx],
            volumes[idx],
            timestamps[idx],
        ) {
            Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
            Err(_) => f64::NAN,
        };
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_better_volume_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_better_volume_warmup_period(handle: *mut BetterVolume) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_better_volume_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_better_volume_is_ready(handle: *mut BetterVolume) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_better_volume_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_better_volume_name(handle: *mut BetterVolume) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_better_volume_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_better_volume_reset(handle: *mut BetterVolume) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_better_volume_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_better_volume_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_better_volume_free(handle: *mut BetterVolume) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `BodySizePct` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_body_size_pct_free`.
#[no_mangle]
pub extern "C" fn wickra_body_size_pct_new() -> *mut BodySizePct {
    Box::into_raw(Box::new(BodySizePct::new()))
}

/// Feed one OHLCV candle; returns the output, or `NaN` during warmup / on a `NULL`
/// handle / if the candle is invalid (e.g. `high < low`).
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_body_size_pct_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_body_size_pct_update(
    handle: *mut BodySizePct,
    open: f64,
    high: f64,
    low: f64,
    close: f64,
    volume: f64,
    timestamp: i64,
) -> f64 {
    let Some(ind) = handle.as_mut() else {
        return f64::NAN;
    };
    match Candle::new(open, high, low, close, volume, timestamp) {
        Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
        Err(_) => f64::NAN,
    }
}

/// Run over the OHLCV series (`open/high/low/close/volume/timestamp[0..n]`) into
/// `out[0..n]` (`NaN` at warmup or on an invalid candle).
///
/// # Safety
/// `handle` valid (from `wickra_body_size_pct_new`, not freed); every input pointer and `out`
/// cover `n` elements.
#[no_mangle]
pub unsafe extern "C" fn wickra_body_size_pct_batch(
    handle: *mut BodySizePct,
    open: *const f64,
    high: *const f64,
    low: *const f64,
    close: *const f64,
    volume: *const f64,
    timestamp: *const i64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null()
        || open.is_null()
        || high.is_null()
        || low.is_null()
        || close.is_null()
        || volume.is_null()
        || timestamp.is_null()
        || out.is_null()
    {
        return;
    }
    let ind = &mut *handle;
    let opens = slice::from_raw_parts(open, n);
    let highs = slice::from_raw_parts(high, n);
    let lows = slice::from_raw_parts(low, n);
    let closes = slice::from_raw_parts(close, n);
    let volumes = slice::from_raw_parts(volume, n);
    let timestamps = slice::from_raw_parts(timestamp, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (idx, slot) in outputs.iter_mut().enumerate() {
        *slot = match Candle::new(
            opens[idx],
            highs[idx],
            lows[idx],
            closes[idx],
            volumes[idx],
            timestamps[idx],
        ) {
            Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
            Err(_) => f64::NAN,
        };
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_body_size_pct_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_body_size_pct_warmup_period(handle: *mut BodySizePct) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_body_size_pct_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_body_size_pct_is_ready(handle: *mut BodySizePct) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_body_size_pct_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_body_size_pct_name(handle: *mut BodySizePct) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_body_size_pct_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_body_size_pct_reset(handle: *mut BodySizePct) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_body_size_pct_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_body_size_pct_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_body_size_pct_free(handle: *mut BodySizePct) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `Breakaway` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_breakaway_free`.
#[no_mangle]
pub extern "C" fn wickra_breakaway_new() -> *mut Breakaway {
    Box::into_raw(Box::new(Breakaway::new()))
}

/// Feed one OHLCV candle; returns the output, or `NaN` during warmup / on a `NULL`
/// handle / if the candle is invalid (e.g. `high < low`).
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_breakaway_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_breakaway_update(
    handle: *mut Breakaway,
    open: f64,
    high: f64,
    low: f64,
    close: f64,
    volume: f64,
    timestamp: i64,
) -> f64 {
    let Some(ind) = handle.as_mut() else {
        return f64::NAN;
    };
    match Candle::new(open, high, low, close, volume, timestamp) {
        Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
        Err(_) => f64::NAN,
    }
}

/// Run over the OHLCV series (`open/high/low/close/volume/timestamp[0..n]`) into
/// `out[0..n]` (`NaN` at warmup or on an invalid candle).
///
/// # Safety
/// `handle` valid (from `wickra_breakaway_new`, not freed); every input pointer and `out`
/// cover `n` elements.
#[no_mangle]
pub unsafe extern "C" fn wickra_breakaway_batch(
    handle: *mut Breakaway,
    open: *const f64,
    high: *const f64,
    low: *const f64,
    close: *const f64,
    volume: *const f64,
    timestamp: *const i64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null()
        || open.is_null()
        || high.is_null()
        || low.is_null()
        || close.is_null()
        || volume.is_null()
        || timestamp.is_null()
        || out.is_null()
    {
        return;
    }
    let ind = &mut *handle;
    let opens = slice::from_raw_parts(open, n);
    let highs = slice::from_raw_parts(high, n);
    let lows = slice::from_raw_parts(low, n);
    let closes = slice::from_raw_parts(close, n);
    let volumes = slice::from_raw_parts(volume, n);
    let timestamps = slice::from_raw_parts(timestamp, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (idx, slot) in outputs.iter_mut().enumerate() {
        *slot = match Candle::new(
            opens[idx],
            highs[idx],
            lows[idx],
            closes[idx],
            volumes[idx],
            timestamps[idx],
        ) {
            Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
            Err(_) => f64::NAN,
        };
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_breakaway_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_breakaway_warmup_period(handle: *mut Breakaway) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_breakaway_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_breakaway_is_ready(handle: *mut Breakaway) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_breakaway_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_breakaway_name(handle: *mut Breakaway) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_breakaway_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_breakaway_reset(handle: *mut Breakaway) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_breakaway_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_breakaway_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_breakaway_free(handle: *mut Breakaway) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `Butterfly` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_butterfly_free`.
#[no_mangle]
pub extern "C" fn wickra_butterfly_new() -> *mut Butterfly {
    Box::into_raw(Box::new(Butterfly::new()))
}

/// Feed one OHLCV candle; returns the output, or `NaN` during warmup / on a `NULL`
/// handle / if the candle is invalid (e.g. `high < low`).
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_butterfly_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_butterfly_update(
    handle: *mut Butterfly,
    open: f64,
    high: f64,
    low: f64,
    close: f64,
    volume: f64,
    timestamp: i64,
) -> f64 {
    let Some(ind) = handle.as_mut() else {
        return f64::NAN;
    };
    match Candle::new(open, high, low, close, volume, timestamp) {
        Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
        Err(_) => f64::NAN,
    }
}

/// Run over the OHLCV series (`open/high/low/close/volume/timestamp[0..n]`) into
/// `out[0..n]` (`NaN` at warmup or on an invalid candle).
///
/// # Safety
/// `handle` valid (from `wickra_butterfly_new`, not freed); every input pointer and `out`
/// cover `n` elements.
#[no_mangle]
pub unsafe extern "C" fn wickra_butterfly_batch(
    handle: *mut Butterfly,
    open: *const f64,
    high: *const f64,
    low: *const f64,
    close: *const f64,
    volume: *const f64,
    timestamp: *const i64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null()
        || open.is_null()
        || high.is_null()
        || low.is_null()
        || close.is_null()
        || volume.is_null()
        || timestamp.is_null()
        || out.is_null()
    {
        return;
    }
    let ind = &mut *handle;
    let opens = slice::from_raw_parts(open, n);
    let highs = slice::from_raw_parts(high, n);
    let lows = slice::from_raw_parts(low, n);
    let closes = slice::from_raw_parts(close, n);
    let volumes = slice::from_raw_parts(volume, n);
    let timestamps = slice::from_raw_parts(timestamp, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (idx, slot) in outputs.iter_mut().enumerate() {
        *slot = match Candle::new(
            opens[idx],
            highs[idx],
            lows[idx],
            closes[idx],
            volumes[idx],
            timestamps[idx],
        ) {
            Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
            Err(_) => f64::NAN,
        };
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_butterfly_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_butterfly_warmup_period(handle: *mut Butterfly) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_butterfly_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_butterfly_is_ready(handle: *mut Butterfly) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_butterfly_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_butterfly_name(handle: *mut Butterfly) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_butterfly_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_butterfly_reset(handle: *mut Butterfly) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_butterfly_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_butterfly_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_butterfly_free(handle: *mut Butterfly) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `Cci` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_cci_free`.
#[no_mangle]
pub extern "C" fn wickra_cci_new(period: usize) -> *mut Cci {
    match Cci::new(period) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one OHLCV candle; returns the output, or `NaN` during warmup / on a `NULL`
/// handle / if the candle is invalid (e.g. `high < low`).
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_cci_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_cci_update(
    handle: *mut Cci,
    open: f64,
    high: f64,
    low: f64,
    close: f64,
    volume: f64,
    timestamp: i64,
) -> f64 {
    let Some(ind) = handle.as_mut() else {
        return f64::NAN;
    };
    match Candle::new(open, high, low, close, volume, timestamp) {
        Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
        Err(_) => f64::NAN,
    }
}

/// Run over the OHLCV series (`open/high/low/close/volume/timestamp[0..n]`) into
/// `out[0..n]` (`NaN` at warmup or on an invalid candle).
///
/// # Safety
/// `handle` valid (from `wickra_cci_new`, not freed); every input pointer and `out`
/// cover `n` elements.
#[no_mangle]
pub unsafe extern "C" fn wickra_cci_batch(
    handle: *mut Cci,
    open: *const f64,
    high: *const f64,
    low: *const f64,
    close: *const f64,
    volume: *const f64,
    timestamp: *const i64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null()
        || open.is_null()
        || high.is_null()
        || low.is_null()
        || close.is_null()
        || volume.is_null()
        || timestamp.is_null()
        || out.is_null()
    {
        return;
    }
    let ind = &mut *handle;
    let opens = slice::from_raw_parts(open, n);
    let highs = slice::from_raw_parts(high, n);
    let lows = slice::from_raw_parts(low, n);
    let closes = slice::from_raw_parts(close, n);
    let volumes = slice::from_raw_parts(volume, n);
    let timestamps = slice::from_raw_parts(timestamp, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (idx, slot) in outputs.iter_mut().enumerate() {
        *slot = match Candle::new(
            opens[idx],
            highs[idx],
            lows[idx],
            closes[idx],
            volumes[idx],
            timestamps[idx],
        ) {
            Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
            Err(_) => f64::NAN,
        };
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_cci_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_cci_warmup_period(handle: *mut Cci) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_cci_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_cci_is_ready(handle: *mut Cci) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_cci_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_cci_name(handle: *mut Cci) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_cci_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_cci_reset(handle: *mut Cci) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_cci_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_cci_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_cci_free(handle: *mut Cci) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `ChaikinOscillator` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_chaikin_oscillator_free`.
#[no_mangle]
pub extern "C" fn wickra_chaikin_oscillator_new(
    fast: usize,
    slow: usize,
) -> *mut ChaikinOscillator {
    match ChaikinOscillator::new(fast, slow) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one OHLCV candle; returns the output, or `NaN` during warmup / on a `NULL`
/// handle / if the candle is invalid (e.g. `high < low`).
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_chaikin_oscillator_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_chaikin_oscillator_update(
    handle: *mut ChaikinOscillator,
    open: f64,
    high: f64,
    low: f64,
    close: f64,
    volume: f64,
    timestamp: i64,
) -> f64 {
    let Some(ind) = handle.as_mut() else {
        return f64::NAN;
    };
    match Candle::new(open, high, low, close, volume, timestamp) {
        Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
        Err(_) => f64::NAN,
    }
}

/// Run over the OHLCV series (`open/high/low/close/volume/timestamp[0..n]`) into
/// `out[0..n]` (`NaN` at warmup or on an invalid candle).
///
/// # Safety
/// `handle` valid (from `wickra_chaikin_oscillator_new`, not freed); every input pointer and `out`
/// cover `n` elements.
#[no_mangle]
pub unsafe extern "C" fn wickra_chaikin_oscillator_batch(
    handle: *mut ChaikinOscillator,
    open: *const f64,
    high: *const f64,
    low: *const f64,
    close: *const f64,
    volume: *const f64,
    timestamp: *const i64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null()
        || open.is_null()
        || high.is_null()
        || low.is_null()
        || close.is_null()
        || volume.is_null()
        || timestamp.is_null()
        || out.is_null()
    {
        return;
    }
    let ind = &mut *handle;
    let opens = slice::from_raw_parts(open, n);
    let highs = slice::from_raw_parts(high, n);
    let lows = slice::from_raw_parts(low, n);
    let closes = slice::from_raw_parts(close, n);
    let volumes = slice::from_raw_parts(volume, n);
    let timestamps = slice::from_raw_parts(timestamp, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (idx, slot) in outputs.iter_mut().enumerate() {
        *slot = match Candle::new(
            opens[idx],
            highs[idx],
            lows[idx],
            closes[idx],
            volumes[idx],
            timestamps[idx],
        ) {
            Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
            Err(_) => f64::NAN,
        };
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_chaikin_oscillator_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_chaikin_oscillator_warmup_period(
    handle: *mut ChaikinOscillator,
) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_chaikin_oscillator_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_chaikin_oscillator_is_ready(
    handle: *mut ChaikinOscillator,
) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_chaikin_oscillator_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_chaikin_oscillator_name(
    handle: *mut ChaikinOscillator,
) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_chaikin_oscillator_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_chaikin_oscillator_reset(handle: *mut ChaikinOscillator) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_chaikin_oscillator_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_chaikin_oscillator_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_chaikin_oscillator_free(handle: *mut ChaikinOscillator) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `ChaikinVolatility` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_chaikin_volatility_free`.
#[no_mangle]
pub extern "C" fn wickra_chaikin_volatility_new(
    ema_period: usize,
    roc_period: usize,
) -> *mut ChaikinVolatility {
    match ChaikinVolatility::new(ema_period, roc_period) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one OHLCV candle; returns the output, or `NaN` during warmup / on a `NULL`
/// handle / if the candle is invalid (e.g. `high < low`).
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_chaikin_volatility_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_chaikin_volatility_update(
    handle: *mut ChaikinVolatility,
    open: f64,
    high: f64,
    low: f64,
    close: f64,
    volume: f64,
    timestamp: i64,
) -> f64 {
    let Some(ind) = handle.as_mut() else {
        return f64::NAN;
    };
    match Candle::new(open, high, low, close, volume, timestamp) {
        Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
        Err(_) => f64::NAN,
    }
}

/// Run over the OHLCV series (`open/high/low/close/volume/timestamp[0..n]`) into
/// `out[0..n]` (`NaN` at warmup or on an invalid candle).
///
/// # Safety
/// `handle` valid (from `wickra_chaikin_volatility_new`, not freed); every input pointer and `out`
/// cover `n` elements.
#[no_mangle]
pub unsafe extern "C" fn wickra_chaikin_volatility_batch(
    handle: *mut ChaikinVolatility,
    open: *const f64,
    high: *const f64,
    low: *const f64,
    close: *const f64,
    volume: *const f64,
    timestamp: *const i64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null()
        || open.is_null()
        || high.is_null()
        || low.is_null()
        || close.is_null()
        || volume.is_null()
        || timestamp.is_null()
        || out.is_null()
    {
        return;
    }
    let ind = &mut *handle;
    let opens = slice::from_raw_parts(open, n);
    let highs = slice::from_raw_parts(high, n);
    let lows = slice::from_raw_parts(low, n);
    let closes = slice::from_raw_parts(close, n);
    let volumes = slice::from_raw_parts(volume, n);
    let timestamps = slice::from_raw_parts(timestamp, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (idx, slot) in outputs.iter_mut().enumerate() {
        *slot = match Candle::new(
            opens[idx],
            highs[idx],
            lows[idx],
            closes[idx],
            volumes[idx],
            timestamps[idx],
        ) {
            Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
            Err(_) => f64::NAN,
        };
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_chaikin_volatility_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_chaikin_volatility_warmup_period(
    handle: *mut ChaikinVolatility,
) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_chaikin_volatility_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_chaikin_volatility_is_ready(
    handle: *mut ChaikinVolatility,
) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_chaikin_volatility_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_chaikin_volatility_name(
    handle: *mut ChaikinVolatility,
) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_chaikin_volatility_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_chaikin_volatility_reset(handle: *mut ChaikinVolatility) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_chaikin_volatility_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_chaikin_volatility_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_chaikin_volatility_free(handle: *mut ChaikinVolatility) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `ChoppinessIndex` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_choppiness_index_free`.
#[no_mangle]
pub extern "C" fn wickra_choppiness_index_new(period: usize) -> *mut ChoppinessIndex {
    match ChoppinessIndex::new(period) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one OHLCV candle; returns the output, or `NaN` during warmup / on a `NULL`
/// handle / if the candle is invalid (e.g. `high < low`).
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_choppiness_index_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_choppiness_index_update(
    handle: *mut ChoppinessIndex,
    open: f64,
    high: f64,
    low: f64,
    close: f64,
    volume: f64,
    timestamp: i64,
) -> f64 {
    let Some(ind) = handle.as_mut() else {
        return f64::NAN;
    };
    match Candle::new(open, high, low, close, volume, timestamp) {
        Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
        Err(_) => f64::NAN,
    }
}

/// Run over the OHLCV series (`open/high/low/close/volume/timestamp[0..n]`) into
/// `out[0..n]` (`NaN` at warmup or on an invalid candle).
///
/// # Safety
/// `handle` valid (from `wickra_choppiness_index_new`, not freed); every input pointer and `out`
/// cover `n` elements.
#[no_mangle]
pub unsafe extern "C" fn wickra_choppiness_index_batch(
    handle: *mut ChoppinessIndex,
    open: *const f64,
    high: *const f64,
    low: *const f64,
    close: *const f64,
    volume: *const f64,
    timestamp: *const i64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null()
        || open.is_null()
        || high.is_null()
        || low.is_null()
        || close.is_null()
        || volume.is_null()
        || timestamp.is_null()
        || out.is_null()
    {
        return;
    }
    let ind = &mut *handle;
    let opens = slice::from_raw_parts(open, n);
    let highs = slice::from_raw_parts(high, n);
    let lows = slice::from_raw_parts(low, n);
    let closes = slice::from_raw_parts(close, n);
    let volumes = slice::from_raw_parts(volume, n);
    let timestamps = slice::from_raw_parts(timestamp, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (idx, slot) in outputs.iter_mut().enumerate() {
        *slot = match Candle::new(
            opens[idx],
            highs[idx],
            lows[idx],
            closes[idx],
            volumes[idx],
            timestamps[idx],
        ) {
            Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
            Err(_) => f64::NAN,
        };
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_choppiness_index_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_choppiness_index_warmup_period(
    handle: *mut ChoppinessIndex,
) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_choppiness_index_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_choppiness_index_is_ready(handle: *mut ChoppinessIndex) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_choppiness_index_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_choppiness_index_name(
    handle: *mut ChoppinessIndex,
) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_choppiness_index_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_choppiness_index_reset(handle: *mut ChoppinessIndex) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_choppiness_index_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_choppiness_index_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_choppiness_index_free(handle: *mut ChoppinessIndex) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `CloseVsOpen` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_close_vs_open_free`.
#[no_mangle]
pub extern "C" fn wickra_close_vs_open_new() -> *mut CloseVsOpen {
    Box::into_raw(Box::new(CloseVsOpen::new()))
}

/// Feed one OHLCV candle; returns the output, or `NaN` during warmup / on a `NULL`
/// handle / if the candle is invalid (e.g. `high < low`).
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_close_vs_open_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_close_vs_open_update(
    handle: *mut CloseVsOpen,
    open: f64,
    high: f64,
    low: f64,
    close: f64,
    volume: f64,
    timestamp: i64,
) -> f64 {
    let Some(ind) = handle.as_mut() else {
        return f64::NAN;
    };
    match Candle::new(open, high, low, close, volume, timestamp) {
        Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
        Err(_) => f64::NAN,
    }
}

/// Run over the OHLCV series (`open/high/low/close/volume/timestamp[0..n]`) into
/// `out[0..n]` (`NaN` at warmup or on an invalid candle).
///
/// # Safety
/// `handle` valid (from `wickra_close_vs_open_new`, not freed); every input pointer and `out`
/// cover `n` elements.
#[no_mangle]
pub unsafe extern "C" fn wickra_close_vs_open_batch(
    handle: *mut CloseVsOpen,
    open: *const f64,
    high: *const f64,
    low: *const f64,
    close: *const f64,
    volume: *const f64,
    timestamp: *const i64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null()
        || open.is_null()
        || high.is_null()
        || low.is_null()
        || close.is_null()
        || volume.is_null()
        || timestamp.is_null()
        || out.is_null()
    {
        return;
    }
    let ind = &mut *handle;
    let opens = slice::from_raw_parts(open, n);
    let highs = slice::from_raw_parts(high, n);
    let lows = slice::from_raw_parts(low, n);
    let closes = slice::from_raw_parts(close, n);
    let volumes = slice::from_raw_parts(volume, n);
    let timestamps = slice::from_raw_parts(timestamp, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (idx, slot) in outputs.iter_mut().enumerate() {
        *slot = match Candle::new(
            opens[idx],
            highs[idx],
            lows[idx],
            closes[idx],
            volumes[idx],
            timestamps[idx],
        ) {
            Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
            Err(_) => f64::NAN,
        };
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_close_vs_open_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_close_vs_open_warmup_period(handle: *mut CloseVsOpen) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_close_vs_open_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_close_vs_open_is_ready(handle: *mut CloseVsOpen) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_close_vs_open_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_close_vs_open_name(handle: *mut CloseVsOpen) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_close_vs_open_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_close_vs_open_reset(handle: *mut CloseVsOpen) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_close_vs_open_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_close_vs_open_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_close_vs_open_free(handle: *mut CloseVsOpen) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `ClosingMarubozu` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_closing_marubozu_free`.
#[no_mangle]
pub extern "C" fn wickra_closing_marubozu_new() -> *mut ClosingMarubozu {
    Box::into_raw(Box::new(ClosingMarubozu::new()))
}

/// Feed one OHLCV candle; returns the output, or `NaN` during warmup / on a `NULL`
/// handle / if the candle is invalid (e.g. `high < low`).
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_closing_marubozu_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_closing_marubozu_update(
    handle: *mut ClosingMarubozu,
    open: f64,
    high: f64,
    low: f64,
    close: f64,
    volume: f64,
    timestamp: i64,
) -> f64 {
    let Some(ind) = handle.as_mut() else {
        return f64::NAN;
    };
    match Candle::new(open, high, low, close, volume, timestamp) {
        Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
        Err(_) => f64::NAN,
    }
}

/// Run over the OHLCV series (`open/high/low/close/volume/timestamp[0..n]`) into
/// `out[0..n]` (`NaN` at warmup or on an invalid candle).
///
/// # Safety
/// `handle` valid (from `wickra_closing_marubozu_new`, not freed); every input pointer and `out`
/// cover `n` elements.
#[no_mangle]
pub unsafe extern "C" fn wickra_closing_marubozu_batch(
    handle: *mut ClosingMarubozu,
    open: *const f64,
    high: *const f64,
    low: *const f64,
    close: *const f64,
    volume: *const f64,
    timestamp: *const i64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null()
        || open.is_null()
        || high.is_null()
        || low.is_null()
        || close.is_null()
        || volume.is_null()
        || timestamp.is_null()
        || out.is_null()
    {
        return;
    }
    let ind = &mut *handle;
    let opens = slice::from_raw_parts(open, n);
    let highs = slice::from_raw_parts(high, n);
    let lows = slice::from_raw_parts(low, n);
    let closes = slice::from_raw_parts(close, n);
    let volumes = slice::from_raw_parts(volume, n);
    let timestamps = slice::from_raw_parts(timestamp, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (idx, slot) in outputs.iter_mut().enumerate() {
        *slot = match Candle::new(
            opens[idx],
            highs[idx],
            lows[idx],
            closes[idx],
            volumes[idx],
            timestamps[idx],
        ) {
            Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
            Err(_) => f64::NAN,
        };
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_closing_marubozu_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_closing_marubozu_warmup_period(
    handle: *mut ClosingMarubozu,
) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_closing_marubozu_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_closing_marubozu_is_ready(handle: *mut ClosingMarubozu) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_closing_marubozu_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_closing_marubozu_name(
    handle: *mut ClosingMarubozu,
) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_closing_marubozu_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_closing_marubozu_reset(handle: *mut ClosingMarubozu) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_closing_marubozu_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_closing_marubozu_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_closing_marubozu_free(handle: *mut ClosingMarubozu) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `ChaikinMoneyFlow` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_chaikin_money_flow_free`.
#[no_mangle]
pub extern "C" fn wickra_chaikin_money_flow_new(period: usize) -> *mut ChaikinMoneyFlow {
    match ChaikinMoneyFlow::new(period) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one OHLCV candle; returns the output, or `NaN` during warmup / on a `NULL`
/// handle / if the candle is invalid (e.g. `high < low`).
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_chaikin_money_flow_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_chaikin_money_flow_update(
    handle: *mut ChaikinMoneyFlow,
    open: f64,
    high: f64,
    low: f64,
    close: f64,
    volume: f64,
    timestamp: i64,
) -> f64 {
    let Some(ind) = handle.as_mut() else {
        return f64::NAN;
    };
    match Candle::new(open, high, low, close, volume, timestamp) {
        Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
        Err(_) => f64::NAN,
    }
}

/// Run over the OHLCV series (`open/high/low/close/volume/timestamp[0..n]`) into
/// `out[0..n]` (`NaN` at warmup or on an invalid candle).
///
/// # Safety
/// `handle` valid (from `wickra_chaikin_money_flow_new`, not freed); every input pointer and `out`
/// cover `n` elements.
#[no_mangle]
pub unsafe extern "C" fn wickra_chaikin_money_flow_batch(
    handle: *mut ChaikinMoneyFlow,
    open: *const f64,
    high: *const f64,
    low: *const f64,
    close: *const f64,
    volume: *const f64,
    timestamp: *const i64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null()
        || open.is_null()
        || high.is_null()
        || low.is_null()
        || close.is_null()
        || volume.is_null()
        || timestamp.is_null()
        || out.is_null()
    {
        return;
    }
    let ind = &mut *handle;
    let opens = slice::from_raw_parts(open, n);
    let highs = slice::from_raw_parts(high, n);
    let lows = slice::from_raw_parts(low, n);
    let closes = slice::from_raw_parts(close, n);
    let volumes = slice::from_raw_parts(volume, n);
    let timestamps = slice::from_raw_parts(timestamp, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (idx, slot) in outputs.iter_mut().enumerate() {
        *slot = match Candle::new(
            opens[idx],
            highs[idx],
            lows[idx],
            closes[idx],
            volumes[idx],
            timestamps[idx],
        ) {
            Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
            Err(_) => f64::NAN,
        };
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_chaikin_money_flow_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_chaikin_money_flow_warmup_period(
    handle: *mut ChaikinMoneyFlow,
) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_chaikin_money_flow_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_chaikin_money_flow_is_ready(handle: *mut ChaikinMoneyFlow) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_chaikin_money_flow_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_chaikin_money_flow_name(
    handle: *mut ChaikinMoneyFlow,
) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_chaikin_money_flow_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_chaikin_money_flow_reset(handle: *mut ChaikinMoneyFlow) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_chaikin_money_flow_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_chaikin_money_flow_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_chaikin_money_flow_free(handle: *mut ChaikinMoneyFlow) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `ConcealingBabySwallow` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_concealing_baby_swallow_free`.
#[no_mangle]
pub extern "C" fn wickra_concealing_baby_swallow_new() -> *mut ConcealingBabySwallow {
    Box::into_raw(Box::new(ConcealingBabySwallow::new()))
}

/// Feed one OHLCV candle; returns the output, or `NaN` during warmup / on a `NULL`
/// handle / if the candle is invalid (e.g. `high < low`).
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_concealing_baby_swallow_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_concealing_baby_swallow_update(
    handle: *mut ConcealingBabySwallow,
    open: f64,
    high: f64,
    low: f64,
    close: f64,
    volume: f64,
    timestamp: i64,
) -> f64 {
    let Some(ind) = handle.as_mut() else {
        return f64::NAN;
    };
    match Candle::new(open, high, low, close, volume, timestamp) {
        Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
        Err(_) => f64::NAN,
    }
}

/// Run over the OHLCV series (`open/high/low/close/volume/timestamp[0..n]`) into
/// `out[0..n]` (`NaN` at warmup or on an invalid candle).
///
/// # Safety
/// `handle` valid (from `wickra_concealing_baby_swallow_new`, not freed); every input pointer and `out`
/// cover `n` elements.
#[no_mangle]
pub unsafe extern "C" fn wickra_concealing_baby_swallow_batch(
    handle: *mut ConcealingBabySwallow,
    open: *const f64,
    high: *const f64,
    low: *const f64,
    close: *const f64,
    volume: *const f64,
    timestamp: *const i64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null()
        || open.is_null()
        || high.is_null()
        || low.is_null()
        || close.is_null()
        || volume.is_null()
        || timestamp.is_null()
        || out.is_null()
    {
        return;
    }
    let ind = &mut *handle;
    let opens = slice::from_raw_parts(open, n);
    let highs = slice::from_raw_parts(high, n);
    let lows = slice::from_raw_parts(low, n);
    let closes = slice::from_raw_parts(close, n);
    let volumes = slice::from_raw_parts(volume, n);
    let timestamps = slice::from_raw_parts(timestamp, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (idx, slot) in outputs.iter_mut().enumerate() {
        *slot = match Candle::new(
            opens[idx],
            highs[idx],
            lows[idx],
            closes[idx],
            volumes[idx],
            timestamps[idx],
        ) {
            Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
            Err(_) => f64::NAN,
        };
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_concealing_baby_swallow_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_concealing_baby_swallow_warmup_period(
    handle: *mut ConcealingBabySwallow,
) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_concealing_baby_swallow_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_concealing_baby_swallow_is_ready(
    handle: *mut ConcealingBabySwallow,
) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_concealing_baby_swallow_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_concealing_baby_swallow_name(
    handle: *mut ConcealingBabySwallow,
) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_concealing_baby_swallow_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_concealing_baby_swallow_reset(handle: *mut ConcealingBabySwallow) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_concealing_baby_swallow_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_concealing_baby_swallow_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_concealing_baby_swallow_free(handle: *mut ConcealingBabySwallow) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `Counterattack` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_counterattack_free`.
#[no_mangle]
pub extern "C" fn wickra_counterattack_new() -> *mut Counterattack {
    Box::into_raw(Box::new(Counterattack::new()))
}

/// Feed one OHLCV candle; returns the output, or `NaN` during warmup / on a `NULL`
/// handle / if the candle is invalid (e.g. `high < low`).
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_counterattack_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_counterattack_update(
    handle: *mut Counterattack,
    open: f64,
    high: f64,
    low: f64,
    close: f64,
    volume: f64,
    timestamp: i64,
) -> f64 {
    let Some(ind) = handle.as_mut() else {
        return f64::NAN;
    };
    match Candle::new(open, high, low, close, volume, timestamp) {
        Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
        Err(_) => f64::NAN,
    }
}

/// Run over the OHLCV series (`open/high/low/close/volume/timestamp[0..n]`) into
/// `out[0..n]` (`NaN` at warmup or on an invalid candle).
///
/// # Safety
/// `handle` valid (from `wickra_counterattack_new`, not freed); every input pointer and `out`
/// cover `n` elements.
#[no_mangle]
pub unsafe extern "C" fn wickra_counterattack_batch(
    handle: *mut Counterattack,
    open: *const f64,
    high: *const f64,
    low: *const f64,
    close: *const f64,
    volume: *const f64,
    timestamp: *const i64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null()
        || open.is_null()
        || high.is_null()
        || low.is_null()
        || close.is_null()
        || volume.is_null()
        || timestamp.is_null()
        || out.is_null()
    {
        return;
    }
    let ind = &mut *handle;
    let opens = slice::from_raw_parts(open, n);
    let highs = slice::from_raw_parts(high, n);
    let lows = slice::from_raw_parts(low, n);
    let closes = slice::from_raw_parts(close, n);
    let volumes = slice::from_raw_parts(volume, n);
    let timestamps = slice::from_raw_parts(timestamp, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (idx, slot) in outputs.iter_mut().enumerate() {
        *slot = match Candle::new(
            opens[idx],
            highs[idx],
            lows[idx],
            closes[idx],
            volumes[idx],
            timestamps[idx],
        ) {
            Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
            Err(_) => f64::NAN,
        };
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_counterattack_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_counterattack_warmup_period(handle: *mut Counterattack) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_counterattack_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_counterattack_is_ready(handle: *mut Counterattack) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_counterattack_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_counterattack_name(handle: *mut Counterattack) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_counterattack_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_counterattack_reset(handle: *mut Counterattack) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_counterattack_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_counterattack_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_counterattack_free(handle: *mut Counterattack) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `Crab` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_crab_free`.
#[no_mangle]
pub extern "C" fn wickra_crab_new() -> *mut Crab {
    Box::into_raw(Box::new(Crab::new()))
}

/// Feed one OHLCV candle; returns the output, or `NaN` during warmup / on a `NULL`
/// handle / if the candle is invalid (e.g. `high < low`).
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_crab_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_crab_update(
    handle: *mut Crab,
    open: f64,
    high: f64,
    low: f64,
    close: f64,
    volume: f64,
    timestamp: i64,
) -> f64 {
    let Some(ind) = handle.as_mut() else {
        return f64::NAN;
    };
    match Candle::new(open, high, low, close, volume, timestamp) {
        Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
        Err(_) => f64::NAN,
    }
}

/// Run over the OHLCV series (`open/high/low/close/volume/timestamp[0..n]`) into
/// `out[0..n]` (`NaN` at warmup or on an invalid candle).
///
/// # Safety
/// `handle` valid (from `wickra_crab_new`, not freed); every input pointer and `out`
/// cover `n` elements.
#[no_mangle]
pub unsafe extern "C" fn wickra_crab_batch(
    handle: *mut Crab,
    open: *const f64,
    high: *const f64,
    low: *const f64,
    close: *const f64,
    volume: *const f64,
    timestamp: *const i64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null()
        || open.is_null()
        || high.is_null()
        || low.is_null()
        || close.is_null()
        || volume.is_null()
        || timestamp.is_null()
        || out.is_null()
    {
        return;
    }
    let ind = &mut *handle;
    let opens = slice::from_raw_parts(open, n);
    let highs = slice::from_raw_parts(high, n);
    let lows = slice::from_raw_parts(low, n);
    let closes = slice::from_raw_parts(close, n);
    let volumes = slice::from_raw_parts(volume, n);
    let timestamps = slice::from_raw_parts(timestamp, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (idx, slot) in outputs.iter_mut().enumerate() {
        *slot = match Candle::new(
            opens[idx],
            highs[idx],
            lows[idx],
            closes[idx],
            volumes[idx],
            timestamps[idx],
        ) {
            Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
            Err(_) => f64::NAN,
        };
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_crab_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_crab_warmup_period(handle: *mut Crab) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_crab_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_crab_is_ready(handle: *mut Crab) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_crab_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_crab_name(handle: *mut Crab) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_crab_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_crab_reset(handle: *mut Crab) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_crab_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_crab_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_crab_free(handle: *mut Crab) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `CupAndHandle` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_cup_and_handle_free`.
#[no_mangle]
pub extern "C" fn wickra_cup_and_handle_new() -> *mut CupAndHandle {
    Box::into_raw(Box::new(CupAndHandle::new()))
}

/// Feed one OHLCV candle; returns the output, or `NaN` during warmup / on a `NULL`
/// handle / if the candle is invalid (e.g. `high < low`).
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_cup_and_handle_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_cup_and_handle_update(
    handle: *mut CupAndHandle,
    open: f64,
    high: f64,
    low: f64,
    close: f64,
    volume: f64,
    timestamp: i64,
) -> f64 {
    let Some(ind) = handle.as_mut() else {
        return f64::NAN;
    };
    match Candle::new(open, high, low, close, volume, timestamp) {
        Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
        Err(_) => f64::NAN,
    }
}

/// Run over the OHLCV series (`open/high/low/close/volume/timestamp[0..n]`) into
/// `out[0..n]` (`NaN` at warmup or on an invalid candle).
///
/// # Safety
/// `handle` valid (from `wickra_cup_and_handle_new`, not freed); every input pointer and `out`
/// cover `n` elements.
#[no_mangle]
pub unsafe extern "C" fn wickra_cup_and_handle_batch(
    handle: *mut CupAndHandle,
    open: *const f64,
    high: *const f64,
    low: *const f64,
    close: *const f64,
    volume: *const f64,
    timestamp: *const i64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null()
        || open.is_null()
        || high.is_null()
        || low.is_null()
        || close.is_null()
        || volume.is_null()
        || timestamp.is_null()
        || out.is_null()
    {
        return;
    }
    let ind = &mut *handle;
    let opens = slice::from_raw_parts(open, n);
    let highs = slice::from_raw_parts(high, n);
    let lows = slice::from_raw_parts(low, n);
    let closes = slice::from_raw_parts(close, n);
    let volumes = slice::from_raw_parts(volume, n);
    let timestamps = slice::from_raw_parts(timestamp, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (idx, slot) in outputs.iter_mut().enumerate() {
        *slot = match Candle::new(
            opens[idx],
            highs[idx],
            lows[idx],
            closes[idx],
            volumes[idx],
            timestamps[idx],
        ) {
            Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
            Err(_) => f64::NAN,
        };
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_cup_and_handle_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_cup_and_handle_warmup_period(handle: *mut CupAndHandle) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_cup_and_handle_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_cup_and_handle_is_ready(handle: *mut CupAndHandle) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_cup_and_handle_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_cup_and_handle_name(handle: *mut CupAndHandle) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_cup_and_handle_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_cup_and_handle_reset(handle: *mut CupAndHandle) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_cup_and_handle_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_cup_and_handle_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_cup_and_handle_free(handle: *mut CupAndHandle) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `Cypher` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_cypher_free`.
#[no_mangle]
pub extern "C" fn wickra_cypher_new() -> *mut Cypher {
    Box::into_raw(Box::new(Cypher::new()))
}

/// Feed one OHLCV candle; returns the output, or `NaN` during warmup / on a `NULL`
/// handle / if the candle is invalid (e.g. `high < low`).
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_cypher_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_cypher_update(
    handle: *mut Cypher,
    open: f64,
    high: f64,
    low: f64,
    close: f64,
    volume: f64,
    timestamp: i64,
) -> f64 {
    let Some(ind) = handle.as_mut() else {
        return f64::NAN;
    };
    match Candle::new(open, high, low, close, volume, timestamp) {
        Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
        Err(_) => f64::NAN,
    }
}

/// Run over the OHLCV series (`open/high/low/close/volume/timestamp[0..n]`) into
/// `out[0..n]` (`NaN` at warmup or on an invalid candle).
///
/// # Safety
/// `handle` valid (from `wickra_cypher_new`, not freed); every input pointer and `out`
/// cover `n` elements.
#[no_mangle]
pub unsafe extern "C" fn wickra_cypher_batch(
    handle: *mut Cypher,
    open: *const f64,
    high: *const f64,
    low: *const f64,
    close: *const f64,
    volume: *const f64,
    timestamp: *const i64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null()
        || open.is_null()
        || high.is_null()
        || low.is_null()
        || close.is_null()
        || volume.is_null()
        || timestamp.is_null()
        || out.is_null()
    {
        return;
    }
    let ind = &mut *handle;
    let opens = slice::from_raw_parts(open, n);
    let highs = slice::from_raw_parts(high, n);
    let lows = slice::from_raw_parts(low, n);
    let closes = slice::from_raw_parts(close, n);
    let volumes = slice::from_raw_parts(volume, n);
    let timestamps = slice::from_raw_parts(timestamp, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (idx, slot) in outputs.iter_mut().enumerate() {
        *slot = match Candle::new(
            opens[idx],
            highs[idx],
            lows[idx],
            closes[idx],
            volumes[idx],
            timestamps[idx],
        ) {
            Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
            Err(_) => f64::NAN,
        };
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_cypher_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_cypher_warmup_period(handle: *mut Cypher) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_cypher_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_cypher_is_ready(handle: *mut Cypher) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_cypher_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_cypher_name(handle: *mut Cypher) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_cypher_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_cypher_reset(handle: *mut Cypher) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_cypher_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_cypher_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_cypher_free(handle: *mut Cypher) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `DemandIndex` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_demand_index_free`.
#[no_mangle]
pub extern "C" fn wickra_demand_index_new(period: usize) -> *mut DemandIndex {
    match DemandIndex::new(period) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one OHLCV candle; returns the output, or `NaN` during warmup / on a `NULL`
/// handle / if the candle is invalid (e.g. `high < low`).
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_demand_index_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_demand_index_update(
    handle: *mut DemandIndex,
    open: f64,
    high: f64,
    low: f64,
    close: f64,
    volume: f64,
    timestamp: i64,
) -> f64 {
    let Some(ind) = handle.as_mut() else {
        return f64::NAN;
    };
    match Candle::new(open, high, low, close, volume, timestamp) {
        Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
        Err(_) => f64::NAN,
    }
}

/// Run over the OHLCV series (`open/high/low/close/volume/timestamp[0..n]`) into
/// `out[0..n]` (`NaN` at warmup or on an invalid candle).
///
/// # Safety
/// `handle` valid (from `wickra_demand_index_new`, not freed); every input pointer and `out`
/// cover `n` elements.
#[no_mangle]
pub unsafe extern "C" fn wickra_demand_index_batch(
    handle: *mut DemandIndex,
    open: *const f64,
    high: *const f64,
    low: *const f64,
    close: *const f64,
    volume: *const f64,
    timestamp: *const i64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null()
        || open.is_null()
        || high.is_null()
        || low.is_null()
        || close.is_null()
        || volume.is_null()
        || timestamp.is_null()
        || out.is_null()
    {
        return;
    }
    let ind = &mut *handle;
    let opens = slice::from_raw_parts(open, n);
    let highs = slice::from_raw_parts(high, n);
    let lows = slice::from_raw_parts(low, n);
    let closes = slice::from_raw_parts(close, n);
    let volumes = slice::from_raw_parts(volume, n);
    let timestamps = slice::from_raw_parts(timestamp, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (idx, slot) in outputs.iter_mut().enumerate() {
        *slot = match Candle::new(
            opens[idx],
            highs[idx],
            lows[idx],
            closes[idx],
            volumes[idx],
            timestamps[idx],
        ) {
            Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
            Err(_) => f64::NAN,
        };
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_demand_index_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_demand_index_warmup_period(handle: *mut DemandIndex) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_demand_index_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_demand_index_is_ready(handle: *mut DemandIndex) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_demand_index_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_demand_index_name(handle: *mut DemandIndex) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_demand_index_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_demand_index_reset(handle: *mut DemandIndex) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_demand_index_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_demand_index_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_demand_index_free(handle: *mut DemandIndex) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `Doji` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_doji_free`.
#[no_mangle]
pub extern "C" fn wickra_doji_new() -> *mut Doji {
    Box::into_raw(Box::new(Doji::new()))
}

/// Feed one OHLCV candle; returns the output, or `NaN` during warmup / on a `NULL`
/// handle / if the candle is invalid (e.g. `high < low`).
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_doji_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_doji_update(
    handle: *mut Doji,
    open: f64,
    high: f64,
    low: f64,
    close: f64,
    volume: f64,
    timestamp: i64,
) -> f64 {
    let Some(ind) = handle.as_mut() else {
        return f64::NAN;
    };
    match Candle::new(open, high, low, close, volume, timestamp) {
        Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
        Err(_) => f64::NAN,
    }
}

/// Run over the OHLCV series (`open/high/low/close/volume/timestamp[0..n]`) into
/// `out[0..n]` (`NaN` at warmup or on an invalid candle).
///
/// # Safety
/// `handle` valid (from `wickra_doji_new`, not freed); every input pointer and `out`
/// cover `n` elements.
#[no_mangle]
pub unsafe extern "C" fn wickra_doji_batch(
    handle: *mut Doji,
    open: *const f64,
    high: *const f64,
    low: *const f64,
    close: *const f64,
    volume: *const f64,
    timestamp: *const i64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null()
        || open.is_null()
        || high.is_null()
        || low.is_null()
        || close.is_null()
        || volume.is_null()
        || timestamp.is_null()
        || out.is_null()
    {
        return;
    }
    let ind = &mut *handle;
    let opens = slice::from_raw_parts(open, n);
    let highs = slice::from_raw_parts(high, n);
    let lows = slice::from_raw_parts(low, n);
    let closes = slice::from_raw_parts(close, n);
    let volumes = slice::from_raw_parts(volume, n);
    let timestamps = slice::from_raw_parts(timestamp, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (idx, slot) in outputs.iter_mut().enumerate() {
        *slot = match Candle::new(
            opens[idx],
            highs[idx],
            lows[idx],
            closes[idx],
            volumes[idx],
            timestamps[idx],
        ) {
            Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
            Err(_) => f64::NAN,
        };
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_doji_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_doji_warmup_period(handle: *mut Doji) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_doji_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_doji_is_ready(handle: *mut Doji) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_doji_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_doji_name(handle: *mut Doji) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_doji_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_doji_reset(handle: *mut Doji) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_doji_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_doji_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_doji_free(handle: *mut Doji) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `DojiStar` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_doji_star_free`.
#[no_mangle]
pub extern "C" fn wickra_doji_star_new() -> *mut DojiStar {
    Box::into_raw(Box::new(DojiStar::new()))
}

/// Feed one OHLCV candle; returns the output, or `NaN` during warmup / on a `NULL`
/// handle / if the candle is invalid (e.g. `high < low`).
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_doji_star_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_doji_star_update(
    handle: *mut DojiStar,
    open: f64,
    high: f64,
    low: f64,
    close: f64,
    volume: f64,
    timestamp: i64,
) -> f64 {
    let Some(ind) = handle.as_mut() else {
        return f64::NAN;
    };
    match Candle::new(open, high, low, close, volume, timestamp) {
        Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
        Err(_) => f64::NAN,
    }
}

/// Run over the OHLCV series (`open/high/low/close/volume/timestamp[0..n]`) into
/// `out[0..n]` (`NaN` at warmup or on an invalid candle).
///
/// # Safety
/// `handle` valid (from `wickra_doji_star_new`, not freed); every input pointer and `out`
/// cover `n` elements.
#[no_mangle]
pub unsafe extern "C" fn wickra_doji_star_batch(
    handle: *mut DojiStar,
    open: *const f64,
    high: *const f64,
    low: *const f64,
    close: *const f64,
    volume: *const f64,
    timestamp: *const i64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null()
        || open.is_null()
        || high.is_null()
        || low.is_null()
        || close.is_null()
        || volume.is_null()
        || timestamp.is_null()
        || out.is_null()
    {
        return;
    }
    let ind = &mut *handle;
    let opens = slice::from_raw_parts(open, n);
    let highs = slice::from_raw_parts(high, n);
    let lows = slice::from_raw_parts(low, n);
    let closes = slice::from_raw_parts(close, n);
    let volumes = slice::from_raw_parts(volume, n);
    let timestamps = slice::from_raw_parts(timestamp, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (idx, slot) in outputs.iter_mut().enumerate() {
        *slot = match Candle::new(
            opens[idx],
            highs[idx],
            lows[idx],
            closes[idx],
            volumes[idx],
            timestamps[idx],
        ) {
            Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
            Err(_) => f64::NAN,
        };
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_doji_star_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_doji_star_warmup_period(handle: *mut DojiStar) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_doji_star_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_doji_star_is_ready(handle: *mut DojiStar) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_doji_star_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_doji_star_name(handle: *mut DojiStar) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_doji_star_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_doji_star_reset(handle: *mut DojiStar) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_doji_star_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_doji_star_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_doji_star_free(handle: *mut DojiStar) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `DoubleTopBottom` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_double_top_bottom_free`.
#[no_mangle]
pub extern "C" fn wickra_double_top_bottom_new() -> *mut DoubleTopBottom {
    Box::into_raw(Box::new(DoubleTopBottom::new()))
}

/// Feed one OHLCV candle; returns the output, or `NaN` during warmup / on a `NULL`
/// handle / if the candle is invalid (e.g. `high < low`).
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_double_top_bottom_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_double_top_bottom_update(
    handle: *mut DoubleTopBottom,
    open: f64,
    high: f64,
    low: f64,
    close: f64,
    volume: f64,
    timestamp: i64,
) -> f64 {
    let Some(ind) = handle.as_mut() else {
        return f64::NAN;
    };
    match Candle::new(open, high, low, close, volume, timestamp) {
        Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
        Err(_) => f64::NAN,
    }
}

/// Run over the OHLCV series (`open/high/low/close/volume/timestamp[0..n]`) into
/// `out[0..n]` (`NaN` at warmup or on an invalid candle).
///
/// # Safety
/// `handle` valid (from `wickra_double_top_bottom_new`, not freed); every input pointer and `out`
/// cover `n` elements.
#[no_mangle]
pub unsafe extern "C" fn wickra_double_top_bottom_batch(
    handle: *mut DoubleTopBottom,
    open: *const f64,
    high: *const f64,
    low: *const f64,
    close: *const f64,
    volume: *const f64,
    timestamp: *const i64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null()
        || open.is_null()
        || high.is_null()
        || low.is_null()
        || close.is_null()
        || volume.is_null()
        || timestamp.is_null()
        || out.is_null()
    {
        return;
    }
    let ind = &mut *handle;
    let opens = slice::from_raw_parts(open, n);
    let highs = slice::from_raw_parts(high, n);
    let lows = slice::from_raw_parts(low, n);
    let closes = slice::from_raw_parts(close, n);
    let volumes = slice::from_raw_parts(volume, n);
    let timestamps = slice::from_raw_parts(timestamp, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (idx, slot) in outputs.iter_mut().enumerate() {
        *slot = match Candle::new(
            opens[idx],
            highs[idx],
            lows[idx],
            closes[idx],
            volumes[idx],
            timestamps[idx],
        ) {
            Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
            Err(_) => f64::NAN,
        };
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_double_top_bottom_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_double_top_bottom_warmup_period(
    handle: *mut DoubleTopBottom,
) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_double_top_bottom_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_double_top_bottom_is_ready(handle: *mut DoubleTopBottom) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_double_top_bottom_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_double_top_bottom_name(
    handle: *mut DoubleTopBottom,
) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_double_top_bottom_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_double_top_bottom_reset(handle: *mut DoubleTopBottom) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_double_top_bottom_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_double_top_bottom_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_double_top_bottom_free(handle: *mut DoubleTopBottom) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `DownsideGapThreeMethods` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_downside_gap_three_methods_free`.
#[no_mangle]
pub extern "C" fn wickra_downside_gap_three_methods_new() -> *mut DownsideGapThreeMethods {
    Box::into_raw(Box::new(DownsideGapThreeMethods::new()))
}

/// Feed one OHLCV candle; returns the output, or `NaN` during warmup / on a `NULL`
/// handle / if the candle is invalid (e.g. `high < low`).
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_downside_gap_three_methods_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_downside_gap_three_methods_update(
    handle: *mut DownsideGapThreeMethods,
    open: f64,
    high: f64,
    low: f64,
    close: f64,
    volume: f64,
    timestamp: i64,
) -> f64 {
    let Some(ind) = handle.as_mut() else {
        return f64::NAN;
    };
    match Candle::new(open, high, low, close, volume, timestamp) {
        Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
        Err(_) => f64::NAN,
    }
}

/// Run over the OHLCV series (`open/high/low/close/volume/timestamp[0..n]`) into
/// `out[0..n]` (`NaN` at warmup or on an invalid candle).
///
/// # Safety
/// `handle` valid (from `wickra_downside_gap_three_methods_new`, not freed); every input pointer and `out`
/// cover `n` elements.
#[no_mangle]
pub unsafe extern "C" fn wickra_downside_gap_three_methods_batch(
    handle: *mut DownsideGapThreeMethods,
    open: *const f64,
    high: *const f64,
    low: *const f64,
    close: *const f64,
    volume: *const f64,
    timestamp: *const i64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null()
        || open.is_null()
        || high.is_null()
        || low.is_null()
        || close.is_null()
        || volume.is_null()
        || timestamp.is_null()
        || out.is_null()
    {
        return;
    }
    let ind = &mut *handle;
    let opens = slice::from_raw_parts(open, n);
    let highs = slice::from_raw_parts(high, n);
    let lows = slice::from_raw_parts(low, n);
    let closes = slice::from_raw_parts(close, n);
    let volumes = slice::from_raw_parts(volume, n);
    let timestamps = slice::from_raw_parts(timestamp, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (idx, slot) in outputs.iter_mut().enumerate() {
        *slot = match Candle::new(
            opens[idx],
            highs[idx],
            lows[idx],
            closes[idx],
            volumes[idx],
            timestamps[idx],
        ) {
            Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
            Err(_) => f64::NAN,
        };
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_downside_gap_three_methods_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_downside_gap_three_methods_warmup_period(
    handle: *mut DownsideGapThreeMethods,
) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_downside_gap_three_methods_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_downside_gap_three_methods_is_ready(
    handle: *mut DownsideGapThreeMethods,
) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_downside_gap_three_methods_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_downside_gap_three_methods_name(
    handle: *mut DownsideGapThreeMethods,
) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_downside_gap_three_methods_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_downside_gap_three_methods_reset(
    handle: *mut DownsideGapThreeMethods,
) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_downside_gap_three_methods_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_downside_gap_three_methods_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_downside_gap_three_methods_free(
    handle: *mut DownsideGapThreeMethods,
) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `DragonflyDoji` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_dragonfly_doji_free`.
#[no_mangle]
pub extern "C" fn wickra_dragonfly_doji_new() -> *mut DragonflyDoji {
    Box::into_raw(Box::new(DragonflyDoji::new()))
}

/// Feed one OHLCV candle; returns the output, or `NaN` during warmup / on a `NULL`
/// handle / if the candle is invalid (e.g. `high < low`).
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_dragonfly_doji_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_dragonfly_doji_update(
    handle: *mut DragonflyDoji,
    open: f64,
    high: f64,
    low: f64,
    close: f64,
    volume: f64,
    timestamp: i64,
) -> f64 {
    let Some(ind) = handle.as_mut() else {
        return f64::NAN;
    };
    match Candle::new(open, high, low, close, volume, timestamp) {
        Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
        Err(_) => f64::NAN,
    }
}

/// Run over the OHLCV series (`open/high/low/close/volume/timestamp[0..n]`) into
/// `out[0..n]` (`NaN` at warmup or on an invalid candle).
///
/// # Safety
/// `handle` valid (from `wickra_dragonfly_doji_new`, not freed); every input pointer and `out`
/// cover `n` elements.
#[no_mangle]
pub unsafe extern "C" fn wickra_dragonfly_doji_batch(
    handle: *mut DragonflyDoji,
    open: *const f64,
    high: *const f64,
    low: *const f64,
    close: *const f64,
    volume: *const f64,
    timestamp: *const i64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null()
        || open.is_null()
        || high.is_null()
        || low.is_null()
        || close.is_null()
        || volume.is_null()
        || timestamp.is_null()
        || out.is_null()
    {
        return;
    }
    let ind = &mut *handle;
    let opens = slice::from_raw_parts(open, n);
    let highs = slice::from_raw_parts(high, n);
    let lows = slice::from_raw_parts(low, n);
    let closes = slice::from_raw_parts(close, n);
    let volumes = slice::from_raw_parts(volume, n);
    let timestamps = slice::from_raw_parts(timestamp, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (idx, slot) in outputs.iter_mut().enumerate() {
        *slot = match Candle::new(
            opens[idx],
            highs[idx],
            lows[idx],
            closes[idx],
            volumes[idx],
            timestamps[idx],
        ) {
            Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
            Err(_) => f64::NAN,
        };
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_dragonfly_doji_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_dragonfly_doji_warmup_period(handle: *mut DragonflyDoji) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_dragonfly_doji_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_dragonfly_doji_is_ready(handle: *mut DragonflyDoji) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_dragonfly_doji_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_dragonfly_doji_name(handle: *mut DragonflyDoji) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_dragonfly_doji_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_dragonfly_doji_reset(handle: *mut DragonflyDoji) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_dragonfly_doji_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_dragonfly_doji_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_dragonfly_doji_free(handle: *mut DragonflyDoji) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `DumplingTop` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_dumpling_top_free`.
#[no_mangle]
pub extern "C" fn wickra_dumpling_top_new(period: usize) -> *mut DumplingTop {
    match DumplingTop::new(period) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one OHLCV candle; returns the output, or `NaN` during warmup / on a `NULL`
/// handle / if the candle is invalid (e.g. `high < low`).
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_dumpling_top_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_dumpling_top_update(
    handle: *mut DumplingTop,
    open: f64,
    high: f64,
    low: f64,
    close: f64,
    volume: f64,
    timestamp: i64,
) -> f64 {
    let Some(ind) = handle.as_mut() else {
        return f64::NAN;
    };
    match Candle::new(open, high, low, close, volume, timestamp) {
        Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
        Err(_) => f64::NAN,
    }
}

/// Run over the OHLCV series (`open/high/low/close/volume/timestamp[0..n]`) into
/// `out[0..n]` (`NaN` at warmup or on an invalid candle).
///
/// # Safety
/// `handle` valid (from `wickra_dumpling_top_new`, not freed); every input pointer and `out`
/// cover `n` elements.
#[no_mangle]
pub unsafe extern "C" fn wickra_dumpling_top_batch(
    handle: *mut DumplingTop,
    open: *const f64,
    high: *const f64,
    low: *const f64,
    close: *const f64,
    volume: *const f64,
    timestamp: *const i64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null()
        || open.is_null()
        || high.is_null()
        || low.is_null()
        || close.is_null()
        || volume.is_null()
        || timestamp.is_null()
        || out.is_null()
    {
        return;
    }
    let ind = &mut *handle;
    let opens = slice::from_raw_parts(open, n);
    let highs = slice::from_raw_parts(high, n);
    let lows = slice::from_raw_parts(low, n);
    let closes = slice::from_raw_parts(close, n);
    let volumes = slice::from_raw_parts(volume, n);
    let timestamps = slice::from_raw_parts(timestamp, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (idx, slot) in outputs.iter_mut().enumerate() {
        *slot = match Candle::new(
            opens[idx],
            highs[idx],
            lows[idx],
            closes[idx],
            volumes[idx],
            timestamps[idx],
        ) {
            Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
            Err(_) => f64::NAN,
        };
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_dumpling_top_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_dumpling_top_warmup_period(handle: *mut DumplingTop) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_dumpling_top_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_dumpling_top_is_ready(handle: *mut DumplingTop) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_dumpling_top_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_dumpling_top_name(handle: *mut DumplingTop) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_dumpling_top_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_dumpling_top_reset(handle: *mut DumplingTop) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_dumpling_top_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_dumpling_top_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_dumpling_top_free(handle: *mut DumplingTop) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `Dx` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_dx_free`.
#[no_mangle]
pub extern "C" fn wickra_dx_new(period: usize) -> *mut Dx {
    match Dx::new(period) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one OHLCV candle; returns the output, or `NaN` during warmup / on a `NULL`
/// handle / if the candle is invalid (e.g. `high < low`).
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_dx_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_dx_update(
    handle: *mut Dx,
    open: f64,
    high: f64,
    low: f64,
    close: f64,
    volume: f64,
    timestamp: i64,
) -> f64 {
    let Some(ind) = handle.as_mut() else {
        return f64::NAN;
    };
    match Candle::new(open, high, low, close, volume, timestamp) {
        Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
        Err(_) => f64::NAN,
    }
}

/// Run over the OHLCV series (`open/high/low/close/volume/timestamp[0..n]`) into
/// `out[0..n]` (`NaN` at warmup or on an invalid candle).
///
/// # Safety
/// `handle` valid (from `wickra_dx_new`, not freed); every input pointer and `out`
/// cover `n` elements.
#[no_mangle]
pub unsafe extern "C" fn wickra_dx_batch(
    handle: *mut Dx,
    open: *const f64,
    high: *const f64,
    low: *const f64,
    close: *const f64,
    volume: *const f64,
    timestamp: *const i64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null()
        || open.is_null()
        || high.is_null()
        || low.is_null()
        || close.is_null()
        || volume.is_null()
        || timestamp.is_null()
        || out.is_null()
    {
        return;
    }
    let ind = &mut *handle;
    let opens = slice::from_raw_parts(open, n);
    let highs = slice::from_raw_parts(high, n);
    let lows = slice::from_raw_parts(low, n);
    let closes = slice::from_raw_parts(close, n);
    let volumes = slice::from_raw_parts(volume, n);
    let timestamps = slice::from_raw_parts(timestamp, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (idx, slot) in outputs.iter_mut().enumerate() {
        *slot = match Candle::new(
            opens[idx],
            highs[idx],
            lows[idx],
            closes[idx],
            volumes[idx],
            timestamps[idx],
        ) {
            Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
            Err(_) => f64::NAN,
        };
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_dx_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_dx_warmup_period(handle: *mut Dx) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_dx_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_dx_is_ready(handle: *mut Dx) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_dx_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_dx_name(handle: *mut Dx) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_dx_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_dx_reset(handle: *mut Dx) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_dx_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_dx_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_dx_free(handle: *mut Dx) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `EaseOfMovement` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_ease_of_movement_free`.
#[no_mangle]
pub extern "C" fn wickra_ease_of_movement_new(period: usize) -> *mut EaseOfMovement {
    match EaseOfMovement::new(period) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one OHLCV candle; returns the output, or `NaN` during warmup / on a `NULL`
/// handle / if the candle is invalid (e.g. `high < low`).
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_ease_of_movement_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_ease_of_movement_update(
    handle: *mut EaseOfMovement,
    open: f64,
    high: f64,
    low: f64,
    close: f64,
    volume: f64,
    timestamp: i64,
) -> f64 {
    let Some(ind) = handle.as_mut() else {
        return f64::NAN;
    };
    match Candle::new(open, high, low, close, volume, timestamp) {
        Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
        Err(_) => f64::NAN,
    }
}

/// Run over the OHLCV series (`open/high/low/close/volume/timestamp[0..n]`) into
/// `out[0..n]` (`NaN` at warmup or on an invalid candle).
///
/// # Safety
/// `handle` valid (from `wickra_ease_of_movement_new`, not freed); every input pointer and `out`
/// cover `n` elements.
#[no_mangle]
pub unsafe extern "C" fn wickra_ease_of_movement_batch(
    handle: *mut EaseOfMovement,
    open: *const f64,
    high: *const f64,
    low: *const f64,
    close: *const f64,
    volume: *const f64,
    timestamp: *const i64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null()
        || open.is_null()
        || high.is_null()
        || low.is_null()
        || close.is_null()
        || volume.is_null()
        || timestamp.is_null()
        || out.is_null()
    {
        return;
    }
    let ind = &mut *handle;
    let opens = slice::from_raw_parts(open, n);
    let highs = slice::from_raw_parts(high, n);
    let lows = slice::from_raw_parts(low, n);
    let closes = slice::from_raw_parts(close, n);
    let volumes = slice::from_raw_parts(volume, n);
    let timestamps = slice::from_raw_parts(timestamp, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (idx, slot) in outputs.iter_mut().enumerate() {
        *slot = match Candle::new(
            opens[idx],
            highs[idx],
            lows[idx],
            closes[idx],
            volumes[idx],
            timestamps[idx],
        ) {
            Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
            Err(_) => f64::NAN,
        };
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_ease_of_movement_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_ease_of_movement_warmup_period(
    handle: *mut EaseOfMovement,
) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_ease_of_movement_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_ease_of_movement_is_ready(handle: *mut EaseOfMovement) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_ease_of_movement_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_ease_of_movement_name(
    handle: *mut EaseOfMovement,
) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_ease_of_movement_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_ease_of_movement_reset(handle: *mut EaseOfMovement) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_ease_of_movement_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_ease_of_movement_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_ease_of_movement_free(handle: *mut EaseOfMovement) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `Engulfing` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_engulfing_free`.
#[no_mangle]
pub extern "C" fn wickra_engulfing_new() -> *mut Engulfing {
    Box::into_raw(Box::new(Engulfing::new()))
}

/// Feed one OHLCV candle; returns the output, or `NaN` during warmup / on a `NULL`
/// handle / if the candle is invalid (e.g. `high < low`).
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_engulfing_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_engulfing_update(
    handle: *mut Engulfing,
    open: f64,
    high: f64,
    low: f64,
    close: f64,
    volume: f64,
    timestamp: i64,
) -> f64 {
    let Some(ind) = handle.as_mut() else {
        return f64::NAN;
    };
    match Candle::new(open, high, low, close, volume, timestamp) {
        Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
        Err(_) => f64::NAN,
    }
}

/// Run over the OHLCV series (`open/high/low/close/volume/timestamp[0..n]`) into
/// `out[0..n]` (`NaN` at warmup or on an invalid candle).
///
/// # Safety
/// `handle` valid (from `wickra_engulfing_new`, not freed); every input pointer and `out`
/// cover `n` elements.
#[no_mangle]
pub unsafe extern "C" fn wickra_engulfing_batch(
    handle: *mut Engulfing,
    open: *const f64,
    high: *const f64,
    low: *const f64,
    close: *const f64,
    volume: *const f64,
    timestamp: *const i64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null()
        || open.is_null()
        || high.is_null()
        || low.is_null()
        || close.is_null()
        || volume.is_null()
        || timestamp.is_null()
        || out.is_null()
    {
        return;
    }
    let ind = &mut *handle;
    let opens = slice::from_raw_parts(open, n);
    let highs = slice::from_raw_parts(high, n);
    let lows = slice::from_raw_parts(low, n);
    let closes = slice::from_raw_parts(close, n);
    let volumes = slice::from_raw_parts(volume, n);
    let timestamps = slice::from_raw_parts(timestamp, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (idx, slot) in outputs.iter_mut().enumerate() {
        *slot = match Candle::new(
            opens[idx],
            highs[idx],
            lows[idx],
            closes[idx],
            volumes[idx],
            timestamps[idx],
        ) {
            Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
            Err(_) => f64::NAN,
        };
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_engulfing_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_engulfing_warmup_period(handle: *mut Engulfing) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_engulfing_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_engulfing_is_ready(handle: *mut Engulfing) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_engulfing_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_engulfing_name(handle: *mut Engulfing) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_engulfing_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_engulfing_reset(handle: *mut Engulfing) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_engulfing_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_engulfing_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_engulfing_free(handle: *mut Engulfing) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `EveningDojiStar` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_evening_doji_star_free`.
#[no_mangle]
pub extern "C" fn wickra_evening_doji_star_new() -> *mut EveningDojiStar {
    Box::into_raw(Box::new(EveningDojiStar::new()))
}

/// Feed one OHLCV candle; returns the output, or `NaN` during warmup / on a `NULL`
/// handle / if the candle is invalid (e.g. `high < low`).
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_evening_doji_star_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_evening_doji_star_update(
    handle: *mut EveningDojiStar,
    open: f64,
    high: f64,
    low: f64,
    close: f64,
    volume: f64,
    timestamp: i64,
) -> f64 {
    let Some(ind) = handle.as_mut() else {
        return f64::NAN;
    };
    match Candle::new(open, high, low, close, volume, timestamp) {
        Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
        Err(_) => f64::NAN,
    }
}

/// Run over the OHLCV series (`open/high/low/close/volume/timestamp[0..n]`) into
/// `out[0..n]` (`NaN` at warmup or on an invalid candle).
///
/// # Safety
/// `handle` valid (from `wickra_evening_doji_star_new`, not freed); every input pointer and `out`
/// cover `n` elements.
#[no_mangle]
pub unsafe extern "C" fn wickra_evening_doji_star_batch(
    handle: *mut EveningDojiStar,
    open: *const f64,
    high: *const f64,
    low: *const f64,
    close: *const f64,
    volume: *const f64,
    timestamp: *const i64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null()
        || open.is_null()
        || high.is_null()
        || low.is_null()
        || close.is_null()
        || volume.is_null()
        || timestamp.is_null()
        || out.is_null()
    {
        return;
    }
    let ind = &mut *handle;
    let opens = slice::from_raw_parts(open, n);
    let highs = slice::from_raw_parts(high, n);
    let lows = slice::from_raw_parts(low, n);
    let closes = slice::from_raw_parts(close, n);
    let volumes = slice::from_raw_parts(volume, n);
    let timestamps = slice::from_raw_parts(timestamp, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (idx, slot) in outputs.iter_mut().enumerate() {
        *slot = match Candle::new(
            opens[idx],
            highs[idx],
            lows[idx],
            closes[idx],
            volumes[idx],
            timestamps[idx],
        ) {
            Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
            Err(_) => f64::NAN,
        };
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_evening_doji_star_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_evening_doji_star_warmup_period(
    handle: *mut EveningDojiStar,
) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_evening_doji_star_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_evening_doji_star_is_ready(handle: *mut EveningDojiStar) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_evening_doji_star_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_evening_doji_star_name(
    handle: *mut EveningDojiStar,
) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_evening_doji_star_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_evening_doji_star_reset(handle: *mut EveningDojiStar) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_evening_doji_star_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_evening_doji_star_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_evening_doji_star_free(handle: *mut EveningDojiStar) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `Evwma` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_evwma_free`.
#[no_mangle]
pub extern "C" fn wickra_evwma_new(period: usize) -> *mut Evwma {
    match Evwma::new(period) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one OHLCV candle; returns the output, or `NaN` during warmup / on a `NULL`
/// handle / if the candle is invalid (e.g. `high < low`).
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_evwma_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_evwma_update(
    handle: *mut Evwma,
    open: f64,
    high: f64,
    low: f64,
    close: f64,
    volume: f64,
    timestamp: i64,
) -> f64 {
    let Some(ind) = handle.as_mut() else {
        return f64::NAN;
    };
    match Candle::new(open, high, low, close, volume, timestamp) {
        Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
        Err(_) => f64::NAN,
    }
}

/// Run over the OHLCV series (`open/high/low/close/volume/timestamp[0..n]`) into
/// `out[0..n]` (`NaN` at warmup or on an invalid candle).
///
/// # Safety
/// `handle` valid (from `wickra_evwma_new`, not freed); every input pointer and `out`
/// cover `n` elements.
#[no_mangle]
pub unsafe extern "C" fn wickra_evwma_batch(
    handle: *mut Evwma,
    open: *const f64,
    high: *const f64,
    low: *const f64,
    close: *const f64,
    volume: *const f64,
    timestamp: *const i64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null()
        || open.is_null()
        || high.is_null()
        || low.is_null()
        || close.is_null()
        || volume.is_null()
        || timestamp.is_null()
        || out.is_null()
    {
        return;
    }
    let ind = &mut *handle;
    let opens = slice::from_raw_parts(open, n);
    let highs = slice::from_raw_parts(high, n);
    let lows = slice::from_raw_parts(low, n);
    let closes = slice::from_raw_parts(close, n);
    let volumes = slice::from_raw_parts(volume, n);
    let timestamps = slice::from_raw_parts(timestamp, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (idx, slot) in outputs.iter_mut().enumerate() {
        *slot = match Candle::new(
            opens[idx],
            highs[idx],
            lows[idx],
            closes[idx],
            volumes[idx],
            timestamps[idx],
        ) {
            Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
            Err(_) => f64::NAN,
        };
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_evwma_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_evwma_warmup_period(handle: *mut Evwma) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_evwma_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_evwma_is_ready(handle: *mut Evwma) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_evwma_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_evwma_name(handle: *mut Evwma) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_evwma_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_evwma_reset(handle: *mut Evwma) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_evwma_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_evwma_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_evwma_free(handle: *mut Evwma) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `FallingThreeMethods` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_falling_three_methods_free`.
#[no_mangle]
pub extern "C" fn wickra_falling_three_methods_new() -> *mut FallingThreeMethods {
    Box::into_raw(Box::new(FallingThreeMethods::new()))
}

/// Feed one OHLCV candle; returns the output, or `NaN` during warmup / on a `NULL`
/// handle / if the candle is invalid (e.g. `high < low`).
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_falling_three_methods_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_falling_three_methods_update(
    handle: *mut FallingThreeMethods,
    open: f64,
    high: f64,
    low: f64,
    close: f64,
    volume: f64,
    timestamp: i64,
) -> f64 {
    let Some(ind) = handle.as_mut() else {
        return f64::NAN;
    };
    match Candle::new(open, high, low, close, volume, timestamp) {
        Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
        Err(_) => f64::NAN,
    }
}

/// Run over the OHLCV series (`open/high/low/close/volume/timestamp[0..n]`) into
/// `out[0..n]` (`NaN` at warmup or on an invalid candle).
///
/// # Safety
/// `handle` valid (from `wickra_falling_three_methods_new`, not freed); every input pointer and `out`
/// cover `n` elements.
#[no_mangle]
pub unsafe extern "C" fn wickra_falling_three_methods_batch(
    handle: *mut FallingThreeMethods,
    open: *const f64,
    high: *const f64,
    low: *const f64,
    close: *const f64,
    volume: *const f64,
    timestamp: *const i64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null()
        || open.is_null()
        || high.is_null()
        || low.is_null()
        || close.is_null()
        || volume.is_null()
        || timestamp.is_null()
        || out.is_null()
    {
        return;
    }
    let ind = &mut *handle;
    let opens = slice::from_raw_parts(open, n);
    let highs = slice::from_raw_parts(high, n);
    let lows = slice::from_raw_parts(low, n);
    let closes = slice::from_raw_parts(close, n);
    let volumes = slice::from_raw_parts(volume, n);
    let timestamps = slice::from_raw_parts(timestamp, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (idx, slot) in outputs.iter_mut().enumerate() {
        *slot = match Candle::new(
            opens[idx],
            highs[idx],
            lows[idx],
            closes[idx],
            volumes[idx],
            timestamps[idx],
        ) {
            Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
            Err(_) => f64::NAN,
        };
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_falling_three_methods_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_falling_three_methods_warmup_period(
    handle: *mut FallingThreeMethods,
) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_falling_three_methods_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_falling_three_methods_is_ready(
    handle: *mut FallingThreeMethods,
) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_falling_three_methods_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_falling_three_methods_name(
    handle: *mut FallingThreeMethods,
) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_falling_three_methods_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_falling_three_methods_reset(handle: *mut FallingThreeMethods) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_falling_three_methods_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_falling_three_methods_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_falling_three_methods_free(handle: *mut FallingThreeMethods) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `FlagPennant` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_flag_pennant_free`.
#[no_mangle]
pub extern "C" fn wickra_flag_pennant_new() -> *mut FlagPennant {
    Box::into_raw(Box::new(FlagPennant::new()))
}

/// Feed one OHLCV candle; returns the output, or `NaN` during warmup / on a `NULL`
/// handle / if the candle is invalid (e.g. `high < low`).
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_flag_pennant_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_flag_pennant_update(
    handle: *mut FlagPennant,
    open: f64,
    high: f64,
    low: f64,
    close: f64,
    volume: f64,
    timestamp: i64,
) -> f64 {
    let Some(ind) = handle.as_mut() else {
        return f64::NAN;
    };
    match Candle::new(open, high, low, close, volume, timestamp) {
        Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
        Err(_) => f64::NAN,
    }
}

/// Run over the OHLCV series (`open/high/low/close/volume/timestamp[0..n]`) into
/// `out[0..n]` (`NaN` at warmup or on an invalid candle).
///
/// # Safety
/// `handle` valid (from `wickra_flag_pennant_new`, not freed); every input pointer and `out`
/// cover `n` elements.
#[no_mangle]
pub unsafe extern "C" fn wickra_flag_pennant_batch(
    handle: *mut FlagPennant,
    open: *const f64,
    high: *const f64,
    low: *const f64,
    close: *const f64,
    volume: *const f64,
    timestamp: *const i64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null()
        || open.is_null()
        || high.is_null()
        || low.is_null()
        || close.is_null()
        || volume.is_null()
        || timestamp.is_null()
        || out.is_null()
    {
        return;
    }
    let ind = &mut *handle;
    let opens = slice::from_raw_parts(open, n);
    let highs = slice::from_raw_parts(high, n);
    let lows = slice::from_raw_parts(low, n);
    let closes = slice::from_raw_parts(close, n);
    let volumes = slice::from_raw_parts(volume, n);
    let timestamps = slice::from_raw_parts(timestamp, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (idx, slot) in outputs.iter_mut().enumerate() {
        *slot = match Candle::new(
            opens[idx],
            highs[idx],
            lows[idx],
            closes[idx],
            volumes[idx],
            timestamps[idx],
        ) {
            Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
            Err(_) => f64::NAN,
        };
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_flag_pennant_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_flag_pennant_warmup_period(handle: *mut FlagPennant) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_flag_pennant_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_flag_pennant_is_ready(handle: *mut FlagPennant) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_flag_pennant_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_flag_pennant_name(handle: *mut FlagPennant) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_flag_pennant_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_flag_pennant_reset(handle: *mut FlagPennant) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_flag_pennant_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_flag_pennant_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_flag_pennant_free(handle: *mut FlagPennant) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `ForceIndex` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_force_index_free`.
#[no_mangle]
pub extern "C" fn wickra_force_index_new(period: usize) -> *mut ForceIndex {
    match ForceIndex::new(period) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one OHLCV candle; returns the output, or `NaN` during warmup / on a `NULL`
/// handle / if the candle is invalid (e.g. `high < low`).
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_force_index_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_force_index_update(
    handle: *mut ForceIndex,
    open: f64,
    high: f64,
    low: f64,
    close: f64,
    volume: f64,
    timestamp: i64,
) -> f64 {
    let Some(ind) = handle.as_mut() else {
        return f64::NAN;
    };
    match Candle::new(open, high, low, close, volume, timestamp) {
        Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
        Err(_) => f64::NAN,
    }
}

/// Run over the OHLCV series (`open/high/low/close/volume/timestamp[0..n]`) into
/// `out[0..n]` (`NaN` at warmup or on an invalid candle).
///
/// # Safety
/// `handle` valid (from `wickra_force_index_new`, not freed); every input pointer and `out`
/// cover `n` elements.
#[no_mangle]
pub unsafe extern "C" fn wickra_force_index_batch(
    handle: *mut ForceIndex,
    open: *const f64,
    high: *const f64,
    low: *const f64,
    close: *const f64,
    volume: *const f64,
    timestamp: *const i64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null()
        || open.is_null()
        || high.is_null()
        || low.is_null()
        || close.is_null()
        || volume.is_null()
        || timestamp.is_null()
        || out.is_null()
    {
        return;
    }
    let ind = &mut *handle;
    let opens = slice::from_raw_parts(open, n);
    let highs = slice::from_raw_parts(high, n);
    let lows = slice::from_raw_parts(low, n);
    let closes = slice::from_raw_parts(close, n);
    let volumes = slice::from_raw_parts(volume, n);
    let timestamps = slice::from_raw_parts(timestamp, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (idx, slot) in outputs.iter_mut().enumerate() {
        *slot = match Candle::new(
            opens[idx],
            highs[idx],
            lows[idx],
            closes[idx],
            volumes[idx],
            timestamps[idx],
        ) {
            Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
            Err(_) => f64::NAN,
        };
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_force_index_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_force_index_warmup_period(handle: *mut ForceIndex) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_force_index_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_force_index_is_ready(handle: *mut ForceIndex) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_force_index_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_force_index_name(handle: *mut ForceIndex) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_force_index_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_force_index_reset(handle: *mut ForceIndex) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_force_index_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_force_index_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_force_index_free(handle: *mut ForceIndex) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `FryPanBottom` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_fry_pan_bottom_free`.
#[no_mangle]
pub extern "C" fn wickra_fry_pan_bottom_new(period: usize) -> *mut FryPanBottom {
    match FryPanBottom::new(period) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one OHLCV candle; returns the output, or `NaN` during warmup / on a `NULL`
/// handle / if the candle is invalid (e.g. `high < low`).
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_fry_pan_bottom_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_fry_pan_bottom_update(
    handle: *mut FryPanBottom,
    open: f64,
    high: f64,
    low: f64,
    close: f64,
    volume: f64,
    timestamp: i64,
) -> f64 {
    let Some(ind) = handle.as_mut() else {
        return f64::NAN;
    };
    match Candle::new(open, high, low, close, volume, timestamp) {
        Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
        Err(_) => f64::NAN,
    }
}

/// Run over the OHLCV series (`open/high/low/close/volume/timestamp[0..n]`) into
/// `out[0..n]` (`NaN` at warmup or on an invalid candle).
///
/// # Safety
/// `handle` valid (from `wickra_fry_pan_bottom_new`, not freed); every input pointer and `out`
/// cover `n` elements.
#[no_mangle]
pub unsafe extern "C" fn wickra_fry_pan_bottom_batch(
    handle: *mut FryPanBottom,
    open: *const f64,
    high: *const f64,
    low: *const f64,
    close: *const f64,
    volume: *const f64,
    timestamp: *const i64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null()
        || open.is_null()
        || high.is_null()
        || low.is_null()
        || close.is_null()
        || volume.is_null()
        || timestamp.is_null()
        || out.is_null()
    {
        return;
    }
    let ind = &mut *handle;
    let opens = slice::from_raw_parts(open, n);
    let highs = slice::from_raw_parts(high, n);
    let lows = slice::from_raw_parts(low, n);
    let closes = slice::from_raw_parts(close, n);
    let volumes = slice::from_raw_parts(volume, n);
    let timestamps = slice::from_raw_parts(timestamp, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (idx, slot) in outputs.iter_mut().enumerate() {
        *slot = match Candle::new(
            opens[idx],
            highs[idx],
            lows[idx],
            closes[idx],
            volumes[idx],
            timestamps[idx],
        ) {
            Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
            Err(_) => f64::NAN,
        };
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_fry_pan_bottom_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_fry_pan_bottom_warmup_period(handle: *mut FryPanBottom) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_fry_pan_bottom_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_fry_pan_bottom_is_ready(handle: *mut FryPanBottom) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_fry_pan_bottom_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_fry_pan_bottom_name(handle: *mut FryPanBottom) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_fry_pan_bottom_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_fry_pan_bottom_reset(handle: *mut FryPanBottom) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_fry_pan_bottom_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_fry_pan_bottom_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_fry_pan_bottom_free(handle: *mut FryPanBottom) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `GapSideBySideWhite` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_gap_side_by_side_white_free`.
#[no_mangle]
pub extern "C" fn wickra_gap_side_by_side_white_new() -> *mut GapSideBySideWhite {
    Box::into_raw(Box::new(GapSideBySideWhite::new()))
}

/// Feed one OHLCV candle; returns the output, or `NaN` during warmup / on a `NULL`
/// handle / if the candle is invalid (e.g. `high < low`).
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_gap_side_by_side_white_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_gap_side_by_side_white_update(
    handle: *mut GapSideBySideWhite,
    open: f64,
    high: f64,
    low: f64,
    close: f64,
    volume: f64,
    timestamp: i64,
) -> f64 {
    let Some(ind) = handle.as_mut() else {
        return f64::NAN;
    };
    match Candle::new(open, high, low, close, volume, timestamp) {
        Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
        Err(_) => f64::NAN,
    }
}

/// Run over the OHLCV series (`open/high/low/close/volume/timestamp[0..n]`) into
/// `out[0..n]` (`NaN` at warmup or on an invalid candle).
///
/// # Safety
/// `handle` valid (from `wickra_gap_side_by_side_white_new`, not freed); every input pointer and `out`
/// cover `n` elements.
#[no_mangle]
pub unsafe extern "C" fn wickra_gap_side_by_side_white_batch(
    handle: *mut GapSideBySideWhite,
    open: *const f64,
    high: *const f64,
    low: *const f64,
    close: *const f64,
    volume: *const f64,
    timestamp: *const i64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null()
        || open.is_null()
        || high.is_null()
        || low.is_null()
        || close.is_null()
        || volume.is_null()
        || timestamp.is_null()
        || out.is_null()
    {
        return;
    }
    let ind = &mut *handle;
    let opens = slice::from_raw_parts(open, n);
    let highs = slice::from_raw_parts(high, n);
    let lows = slice::from_raw_parts(low, n);
    let closes = slice::from_raw_parts(close, n);
    let volumes = slice::from_raw_parts(volume, n);
    let timestamps = slice::from_raw_parts(timestamp, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (idx, slot) in outputs.iter_mut().enumerate() {
        *slot = match Candle::new(
            opens[idx],
            highs[idx],
            lows[idx],
            closes[idx],
            volumes[idx],
            timestamps[idx],
        ) {
            Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
            Err(_) => f64::NAN,
        };
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_gap_side_by_side_white_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_gap_side_by_side_white_warmup_period(
    handle: *mut GapSideBySideWhite,
) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_gap_side_by_side_white_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_gap_side_by_side_white_is_ready(
    handle: *mut GapSideBySideWhite,
) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_gap_side_by_side_white_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_gap_side_by_side_white_name(
    handle: *mut GapSideBySideWhite,
) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_gap_side_by_side_white_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_gap_side_by_side_white_reset(handle: *mut GapSideBySideWhite) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_gap_side_by_side_white_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_gap_side_by_side_white_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_gap_side_by_side_white_free(handle: *mut GapSideBySideWhite) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `GarmanKlassVolatility` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_garman_klass_volatility_free`.
#[no_mangle]
pub extern "C" fn wickra_garman_klass_volatility_new(
    period: usize,
    trading_periods: usize,
) -> *mut GarmanKlassVolatility {
    match GarmanKlassVolatility::new(period, trading_periods) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one OHLCV candle; returns the output, or `NaN` during warmup / on a `NULL`
/// handle / if the candle is invalid (e.g. `high < low`).
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_garman_klass_volatility_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_garman_klass_volatility_update(
    handle: *mut GarmanKlassVolatility,
    open: f64,
    high: f64,
    low: f64,
    close: f64,
    volume: f64,
    timestamp: i64,
) -> f64 {
    let Some(ind) = handle.as_mut() else {
        return f64::NAN;
    };
    match Candle::new(open, high, low, close, volume, timestamp) {
        Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
        Err(_) => f64::NAN,
    }
}

/// Run over the OHLCV series (`open/high/low/close/volume/timestamp[0..n]`) into
/// `out[0..n]` (`NaN` at warmup or on an invalid candle).
///
/// # Safety
/// `handle` valid (from `wickra_garman_klass_volatility_new`, not freed); every input pointer and `out`
/// cover `n` elements.
#[no_mangle]
pub unsafe extern "C" fn wickra_garman_klass_volatility_batch(
    handle: *mut GarmanKlassVolatility,
    open: *const f64,
    high: *const f64,
    low: *const f64,
    close: *const f64,
    volume: *const f64,
    timestamp: *const i64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null()
        || open.is_null()
        || high.is_null()
        || low.is_null()
        || close.is_null()
        || volume.is_null()
        || timestamp.is_null()
        || out.is_null()
    {
        return;
    }
    let ind = &mut *handle;
    let opens = slice::from_raw_parts(open, n);
    let highs = slice::from_raw_parts(high, n);
    let lows = slice::from_raw_parts(low, n);
    let closes = slice::from_raw_parts(close, n);
    let volumes = slice::from_raw_parts(volume, n);
    let timestamps = slice::from_raw_parts(timestamp, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (idx, slot) in outputs.iter_mut().enumerate() {
        *slot = match Candle::new(
            opens[idx],
            highs[idx],
            lows[idx],
            closes[idx],
            volumes[idx],
            timestamps[idx],
        ) {
            Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
            Err(_) => f64::NAN,
        };
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_garman_klass_volatility_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_garman_klass_volatility_warmup_period(
    handle: *mut GarmanKlassVolatility,
) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_garman_klass_volatility_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_garman_klass_volatility_is_ready(
    handle: *mut GarmanKlassVolatility,
) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_garman_klass_volatility_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_garman_klass_volatility_name(
    handle: *mut GarmanKlassVolatility,
) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_garman_klass_volatility_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_garman_klass_volatility_reset(handle: *mut GarmanKlassVolatility) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_garman_klass_volatility_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_garman_klass_volatility_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_garman_klass_volatility_free(handle: *mut GarmanKlassVolatility) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `Gartley` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_gartley_free`.
#[no_mangle]
pub extern "C" fn wickra_gartley_new() -> *mut Gartley {
    Box::into_raw(Box::new(Gartley::new()))
}

/// Feed one OHLCV candle; returns the output, or `NaN` during warmup / on a `NULL`
/// handle / if the candle is invalid (e.g. `high < low`).
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_gartley_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_gartley_update(
    handle: *mut Gartley,
    open: f64,
    high: f64,
    low: f64,
    close: f64,
    volume: f64,
    timestamp: i64,
) -> f64 {
    let Some(ind) = handle.as_mut() else {
        return f64::NAN;
    };
    match Candle::new(open, high, low, close, volume, timestamp) {
        Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
        Err(_) => f64::NAN,
    }
}

/// Run over the OHLCV series (`open/high/low/close/volume/timestamp[0..n]`) into
/// `out[0..n]` (`NaN` at warmup or on an invalid candle).
///
/// # Safety
/// `handle` valid (from `wickra_gartley_new`, not freed); every input pointer and `out`
/// cover `n` elements.
#[no_mangle]
pub unsafe extern "C" fn wickra_gartley_batch(
    handle: *mut Gartley,
    open: *const f64,
    high: *const f64,
    low: *const f64,
    close: *const f64,
    volume: *const f64,
    timestamp: *const i64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null()
        || open.is_null()
        || high.is_null()
        || low.is_null()
        || close.is_null()
        || volume.is_null()
        || timestamp.is_null()
        || out.is_null()
    {
        return;
    }
    let ind = &mut *handle;
    let opens = slice::from_raw_parts(open, n);
    let highs = slice::from_raw_parts(high, n);
    let lows = slice::from_raw_parts(low, n);
    let closes = slice::from_raw_parts(close, n);
    let volumes = slice::from_raw_parts(volume, n);
    let timestamps = slice::from_raw_parts(timestamp, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (idx, slot) in outputs.iter_mut().enumerate() {
        *slot = match Candle::new(
            opens[idx],
            highs[idx],
            lows[idx],
            closes[idx],
            volumes[idx],
            timestamps[idx],
        ) {
            Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
            Err(_) => f64::NAN,
        };
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_gartley_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_gartley_warmup_period(handle: *mut Gartley) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_gartley_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_gartley_is_ready(handle: *mut Gartley) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_gartley_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_gartley_name(handle: *mut Gartley) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_gartley_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_gartley_reset(handle: *mut Gartley) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_gartley_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_gartley_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_gartley_free(handle: *mut Gartley) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `GravestoneDoji` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_gravestone_doji_free`.
#[no_mangle]
pub extern "C" fn wickra_gravestone_doji_new() -> *mut GravestoneDoji {
    Box::into_raw(Box::new(GravestoneDoji::new()))
}

/// Feed one OHLCV candle; returns the output, or `NaN` during warmup / on a `NULL`
/// handle / if the candle is invalid (e.g. `high < low`).
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_gravestone_doji_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_gravestone_doji_update(
    handle: *mut GravestoneDoji,
    open: f64,
    high: f64,
    low: f64,
    close: f64,
    volume: f64,
    timestamp: i64,
) -> f64 {
    let Some(ind) = handle.as_mut() else {
        return f64::NAN;
    };
    match Candle::new(open, high, low, close, volume, timestamp) {
        Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
        Err(_) => f64::NAN,
    }
}

/// Run over the OHLCV series (`open/high/low/close/volume/timestamp[0..n]`) into
/// `out[0..n]` (`NaN` at warmup or on an invalid candle).
///
/// # Safety
/// `handle` valid (from `wickra_gravestone_doji_new`, not freed); every input pointer and `out`
/// cover `n` elements.
#[no_mangle]
pub unsafe extern "C" fn wickra_gravestone_doji_batch(
    handle: *mut GravestoneDoji,
    open: *const f64,
    high: *const f64,
    low: *const f64,
    close: *const f64,
    volume: *const f64,
    timestamp: *const i64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null()
        || open.is_null()
        || high.is_null()
        || low.is_null()
        || close.is_null()
        || volume.is_null()
        || timestamp.is_null()
        || out.is_null()
    {
        return;
    }
    let ind = &mut *handle;
    let opens = slice::from_raw_parts(open, n);
    let highs = slice::from_raw_parts(high, n);
    let lows = slice::from_raw_parts(low, n);
    let closes = slice::from_raw_parts(close, n);
    let volumes = slice::from_raw_parts(volume, n);
    let timestamps = slice::from_raw_parts(timestamp, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (idx, slot) in outputs.iter_mut().enumerate() {
        *slot = match Candle::new(
            opens[idx],
            highs[idx],
            lows[idx],
            closes[idx],
            volumes[idx],
            timestamps[idx],
        ) {
            Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
            Err(_) => f64::NAN,
        };
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_gravestone_doji_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_gravestone_doji_warmup_period(
    handle: *mut GravestoneDoji,
) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_gravestone_doji_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_gravestone_doji_is_ready(handle: *mut GravestoneDoji) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_gravestone_doji_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_gravestone_doji_name(handle: *mut GravestoneDoji) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_gravestone_doji_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_gravestone_doji_reset(handle: *mut GravestoneDoji) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_gravestone_doji_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_gravestone_doji_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_gravestone_doji_free(handle: *mut GravestoneDoji) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `Hammer` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_hammer_free`.
#[no_mangle]
pub extern "C" fn wickra_hammer_new() -> *mut Hammer {
    Box::into_raw(Box::new(Hammer::new()))
}

/// Feed one OHLCV candle; returns the output, or `NaN` during warmup / on a `NULL`
/// handle / if the candle is invalid (e.g. `high < low`).
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_hammer_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_hammer_update(
    handle: *mut Hammer,
    open: f64,
    high: f64,
    low: f64,
    close: f64,
    volume: f64,
    timestamp: i64,
) -> f64 {
    let Some(ind) = handle.as_mut() else {
        return f64::NAN;
    };
    match Candle::new(open, high, low, close, volume, timestamp) {
        Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
        Err(_) => f64::NAN,
    }
}

/// Run over the OHLCV series (`open/high/low/close/volume/timestamp[0..n]`) into
/// `out[0..n]` (`NaN` at warmup or on an invalid candle).
///
/// # Safety
/// `handle` valid (from `wickra_hammer_new`, not freed); every input pointer and `out`
/// cover `n` elements.
#[no_mangle]
pub unsafe extern "C" fn wickra_hammer_batch(
    handle: *mut Hammer,
    open: *const f64,
    high: *const f64,
    low: *const f64,
    close: *const f64,
    volume: *const f64,
    timestamp: *const i64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null()
        || open.is_null()
        || high.is_null()
        || low.is_null()
        || close.is_null()
        || volume.is_null()
        || timestamp.is_null()
        || out.is_null()
    {
        return;
    }
    let ind = &mut *handle;
    let opens = slice::from_raw_parts(open, n);
    let highs = slice::from_raw_parts(high, n);
    let lows = slice::from_raw_parts(low, n);
    let closes = slice::from_raw_parts(close, n);
    let volumes = slice::from_raw_parts(volume, n);
    let timestamps = slice::from_raw_parts(timestamp, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (idx, slot) in outputs.iter_mut().enumerate() {
        *slot = match Candle::new(
            opens[idx],
            highs[idx],
            lows[idx],
            closes[idx],
            volumes[idx],
            timestamps[idx],
        ) {
            Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
            Err(_) => f64::NAN,
        };
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_hammer_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_hammer_warmup_period(handle: *mut Hammer) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_hammer_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_hammer_is_ready(handle: *mut Hammer) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_hammer_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_hammer_name(handle: *mut Hammer) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_hammer_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_hammer_reset(handle: *mut Hammer) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_hammer_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_hammer_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_hammer_free(handle: *mut Hammer) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `HangingMan` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_hanging_man_free`.
#[no_mangle]
pub extern "C" fn wickra_hanging_man_new() -> *mut HangingMan {
    Box::into_raw(Box::new(HangingMan::new()))
}

/// Feed one OHLCV candle; returns the output, or `NaN` during warmup / on a `NULL`
/// handle / if the candle is invalid (e.g. `high < low`).
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_hanging_man_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_hanging_man_update(
    handle: *mut HangingMan,
    open: f64,
    high: f64,
    low: f64,
    close: f64,
    volume: f64,
    timestamp: i64,
) -> f64 {
    let Some(ind) = handle.as_mut() else {
        return f64::NAN;
    };
    match Candle::new(open, high, low, close, volume, timestamp) {
        Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
        Err(_) => f64::NAN,
    }
}

/// Run over the OHLCV series (`open/high/low/close/volume/timestamp[0..n]`) into
/// `out[0..n]` (`NaN` at warmup or on an invalid candle).
///
/// # Safety
/// `handle` valid (from `wickra_hanging_man_new`, not freed); every input pointer and `out`
/// cover `n` elements.
#[no_mangle]
pub unsafe extern "C" fn wickra_hanging_man_batch(
    handle: *mut HangingMan,
    open: *const f64,
    high: *const f64,
    low: *const f64,
    close: *const f64,
    volume: *const f64,
    timestamp: *const i64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null()
        || open.is_null()
        || high.is_null()
        || low.is_null()
        || close.is_null()
        || volume.is_null()
        || timestamp.is_null()
        || out.is_null()
    {
        return;
    }
    let ind = &mut *handle;
    let opens = slice::from_raw_parts(open, n);
    let highs = slice::from_raw_parts(high, n);
    let lows = slice::from_raw_parts(low, n);
    let closes = slice::from_raw_parts(close, n);
    let volumes = slice::from_raw_parts(volume, n);
    let timestamps = slice::from_raw_parts(timestamp, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (idx, slot) in outputs.iter_mut().enumerate() {
        *slot = match Candle::new(
            opens[idx],
            highs[idx],
            lows[idx],
            closes[idx],
            volumes[idx],
            timestamps[idx],
        ) {
            Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
            Err(_) => f64::NAN,
        };
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_hanging_man_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_hanging_man_warmup_period(handle: *mut HangingMan) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_hanging_man_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_hanging_man_is_ready(handle: *mut HangingMan) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_hanging_man_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_hanging_man_name(handle: *mut HangingMan) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_hanging_man_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_hanging_man_reset(handle: *mut HangingMan) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_hanging_man_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_hanging_man_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_hanging_man_free(handle: *mut HangingMan) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `Harami` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_harami_free`.
#[no_mangle]
pub extern "C" fn wickra_harami_new() -> *mut Harami {
    Box::into_raw(Box::new(Harami::new()))
}

/// Feed one OHLCV candle; returns the output, or `NaN` during warmup / on a `NULL`
/// handle / if the candle is invalid (e.g. `high < low`).
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_harami_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_harami_update(
    handle: *mut Harami,
    open: f64,
    high: f64,
    low: f64,
    close: f64,
    volume: f64,
    timestamp: i64,
) -> f64 {
    let Some(ind) = handle.as_mut() else {
        return f64::NAN;
    };
    match Candle::new(open, high, low, close, volume, timestamp) {
        Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
        Err(_) => f64::NAN,
    }
}

/// Run over the OHLCV series (`open/high/low/close/volume/timestamp[0..n]`) into
/// `out[0..n]` (`NaN` at warmup or on an invalid candle).
///
/// # Safety
/// `handle` valid (from `wickra_harami_new`, not freed); every input pointer and `out`
/// cover `n` elements.
#[no_mangle]
pub unsafe extern "C" fn wickra_harami_batch(
    handle: *mut Harami,
    open: *const f64,
    high: *const f64,
    low: *const f64,
    close: *const f64,
    volume: *const f64,
    timestamp: *const i64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null()
        || open.is_null()
        || high.is_null()
        || low.is_null()
        || close.is_null()
        || volume.is_null()
        || timestamp.is_null()
        || out.is_null()
    {
        return;
    }
    let ind = &mut *handle;
    let opens = slice::from_raw_parts(open, n);
    let highs = slice::from_raw_parts(high, n);
    let lows = slice::from_raw_parts(low, n);
    let closes = slice::from_raw_parts(close, n);
    let volumes = slice::from_raw_parts(volume, n);
    let timestamps = slice::from_raw_parts(timestamp, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (idx, slot) in outputs.iter_mut().enumerate() {
        *slot = match Candle::new(
            opens[idx],
            highs[idx],
            lows[idx],
            closes[idx],
            volumes[idx],
            timestamps[idx],
        ) {
            Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
            Err(_) => f64::NAN,
        };
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_harami_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_harami_warmup_period(handle: *mut Harami) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_harami_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_harami_is_ready(handle: *mut Harami) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_harami_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_harami_name(handle: *mut Harami) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_harami_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_harami_reset(handle: *mut Harami) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_harami_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_harami_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_harami_free(handle: *mut Harami) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `HaramiCross` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_harami_cross_free`.
#[no_mangle]
pub extern "C" fn wickra_harami_cross_new() -> *mut HaramiCross {
    Box::into_raw(Box::new(HaramiCross::new()))
}

/// Feed one OHLCV candle; returns the output, or `NaN` during warmup / on a `NULL`
/// handle / if the candle is invalid (e.g. `high < low`).
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_harami_cross_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_harami_cross_update(
    handle: *mut HaramiCross,
    open: f64,
    high: f64,
    low: f64,
    close: f64,
    volume: f64,
    timestamp: i64,
) -> f64 {
    let Some(ind) = handle.as_mut() else {
        return f64::NAN;
    };
    match Candle::new(open, high, low, close, volume, timestamp) {
        Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
        Err(_) => f64::NAN,
    }
}

/// Run over the OHLCV series (`open/high/low/close/volume/timestamp[0..n]`) into
/// `out[0..n]` (`NaN` at warmup or on an invalid candle).
///
/// # Safety
/// `handle` valid (from `wickra_harami_cross_new`, not freed); every input pointer and `out`
/// cover `n` elements.
#[no_mangle]
pub unsafe extern "C" fn wickra_harami_cross_batch(
    handle: *mut HaramiCross,
    open: *const f64,
    high: *const f64,
    low: *const f64,
    close: *const f64,
    volume: *const f64,
    timestamp: *const i64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null()
        || open.is_null()
        || high.is_null()
        || low.is_null()
        || close.is_null()
        || volume.is_null()
        || timestamp.is_null()
        || out.is_null()
    {
        return;
    }
    let ind = &mut *handle;
    let opens = slice::from_raw_parts(open, n);
    let highs = slice::from_raw_parts(high, n);
    let lows = slice::from_raw_parts(low, n);
    let closes = slice::from_raw_parts(close, n);
    let volumes = slice::from_raw_parts(volume, n);
    let timestamps = slice::from_raw_parts(timestamp, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (idx, slot) in outputs.iter_mut().enumerate() {
        *slot = match Candle::new(
            opens[idx],
            highs[idx],
            lows[idx],
            closes[idx],
            volumes[idx],
            timestamps[idx],
        ) {
            Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
            Err(_) => f64::NAN,
        };
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_harami_cross_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_harami_cross_warmup_period(handle: *mut HaramiCross) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_harami_cross_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_harami_cross_is_ready(handle: *mut HaramiCross) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_harami_cross_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_harami_cross_name(handle: *mut HaramiCross) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_harami_cross_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_harami_cross_reset(handle: *mut HaramiCross) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_harami_cross_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_harami_cross_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_harami_cross_free(handle: *mut HaramiCross) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `HeadAndShoulders` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_head_and_shoulders_free`.
#[no_mangle]
pub extern "C" fn wickra_head_and_shoulders_new() -> *mut HeadAndShoulders {
    Box::into_raw(Box::new(HeadAndShoulders::new()))
}

/// Feed one OHLCV candle; returns the output, or `NaN` during warmup / on a `NULL`
/// handle / if the candle is invalid (e.g. `high < low`).
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_head_and_shoulders_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_head_and_shoulders_update(
    handle: *mut HeadAndShoulders,
    open: f64,
    high: f64,
    low: f64,
    close: f64,
    volume: f64,
    timestamp: i64,
) -> f64 {
    let Some(ind) = handle.as_mut() else {
        return f64::NAN;
    };
    match Candle::new(open, high, low, close, volume, timestamp) {
        Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
        Err(_) => f64::NAN,
    }
}

/// Run over the OHLCV series (`open/high/low/close/volume/timestamp[0..n]`) into
/// `out[0..n]` (`NaN` at warmup or on an invalid candle).
///
/// # Safety
/// `handle` valid (from `wickra_head_and_shoulders_new`, not freed); every input pointer and `out`
/// cover `n` elements.
#[no_mangle]
pub unsafe extern "C" fn wickra_head_and_shoulders_batch(
    handle: *mut HeadAndShoulders,
    open: *const f64,
    high: *const f64,
    low: *const f64,
    close: *const f64,
    volume: *const f64,
    timestamp: *const i64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null()
        || open.is_null()
        || high.is_null()
        || low.is_null()
        || close.is_null()
        || volume.is_null()
        || timestamp.is_null()
        || out.is_null()
    {
        return;
    }
    let ind = &mut *handle;
    let opens = slice::from_raw_parts(open, n);
    let highs = slice::from_raw_parts(high, n);
    let lows = slice::from_raw_parts(low, n);
    let closes = slice::from_raw_parts(close, n);
    let volumes = slice::from_raw_parts(volume, n);
    let timestamps = slice::from_raw_parts(timestamp, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (idx, slot) in outputs.iter_mut().enumerate() {
        *slot = match Candle::new(
            opens[idx],
            highs[idx],
            lows[idx],
            closes[idx],
            volumes[idx],
            timestamps[idx],
        ) {
            Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
            Err(_) => f64::NAN,
        };
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_head_and_shoulders_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_head_and_shoulders_warmup_period(
    handle: *mut HeadAndShoulders,
) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_head_and_shoulders_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_head_and_shoulders_is_ready(handle: *mut HeadAndShoulders) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_head_and_shoulders_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_head_and_shoulders_name(
    handle: *mut HeadAndShoulders,
) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_head_and_shoulders_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_head_and_shoulders_reset(handle: *mut HeadAndShoulders) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_head_and_shoulders_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_head_and_shoulders_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_head_and_shoulders_free(handle: *mut HeadAndShoulders) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `HeikinAshiOscillator` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_heikin_ashi_oscillator_free`.
#[no_mangle]
pub extern "C" fn wickra_heikin_ashi_oscillator_new(period: usize) -> *mut HeikinAshiOscillator {
    match HeikinAshiOscillator::new(period) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one OHLCV candle; returns the output, or `NaN` during warmup / on a `NULL`
/// handle / if the candle is invalid (e.g. `high < low`).
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_heikin_ashi_oscillator_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_heikin_ashi_oscillator_update(
    handle: *mut HeikinAshiOscillator,
    open: f64,
    high: f64,
    low: f64,
    close: f64,
    volume: f64,
    timestamp: i64,
) -> f64 {
    let Some(ind) = handle.as_mut() else {
        return f64::NAN;
    };
    match Candle::new(open, high, low, close, volume, timestamp) {
        Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
        Err(_) => f64::NAN,
    }
}

/// Run over the OHLCV series (`open/high/low/close/volume/timestamp[0..n]`) into
/// `out[0..n]` (`NaN` at warmup or on an invalid candle).
///
/// # Safety
/// `handle` valid (from `wickra_heikin_ashi_oscillator_new`, not freed); every input pointer and `out`
/// cover `n` elements.
#[no_mangle]
pub unsafe extern "C" fn wickra_heikin_ashi_oscillator_batch(
    handle: *mut HeikinAshiOscillator,
    open: *const f64,
    high: *const f64,
    low: *const f64,
    close: *const f64,
    volume: *const f64,
    timestamp: *const i64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null()
        || open.is_null()
        || high.is_null()
        || low.is_null()
        || close.is_null()
        || volume.is_null()
        || timestamp.is_null()
        || out.is_null()
    {
        return;
    }
    let ind = &mut *handle;
    let opens = slice::from_raw_parts(open, n);
    let highs = slice::from_raw_parts(high, n);
    let lows = slice::from_raw_parts(low, n);
    let closes = slice::from_raw_parts(close, n);
    let volumes = slice::from_raw_parts(volume, n);
    let timestamps = slice::from_raw_parts(timestamp, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (idx, slot) in outputs.iter_mut().enumerate() {
        *slot = match Candle::new(
            opens[idx],
            highs[idx],
            lows[idx],
            closes[idx],
            volumes[idx],
            timestamps[idx],
        ) {
            Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
            Err(_) => f64::NAN,
        };
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_heikin_ashi_oscillator_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_heikin_ashi_oscillator_warmup_period(
    handle: *mut HeikinAshiOscillator,
) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_heikin_ashi_oscillator_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_heikin_ashi_oscillator_is_ready(
    handle: *mut HeikinAshiOscillator,
) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_heikin_ashi_oscillator_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_heikin_ashi_oscillator_name(
    handle: *mut HeikinAshiOscillator,
) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_heikin_ashi_oscillator_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_heikin_ashi_oscillator_reset(handle: *mut HeikinAshiOscillator) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_heikin_ashi_oscillator_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_heikin_ashi_oscillator_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_heikin_ashi_oscillator_free(handle: *mut HeikinAshiOscillator) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `HighLowRange` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_high_low_range_free`.
#[no_mangle]
pub extern "C" fn wickra_high_low_range_new() -> *mut HighLowRange {
    Box::into_raw(Box::new(HighLowRange::new()))
}

/// Feed one OHLCV candle; returns the output, or `NaN` during warmup / on a `NULL`
/// handle / if the candle is invalid (e.g. `high < low`).
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_high_low_range_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_high_low_range_update(
    handle: *mut HighLowRange,
    open: f64,
    high: f64,
    low: f64,
    close: f64,
    volume: f64,
    timestamp: i64,
) -> f64 {
    let Some(ind) = handle.as_mut() else {
        return f64::NAN;
    };
    match Candle::new(open, high, low, close, volume, timestamp) {
        Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
        Err(_) => f64::NAN,
    }
}

/// Run over the OHLCV series (`open/high/low/close/volume/timestamp[0..n]`) into
/// `out[0..n]` (`NaN` at warmup or on an invalid candle).
///
/// # Safety
/// `handle` valid (from `wickra_high_low_range_new`, not freed); every input pointer and `out`
/// cover `n` elements.
#[no_mangle]
pub unsafe extern "C" fn wickra_high_low_range_batch(
    handle: *mut HighLowRange,
    open: *const f64,
    high: *const f64,
    low: *const f64,
    close: *const f64,
    volume: *const f64,
    timestamp: *const i64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null()
        || open.is_null()
        || high.is_null()
        || low.is_null()
        || close.is_null()
        || volume.is_null()
        || timestamp.is_null()
        || out.is_null()
    {
        return;
    }
    let ind = &mut *handle;
    let opens = slice::from_raw_parts(open, n);
    let highs = slice::from_raw_parts(high, n);
    let lows = slice::from_raw_parts(low, n);
    let closes = slice::from_raw_parts(close, n);
    let volumes = slice::from_raw_parts(volume, n);
    let timestamps = slice::from_raw_parts(timestamp, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (idx, slot) in outputs.iter_mut().enumerate() {
        *slot = match Candle::new(
            opens[idx],
            highs[idx],
            lows[idx],
            closes[idx],
            volumes[idx],
            timestamps[idx],
        ) {
            Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
            Err(_) => f64::NAN,
        };
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_high_low_range_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_high_low_range_warmup_period(handle: *mut HighLowRange) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_high_low_range_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_high_low_range_is_ready(handle: *mut HighLowRange) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_high_low_range_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_high_low_range_name(handle: *mut HighLowRange) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_high_low_range_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_high_low_range_reset(handle: *mut HighLowRange) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_high_low_range_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_high_low_range_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_high_low_range_free(handle: *mut HighLowRange) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `HighWave` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_high_wave_free`.
#[no_mangle]
pub extern "C" fn wickra_high_wave_new() -> *mut HighWave {
    Box::into_raw(Box::new(HighWave::new()))
}

/// Feed one OHLCV candle; returns the output, or `NaN` during warmup / on a `NULL`
/// handle / if the candle is invalid (e.g. `high < low`).
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_high_wave_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_high_wave_update(
    handle: *mut HighWave,
    open: f64,
    high: f64,
    low: f64,
    close: f64,
    volume: f64,
    timestamp: i64,
) -> f64 {
    let Some(ind) = handle.as_mut() else {
        return f64::NAN;
    };
    match Candle::new(open, high, low, close, volume, timestamp) {
        Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
        Err(_) => f64::NAN,
    }
}

/// Run over the OHLCV series (`open/high/low/close/volume/timestamp[0..n]`) into
/// `out[0..n]` (`NaN` at warmup or on an invalid candle).
///
/// # Safety
/// `handle` valid (from `wickra_high_wave_new`, not freed); every input pointer and `out`
/// cover `n` elements.
#[no_mangle]
pub unsafe extern "C" fn wickra_high_wave_batch(
    handle: *mut HighWave,
    open: *const f64,
    high: *const f64,
    low: *const f64,
    close: *const f64,
    volume: *const f64,
    timestamp: *const i64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null()
        || open.is_null()
        || high.is_null()
        || low.is_null()
        || close.is_null()
        || volume.is_null()
        || timestamp.is_null()
        || out.is_null()
    {
        return;
    }
    let ind = &mut *handle;
    let opens = slice::from_raw_parts(open, n);
    let highs = slice::from_raw_parts(high, n);
    let lows = slice::from_raw_parts(low, n);
    let closes = slice::from_raw_parts(close, n);
    let volumes = slice::from_raw_parts(volume, n);
    let timestamps = slice::from_raw_parts(timestamp, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (idx, slot) in outputs.iter_mut().enumerate() {
        *slot = match Candle::new(
            opens[idx],
            highs[idx],
            lows[idx],
            closes[idx],
            volumes[idx],
            timestamps[idx],
        ) {
            Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
            Err(_) => f64::NAN,
        };
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_high_wave_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_high_wave_warmup_period(handle: *mut HighWave) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_high_wave_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_high_wave_is_ready(handle: *mut HighWave) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_high_wave_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_high_wave_name(handle: *mut HighWave) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_high_wave_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_high_wave_reset(handle: *mut HighWave) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_high_wave_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_high_wave_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_high_wave_free(handle: *mut HighWave) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `Hikkake` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_hikkake_free`.
#[no_mangle]
pub extern "C" fn wickra_hikkake_new() -> *mut Hikkake {
    Box::into_raw(Box::new(Hikkake::new()))
}

/// Feed one OHLCV candle; returns the output, or `NaN` during warmup / on a `NULL`
/// handle / if the candle is invalid (e.g. `high < low`).
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_hikkake_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_hikkake_update(
    handle: *mut Hikkake,
    open: f64,
    high: f64,
    low: f64,
    close: f64,
    volume: f64,
    timestamp: i64,
) -> f64 {
    let Some(ind) = handle.as_mut() else {
        return f64::NAN;
    };
    match Candle::new(open, high, low, close, volume, timestamp) {
        Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
        Err(_) => f64::NAN,
    }
}

/// Run over the OHLCV series (`open/high/low/close/volume/timestamp[0..n]`) into
/// `out[0..n]` (`NaN` at warmup or on an invalid candle).
///
/// # Safety
/// `handle` valid (from `wickra_hikkake_new`, not freed); every input pointer and `out`
/// cover `n` elements.
#[no_mangle]
pub unsafe extern "C" fn wickra_hikkake_batch(
    handle: *mut Hikkake,
    open: *const f64,
    high: *const f64,
    low: *const f64,
    close: *const f64,
    volume: *const f64,
    timestamp: *const i64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null()
        || open.is_null()
        || high.is_null()
        || low.is_null()
        || close.is_null()
        || volume.is_null()
        || timestamp.is_null()
        || out.is_null()
    {
        return;
    }
    let ind = &mut *handle;
    let opens = slice::from_raw_parts(open, n);
    let highs = slice::from_raw_parts(high, n);
    let lows = slice::from_raw_parts(low, n);
    let closes = slice::from_raw_parts(close, n);
    let volumes = slice::from_raw_parts(volume, n);
    let timestamps = slice::from_raw_parts(timestamp, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (idx, slot) in outputs.iter_mut().enumerate() {
        *slot = match Candle::new(
            opens[idx],
            highs[idx],
            lows[idx],
            closes[idx],
            volumes[idx],
            timestamps[idx],
        ) {
            Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
            Err(_) => f64::NAN,
        };
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_hikkake_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_hikkake_warmup_period(handle: *mut Hikkake) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_hikkake_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_hikkake_is_ready(handle: *mut Hikkake) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_hikkake_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_hikkake_name(handle: *mut Hikkake) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_hikkake_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_hikkake_reset(handle: *mut Hikkake) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_hikkake_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_hikkake_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_hikkake_free(handle: *mut Hikkake) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `HikkakeModified` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_hikkake_modified_free`.
#[no_mangle]
pub extern "C" fn wickra_hikkake_modified_new() -> *mut HikkakeModified {
    Box::into_raw(Box::new(HikkakeModified::new()))
}

/// Feed one OHLCV candle; returns the output, or `NaN` during warmup / on a `NULL`
/// handle / if the candle is invalid (e.g. `high < low`).
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_hikkake_modified_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_hikkake_modified_update(
    handle: *mut HikkakeModified,
    open: f64,
    high: f64,
    low: f64,
    close: f64,
    volume: f64,
    timestamp: i64,
) -> f64 {
    let Some(ind) = handle.as_mut() else {
        return f64::NAN;
    };
    match Candle::new(open, high, low, close, volume, timestamp) {
        Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
        Err(_) => f64::NAN,
    }
}

/// Run over the OHLCV series (`open/high/low/close/volume/timestamp[0..n]`) into
/// `out[0..n]` (`NaN` at warmup or on an invalid candle).
///
/// # Safety
/// `handle` valid (from `wickra_hikkake_modified_new`, not freed); every input pointer and `out`
/// cover `n` elements.
#[no_mangle]
pub unsafe extern "C" fn wickra_hikkake_modified_batch(
    handle: *mut HikkakeModified,
    open: *const f64,
    high: *const f64,
    low: *const f64,
    close: *const f64,
    volume: *const f64,
    timestamp: *const i64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null()
        || open.is_null()
        || high.is_null()
        || low.is_null()
        || close.is_null()
        || volume.is_null()
        || timestamp.is_null()
        || out.is_null()
    {
        return;
    }
    let ind = &mut *handle;
    let opens = slice::from_raw_parts(open, n);
    let highs = slice::from_raw_parts(high, n);
    let lows = slice::from_raw_parts(low, n);
    let closes = slice::from_raw_parts(close, n);
    let volumes = slice::from_raw_parts(volume, n);
    let timestamps = slice::from_raw_parts(timestamp, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (idx, slot) in outputs.iter_mut().enumerate() {
        *slot = match Candle::new(
            opens[idx],
            highs[idx],
            lows[idx],
            closes[idx],
            volumes[idx],
            timestamps[idx],
        ) {
            Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
            Err(_) => f64::NAN,
        };
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_hikkake_modified_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_hikkake_modified_warmup_period(
    handle: *mut HikkakeModified,
) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_hikkake_modified_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_hikkake_modified_is_ready(handle: *mut HikkakeModified) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_hikkake_modified_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_hikkake_modified_name(
    handle: *mut HikkakeModified,
) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_hikkake_modified_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_hikkake_modified_reset(handle: *mut HikkakeModified) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_hikkake_modified_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_hikkake_modified_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_hikkake_modified_free(handle: *mut HikkakeModified) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `HiLoActivator` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_hi_lo_activator_free`.
#[no_mangle]
pub extern "C" fn wickra_hi_lo_activator_new(period: usize) -> *mut HiLoActivator {
    match HiLoActivator::new(period) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one OHLCV candle; returns the output, or `NaN` during warmup / on a `NULL`
/// handle / if the candle is invalid (e.g. `high < low`).
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_hi_lo_activator_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_hi_lo_activator_update(
    handle: *mut HiLoActivator,
    open: f64,
    high: f64,
    low: f64,
    close: f64,
    volume: f64,
    timestamp: i64,
) -> f64 {
    let Some(ind) = handle.as_mut() else {
        return f64::NAN;
    };
    match Candle::new(open, high, low, close, volume, timestamp) {
        Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
        Err(_) => f64::NAN,
    }
}

/// Run over the OHLCV series (`open/high/low/close/volume/timestamp[0..n]`) into
/// `out[0..n]` (`NaN` at warmup or on an invalid candle).
///
/// # Safety
/// `handle` valid (from `wickra_hi_lo_activator_new`, not freed); every input pointer and `out`
/// cover `n` elements.
#[no_mangle]
pub unsafe extern "C" fn wickra_hi_lo_activator_batch(
    handle: *mut HiLoActivator,
    open: *const f64,
    high: *const f64,
    low: *const f64,
    close: *const f64,
    volume: *const f64,
    timestamp: *const i64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null()
        || open.is_null()
        || high.is_null()
        || low.is_null()
        || close.is_null()
        || volume.is_null()
        || timestamp.is_null()
        || out.is_null()
    {
        return;
    }
    let ind = &mut *handle;
    let opens = slice::from_raw_parts(open, n);
    let highs = slice::from_raw_parts(high, n);
    let lows = slice::from_raw_parts(low, n);
    let closes = slice::from_raw_parts(close, n);
    let volumes = slice::from_raw_parts(volume, n);
    let timestamps = slice::from_raw_parts(timestamp, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (idx, slot) in outputs.iter_mut().enumerate() {
        *slot = match Candle::new(
            opens[idx],
            highs[idx],
            lows[idx],
            closes[idx],
            volumes[idx],
            timestamps[idx],
        ) {
            Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
            Err(_) => f64::NAN,
        };
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_hi_lo_activator_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_hi_lo_activator_warmup_period(handle: *mut HiLoActivator) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_hi_lo_activator_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_hi_lo_activator_is_ready(handle: *mut HiLoActivator) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_hi_lo_activator_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_hi_lo_activator_name(handle: *mut HiLoActivator) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_hi_lo_activator_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_hi_lo_activator_reset(handle: *mut HiLoActivator) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_hi_lo_activator_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_hi_lo_activator_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_hi_lo_activator_free(handle: *mut HiLoActivator) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `HomingPigeon` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_homing_pigeon_free`.
#[no_mangle]
pub extern "C" fn wickra_homing_pigeon_new() -> *mut HomingPigeon {
    Box::into_raw(Box::new(HomingPigeon::new()))
}

/// Feed one OHLCV candle; returns the output, or `NaN` during warmup / on a `NULL`
/// handle / if the candle is invalid (e.g. `high < low`).
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_homing_pigeon_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_homing_pigeon_update(
    handle: *mut HomingPigeon,
    open: f64,
    high: f64,
    low: f64,
    close: f64,
    volume: f64,
    timestamp: i64,
) -> f64 {
    let Some(ind) = handle.as_mut() else {
        return f64::NAN;
    };
    match Candle::new(open, high, low, close, volume, timestamp) {
        Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
        Err(_) => f64::NAN,
    }
}

/// Run over the OHLCV series (`open/high/low/close/volume/timestamp[0..n]`) into
/// `out[0..n]` (`NaN` at warmup or on an invalid candle).
///
/// # Safety
/// `handle` valid (from `wickra_homing_pigeon_new`, not freed); every input pointer and `out`
/// cover `n` elements.
#[no_mangle]
pub unsafe extern "C" fn wickra_homing_pigeon_batch(
    handle: *mut HomingPigeon,
    open: *const f64,
    high: *const f64,
    low: *const f64,
    close: *const f64,
    volume: *const f64,
    timestamp: *const i64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null()
        || open.is_null()
        || high.is_null()
        || low.is_null()
        || close.is_null()
        || volume.is_null()
        || timestamp.is_null()
        || out.is_null()
    {
        return;
    }
    let ind = &mut *handle;
    let opens = slice::from_raw_parts(open, n);
    let highs = slice::from_raw_parts(high, n);
    let lows = slice::from_raw_parts(low, n);
    let closes = slice::from_raw_parts(close, n);
    let volumes = slice::from_raw_parts(volume, n);
    let timestamps = slice::from_raw_parts(timestamp, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (idx, slot) in outputs.iter_mut().enumerate() {
        *slot = match Candle::new(
            opens[idx],
            highs[idx],
            lows[idx],
            closes[idx],
            volumes[idx],
            timestamps[idx],
        ) {
            Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
            Err(_) => f64::NAN,
        };
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_homing_pigeon_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_homing_pigeon_warmup_period(handle: *mut HomingPigeon) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_homing_pigeon_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_homing_pigeon_is_ready(handle: *mut HomingPigeon) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_homing_pigeon_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_homing_pigeon_name(handle: *mut HomingPigeon) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_homing_pigeon_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_homing_pigeon_reset(handle: *mut HomingPigeon) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_homing_pigeon_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_homing_pigeon_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_homing_pigeon_free(handle: *mut HomingPigeon) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `IdenticalThreeCrows` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_identical_three_crows_free`.
#[no_mangle]
pub extern "C" fn wickra_identical_three_crows_new() -> *mut IdenticalThreeCrows {
    Box::into_raw(Box::new(IdenticalThreeCrows::new()))
}

/// Feed one OHLCV candle; returns the output, or `NaN` during warmup / on a `NULL`
/// handle / if the candle is invalid (e.g. `high < low`).
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_identical_three_crows_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_identical_three_crows_update(
    handle: *mut IdenticalThreeCrows,
    open: f64,
    high: f64,
    low: f64,
    close: f64,
    volume: f64,
    timestamp: i64,
) -> f64 {
    let Some(ind) = handle.as_mut() else {
        return f64::NAN;
    };
    match Candle::new(open, high, low, close, volume, timestamp) {
        Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
        Err(_) => f64::NAN,
    }
}

/// Run over the OHLCV series (`open/high/low/close/volume/timestamp[0..n]`) into
/// `out[0..n]` (`NaN` at warmup or on an invalid candle).
///
/// # Safety
/// `handle` valid (from `wickra_identical_three_crows_new`, not freed); every input pointer and `out`
/// cover `n` elements.
#[no_mangle]
pub unsafe extern "C" fn wickra_identical_three_crows_batch(
    handle: *mut IdenticalThreeCrows,
    open: *const f64,
    high: *const f64,
    low: *const f64,
    close: *const f64,
    volume: *const f64,
    timestamp: *const i64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null()
        || open.is_null()
        || high.is_null()
        || low.is_null()
        || close.is_null()
        || volume.is_null()
        || timestamp.is_null()
        || out.is_null()
    {
        return;
    }
    let ind = &mut *handle;
    let opens = slice::from_raw_parts(open, n);
    let highs = slice::from_raw_parts(high, n);
    let lows = slice::from_raw_parts(low, n);
    let closes = slice::from_raw_parts(close, n);
    let volumes = slice::from_raw_parts(volume, n);
    let timestamps = slice::from_raw_parts(timestamp, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (idx, slot) in outputs.iter_mut().enumerate() {
        *slot = match Candle::new(
            opens[idx],
            highs[idx],
            lows[idx],
            closes[idx],
            volumes[idx],
            timestamps[idx],
        ) {
            Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
            Err(_) => f64::NAN,
        };
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_identical_three_crows_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_identical_three_crows_warmup_period(
    handle: *mut IdenticalThreeCrows,
) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_identical_three_crows_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_identical_three_crows_is_ready(
    handle: *mut IdenticalThreeCrows,
) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_identical_three_crows_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_identical_three_crows_name(
    handle: *mut IdenticalThreeCrows,
) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_identical_three_crows_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_identical_three_crows_reset(handle: *mut IdenticalThreeCrows) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_identical_three_crows_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_identical_three_crows_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_identical_three_crows_free(handle: *mut IdenticalThreeCrows) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `InNeck` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_in_neck_free`.
#[no_mangle]
pub extern "C" fn wickra_in_neck_new() -> *mut InNeck {
    Box::into_raw(Box::new(InNeck::new()))
}

/// Feed one OHLCV candle; returns the output, or `NaN` during warmup / on a `NULL`
/// handle / if the candle is invalid (e.g. `high < low`).
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_in_neck_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_in_neck_update(
    handle: *mut InNeck,
    open: f64,
    high: f64,
    low: f64,
    close: f64,
    volume: f64,
    timestamp: i64,
) -> f64 {
    let Some(ind) = handle.as_mut() else {
        return f64::NAN;
    };
    match Candle::new(open, high, low, close, volume, timestamp) {
        Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
        Err(_) => f64::NAN,
    }
}

/// Run over the OHLCV series (`open/high/low/close/volume/timestamp[0..n]`) into
/// `out[0..n]` (`NaN` at warmup or on an invalid candle).
///
/// # Safety
/// `handle` valid (from `wickra_in_neck_new`, not freed); every input pointer and `out`
/// cover `n` elements.
#[no_mangle]
pub unsafe extern "C" fn wickra_in_neck_batch(
    handle: *mut InNeck,
    open: *const f64,
    high: *const f64,
    low: *const f64,
    close: *const f64,
    volume: *const f64,
    timestamp: *const i64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null()
        || open.is_null()
        || high.is_null()
        || low.is_null()
        || close.is_null()
        || volume.is_null()
        || timestamp.is_null()
        || out.is_null()
    {
        return;
    }
    let ind = &mut *handle;
    let opens = slice::from_raw_parts(open, n);
    let highs = slice::from_raw_parts(high, n);
    let lows = slice::from_raw_parts(low, n);
    let closes = slice::from_raw_parts(close, n);
    let volumes = slice::from_raw_parts(volume, n);
    let timestamps = slice::from_raw_parts(timestamp, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (idx, slot) in outputs.iter_mut().enumerate() {
        *slot = match Candle::new(
            opens[idx],
            highs[idx],
            lows[idx],
            closes[idx],
            volumes[idx],
            timestamps[idx],
        ) {
            Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
            Err(_) => f64::NAN,
        };
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_in_neck_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_in_neck_warmup_period(handle: *mut InNeck) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_in_neck_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_in_neck_is_ready(handle: *mut InNeck) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_in_neck_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_in_neck_name(handle: *mut InNeck) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_in_neck_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_in_neck_reset(handle: *mut InNeck) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_in_neck_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_in_neck_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_in_neck_free(handle: *mut InNeck) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `Inertia` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_inertia_free`.
#[no_mangle]
pub extern "C" fn wickra_inertia_new(rvi_period: usize, linreg_period: usize) -> *mut Inertia {
    match Inertia::new(rvi_period, linreg_period) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one OHLCV candle; returns the output, or `NaN` during warmup / on a `NULL`
/// handle / if the candle is invalid (e.g. `high < low`).
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_inertia_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_inertia_update(
    handle: *mut Inertia,
    open: f64,
    high: f64,
    low: f64,
    close: f64,
    volume: f64,
    timestamp: i64,
) -> f64 {
    let Some(ind) = handle.as_mut() else {
        return f64::NAN;
    };
    match Candle::new(open, high, low, close, volume, timestamp) {
        Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
        Err(_) => f64::NAN,
    }
}

/// Run over the OHLCV series (`open/high/low/close/volume/timestamp[0..n]`) into
/// `out[0..n]` (`NaN` at warmup or on an invalid candle).
///
/// # Safety
/// `handle` valid (from `wickra_inertia_new`, not freed); every input pointer and `out`
/// cover `n` elements.
#[no_mangle]
pub unsafe extern "C" fn wickra_inertia_batch(
    handle: *mut Inertia,
    open: *const f64,
    high: *const f64,
    low: *const f64,
    close: *const f64,
    volume: *const f64,
    timestamp: *const i64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null()
        || open.is_null()
        || high.is_null()
        || low.is_null()
        || close.is_null()
        || volume.is_null()
        || timestamp.is_null()
        || out.is_null()
    {
        return;
    }
    let ind = &mut *handle;
    let opens = slice::from_raw_parts(open, n);
    let highs = slice::from_raw_parts(high, n);
    let lows = slice::from_raw_parts(low, n);
    let closes = slice::from_raw_parts(close, n);
    let volumes = slice::from_raw_parts(volume, n);
    let timestamps = slice::from_raw_parts(timestamp, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (idx, slot) in outputs.iter_mut().enumerate() {
        *slot = match Candle::new(
            opens[idx],
            highs[idx],
            lows[idx],
            closes[idx],
            volumes[idx],
            timestamps[idx],
        ) {
            Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
            Err(_) => f64::NAN,
        };
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_inertia_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_inertia_warmup_period(handle: *mut Inertia) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_inertia_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_inertia_is_ready(handle: *mut Inertia) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_inertia_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_inertia_name(handle: *mut Inertia) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_inertia_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_inertia_reset(handle: *mut Inertia) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_inertia_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_inertia_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_inertia_free(handle: *mut Inertia) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `IntradayIntensity` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_intraday_intensity_free`.
#[no_mangle]
pub extern "C" fn wickra_intraday_intensity_new() -> *mut IntradayIntensity {
    Box::into_raw(Box::new(IntradayIntensity::new()))
}

/// Feed one OHLCV candle; returns the output, or `NaN` during warmup / on a `NULL`
/// handle / if the candle is invalid (e.g. `high < low`).
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_intraday_intensity_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_intraday_intensity_update(
    handle: *mut IntradayIntensity,
    open: f64,
    high: f64,
    low: f64,
    close: f64,
    volume: f64,
    timestamp: i64,
) -> f64 {
    let Some(ind) = handle.as_mut() else {
        return f64::NAN;
    };
    match Candle::new(open, high, low, close, volume, timestamp) {
        Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
        Err(_) => f64::NAN,
    }
}

/// Run over the OHLCV series (`open/high/low/close/volume/timestamp[0..n]`) into
/// `out[0..n]` (`NaN` at warmup or on an invalid candle).
///
/// # Safety
/// `handle` valid (from `wickra_intraday_intensity_new`, not freed); every input pointer and `out`
/// cover `n` elements.
#[no_mangle]
pub unsafe extern "C" fn wickra_intraday_intensity_batch(
    handle: *mut IntradayIntensity,
    open: *const f64,
    high: *const f64,
    low: *const f64,
    close: *const f64,
    volume: *const f64,
    timestamp: *const i64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null()
        || open.is_null()
        || high.is_null()
        || low.is_null()
        || close.is_null()
        || volume.is_null()
        || timestamp.is_null()
        || out.is_null()
    {
        return;
    }
    let ind = &mut *handle;
    let opens = slice::from_raw_parts(open, n);
    let highs = slice::from_raw_parts(high, n);
    let lows = slice::from_raw_parts(low, n);
    let closes = slice::from_raw_parts(close, n);
    let volumes = slice::from_raw_parts(volume, n);
    let timestamps = slice::from_raw_parts(timestamp, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (idx, slot) in outputs.iter_mut().enumerate() {
        *slot = match Candle::new(
            opens[idx],
            highs[idx],
            lows[idx],
            closes[idx],
            volumes[idx],
            timestamps[idx],
        ) {
            Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
            Err(_) => f64::NAN,
        };
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_intraday_intensity_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_intraday_intensity_warmup_period(
    handle: *mut IntradayIntensity,
) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_intraday_intensity_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_intraday_intensity_is_ready(
    handle: *mut IntradayIntensity,
) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_intraday_intensity_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_intraday_intensity_name(
    handle: *mut IntradayIntensity,
) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_intraday_intensity_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_intraday_intensity_reset(handle: *mut IntradayIntensity) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_intraday_intensity_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_intraday_intensity_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_intraday_intensity_free(handle: *mut IntradayIntensity) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `IntradayMomentumIndex` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_intraday_momentum_index_free`.
#[no_mangle]
pub extern "C" fn wickra_intraday_momentum_index_new(period: usize) -> *mut IntradayMomentumIndex {
    match IntradayMomentumIndex::new(period) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one OHLCV candle; returns the output, or `NaN` during warmup / on a `NULL`
/// handle / if the candle is invalid (e.g. `high < low`).
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_intraday_momentum_index_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_intraday_momentum_index_update(
    handle: *mut IntradayMomentumIndex,
    open: f64,
    high: f64,
    low: f64,
    close: f64,
    volume: f64,
    timestamp: i64,
) -> f64 {
    let Some(ind) = handle.as_mut() else {
        return f64::NAN;
    };
    match Candle::new(open, high, low, close, volume, timestamp) {
        Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
        Err(_) => f64::NAN,
    }
}

/// Run over the OHLCV series (`open/high/low/close/volume/timestamp[0..n]`) into
/// `out[0..n]` (`NaN` at warmup or on an invalid candle).
///
/// # Safety
/// `handle` valid (from `wickra_intraday_momentum_index_new`, not freed); every input pointer and `out`
/// cover `n` elements.
#[no_mangle]
pub unsafe extern "C" fn wickra_intraday_momentum_index_batch(
    handle: *mut IntradayMomentumIndex,
    open: *const f64,
    high: *const f64,
    low: *const f64,
    close: *const f64,
    volume: *const f64,
    timestamp: *const i64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null()
        || open.is_null()
        || high.is_null()
        || low.is_null()
        || close.is_null()
        || volume.is_null()
        || timestamp.is_null()
        || out.is_null()
    {
        return;
    }
    let ind = &mut *handle;
    let opens = slice::from_raw_parts(open, n);
    let highs = slice::from_raw_parts(high, n);
    let lows = slice::from_raw_parts(low, n);
    let closes = slice::from_raw_parts(close, n);
    let volumes = slice::from_raw_parts(volume, n);
    let timestamps = slice::from_raw_parts(timestamp, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (idx, slot) in outputs.iter_mut().enumerate() {
        *slot = match Candle::new(
            opens[idx],
            highs[idx],
            lows[idx],
            closes[idx],
            volumes[idx],
            timestamps[idx],
        ) {
            Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
            Err(_) => f64::NAN,
        };
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_intraday_momentum_index_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_intraday_momentum_index_warmup_period(
    handle: *mut IntradayMomentumIndex,
) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_intraday_momentum_index_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_intraday_momentum_index_is_ready(
    handle: *mut IntradayMomentumIndex,
) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_intraday_momentum_index_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_intraday_momentum_index_name(
    handle: *mut IntradayMomentumIndex,
) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_intraday_momentum_index_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_intraday_momentum_index_reset(handle: *mut IntradayMomentumIndex) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_intraday_momentum_index_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_intraday_momentum_index_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_intraday_momentum_index_free(handle: *mut IntradayMomentumIndex) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `InvertedHammer` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_inverted_hammer_free`.
#[no_mangle]
pub extern "C" fn wickra_inverted_hammer_new() -> *mut InvertedHammer {
    Box::into_raw(Box::new(InvertedHammer::new()))
}

/// Feed one OHLCV candle; returns the output, or `NaN` during warmup / on a `NULL`
/// handle / if the candle is invalid (e.g. `high < low`).
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_inverted_hammer_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_inverted_hammer_update(
    handle: *mut InvertedHammer,
    open: f64,
    high: f64,
    low: f64,
    close: f64,
    volume: f64,
    timestamp: i64,
) -> f64 {
    let Some(ind) = handle.as_mut() else {
        return f64::NAN;
    };
    match Candle::new(open, high, low, close, volume, timestamp) {
        Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
        Err(_) => f64::NAN,
    }
}

/// Run over the OHLCV series (`open/high/low/close/volume/timestamp[0..n]`) into
/// `out[0..n]` (`NaN` at warmup or on an invalid candle).
///
/// # Safety
/// `handle` valid (from `wickra_inverted_hammer_new`, not freed); every input pointer and `out`
/// cover `n` elements.
#[no_mangle]
pub unsafe extern "C" fn wickra_inverted_hammer_batch(
    handle: *mut InvertedHammer,
    open: *const f64,
    high: *const f64,
    low: *const f64,
    close: *const f64,
    volume: *const f64,
    timestamp: *const i64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null()
        || open.is_null()
        || high.is_null()
        || low.is_null()
        || close.is_null()
        || volume.is_null()
        || timestamp.is_null()
        || out.is_null()
    {
        return;
    }
    let ind = &mut *handle;
    let opens = slice::from_raw_parts(open, n);
    let highs = slice::from_raw_parts(high, n);
    let lows = slice::from_raw_parts(low, n);
    let closes = slice::from_raw_parts(close, n);
    let volumes = slice::from_raw_parts(volume, n);
    let timestamps = slice::from_raw_parts(timestamp, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (idx, slot) in outputs.iter_mut().enumerate() {
        *slot = match Candle::new(
            opens[idx],
            highs[idx],
            lows[idx],
            closes[idx],
            volumes[idx],
            timestamps[idx],
        ) {
            Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
            Err(_) => f64::NAN,
        };
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_inverted_hammer_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_inverted_hammer_warmup_period(
    handle: *mut InvertedHammer,
) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_inverted_hammer_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_inverted_hammer_is_ready(handle: *mut InvertedHammer) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_inverted_hammer_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_inverted_hammer_name(handle: *mut InvertedHammer) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_inverted_hammer_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_inverted_hammer_reset(handle: *mut InvertedHammer) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_inverted_hammer_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_inverted_hammer_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_inverted_hammer_free(handle: *mut InvertedHammer) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `Kicking` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_kicking_free`.
#[no_mangle]
pub extern "C" fn wickra_kicking_new() -> *mut Kicking {
    Box::into_raw(Box::new(Kicking::new()))
}

/// Feed one OHLCV candle; returns the output, or `NaN` during warmup / on a `NULL`
/// handle / if the candle is invalid (e.g. `high < low`).
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_kicking_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_kicking_update(
    handle: *mut Kicking,
    open: f64,
    high: f64,
    low: f64,
    close: f64,
    volume: f64,
    timestamp: i64,
) -> f64 {
    let Some(ind) = handle.as_mut() else {
        return f64::NAN;
    };
    match Candle::new(open, high, low, close, volume, timestamp) {
        Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
        Err(_) => f64::NAN,
    }
}

/// Run over the OHLCV series (`open/high/low/close/volume/timestamp[0..n]`) into
/// `out[0..n]` (`NaN` at warmup or on an invalid candle).
///
/// # Safety
/// `handle` valid (from `wickra_kicking_new`, not freed); every input pointer and `out`
/// cover `n` elements.
#[no_mangle]
pub unsafe extern "C" fn wickra_kicking_batch(
    handle: *mut Kicking,
    open: *const f64,
    high: *const f64,
    low: *const f64,
    close: *const f64,
    volume: *const f64,
    timestamp: *const i64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null()
        || open.is_null()
        || high.is_null()
        || low.is_null()
        || close.is_null()
        || volume.is_null()
        || timestamp.is_null()
        || out.is_null()
    {
        return;
    }
    let ind = &mut *handle;
    let opens = slice::from_raw_parts(open, n);
    let highs = slice::from_raw_parts(high, n);
    let lows = slice::from_raw_parts(low, n);
    let closes = slice::from_raw_parts(close, n);
    let volumes = slice::from_raw_parts(volume, n);
    let timestamps = slice::from_raw_parts(timestamp, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (idx, slot) in outputs.iter_mut().enumerate() {
        *slot = match Candle::new(
            opens[idx],
            highs[idx],
            lows[idx],
            closes[idx],
            volumes[idx],
            timestamps[idx],
        ) {
            Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
            Err(_) => f64::NAN,
        };
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_kicking_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_kicking_warmup_period(handle: *mut Kicking) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_kicking_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_kicking_is_ready(handle: *mut Kicking) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_kicking_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_kicking_name(handle: *mut Kicking) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_kicking_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_kicking_reset(handle: *mut Kicking) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_kicking_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_kicking_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_kicking_free(handle: *mut Kicking) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `KickingByLength` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_kicking_by_length_free`.
#[no_mangle]
pub extern "C" fn wickra_kicking_by_length_new() -> *mut KickingByLength {
    Box::into_raw(Box::new(KickingByLength::new()))
}

/// Feed one OHLCV candle; returns the output, or `NaN` during warmup / on a `NULL`
/// handle / if the candle is invalid (e.g. `high < low`).
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_kicking_by_length_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_kicking_by_length_update(
    handle: *mut KickingByLength,
    open: f64,
    high: f64,
    low: f64,
    close: f64,
    volume: f64,
    timestamp: i64,
) -> f64 {
    let Some(ind) = handle.as_mut() else {
        return f64::NAN;
    };
    match Candle::new(open, high, low, close, volume, timestamp) {
        Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
        Err(_) => f64::NAN,
    }
}

/// Run over the OHLCV series (`open/high/low/close/volume/timestamp[0..n]`) into
/// `out[0..n]` (`NaN` at warmup or on an invalid candle).
///
/// # Safety
/// `handle` valid (from `wickra_kicking_by_length_new`, not freed); every input pointer and `out`
/// cover `n` elements.
#[no_mangle]
pub unsafe extern "C" fn wickra_kicking_by_length_batch(
    handle: *mut KickingByLength,
    open: *const f64,
    high: *const f64,
    low: *const f64,
    close: *const f64,
    volume: *const f64,
    timestamp: *const i64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null()
        || open.is_null()
        || high.is_null()
        || low.is_null()
        || close.is_null()
        || volume.is_null()
        || timestamp.is_null()
        || out.is_null()
    {
        return;
    }
    let ind = &mut *handle;
    let opens = slice::from_raw_parts(open, n);
    let highs = slice::from_raw_parts(high, n);
    let lows = slice::from_raw_parts(low, n);
    let closes = slice::from_raw_parts(close, n);
    let volumes = slice::from_raw_parts(volume, n);
    let timestamps = slice::from_raw_parts(timestamp, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (idx, slot) in outputs.iter_mut().enumerate() {
        *slot = match Candle::new(
            opens[idx],
            highs[idx],
            lows[idx],
            closes[idx],
            volumes[idx],
            timestamps[idx],
        ) {
            Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
            Err(_) => f64::NAN,
        };
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_kicking_by_length_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_kicking_by_length_warmup_period(
    handle: *mut KickingByLength,
) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_kicking_by_length_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_kicking_by_length_is_ready(handle: *mut KickingByLength) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_kicking_by_length_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_kicking_by_length_name(
    handle: *mut KickingByLength,
) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_kicking_by_length_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_kicking_by_length_reset(handle: *mut KickingByLength) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_kicking_by_length_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_kicking_by_length_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_kicking_by_length_free(handle: *mut KickingByLength) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `Kvo` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_kvo_free`.
#[no_mangle]
pub extern "C" fn wickra_kvo_new(fast: usize, slow: usize) -> *mut Kvo {
    match Kvo::new(fast, slow) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one OHLCV candle; returns the output, or `NaN` during warmup / on a `NULL`
/// handle / if the candle is invalid (e.g. `high < low`).
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_kvo_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_kvo_update(
    handle: *mut Kvo,
    open: f64,
    high: f64,
    low: f64,
    close: f64,
    volume: f64,
    timestamp: i64,
) -> f64 {
    let Some(ind) = handle.as_mut() else {
        return f64::NAN;
    };
    match Candle::new(open, high, low, close, volume, timestamp) {
        Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
        Err(_) => f64::NAN,
    }
}

/// Run over the OHLCV series (`open/high/low/close/volume/timestamp[0..n]`) into
/// `out[0..n]` (`NaN` at warmup or on an invalid candle).
///
/// # Safety
/// `handle` valid (from `wickra_kvo_new`, not freed); every input pointer and `out`
/// cover `n` elements.
#[no_mangle]
pub unsafe extern "C" fn wickra_kvo_batch(
    handle: *mut Kvo,
    open: *const f64,
    high: *const f64,
    low: *const f64,
    close: *const f64,
    volume: *const f64,
    timestamp: *const i64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null()
        || open.is_null()
        || high.is_null()
        || low.is_null()
        || close.is_null()
        || volume.is_null()
        || timestamp.is_null()
        || out.is_null()
    {
        return;
    }
    let ind = &mut *handle;
    let opens = slice::from_raw_parts(open, n);
    let highs = slice::from_raw_parts(high, n);
    let lows = slice::from_raw_parts(low, n);
    let closes = slice::from_raw_parts(close, n);
    let volumes = slice::from_raw_parts(volume, n);
    let timestamps = slice::from_raw_parts(timestamp, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (idx, slot) in outputs.iter_mut().enumerate() {
        *slot = match Candle::new(
            opens[idx],
            highs[idx],
            lows[idx],
            closes[idx],
            volumes[idx],
            timestamps[idx],
        ) {
            Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
            Err(_) => f64::NAN,
        };
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_kvo_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_kvo_warmup_period(handle: *mut Kvo) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_kvo_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_kvo_is_ready(handle: *mut Kvo) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_kvo_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_kvo_name(handle: *mut Kvo) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_kvo_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_kvo_reset(handle: *mut Kvo) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_kvo_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_kvo_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_kvo_free(handle: *mut Kvo) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `LadderBottom` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_ladder_bottom_free`.
#[no_mangle]
pub extern "C" fn wickra_ladder_bottom_new() -> *mut LadderBottom {
    Box::into_raw(Box::new(LadderBottom::new()))
}

/// Feed one OHLCV candle; returns the output, or `NaN` during warmup / on a `NULL`
/// handle / if the candle is invalid (e.g. `high < low`).
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_ladder_bottom_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_ladder_bottom_update(
    handle: *mut LadderBottom,
    open: f64,
    high: f64,
    low: f64,
    close: f64,
    volume: f64,
    timestamp: i64,
) -> f64 {
    let Some(ind) = handle.as_mut() else {
        return f64::NAN;
    };
    match Candle::new(open, high, low, close, volume, timestamp) {
        Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
        Err(_) => f64::NAN,
    }
}

/// Run over the OHLCV series (`open/high/low/close/volume/timestamp[0..n]`) into
/// `out[0..n]` (`NaN` at warmup or on an invalid candle).
///
/// # Safety
/// `handle` valid (from `wickra_ladder_bottom_new`, not freed); every input pointer and `out`
/// cover `n` elements.
#[no_mangle]
pub unsafe extern "C" fn wickra_ladder_bottom_batch(
    handle: *mut LadderBottom,
    open: *const f64,
    high: *const f64,
    low: *const f64,
    close: *const f64,
    volume: *const f64,
    timestamp: *const i64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null()
        || open.is_null()
        || high.is_null()
        || low.is_null()
        || close.is_null()
        || volume.is_null()
        || timestamp.is_null()
        || out.is_null()
    {
        return;
    }
    let ind = &mut *handle;
    let opens = slice::from_raw_parts(open, n);
    let highs = slice::from_raw_parts(high, n);
    let lows = slice::from_raw_parts(low, n);
    let closes = slice::from_raw_parts(close, n);
    let volumes = slice::from_raw_parts(volume, n);
    let timestamps = slice::from_raw_parts(timestamp, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (idx, slot) in outputs.iter_mut().enumerate() {
        *slot = match Candle::new(
            opens[idx],
            highs[idx],
            lows[idx],
            closes[idx],
            volumes[idx],
            timestamps[idx],
        ) {
            Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
            Err(_) => f64::NAN,
        };
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_ladder_bottom_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_ladder_bottom_warmup_period(handle: *mut LadderBottom) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_ladder_bottom_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_ladder_bottom_is_ready(handle: *mut LadderBottom) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_ladder_bottom_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_ladder_bottom_name(handle: *mut LadderBottom) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_ladder_bottom_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_ladder_bottom_reset(handle: *mut LadderBottom) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_ladder_bottom_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_ladder_bottom_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_ladder_bottom_free(handle: *mut LadderBottom) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `LongLeggedDoji` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_long_legged_doji_free`.
#[no_mangle]
pub extern "C" fn wickra_long_legged_doji_new() -> *mut LongLeggedDoji {
    Box::into_raw(Box::new(LongLeggedDoji::new()))
}

/// Feed one OHLCV candle; returns the output, or `NaN` during warmup / on a `NULL`
/// handle / if the candle is invalid (e.g. `high < low`).
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_long_legged_doji_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_long_legged_doji_update(
    handle: *mut LongLeggedDoji,
    open: f64,
    high: f64,
    low: f64,
    close: f64,
    volume: f64,
    timestamp: i64,
) -> f64 {
    let Some(ind) = handle.as_mut() else {
        return f64::NAN;
    };
    match Candle::new(open, high, low, close, volume, timestamp) {
        Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
        Err(_) => f64::NAN,
    }
}

/// Run over the OHLCV series (`open/high/low/close/volume/timestamp[0..n]`) into
/// `out[0..n]` (`NaN` at warmup or on an invalid candle).
///
/// # Safety
/// `handle` valid (from `wickra_long_legged_doji_new`, not freed); every input pointer and `out`
/// cover `n` elements.
#[no_mangle]
pub unsafe extern "C" fn wickra_long_legged_doji_batch(
    handle: *mut LongLeggedDoji,
    open: *const f64,
    high: *const f64,
    low: *const f64,
    close: *const f64,
    volume: *const f64,
    timestamp: *const i64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null()
        || open.is_null()
        || high.is_null()
        || low.is_null()
        || close.is_null()
        || volume.is_null()
        || timestamp.is_null()
        || out.is_null()
    {
        return;
    }
    let ind = &mut *handle;
    let opens = slice::from_raw_parts(open, n);
    let highs = slice::from_raw_parts(high, n);
    let lows = slice::from_raw_parts(low, n);
    let closes = slice::from_raw_parts(close, n);
    let volumes = slice::from_raw_parts(volume, n);
    let timestamps = slice::from_raw_parts(timestamp, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (idx, slot) in outputs.iter_mut().enumerate() {
        *slot = match Candle::new(
            opens[idx],
            highs[idx],
            lows[idx],
            closes[idx],
            volumes[idx],
            timestamps[idx],
        ) {
            Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
            Err(_) => f64::NAN,
        };
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_long_legged_doji_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_long_legged_doji_warmup_period(
    handle: *mut LongLeggedDoji,
) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_long_legged_doji_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_long_legged_doji_is_ready(handle: *mut LongLeggedDoji) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_long_legged_doji_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_long_legged_doji_name(
    handle: *mut LongLeggedDoji,
) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_long_legged_doji_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_long_legged_doji_reset(handle: *mut LongLeggedDoji) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_long_legged_doji_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_long_legged_doji_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_long_legged_doji_free(handle: *mut LongLeggedDoji) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `LongLine` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_long_line_free`.
#[no_mangle]
pub extern "C" fn wickra_long_line_new() -> *mut LongLine {
    Box::into_raw(Box::new(LongLine::new()))
}

/// Feed one OHLCV candle; returns the output, or `NaN` during warmup / on a `NULL`
/// handle / if the candle is invalid (e.g. `high < low`).
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_long_line_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_long_line_update(
    handle: *mut LongLine,
    open: f64,
    high: f64,
    low: f64,
    close: f64,
    volume: f64,
    timestamp: i64,
) -> f64 {
    let Some(ind) = handle.as_mut() else {
        return f64::NAN;
    };
    match Candle::new(open, high, low, close, volume, timestamp) {
        Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
        Err(_) => f64::NAN,
    }
}

/// Run over the OHLCV series (`open/high/low/close/volume/timestamp[0..n]`) into
/// `out[0..n]` (`NaN` at warmup or on an invalid candle).
///
/// # Safety
/// `handle` valid (from `wickra_long_line_new`, not freed); every input pointer and `out`
/// cover `n` elements.
#[no_mangle]
pub unsafe extern "C" fn wickra_long_line_batch(
    handle: *mut LongLine,
    open: *const f64,
    high: *const f64,
    low: *const f64,
    close: *const f64,
    volume: *const f64,
    timestamp: *const i64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null()
        || open.is_null()
        || high.is_null()
        || low.is_null()
        || close.is_null()
        || volume.is_null()
        || timestamp.is_null()
        || out.is_null()
    {
        return;
    }
    let ind = &mut *handle;
    let opens = slice::from_raw_parts(open, n);
    let highs = slice::from_raw_parts(high, n);
    let lows = slice::from_raw_parts(low, n);
    let closes = slice::from_raw_parts(close, n);
    let volumes = slice::from_raw_parts(volume, n);
    let timestamps = slice::from_raw_parts(timestamp, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (idx, slot) in outputs.iter_mut().enumerate() {
        *slot = match Candle::new(
            opens[idx],
            highs[idx],
            lows[idx],
            closes[idx],
            volumes[idx],
            timestamps[idx],
        ) {
            Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
            Err(_) => f64::NAN,
        };
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_long_line_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_long_line_warmup_period(handle: *mut LongLine) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_long_line_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_long_line_is_ready(handle: *mut LongLine) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_long_line_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_long_line_name(handle: *mut LongLine) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_long_line_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_long_line_reset(handle: *mut LongLine) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_long_line_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_long_line_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_long_line_free(handle: *mut LongLine) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `MarketFacilitationIndex` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_market_facilitation_index_free`.
#[no_mangle]
pub extern "C" fn wickra_market_facilitation_index_new() -> *mut MarketFacilitationIndex {
    Box::into_raw(Box::new(MarketFacilitationIndex::new()))
}

/// Feed one OHLCV candle; returns the output, or `NaN` during warmup / on a `NULL`
/// handle / if the candle is invalid (e.g. `high < low`).
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_market_facilitation_index_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_market_facilitation_index_update(
    handle: *mut MarketFacilitationIndex,
    open: f64,
    high: f64,
    low: f64,
    close: f64,
    volume: f64,
    timestamp: i64,
) -> f64 {
    let Some(ind) = handle.as_mut() else {
        return f64::NAN;
    };
    match Candle::new(open, high, low, close, volume, timestamp) {
        Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
        Err(_) => f64::NAN,
    }
}

/// Run over the OHLCV series (`open/high/low/close/volume/timestamp[0..n]`) into
/// `out[0..n]` (`NaN` at warmup or on an invalid candle).
///
/// # Safety
/// `handle` valid (from `wickra_market_facilitation_index_new`, not freed); every input pointer and `out`
/// cover `n` elements.
#[no_mangle]
pub unsafe extern "C" fn wickra_market_facilitation_index_batch(
    handle: *mut MarketFacilitationIndex,
    open: *const f64,
    high: *const f64,
    low: *const f64,
    close: *const f64,
    volume: *const f64,
    timestamp: *const i64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null()
        || open.is_null()
        || high.is_null()
        || low.is_null()
        || close.is_null()
        || volume.is_null()
        || timestamp.is_null()
        || out.is_null()
    {
        return;
    }
    let ind = &mut *handle;
    let opens = slice::from_raw_parts(open, n);
    let highs = slice::from_raw_parts(high, n);
    let lows = slice::from_raw_parts(low, n);
    let closes = slice::from_raw_parts(close, n);
    let volumes = slice::from_raw_parts(volume, n);
    let timestamps = slice::from_raw_parts(timestamp, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (idx, slot) in outputs.iter_mut().enumerate() {
        *slot = match Candle::new(
            opens[idx],
            highs[idx],
            lows[idx],
            closes[idx],
            volumes[idx],
            timestamps[idx],
        ) {
            Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
            Err(_) => f64::NAN,
        };
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_market_facilitation_index_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_market_facilitation_index_warmup_period(
    handle: *mut MarketFacilitationIndex,
) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_market_facilitation_index_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_market_facilitation_index_is_ready(
    handle: *mut MarketFacilitationIndex,
) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_market_facilitation_index_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_market_facilitation_index_name(
    handle: *mut MarketFacilitationIndex,
) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_market_facilitation_index_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_market_facilitation_index_reset(
    handle: *mut MarketFacilitationIndex,
) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_market_facilitation_index_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_market_facilitation_index_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_market_facilitation_index_free(
    handle: *mut MarketFacilitationIndex,
) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `Marubozu` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_marubozu_free`.
#[no_mangle]
pub extern "C" fn wickra_marubozu_new() -> *mut Marubozu {
    Box::into_raw(Box::new(Marubozu::new()))
}

/// Feed one OHLCV candle; returns the output, or `NaN` during warmup / on a `NULL`
/// handle / if the candle is invalid (e.g. `high < low`).
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_marubozu_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_marubozu_update(
    handle: *mut Marubozu,
    open: f64,
    high: f64,
    low: f64,
    close: f64,
    volume: f64,
    timestamp: i64,
) -> f64 {
    let Some(ind) = handle.as_mut() else {
        return f64::NAN;
    };
    match Candle::new(open, high, low, close, volume, timestamp) {
        Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
        Err(_) => f64::NAN,
    }
}

/// Run over the OHLCV series (`open/high/low/close/volume/timestamp[0..n]`) into
/// `out[0..n]` (`NaN` at warmup or on an invalid candle).
///
/// # Safety
/// `handle` valid (from `wickra_marubozu_new`, not freed); every input pointer and `out`
/// cover `n` elements.
#[no_mangle]
pub unsafe extern "C" fn wickra_marubozu_batch(
    handle: *mut Marubozu,
    open: *const f64,
    high: *const f64,
    low: *const f64,
    close: *const f64,
    volume: *const f64,
    timestamp: *const i64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null()
        || open.is_null()
        || high.is_null()
        || low.is_null()
        || close.is_null()
        || volume.is_null()
        || timestamp.is_null()
        || out.is_null()
    {
        return;
    }
    let ind = &mut *handle;
    let opens = slice::from_raw_parts(open, n);
    let highs = slice::from_raw_parts(high, n);
    let lows = slice::from_raw_parts(low, n);
    let closes = slice::from_raw_parts(close, n);
    let volumes = slice::from_raw_parts(volume, n);
    let timestamps = slice::from_raw_parts(timestamp, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (idx, slot) in outputs.iter_mut().enumerate() {
        *slot = match Candle::new(
            opens[idx],
            highs[idx],
            lows[idx],
            closes[idx],
            volumes[idx],
            timestamps[idx],
        ) {
            Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
            Err(_) => f64::NAN,
        };
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_marubozu_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_marubozu_warmup_period(handle: *mut Marubozu) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_marubozu_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_marubozu_is_ready(handle: *mut Marubozu) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_marubozu_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_marubozu_name(handle: *mut Marubozu) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_marubozu_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_marubozu_reset(handle: *mut Marubozu) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_marubozu_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_marubozu_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_marubozu_free(handle: *mut Marubozu) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `MassIndex` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_mass_index_free`.
#[no_mangle]
pub extern "C" fn wickra_mass_index_new(ema_period: usize, sum_period: usize) -> *mut MassIndex {
    match MassIndex::new(ema_period, sum_period) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one OHLCV candle; returns the output, or `NaN` during warmup / on a `NULL`
/// handle / if the candle is invalid (e.g. `high < low`).
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_mass_index_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_mass_index_update(
    handle: *mut MassIndex,
    open: f64,
    high: f64,
    low: f64,
    close: f64,
    volume: f64,
    timestamp: i64,
) -> f64 {
    let Some(ind) = handle.as_mut() else {
        return f64::NAN;
    };
    match Candle::new(open, high, low, close, volume, timestamp) {
        Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
        Err(_) => f64::NAN,
    }
}

/// Run over the OHLCV series (`open/high/low/close/volume/timestamp[0..n]`) into
/// `out[0..n]` (`NaN` at warmup or on an invalid candle).
///
/// # Safety
/// `handle` valid (from `wickra_mass_index_new`, not freed); every input pointer and `out`
/// cover `n` elements.
#[no_mangle]
pub unsafe extern "C" fn wickra_mass_index_batch(
    handle: *mut MassIndex,
    open: *const f64,
    high: *const f64,
    low: *const f64,
    close: *const f64,
    volume: *const f64,
    timestamp: *const i64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null()
        || open.is_null()
        || high.is_null()
        || low.is_null()
        || close.is_null()
        || volume.is_null()
        || timestamp.is_null()
        || out.is_null()
    {
        return;
    }
    let ind = &mut *handle;
    let opens = slice::from_raw_parts(open, n);
    let highs = slice::from_raw_parts(high, n);
    let lows = slice::from_raw_parts(low, n);
    let closes = slice::from_raw_parts(close, n);
    let volumes = slice::from_raw_parts(volume, n);
    let timestamps = slice::from_raw_parts(timestamp, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (idx, slot) in outputs.iter_mut().enumerate() {
        *slot = match Candle::new(
            opens[idx],
            highs[idx],
            lows[idx],
            closes[idx],
            volumes[idx],
            timestamps[idx],
        ) {
            Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
            Err(_) => f64::NAN,
        };
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_mass_index_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_mass_index_warmup_period(handle: *mut MassIndex) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_mass_index_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_mass_index_is_ready(handle: *mut MassIndex) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_mass_index_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_mass_index_name(handle: *mut MassIndex) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_mass_index_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_mass_index_reset(handle: *mut MassIndex) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_mass_index_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_mass_index_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_mass_index_free(handle: *mut MassIndex) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `MatHold` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_mat_hold_free`.
#[no_mangle]
pub extern "C" fn wickra_mat_hold_new() -> *mut MatHold {
    Box::into_raw(Box::new(MatHold::new()))
}

/// Feed one OHLCV candle; returns the output, or `NaN` during warmup / on a `NULL`
/// handle / if the candle is invalid (e.g. `high < low`).
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_mat_hold_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_mat_hold_update(
    handle: *mut MatHold,
    open: f64,
    high: f64,
    low: f64,
    close: f64,
    volume: f64,
    timestamp: i64,
) -> f64 {
    let Some(ind) = handle.as_mut() else {
        return f64::NAN;
    };
    match Candle::new(open, high, low, close, volume, timestamp) {
        Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
        Err(_) => f64::NAN,
    }
}

/// Run over the OHLCV series (`open/high/low/close/volume/timestamp[0..n]`) into
/// `out[0..n]` (`NaN` at warmup or on an invalid candle).
///
/// # Safety
/// `handle` valid (from `wickra_mat_hold_new`, not freed); every input pointer and `out`
/// cover `n` elements.
#[no_mangle]
pub unsafe extern "C" fn wickra_mat_hold_batch(
    handle: *mut MatHold,
    open: *const f64,
    high: *const f64,
    low: *const f64,
    close: *const f64,
    volume: *const f64,
    timestamp: *const i64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null()
        || open.is_null()
        || high.is_null()
        || low.is_null()
        || close.is_null()
        || volume.is_null()
        || timestamp.is_null()
        || out.is_null()
    {
        return;
    }
    let ind = &mut *handle;
    let opens = slice::from_raw_parts(open, n);
    let highs = slice::from_raw_parts(high, n);
    let lows = slice::from_raw_parts(low, n);
    let closes = slice::from_raw_parts(close, n);
    let volumes = slice::from_raw_parts(volume, n);
    let timestamps = slice::from_raw_parts(timestamp, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (idx, slot) in outputs.iter_mut().enumerate() {
        *slot = match Candle::new(
            opens[idx],
            highs[idx],
            lows[idx],
            closes[idx],
            volumes[idx],
            timestamps[idx],
        ) {
            Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
            Err(_) => f64::NAN,
        };
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_mat_hold_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_mat_hold_warmup_period(handle: *mut MatHold) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_mat_hold_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_mat_hold_is_ready(handle: *mut MatHold) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_mat_hold_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_mat_hold_name(handle: *mut MatHold) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_mat_hold_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_mat_hold_reset(handle: *mut MatHold) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_mat_hold_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_mat_hold_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_mat_hold_free(handle: *mut MatHold) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `MatchingLow` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_matching_low_free`.
#[no_mangle]
pub extern "C" fn wickra_matching_low_new() -> *mut MatchingLow {
    Box::into_raw(Box::new(MatchingLow::new()))
}

/// Feed one OHLCV candle; returns the output, or `NaN` during warmup / on a `NULL`
/// handle / if the candle is invalid (e.g. `high < low`).
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_matching_low_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_matching_low_update(
    handle: *mut MatchingLow,
    open: f64,
    high: f64,
    low: f64,
    close: f64,
    volume: f64,
    timestamp: i64,
) -> f64 {
    let Some(ind) = handle.as_mut() else {
        return f64::NAN;
    };
    match Candle::new(open, high, low, close, volume, timestamp) {
        Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
        Err(_) => f64::NAN,
    }
}

/// Run over the OHLCV series (`open/high/low/close/volume/timestamp[0..n]`) into
/// `out[0..n]` (`NaN` at warmup or on an invalid candle).
///
/// # Safety
/// `handle` valid (from `wickra_matching_low_new`, not freed); every input pointer and `out`
/// cover `n` elements.
#[no_mangle]
pub unsafe extern "C" fn wickra_matching_low_batch(
    handle: *mut MatchingLow,
    open: *const f64,
    high: *const f64,
    low: *const f64,
    close: *const f64,
    volume: *const f64,
    timestamp: *const i64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null()
        || open.is_null()
        || high.is_null()
        || low.is_null()
        || close.is_null()
        || volume.is_null()
        || timestamp.is_null()
        || out.is_null()
    {
        return;
    }
    let ind = &mut *handle;
    let opens = slice::from_raw_parts(open, n);
    let highs = slice::from_raw_parts(high, n);
    let lows = slice::from_raw_parts(low, n);
    let closes = slice::from_raw_parts(close, n);
    let volumes = slice::from_raw_parts(volume, n);
    let timestamps = slice::from_raw_parts(timestamp, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (idx, slot) in outputs.iter_mut().enumerate() {
        *slot = match Candle::new(
            opens[idx],
            highs[idx],
            lows[idx],
            closes[idx],
            volumes[idx],
            timestamps[idx],
        ) {
            Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
            Err(_) => f64::NAN,
        };
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_matching_low_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_matching_low_warmup_period(handle: *mut MatchingLow) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_matching_low_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_matching_low_is_ready(handle: *mut MatchingLow) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_matching_low_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_matching_low_name(handle: *mut MatchingLow) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_matching_low_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_matching_low_reset(handle: *mut MatchingLow) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_matching_low_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_matching_low_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_matching_low_free(handle: *mut MatchingLow) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `MedianPrice` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_median_price_free`.
#[no_mangle]
pub extern "C" fn wickra_median_price_new() -> *mut MedianPrice {
    Box::into_raw(Box::new(MedianPrice::new()))
}

/// Feed one OHLCV candle; returns the output, or `NaN` during warmup / on a `NULL`
/// handle / if the candle is invalid (e.g. `high < low`).
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_median_price_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_median_price_update(
    handle: *mut MedianPrice,
    open: f64,
    high: f64,
    low: f64,
    close: f64,
    volume: f64,
    timestamp: i64,
) -> f64 {
    let Some(ind) = handle.as_mut() else {
        return f64::NAN;
    };
    match Candle::new(open, high, low, close, volume, timestamp) {
        Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
        Err(_) => f64::NAN,
    }
}

/// Run over the OHLCV series (`open/high/low/close/volume/timestamp[0..n]`) into
/// `out[0..n]` (`NaN` at warmup or on an invalid candle).
///
/// # Safety
/// `handle` valid (from `wickra_median_price_new`, not freed); every input pointer and `out`
/// cover `n` elements.
#[no_mangle]
pub unsafe extern "C" fn wickra_median_price_batch(
    handle: *mut MedianPrice,
    open: *const f64,
    high: *const f64,
    low: *const f64,
    close: *const f64,
    volume: *const f64,
    timestamp: *const i64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null()
        || open.is_null()
        || high.is_null()
        || low.is_null()
        || close.is_null()
        || volume.is_null()
        || timestamp.is_null()
        || out.is_null()
    {
        return;
    }
    let ind = &mut *handle;
    let opens = slice::from_raw_parts(open, n);
    let highs = slice::from_raw_parts(high, n);
    let lows = slice::from_raw_parts(low, n);
    let closes = slice::from_raw_parts(close, n);
    let volumes = slice::from_raw_parts(volume, n);
    let timestamps = slice::from_raw_parts(timestamp, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (idx, slot) in outputs.iter_mut().enumerate() {
        *slot = match Candle::new(
            opens[idx],
            highs[idx],
            lows[idx],
            closes[idx],
            volumes[idx],
            timestamps[idx],
        ) {
            Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
            Err(_) => f64::NAN,
        };
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_median_price_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_median_price_warmup_period(handle: *mut MedianPrice) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_median_price_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_median_price_is_ready(handle: *mut MedianPrice) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_median_price_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_median_price_name(handle: *mut MedianPrice) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_median_price_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_median_price_reset(handle: *mut MedianPrice) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_median_price_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_median_price_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_median_price_free(handle: *mut MedianPrice) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `Mfi` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_mfi_free`.
#[no_mangle]
pub extern "C" fn wickra_mfi_new(period: usize) -> *mut Mfi {
    match Mfi::new(period) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one OHLCV candle; returns the output, or `NaN` during warmup / on a `NULL`
/// handle / if the candle is invalid (e.g. `high < low`).
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_mfi_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_mfi_update(
    handle: *mut Mfi,
    open: f64,
    high: f64,
    low: f64,
    close: f64,
    volume: f64,
    timestamp: i64,
) -> f64 {
    let Some(ind) = handle.as_mut() else {
        return f64::NAN;
    };
    match Candle::new(open, high, low, close, volume, timestamp) {
        Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
        Err(_) => f64::NAN,
    }
}

/// Run over the OHLCV series (`open/high/low/close/volume/timestamp[0..n]`) into
/// `out[0..n]` (`NaN` at warmup or on an invalid candle).
///
/// # Safety
/// `handle` valid (from `wickra_mfi_new`, not freed); every input pointer and `out`
/// cover `n` elements.
#[no_mangle]
pub unsafe extern "C" fn wickra_mfi_batch(
    handle: *mut Mfi,
    open: *const f64,
    high: *const f64,
    low: *const f64,
    close: *const f64,
    volume: *const f64,
    timestamp: *const i64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null()
        || open.is_null()
        || high.is_null()
        || low.is_null()
        || close.is_null()
        || volume.is_null()
        || timestamp.is_null()
        || out.is_null()
    {
        return;
    }
    let ind = &mut *handle;
    let opens = slice::from_raw_parts(open, n);
    let highs = slice::from_raw_parts(high, n);
    let lows = slice::from_raw_parts(low, n);
    let closes = slice::from_raw_parts(close, n);
    let volumes = slice::from_raw_parts(volume, n);
    let timestamps = slice::from_raw_parts(timestamp, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (idx, slot) in outputs.iter_mut().enumerate() {
        *slot = match Candle::new(
            opens[idx],
            highs[idx],
            lows[idx],
            closes[idx],
            volumes[idx],
            timestamps[idx],
        ) {
            Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
            Err(_) => f64::NAN,
        };
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_mfi_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_mfi_warmup_period(handle: *mut Mfi) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_mfi_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_mfi_is_ready(handle: *mut Mfi) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_mfi_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_mfi_name(handle: *mut Mfi) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_mfi_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_mfi_reset(handle: *mut Mfi) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_mfi_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_mfi_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_mfi_free(handle: *mut Mfi) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `MidPrice` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_mid_price_free`.
#[no_mangle]
pub extern "C" fn wickra_mid_price_new(period: usize) -> *mut MidPrice {
    match MidPrice::new(period) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one OHLCV candle; returns the output, or `NaN` during warmup / on a `NULL`
/// handle / if the candle is invalid (e.g. `high < low`).
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_mid_price_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_mid_price_update(
    handle: *mut MidPrice,
    open: f64,
    high: f64,
    low: f64,
    close: f64,
    volume: f64,
    timestamp: i64,
) -> f64 {
    let Some(ind) = handle.as_mut() else {
        return f64::NAN;
    };
    match Candle::new(open, high, low, close, volume, timestamp) {
        Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
        Err(_) => f64::NAN,
    }
}

/// Run over the OHLCV series (`open/high/low/close/volume/timestamp[0..n]`) into
/// `out[0..n]` (`NaN` at warmup or on an invalid candle).
///
/// # Safety
/// `handle` valid (from `wickra_mid_price_new`, not freed); every input pointer and `out`
/// cover `n` elements.
#[no_mangle]
pub unsafe extern "C" fn wickra_mid_price_batch(
    handle: *mut MidPrice,
    open: *const f64,
    high: *const f64,
    low: *const f64,
    close: *const f64,
    volume: *const f64,
    timestamp: *const i64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null()
        || open.is_null()
        || high.is_null()
        || low.is_null()
        || close.is_null()
        || volume.is_null()
        || timestamp.is_null()
        || out.is_null()
    {
        return;
    }
    let ind = &mut *handle;
    let opens = slice::from_raw_parts(open, n);
    let highs = slice::from_raw_parts(high, n);
    let lows = slice::from_raw_parts(low, n);
    let closes = slice::from_raw_parts(close, n);
    let volumes = slice::from_raw_parts(volume, n);
    let timestamps = slice::from_raw_parts(timestamp, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (idx, slot) in outputs.iter_mut().enumerate() {
        *slot = match Candle::new(
            opens[idx],
            highs[idx],
            lows[idx],
            closes[idx],
            volumes[idx],
            timestamps[idx],
        ) {
            Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
            Err(_) => f64::NAN,
        };
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_mid_price_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_mid_price_warmup_period(handle: *mut MidPrice) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_mid_price_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_mid_price_is_ready(handle: *mut MidPrice) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_mid_price_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_mid_price_name(handle: *mut MidPrice) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_mid_price_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_mid_price_reset(handle: *mut MidPrice) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_mid_price_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_mid_price_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_mid_price_free(handle: *mut MidPrice) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `MinusDi` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_minus_di_free`.
#[no_mangle]
pub extern "C" fn wickra_minus_di_new(period: usize) -> *mut MinusDi {
    match MinusDi::new(period) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one OHLCV candle; returns the output, or `NaN` during warmup / on a `NULL`
/// handle / if the candle is invalid (e.g. `high < low`).
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_minus_di_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_minus_di_update(
    handle: *mut MinusDi,
    open: f64,
    high: f64,
    low: f64,
    close: f64,
    volume: f64,
    timestamp: i64,
) -> f64 {
    let Some(ind) = handle.as_mut() else {
        return f64::NAN;
    };
    match Candle::new(open, high, low, close, volume, timestamp) {
        Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
        Err(_) => f64::NAN,
    }
}

/// Run over the OHLCV series (`open/high/low/close/volume/timestamp[0..n]`) into
/// `out[0..n]` (`NaN` at warmup or on an invalid candle).
///
/// # Safety
/// `handle` valid (from `wickra_minus_di_new`, not freed); every input pointer and `out`
/// cover `n` elements.
#[no_mangle]
pub unsafe extern "C" fn wickra_minus_di_batch(
    handle: *mut MinusDi,
    open: *const f64,
    high: *const f64,
    low: *const f64,
    close: *const f64,
    volume: *const f64,
    timestamp: *const i64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null()
        || open.is_null()
        || high.is_null()
        || low.is_null()
        || close.is_null()
        || volume.is_null()
        || timestamp.is_null()
        || out.is_null()
    {
        return;
    }
    let ind = &mut *handle;
    let opens = slice::from_raw_parts(open, n);
    let highs = slice::from_raw_parts(high, n);
    let lows = slice::from_raw_parts(low, n);
    let closes = slice::from_raw_parts(close, n);
    let volumes = slice::from_raw_parts(volume, n);
    let timestamps = slice::from_raw_parts(timestamp, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (idx, slot) in outputs.iter_mut().enumerate() {
        *slot = match Candle::new(
            opens[idx],
            highs[idx],
            lows[idx],
            closes[idx],
            volumes[idx],
            timestamps[idx],
        ) {
            Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
            Err(_) => f64::NAN,
        };
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_minus_di_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_minus_di_warmup_period(handle: *mut MinusDi) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_minus_di_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_minus_di_is_ready(handle: *mut MinusDi) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_minus_di_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_minus_di_name(handle: *mut MinusDi) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_minus_di_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_minus_di_reset(handle: *mut MinusDi) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_minus_di_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_minus_di_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_minus_di_free(handle: *mut MinusDi) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `MinusDm` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_minus_dm_free`.
#[no_mangle]
pub extern "C" fn wickra_minus_dm_new(period: usize) -> *mut MinusDm {
    match MinusDm::new(period) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one OHLCV candle; returns the output, or `NaN` during warmup / on a `NULL`
/// handle / if the candle is invalid (e.g. `high < low`).
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_minus_dm_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_minus_dm_update(
    handle: *mut MinusDm,
    open: f64,
    high: f64,
    low: f64,
    close: f64,
    volume: f64,
    timestamp: i64,
) -> f64 {
    let Some(ind) = handle.as_mut() else {
        return f64::NAN;
    };
    match Candle::new(open, high, low, close, volume, timestamp) {
        Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
        Err(_) => f64::NAN,
    }
}

/// Run over the OHLCV series (`open/high/low/close/volume/timestamp[0..n]`) into
/// `out[0..n]` (`NaN` at warmup or on an invalid candle).
///
/// # Safety
/// `handle` valid (from `wickra_minus_dm_new`, not freed); every input pointer and `out`
/// cover `n` elements.
#[no_mangle]
pub unsafe extern "C" fn wickra_minus_dm_batch(
    handle: *mut MinusDm,
    open: *const f64,
    high: *const f64,
    low: *const f64,
    close: *const f64,
    volume: *const f64,
    timestamp: *const i64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null()
        || open.is_null()
        || high.is_null()
        || low.is_null()
        || close.is_null()
        || volume.is_null()
        || timestamp.is_null()
        || out.is_null()
    {
        return;
    }
    let ind = &mut *handle;
    let opens = slice::from_raw_parts(open, n);
    let highs = slice::from_raw_parts(high, n);
    let lows = slice::from_raw_parts(low, n);
    let closes = slice::from_raw_parts(close, n);
    let volumes = slice::from_raw_parts(volume, n);
    let timestamps = slice::from_raw_parts(timestamp, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (idx, slot) in outputs.iter_mut().enumerate() {
        *slot = match Candle::new(
            opens[idx],
            highs[idx],
            lows[idx],
            closes[idx],
            volumes[idx],
            timestamps[idx],
        ) {
            Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
            Err(_) => f64::NAN,
        };
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_minus_dm_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_minus_dm_warmup_period(handle: *mut MinusDm) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_minus_dm_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_minus_dm_is_ready(handle: *mut MinusDm) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_minus_dm_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_minus_dm_name(handle: *mut MinusDm) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_minus_dm_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_minus_dm_reset(handle: *mut MinusDm) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_minus_dm_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_minus_dm_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_minus_dm_free(handle: *mut MinusDm) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `MorningDojiStar` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_morning_doji_star_free`.
#[no_mangle]
pub extern "C" fn wickra_morning_doji_star_new() -> *mut MorningDojiStar {
    Box::into_raw(Box::new(MorningDojiStar::new()))
}

/// Feed one OHLCV candle; returns the output, or `NaN` during warmup / on a `NULL`
/// handle / if the candle is invalid (e.g. `high < low`).
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_morning_doji_star_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_morning_doji_star_update(
    handle: *mut MorningDojiStar,
    open: f64,
    high: f64,
    low: f64,
    close: f64,
    volume: f64,
    timestamp: i64,
) -> f64 {
    let Some(ind) = handle.as_mut() else {
        return f64::NAN;
    };
    match Candle::new(open, high, low, close, volume, timestamp) {
        Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
        Err(_) => f64::NAN,
    }
}

/// Run over the OHLCV series (`open/high/low/close/volume/timestamp[0..n]`) into
/// `out[0..n]` (`NaN` at warmup or on an invalid candle).
///
/// # Safety
/// `handle` valid (from `wickra_morning_doji_star_new`, not freed); every input pointer and `out`
/// cover `n` elements.
#[no_mangle]
pub unsafe extern "C" fn wickra_morning_doji_star_batch(
    handle: *mut MorningDojiStar,
    open: *const f64,
    high: *const f64,
    low: *const f64,
    close: *const f64,
    volume: *const f64,
    timestamp: *const i64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null()
        || open.is_null()
        || high.is_null()
        || low.is_null()
        || close.is_null()
        || volume.is_null()
        || timestamp.is_null()
        || out.is_null()
    {
        return;
    }
    let ind = &mut *handle;
    let opens = slice::from_raw_parts(open, n);
    let highs = slice::from_raw_parts(high, n);
    let lows = slice::from_raw_parts(low, n);
    let closes = slice::from_raw_parts(close, n);
    let volumes = slice::from_raw_parts(volume, n);
    let timestamps = slice::from_raw_parts(timestamp, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (idx, slot) in outputs.iter_mut().enumerate() {
        *slot = match Candle::new(
            opens[idx],
            highs[idx],
            lows[idx],
            closes[idx],
            volumes[idx],
            timestamps[idx],
        ) {
            Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
            Err(_) => f64::NAN,
        };
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_morning_doji_star_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_morning_doji_star_warmup_period(
    handle: *mut MorningDojiStar,
) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_morning_doji_star_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_morning_doji_star_is_ready(handle: *mut MorningDojiStar) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_morning_doji_star_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_morning_doji_star_name(
    handle: *mut MorningDojiStar,
) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_morning_doji_star_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_morning_doji_star_reset(handle: *mut MorningDojiStar) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_morning_doji_star_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_morning_doji_star_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_morning_doji_star_free(handle: *mut MorningDojiStar) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `MorningEveningStar` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_morning_evening_star_free`.
#[no_mangle]
pub extern "C" fn wickra_morning_evening_star_new() -> *mut MorningEveningStar {
    Box::into_raw(Box::new(MorningEveningStar::new()))
}

/// Feed one OHLCV candle; returns the output, or `NaN` during warmup / on a `NULL`
/// handle / if the candle is invalid (e.g. `high < low`).
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_morning_evening_star_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_morning_evening_star_update(
    handle: *mut MorningEveningStar,
    open: f64,
    high: f64,
    low: f64,
    close: f64,
    volume: f64,
    timestamp: i64,
) -> f64 {
    let Some(ind) = handle.as_mut() else {
        return f64::NAN;
    };
    match Candle::new(open, high, low, close, volume, timestamp) {
        Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
        Err(_) => f64::NAN,
    }
}

/// Run over the OHLCV series (`open/high/low/close/volume/timestamp[0..n]`) into
/// `out[0..n]` (`NaN` at warmup or on an invalid candle).
///
/// # Safety
/// `handle` valid (from `wickra_morning_evening_star_new`, not freed); every input pointer and `out`
/// cover `n` elements.
#[no_mangle]
pub unsafe extern "C" fn wickra_morning_evening_star_batch(
    handle: *mut MorningEveningStar,
    open: *const f64,
    high: *const f64,
    low: *const f64,
    close: *const f64,
    volume: *const f64,
    timestamp: *const i64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null()
        || open.is_null()
        || high.is_null()
        || low.is_null()
        || close.is_null()
        || volume.is_null()
        || timestamp.is_null()
        || out.is_null()
    {
        return;
    }
    let ind = &mut *handle;
    let opens = slice::from_raw_parts(open, n);
    let highs = slice::from_raw_parts(high, n);
    let lows = slice::from_raw_parts(low, n);
    let closes = slice::from_raw_parts(close, n);
    let volumes = slice::from_raw_parts(volume, n);
    let timestamps = slice::from_raw_parts(timestamp, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (idx, slot) in outputs.iter_mut().enumerate() {
        *slot = match Candle::new(
            opens[idx],
            highs[idx],
            lows[idx],
            closes[idx],
            volumes[idx],
            timestamps[idx],
        ) {
            Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
            Err(_) => f64::NAN,
        };
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_morning_evening_star_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_morning_evening_star_warmup_period(
    handle: *mut MorningEveningStar,
) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_morning_evening_star_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_morning_evening_star_is_ready(
    handle: *mut MorningEveningStar,
) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_morning_evening_star_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_morning_evening_star_name(
    handle: *mut MorningEveningStar,
) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_morning_evening_star_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_morning_evening_star_reset(handle: *mut MorningEveningStar) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_morning_evening_star_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_morning_evening_star_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_morning_evening_star_free(handle: *mut MorningEveningStar) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `NakedPoc` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_naked_poc_free`.
#[no_mangle]
pub extern "C" fn wickra_naked_poc_new(session_len: usize, bins: usize) -> *mut NakedPoc {
    match NakedPoc::new(session_len, bins) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one OHLCV candle; returns the output, or `NaN` during warmup / on a `NULL`
/// handle / if the candle is invalid (e.g. `high < low`).
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_naked_poc_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_naked_poc_update(
    handle: *mut NakedPoc,
    open: f64,
    high: f64,
    low: f64,
    close: f64,
    volume: f64,
    timestamp: i64,
) -> f64 {
    let Some(ind) = handle.as_mut() else {
        return f64::NAN;
    };
    match Candle::new(open, high, low, close, volume, timestamp) {
        Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
        Err(_) => f64::NAN,
    }
}

/// Run over the OHLCV series (`open/high/low/close/volume/timestamp[0..n]`) into
/// `out[0..n]` (`NaN` at warmup or on an invalid candle).
///
/// # Safety
/// `handle` valid (from `wickra_naked_poc_new`, not freed); every input pointer and `out`
/// cover `n` elements.
#[no_mangle]
pub unsafe extern "C" fn wickra_naked_poc_batch(
    handle: *mut NakedPoc,
    open: *const f64,
    high: *const f64,
    low: *const f64,
    close: *const f64,
    volume: *const f64,
    timestamp: *const i64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null()
        || open.is_null()
        || high.is_null()
        || low.is_null()
        || close.is_null()
        || volume.is_null()
        || timestamp.is_null()
        || out.is_null()
    {
        return;
    }
    let ind = &mut *handle;
    let opens = slice::from_raw_parts(open, n);
    let highs = slice::from_raw_parts(high, n);
    let lows = slice::from_raw_parts(low, n);
    let closes = slice::from_raw_parts(close, n);
    let volumes = slice::from_raw_parts(volume, n);
    let timestamps = slice::from_raw_parts(timestamp, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (idx, slot) in outputs.iter_mut().enumerate() {
        *slot = match Candle::new(
            opens[idx],
            highs[idx],
            lows[idx],
            closes[idx],
            volumes[idx],
            timestamps[idx],
        ) {
            Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
            Err(_) => f64::NAN,
        };
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_naked_poc_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_naked_poc_warmup_period(handle: *mut NakedPoc) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_naked_poc_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_naked_poc_is_ready(handle: *mut NakedPoc) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_naked_poc_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_naked_poc_name(handle: *mut NakedPoc) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_naked_poc_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_naked_poc_reset(handle: *mut NakedPoc) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_naked_poc_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_naked_poc_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_naked_poc_free(handle: *mut NakedPoc) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `Natr` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_natr_free`.
#[no_mangle]
pub extern "C" fn wickra_natr_new(period: usize) -> *mut Natr {
    match Natr::new(period) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one OHLCV candle; returns the output, or `NaN` during warmup / on a `NULL`
/// handle / if the candle is invalid (e.g. `high < low`).
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_natr_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_natr_update(
    handle: *mut Natr,
    open: f64,
    high: f64,
    low: f64,
    close: f64,
    volume: f64,
    timestamp: i64,
) -> f64 {
    let Some(ind) = handle.as_mut() else {
        return f64::NAN;
    };
    match Candle::new(open, high, low, close, volume, timestamp) {
        Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
        Err(_) => f64::NAN,
    }
}

/// Run over the OHLCV series (`open/high/low/close/volume/timestamp[0..n]`) into
/// `out[0..n]` (`NaN` at warmup or on an invalid candle).
///
/// # Safety
/// `handle` valid (from `wickra_natr_new`, not freed); every input pointer and `out`
/// cover `n` elements.
#[no_mangle]
pub unsafe extern "C" fn wickra_natr_batch(
    handle: *mut Natr,
    open: *const f64,
    high: *const f64,
    low: *const f64,
    close: *const f64,
    volume: *const f64,
    timestamp: *const i64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null()
        || open.is_null()
        || high.is_null()
        || low.is_null()
        || close.is_null()
        || volume.is_null()
        || timestamp.is_null()
        || out.is_null()
    {
        return;
    }
    let ind = &mut *handle;
    let opens = slice::from_raw_parts(open, n);
    let highs = slice::from_raw_parts(high, n);
    let lows = slice::from_raw_parts(low, n);
    let closes = slice::from_raw_parts(close, n);
    let volumes = slice::from_raw_parts(volume, n);
    let timestamps = slice::from_raw_parts(timestamp, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (idx, slot) in outputs.iter_mut().enumerate() {
        *slot = match Candle::new(
            opens[idx],
            highs[idx],
            lows[idx],
            closes[idx],
            volumes[idx],
            timestamps[idx],
        ) {
            Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
            Err(_) => f64::NAN,
        };
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_natr_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_natr_warmup_period(handle: *mut Natr) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_natr_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_natr_is_ready(handle: *mut Natr) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_natr_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_natr_name(handle: *mut Natr) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_natr_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_natr_reset(handle: *mut Natr) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_natr_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_natr_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_natr_free(handle: *mut Natr) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `NewPriceLines` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_new_price_lines_free`.
#[no_mangle]
pub extern "C" fn wickra_new_price_lines_new(count: usize) -> *mut NewPriceLines {
    match NewPriceLines::new(count) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one OHLCV candle; returns the output, or `NaN` during warmup / on a `NULL`
/// handle / if the candle is invalid (e.g. `high < low`).
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_new_price_lines_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_new_price_lines_update(
    handle: *mut NewPriceLines,
    open: f64,
    high: f64,
    low: f64,
    close: f64,
    volume: f64,
    timestamp: i64,
) -> f64 {
    let Some(ind) = handle.as_mut() else {
        return f64::NAN;
    };
    match Candle::new(open, high, low, close, volume, timestamp) {
        Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
        Err(_) => f64::NAN,
    }
}

/// Run over the OHLCV series (`open/high/low/close/volume/timestamp[0..n]`) into
/// `out[0..n]` (`NaN` at warmup or on an invalid candle).
///
/// # Safety
/// `handle` valid (from `wickra_new_price_lines_new`, not freed); every input pointer and `out`
/// cover `n` elements.
#[no_mangle]
pub unsafe extern "C" fn wickra_new_price_lines_batch(
    handle: *mut NewPriceLines,
    open: *const f64,
    high: *const f64,
    low: *const f64,
    close: *const f64,
    volume: *const f64,
    timestamp: *const i64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null()
        || open.is_null()
        || high.is_null()
        || low.is_null()
        || close.is_null()
        || volume.is_null()
        || timestamp.is_null()
        || out.is_null()
    {
        return;
    }
    let ind = &mut *handle;
    let opens = slice::from_raw_parts(open, n);
    let highs = slice::from_raw_parts(high, n);
    let lows = slice::from_raw_parts(low, n);
    let closes = slice::from_raw_parts(close, n);
    let volumes = slice::from_raw_parts(volume, n);
    let timestamps = slice::from_raw_parts(timestamp, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (idx, slot) in outputs.iter_mut().enumerate() {
        *slot = match Candle::new(
            opens[idx],
            highs[idx],
            lows[idx],
            closes[idx],
            volumes[idx],
            timestamps[idx],
        ) {
            Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
            Err(_) => f64::NAN,
        };
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_new_price_lines_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_new_price_lines_warmup_period(handle: *mut NewPriceLines) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_new_price_lines_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_new_price_lines_is_ready(handle: *mut NewPriceLines) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_new_price_lines_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_new_price_lines_name(handle: *mut NewPriceLines) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_new_price_lines_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_new_price_lines_reset(handle: *mut NewPriceLines) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_new_price_lines_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_new_price_lines_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_new_price_lines_free(handle: *mut NewPriceLines) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `Nvi` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_nvi_free`.
#[no_mangle]
pub extern "C" fn wickra_nvi_new() -> *mut Nvi {
    Box::into_raw(Box::new(Nvi::new()))
}

/// Feed one OHLCV candle; returns the output, or `NaN` during warmup / on a `NULL`
/// handle / if the candle is invalid (e.g. `high < low`).
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_nvi_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_nvi_update(
    handle: *mut Nvi,
    open: f64,
    high: f64,
    low: f64,
    close: f64,
    volume: f64,
    timestamp: i64,
) -> f64 {
    let Some(ind) = handle.as_mut() else {
        return f64::NAN;
    };
    match Candle::new(open, high, low, close, volume, timestamp) {
        Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
        Err(_) => f64::NAN,
    }
}

/// Run over the OHLCV series (`open/high/low/close/volume/timestamp[0..n]`) into
/// `out[0..n]` (`NaN` at warmup or on an invalid candle).
///
/// # Safety
/// `handle` valid (from `wickra_nvi_new`, not freed); every input pointer and `out`
/// cover `n` elements.
#[no_mangle]
pub unsafe extern "C" fn wickra_nvi_batch(
    handle: *mut Nvi,
    open: *const f64,
    high: *const f64,
    low: *const f64,
    close: *const f64,
    volume: *const f64,
    timestamp: *const i64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null()
        || open.is_null()
        || high.is_null()
        || low.is_null()
        || close.is_null()
        || volume.is_null()
        || timestamp.is_null()
        || out.is_null()
    {
        return;
    }
    let ind = &mut *handle;
    let opens = slice::from_raw_parts(open, n);
    let highs = slice::from_raw_parts(high, n);
    let lows = slice::from_raw_parts(low, n);
    let closes = slice::from_raw_parts(close, n);
    let volumes = slice::from_raw_parts(volume, n);
    let timestamps = slice::from_raw_parts(timestamp, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (idx, slot) in outputs.iter_mut().enumerate() {
        *slot = match Candle::new(
            opens[idx],
            highs[idx],
            lows[idx],
            closes[idx],
            volumes[idx],
            timestamps[idx],
        ) {
            Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
            Err(_) => f64::NAN,
        };
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_nvi_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_nvi_warmup_period(handle: *mut Nvi) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_nvi_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_nvi_is_ready(handle: *mut Nvi) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_nvi_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_nvi_name(handle: *mut Nvi) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_nvi_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_nvi_reset(handle: *mut Nvi) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_nvi_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_nvi_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_nvi_free(handle: *mut Nvi) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `Obv` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_obv_free`.
#[no_mangle]
pub extern "C" fn wickra_obv_new() -> *mut Obv {
    Box::into_raw(Box::new(Obv::new()))
}

/// Feed one OHLCV candle; returns the output, or `NaN` during warmup / on a `NULL`
/// handle / if the candle is invalid (e.g. `high < low`).
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_obv_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_obv_update(
    handle: *mut Obv,
    open: f64,
    high: f64,
    low: f64,
    close: f64,
    volume: f64,
    timestamp: i64,
) -> f64 {
    let Some(ind) = handle.as_mut() else {
        return f64::NAN;
    };
    match Candle::new(open, high, low, close, volume, timestamp) {
        Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
        Err(_) => f64::NAN,
    }
}

/// Run over the OHLCV series (`open/high/low/close/volume/timestamp[0..n]`) into
/// `out[0..n]` (`NaN` at warmup or on an invalid candle).
///
/// # Safety
/// `handle` valid (from `wickra_obv_new`, not freed); every input pointer and `out`
/// cover `n` elements.
#[no_mangle]
pub unsafe extern "C" fn wickra_obv_batch(
    handle: *mut Obv,
    open: *const f64,
    high: *const f64,
    low: *const f64,
    close: *const f64,
    volume: *const f64,
    timestamp: *const i64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null()
        || open.is_null()
        || high.is_null()
        || low.is_null()
        || close.is_null()
        || volume.is_null()
        || timestamp.is_null()
        || out.is_null()
    {
        return;
    }
    let ind = &mut *handle;
    let opens = slice::from_raw_parts(open, n);
    let highs = slice::from_raw_parts(high, n);
    let lows = slice::from_raw_parts(low, n);
    let closes = slice::from_raw_parts(close, n);
    let volumes = slice::from_raw_parts(volume, n);
    let timestamps = slice::from_raw_parts(timestamp, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (idx, slot) in outputs.iter_mut().enumerate() {
        *slot = match Candle::new(
            opens[idx],
            highs[idx],
            lows[idx],
            closes[idx],
            volumes[idx],
            timestamps[idx],
        ) {
            Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
            Err(_) => f64::NAN,
        };
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_obv_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_obv_warmup_period(handle: *mut Obv) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_obv_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_obv_is_ready(handle: *mut Obv) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_obv_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_obv_name(handle: *mut Obv) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_obv_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_obv_reset(handle: *mut Obv) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_obv_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_obv_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_obv_free(handle: *mut Obv) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `OnNeck` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_on_neck_free`.
#[no_mangle]
pub extern "C" fn wickra_on_neck_new() -> *mut OnNeck {
    Box::into_raw(Box::new(OnNeck::new()))
}

/// Feed one OHLCV candle; returns the output, or `NaN` during warmup / on a `NULL`
/// handle / if the candle is invalid (e.g. `high < low`).
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_on_neck_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_on_neck_update(
    handle: *mut OnNeck,
    open: f64,
    high: f64,
    low: f64,
    close: f64,
    volume: f64,
    timestamp: i64,
) -> f64 {
    let Some(ind) = handle.as_mut() else {
        return f64::NAN;
    };
    match Candle::new(open, high, low, close, volume, timestamp) {
        Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
        Err(_) => f64::NAN,
    }
}

/// Run over the OHLCV series (`open/high/low/close/volume/timestamp[0..n]`) into
/// `out[0..n]` (`NaN` at warmup or on an invalid candle).
///
/// # Safety
/// `handle` valid (from `wickra_on_neck_new`, not freed); every input pointer and `out`
/// cover `n` elements.
#[no_mangle]
pub unsafe extern "C" fn wickra_on_neck_batch(
    handle: *mut OnNeck,
    open: *const f64,
    high: *const f64,
    low: *const f64,
    close: *const f64,
    volume: *const f64,
    timestamp: *const i64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null()
        || open.is_null()
        || high.is_null()
        || low.is_null()
        || close.is_null()
        || volume.is_null()
        || timestamp.is_null()
        || out.is_null()
    {
        return;
    }
    let ind = &mut *handle;
    let opens = slice::from_raw_parts(open, n);
    let highs = slice::from_raw_parts(high, n);
    let lows = slice::from_raw_parts(low, n);
    let closes = slice::from_raw_parts(close, n);
    let volumes = slice::from_raw_parts(volume, n);
    let timestamps = slice::from_raw_parts(timestamp, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (idx, slot) in outputs.iter_mut().enumerate() {
        *slot = match Candle::new(
            opens[idx],
            highs[idx],
            lows[idx],
            closes[idx],
            volumes[idx],
            timestamps[idx],
        ) {
            Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
            Err(_) => f64::NAN,
        };
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_on_neck_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_on_neck_warmup_period(handle: *mut OnNeck) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_on_neck_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_on_neck_is_ready(handle: *mut OnNeck) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_on_neck_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_on_neck_name(handle: *mut OnNeck) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_on_neck_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_on_neck_reset(handle: *mut OnNeck) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_on_neck_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_on_neck_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_on_neck_free(handle: *mut OnNeck) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `OpeningMarubozu` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_opening_marubozu_free`.
#[no_mangle]
pub extern "C" fn wickra_opening_marubozu_new() -> *mut OpeningMarubozu {
    Box::into_raw(Box::new(OpeningMarubozu::new()))
}

/// Feed one OHLCV candle; returns the output, or `NaN` during warmup / on a `NULL`
/// handle / if the candle is invalid (e.g. `high < low`).
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_opening_marubozu_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_opening_marubozu_update(
    handle: *mut OpeningMarubozu,
    open: f64,
    high: f64,
    low: f64,
    close: f64,
    volume: f64,
    timestamp: i64,
) -> f64 {
    let Some(ind) = handle.as_mut() else {
        return f64::NAN;
    };
    match Candle::new(open, high, low, close, volume, timestamp) {
        Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
        Err(_) => f64::NAN,
    }
}

/// Run over the OHLCV series (`open/high/low/close/volume/timestamp[0..n]`) into
/// `out[0..n]` (`NaN` at warmup or on an invalid candle).
///
/// # Safety
/// `handle` valid (from `wickra_opening_marubozu_new`, not freed); every input pointer and `out`
/// cover `n` elements.
#[no_mangle]
pub unsafe extern "C" fn wickra_opening_marubozu_batch(
    handle: *mut OpeningMarubozu,
    open: *const f64,
    high: *const f64,
    low: *const f64,
    close: *const f64,
    volume: *const f64,
    timestamp: *const i64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null()
        || open.is_null()
        || high.is_null()
        || low.is_null()
        || close.is_null()
        || volume.is_null()
        || timestamp.is_null()
        || out.is_null()
    {
        return;
    }
    let ind = &mut *handle;
    let opens = slice::from_raw_parts(open, n);
    let highs = slice::from_raw_parts(high, n);
    let lows = slice::from_raw_parts(low, n);
    let closes = slice::from_raw_parts(close, n);
    let volumes = slice::from_raw_parts(volume, n);
    let timestamps = slice::from_raw_parts(timestamp, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (idx, slot) in outputs.iter_mut().enumerate() {
        *slot = match Candle::new(
            opens[idx],
            highs[idx],
            lows[idx],
            closes[idx],
            volumes[idx],
            timestamps[idx],
        ) {
            Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
            Err(_) => f64::NAN,
        };
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_opening_marubozu_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_opening_marubozu_warmup_period(
    handle: *mut OpeningMarubozu,
) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_opening_marubozu_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_opening_marubozu_is_ready(handle: *mut OpeningMarubozu) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_opening_marubozu_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_opening_marubozu_name(
    handle: *mut OpeningMarubozu,
) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_opening_marubozu_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_opening_marubozu_reset(handle: *mut OpeningMarubozu) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_opening_marubozu_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_opening_marubozu_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_opening_marubozu_free(handle: *mut OpeningMarubozu) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `OvernightGap` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_overnight_gap_free`.
#[no_mangle]
pub extern "C" fn wickra_overnight_gap_new(utc_offset_minutes: i32) -> *mut OvernightGap {
    Box::into_raw(Box::new(OvernightGap::new(utc_offset_minutes)))
}

/// Feed one OHLCV candle; returns the output, or `NaN` during warmup / on a `NULL`
/// handle / if the candle is invalid (e.g. `high < low`).
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_overnight_gap_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_overnight_gap_update(
    handle: *mut OvernightGap,
    open: f64,
    high: f64,
    low: f64,
    close: f64,
    volume: f64,
    timestamp: i64,
) -> f64 {
    let Some(ind) = handle.as_mut() else {
        return f64::NAN;
    };
    match Candle::new(open, high, low, close, volume, timestamp) {
        Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
        Err(_) => f64::NAN,
    }
}

/// Run over the OHLCV series (`open/high/low/close/volume/timestamp[0..n]`) into
/// `out[0..n]` (`NaN` at warmup or on an invalid candle).
///
/// # Safety
/// `handle` valid (from `wickra_overnight_gap_new`, not freed); every input pointer and `out`
/// cover `n` elements.
#[no_mangle]
pub unsafe extern "C" fn wickra_overnight_gap_batch(
    handle: *mut OvernightGap,
    open: *const f64,
    high: *const f64,
    low: *const f64,
    close: *const f64,
    volume: *const f64,
    timestamp: *const i64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null()
        || open.is_null()
        || high.is_null()
        || low.is_null()
        || close.is_null()
        || volume.is_null()
        || timestamp.is_null()
        || out.is_null()
    {
        return;
    }
    let ind = &mut *handle;
    let opens = slice::from_raw_parts(open, n);
    let highs = slice::from_raw_parts(high, n);
    let lows = slice::from_raw_parts(low, n);
    let closes = slice::from_raw_parts(close, n);
    let volumes = slice::from_raw_parts(volume, n);
    let timestamps = slice::from_raw_parts(timestamp, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (idx, slot) in outputs.iter_mut().enumerate() {
        *slot = match Candle::new(
            opens[idx],
            highs[idx],
            lows[idx],
            closes[idx],
            volumes[idx],
            timestamps[idx],
        ) {
            Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
            Err(_) => f64::NAN,
        };
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_overnight_gap_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_overnight_gap_warmup_period(handle: *mut OvernightGap) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_overnight_gap_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_overnight_gap_is_ready(handle: *mut OvernightGap) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_overnight_gap_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_overnight_gap_name(handle: *mut OvernightGap) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_overnight_gap_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_overnight_gap_reset(handle: *mut OvernightGap) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_overnight_gap_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_overnight_gap_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_overnight_gap_free(handle: *mut OvernightGap) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `ParkinsonVolatility` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_parkinson_volatility_free`.
#[no_mangle]
pub extern "C" fn wickra_parkinson_volatility_new(
    period: usize,
    trading_periods: usize,
) -> *mut ParkinsonVolatility {
    match ParkinsonVolatility::new(period, trading_periods) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one OHLCV candle; returns the output, or `NaN` during warmup / on a `NULL`
/// handle / if the candle is invalid (e.g. `high < low`).
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_parkinson_volatility_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_parkinson_volatility_update(
    handle: *mut ParkinsonVolatility,
    open: f64,
    high: f64,
    low: f64,
    close: f64,
    volume: f64,
    timestamp: i64,
) -> f64 {
    let Some(ind) = handle.as_mut() else {
        return f64::NAN;
    };
    match Candle::new(open, high, low, close, volume, timestamp) {
        Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
        Err(_) => f64::NAN,
    }
}

/// Run over the OHLCV series (`open/high/low/close/volume/timestamp[0..n]`) into
/// `out[0..n]` (`NaN` at warmup or on an invalid candle).
///
/// # Safety
/// `handle` valid (from `wickra_parkinson_volatility_new`, not freed); every input pointer and `out`
/// cover `n` elements.
#[no_mangle]
pub unsafe extern "C" fn wickra_parkinson_volatility_batch(
    handle: *mut ParkinsonVolatility,
    open: *const f64,
    high: *const f64,
    low: *const f64,
    close: *const f64,
    volume: *const f64,
    timestamp: *const i64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null()
        || open.is_null()
        || high.is_null()
        || low.is_null()
        || close.is_null()
        || volume.is_null()
        || timestamp.is_null()
        || out.is_null()
    {
        return;
    }
    let ind = &mut *handle;
    let opens = slice::from_raw_parts(open, n);
    let highs = slice::from_raw_parts(high, n);
    let lows = slice::from_raw_parts(low, n);
    let closes = slice::from_raw_parts(close, n);
    let volumes = slice::from_raw_parts(volume, n);
    let timestamps = slice::from_raw_parts(timestamp, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (idx, slot) in outputs.iter_mut().enumerate() {
        *slot = match Candle::new(
            opens[idx],
            highs[idx],
            lows[idx],
            closes[idx],
            volumes[idx],
            timestamps[idx],
        ) {
            Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
            Err(_) => f64::NAN,
        };
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_parkinson_volatility_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_parkinson_volatility_warmup_period(
    handle: *mut ParkinsonVolatility,
) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_parkinson_volatility_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_parkinson_volatility_is_ready(
    handle: *mut ParkinsonVolatility,
) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_parkinson_volatility_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_parkinson_volatility_name(
    handle: *mut ParkinsonVolatility,
) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_parkinson_volatility_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_parkinson_volatility_reset(handle: *mut ParkinsonVolatility) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_parkinson_volatility_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_parkinson_volatility_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_parkinson_volatility_free(handle: *mut ParkinsonVolatility) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `Pgo` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_pgo_free`.
#[no_mangle]
pub extern "C" fn wickra_pgo_new(period: usize) -> *mut Pgo {
    match Pgo::new(period) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one OHLCV candle; returns the output, or `NaN` during warmup / on a `NULL`
/// handle / if the candle is invalid (e.g. `high < low`).
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_pgo_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_pgo_update(
    handle: *mut Pgo,
    open: f64,
    high: f64,
    low: f64,
    close: f64,
    volume: f64,
    timestamp: i64,
) -> f64 {
    let Some(ind) = handle.as_mut() else {
        return f64::NAN;
    };
    match Candle::new(open, high, low, close, volume, timestamp) {
        Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
        Err(_) => f64::NAN,
    }
}

/// Run over the OHLCV series (`open/high/low/close/volume/timestamp[0..n]`) into
/// `out[0..n]` (`NaN` at warmup or on an invalid candle).
///
/// # Safety
/// `handle` valid (from `wickra_pgo_new`, not freed); every input pointer and `out`
/// cover `n` elements.
#[no_mangle]
pub unsafe extern "C" fn wickra_pgo_batch(
    handle: *mut Pgo,
    open: *const f64,
    high: *const f64,
    low: *const f64,
    close: *const f64,
    volume: *const f64,
    timestamp: *const i64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null()
        || open.is_null()
        || high.is_null()
        || low.is_null()
        || close.is_null()
        || volume.is_null()
        || timestamp.is_null()
        || out.is_null()
    {
        return;
    }
    let ind = &mut *handle;
    let opens = slice::from_raw_parts(open, n);
    let highs = slice::from_raw_parts(high, n);
    let lows = slice::from_raw_parts(low, n);
    let closes = slice::from_raw_parts(close, n);
    let volumes = slice::from_raw_parts(volume, n);
    let timestamps = slice::from_raw_parts(timestamp, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (idx, slot) in outputs.iter_mut().enumerate() {
        *slot = match Candle::new(
            opens[idx],
            highs[idx],
            lows[idx],
            closes[idx],
            volumes[idx],
            timestamps[idx],
        ) {
            Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
            Err(_) => f64::NAN,
        };
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_pgo_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_pgo_warmup_period(handle: *mut Pgo) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_pgo_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_pgo_is_ready(handle: *mut Pgo) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_pgo_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_pgo_name(handle: *mut Pgo) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_pgo_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_pgo_reset(handle: *mut Pgo) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_pgo_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_pgo_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_pgo_free(handle: *mut Pgo) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `PiercingDarkCloud` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_piercing_dark_cloud_free`.
#[no_mangle]
pub extern "C" fn wickra_piercing_dark_cloud_new() -> *mut PiercingDarkCloud {
    Box::into_raw(Box::new(PiercingDarkCloud::new()))
}

/// Feed one OHLCV candle; returns the output, or `NaN` during warmup / on a `NULL`
/// handle / if the candle is invalid (e.g. `high < low`).
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_piercing_dark_cloud_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_piercing_dark_cloud_update(
    handle: *mut PiercingDarkCloud,
    open: f64,
    high: f64,
    low: f64,
    close: f64,
    volume: f64,
    timestamp: i64,
) -> f64 {
    let Some(ind) = handle.as_mut() else {
        return f64::NAN;
    };
    match Candle::new(open, high, low, close, volume, timestamp) {
        Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
        Err(_) => f64::NAN,
    }
}

/// Run over the OHLCV series (`open/high/low/close/volume/timestamp[0..n]`) into
/// `out[0..n]` (`NaN` at warmup or on an invalid candle).
///
/// # Safety
/// `handle` valid (from `wickra_piercing_dark_cloud_new`, not freed); every input pointer and `out`
/// cover `n` elements.
#[no_mangle]
pub unsafe extern "C" fn wickra_piercing_dark_cloud_batch(
    handle: *mut PiercingDarkCloud,
    open: *const f64,
    high: *const f64,
    low: *const f64,
    close: *const f64,
    volume: *const f64,
    timestamp: *const i64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null()
        || open.is_null()
        || high.is_null()
        || low.is_null()
        || close.is_null()
        || volume.is_null()
        || timestamp.is_null()
        || out.is_null()
    {
        return;
    }
    let ind = &mut *handle;
    let opens = slice::from_raw_parts(open, n);
    let highs = slice::from_raw_parts(high, n);
    let lows = slice::from_raw_parts(low, n);
    let closes = slice::from_raw_parts(close, n);
    let volumes = slice::from_raw_parts(volume, n);
    let timestamps = slice::from_raw_parts(timestamp, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (idx, slot) in outputs.iter_mut().enumerate() {
        *slot = match Candle::new(
            opens[idx],
            highs[idx],
            lows[idx],
            closes[idx],
            volumes[idx],
            timestamps[idx],
        ) {
            Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
            Err(_) => f64::NAN,
        };
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_piercing_dark_cloud_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_piercing_dark_cloud_warmup_period(
    handle: *mut PiercingDarkCloud,
) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_piercing_dark_cloud_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_piercing_dark_cloud_is_ready(
    handle: *mut PiercingDarkCloud,
) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_piercing_dark_cloud_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_piercing_dark_cloud_name(
    handle: *mut PiercingDarkCloud,
) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_piercing_dark_cloud_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_piercing_dark_cloud_reset(handle: *mut PiercingDarkCloud) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_piercing_dark_cloud_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_piercing_dark_cloud_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_piercing_dark_cloud_free(handle: *mut PiercingDarkCloud) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `PivotReversal` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_pivot_reversal_free`.
#[no_mangle]
pub extern "C" fn wickra_pivot_reversal_new(left: usize, right: usize) -> *mut PivotReversal {
    match PivotReversal::new(left, right) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one OHLCV candle; returns the output, or `NaN` during warmup / on a `NULL`
/// handle / if the candle is invalid (e.g. `high < low`).
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_pivot_reversal_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_pivot_reversal_update(
    handle: *mut PivotReversal,
    open: f64,
    high: f64,
    low: f64,
    close: f64,
    volume: f64,
    timestamp: i64,
) -> f64 {
    let Some(ind) = handle.as_mut() else {
        return f64::NAN;
    };
    match Candle::new(open, high, low, close, volume, timestamp) {
        Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
        Err(_) => f64::NAN,
    }
}

/// Run over the OHLCV series (`open/high/low/close/volume/timestamp[0..n]`) into
/// `out[0..n]` (`NaN` at warmup or on an invalid candle).
///
/// # Safety
/// `handle` valid (from `wickra_pivot_reversal_new`, not freed); every input pointer and `out`
/// cover `n` elements.
#[no_mangle]
pub unsafe extern "C" fn wickra_pivot_reversal_batch(
    handle: *mut PivotReversal,
    open: *const f64,
    high: *const f64,
    low: *const f64,
    close: *const f64,
    volume: *const f64,
    timestamp: *const i64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null()
        || open.is_null()
        || high.is_null()
        || low.is_null()
        || close.is_null()
        || volume.is_null()
        || timestamp.is_null()
        || out.is_null()
    {
        return;
    }
    let ind = &mut *handle;
    let opens = slice::from_raw_parts(open, n);
    let highs = slice::from_raw_parts(high, n);
    let lows = slice::from_raw_parts(low, n);
    let closes = slice::from_raw_parts(close, n);
    let volumes = slice::from_raw_parts(volume, n);
    let timestamps = slice::from_raw_parts(timestamp, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (idx, slot) in outputs.iter_mut().enumerate() {
        *slot = match Candle::new(
            opens[idx],
            highs[idx],
            lows[idx],
            closes[idx],
            volumes[idx],
            timestamps[idx],
        ) {
            Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
            Err(_) => f64::NAN,
        };
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_pivot_reversal_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_pivot_reversal_warmup_period(handle: *mut PivotReversal) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_pivot_reversal_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_pivot_reversal_is_ready(handle: *mut PivotReversal) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_pivot_reversal_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_pivot_reversal_name(handle: *mut PivotReversal) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_pivot_reversal_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_pivot_reversal_reset(handle: *mut PivotReversal) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_pivot_reversal_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_pivot_reversal_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_pivot_reversal_free(handle: *mut PivotReversal) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `PlusDi` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_plus_di_free`.
#[no_mangle]
pub extern "C" fn wickra_plus_di_new(period: usize) -> *mut PlusDi {
    match PlusDi::new(period) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one OHLCV candle; returns the output, or `NaN` during warmup / on a `NULL`
/// handle / if the candle is invalid (e.g. `high < low`).
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_plus_di_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_plus_di_update(
    handle: *mut PlusDi,
    open: f64,
    high: f64,
    low: f64,
    close: f64,
    volume: f64,
    timestamp: i64,
) -> f64 {
    let Some(ind) = handle.as_mut() else {
        return f64::NAN;
    };
    match Candle::new(open, high, low, close, volume, timestamp) {
        Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
        Err(_) => f64::NAN,
    }
}

/// Run over the OHLCV series (`open/high/low/close/volume/timestamp[0..n]`) into
/// `out[0..n]` (`NaN` at warmup or on an invalid candle).
///
/// # Safety
/// `handle` valid (from `wickra_plus_di_new`, not freed); every input pointer and `out`
/// cover `n` elements.
#[no_mangle]
pub unsafe extern "C" fn wickra_plus_di_batch(
    handle: *mut PlusDi,
    open: *const f64,
    high: *const f64,
    low: *const f64,
    close: *const f64,
    volume: *const f64,
    timestamp: *const i64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null()
        || open.is_null()
        || high.is_null()
        || low.is_null()
        || close.is_null()
        || volume.is_null()
        || timestamp.is_null()
        || out.is_null()
    {
        return;
    }
    let ind = &mut *handle;
    let opens = slice::from_raw_parts(open, n);
    let highs = slice::from_raw_parts(high, n);
    let lows = slice::from_raw_parts(low, n);
    let closes = slice::from_raw_parts(close, n);
    let volumes = slice::from_raw_parts(volume, n);
    let timestamps = slice::from_raw_parts(timestamp, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (idx, slot) in outputs.iter_mut().enumerate() {
        *slot = match Candle::new(
            opens[idx],
            highs[idx],
            lows[idx],
            closes[idx],
            volumes[idx],
            timestamps[idx],
        ) {
            Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
            Err(_) => f64::NAN,
        };
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_plus_di_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_plus_di_warmup_period(handle: *mut PlusDi) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_plus_di_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_plus_di_is_ready(handle: *mut PlusDi) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_plus_di_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_plus_di_name(handle: *mut PlusDi) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_plus_di_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_plus_di_reset(handle: *mut PlusDi) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_plus_di_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_plus_di_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_plus_di_free(handle: *mut PlusDi) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `PlusDm` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_plus_dm_free`.
#[no_mangle]
pub extern "C" fn wickra_plus_dm_new(period: usize) -> *mut PlusDm {
    match PlusDm::new(period) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one OHLCV candle; returns the output, or `NaN` during warmup / on a `NULL`
/// handle / if the candle is invalid (e.g. `high < low`).
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_plus_dm_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_plus_dm_update(
    handle: *mut PlusDm,
    open: f64,
    high: f64,
    low: f64,
    close: f64,
    volume: f64,
    timestamp: i64,
) -> f64 {
    let Some(ind) = handle.as_mut() else {
        return f64::NAN;
    };
    match Candle::new(open, high, low, close, volume, timestamp) {
        Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
        Err(_) => f64::NAN,
    }
}

/// Run over the OHLCV series (`open/high/low/close/volume/timestamp[0..n]`) into
/// `out[0..n]` (`NaN` at warmup or on an invalid candle).
///
/// # Safety
/// `handle` valid (from `wickra_plus_dm_new`, not freed); every input pointer and `out`
/// cover `n` elements.
#[no_mangle]
pub unsafe extern "C" fn wickra_plus_dm_batch(
    handle: *mut PlusDm,
    open: *const f64,
    high: *const f64,
    low: *const f64,
    close: *const f64,
    volume: *const f64,
    timestamp: *const i64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null()
        || open.is_null()
        || high.is_null()
        || low.is_null()
        || close.is_null()
        || volume.is_null()
        || timestamp.is_null()
        || out.is_null()
    {
        return;
    }
    let ind = &mut *handle;
    let opens = slice::from_raw_parts(open, n);
    let highs = slice::from_raw_parts(high, n);
    let lows = slice::from_raw_parts(low, n);
    let closes = slice::from_raw_parts(close, n);
    let volumes = slice::from_raw_parts(volume, n);
    let timestamps = slice::from_raw_parts(timestamp, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (idx, slot) in outputs.iter_mut().enumerate() {
        *slot = match Candle::new(
            opens[idx],
            highs[idx],
            lows[idx],
            closes[idx],
            volumes[idx],
            timestamps[idx],
        ) {
            Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
            Err(_) => f64::NAN,
        };
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_plus_dm_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_plus_dm_warmup_period(handle: *mut PlusDm) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_plus_dm_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_plus_dm_is_ready(handle: *mut PlusDm) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_plus_dm_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_plus_dm_name(handle: *mut PlusDm) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_plus_dm_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_plus_dm_reset(handle: *mut PlusDm) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_plus_dm_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_plus_dm_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_plus_dm_free(handle: *mut PlusDm) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `ProfileShape` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_profile_shape_free`.
#[no_mangle]
pub extern "C" fn wickra_profile_shape_new(period: usize, bins: usize) -> *mut ProfileShape {
    match ProfileShape::new(period, bins) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one OHLCV candle; returns the output, or `NaN` during warmup / on a `NULL`
/// handle / if the candle is invalid (e.g. `high < low`).
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_profile_shape_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_profile_shape_update(
    handle: *mut ProfileShape,
    open: f64,
    high: f64,
    low: f64,
    close: f64,
    volume: f64,
    timestamp: i64,
) -> f64 {
    let Some(ind) = handle.as_mut() else {
        return f64::NAN;
    };
    match Candle::new(open, high, low, close, volume, timestamp) {
        Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
        Err(_) => f64::NAN,
    }
}

/// Run over the OHLCV series (`open/high/low/close/volume/timestamp[0..n]`) into
/// `out[0..n]` (`NaN` at warmup or on an invalid candle).
///
/// # Safety
/// `handle` valid (from `wickra_profile_shape_new`, not freed); every input pointer and `out`
/// cover `n` elements.
#[no_mangle]
pub unsafe extern "C" fn wickra_profile_shape_batch(
    handle: *mut ProfileShape,
    open: *const f64,
    high: *const f64,
    low: *const f64,
    close: *const f64,
    volume: *const f64,
    timestamp: *const i64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null()
        || open.is_null()
        || high.is_null()
        || low.is_null()
        || close.is_null()
        || volume.is_null()
        || timestamp.is_null()
        || out.is_null()
    {
        return;
    }
    let ind = &mut *handle;
    let opens = slice::from_raw_parts(open, n);
    let highs = slice::from_raw_parts(high, n);
    let lows = slice::from_raw_parts(low, n);
    let closes = slice::from_raw_parts(close, n);
    let volumes = slice::from_raw_parts(volume, n);
    let timestamps = slice::from_raw_parts(timestamp, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (idx, slot) in outputs.iter_mut().enumerate() {
        *slot = match Candle::new(
            opens[idx],
            highs[idx],
            lows[idx],
            closes[idx],
            volumes[idx],
            timestamps[idx],
        ) {
            Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
            Err(_) => f64::NAN,
        };
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_profile_shape_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_profile_shape_warmup_period(handle: *mut ProfileShape) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_profile_shape_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_profile_shape_is_ready(handle: *mut ProfileShape) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_profile_shape_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_profile_shape_name(handle: *mut ProfileShape) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_profile_shape_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_profile_shape_reset(handle: *mut ProfileShape) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_profile_shape_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_profile_shape_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_profile_shape_free(handle: *mut ProfileShape) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `ProjectionOscillator` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_projection_oscillator_free`.
#[no_mangle]
pub extern "C" fn wickra_projection_oscillator_new(period: usize) -> *mut ProjectionOscillator {
    match ProjectionOscillator::new(period) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one OHLCV candle; returns the output, or `NaN` during warmup / on a `NULL`
/// handle / if the candle is invalid (e.g. `high < low`).
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_projection_oscillator_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_projection_oscillator_update(
    handle: *mut ProjectionOscillator,
    open: f64,
    high: f64,
    low: f64,
    close: f64,
    volume: f64,
    timestamp: i64,
) -> f64 {
    let Some(ind) = handle.as_mut() else {
        return f64::NAN;
    };
    match Candle::new(open, high, low, close, volume, timestamp) {
        Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
        Err(_) => f64::NAN,
    }
}

/// Run over the OHLCV series (`open/high/low/close/volume/timestamp[0..n]`) into
/// `out[0..n]` (`NaN` at warmup or on an invalid candle).
///
/// # Safety
/// `handle` valid (from `wickra_projection_oscillator_new`, not freed); every input pointer and `out`
/// cover `n` elements.
#[no_mangle]
pub unsafe extern "C" fn wickra_projection_oscillator_batch(
    handle: *mut ProjectionOscillator,
    open: *const f64,
    high: *const f64,
    low: *const f64,
    close: *const f64,
    volume: *const f64,
    timestamp: *const i64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null()
        || open.is_null()
        || high.is_null()
        || low.is_null()
        || close.is_null()
        || volume.is_null()
        || timestamp.is_null()
        || out.is_null()
    {
        return;
    }
    let ind = &mut *handle;
    let opens = slice::from_raw_parts(open, n);
    let highs = slice::from_raw_parts(high, n);
    let lows = slice::from_raw_parts(low, n);
    let closes = slice::from_raw_parts(close, n);
    let volumes = slice::from_raw_parts(volume, n);
    let timestamps = slice::from_raw_parts(timestamp, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (idx, slot) in outputs.iter_mut().enumerate() {
        *slot = match Candle::new(
            opens[idx],
            highs[idx],
            lows[idx],
            closes[idx],
            volumes[idx],
            timestamps[idx],
        ) {
            Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
            Err(_) => f64::NAN,
        };
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_projection_oscillator_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_projection_oscillator_warmup_period(
    handle: *mut ProjectionOscillator,
) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_projection_oscillator_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_projection_oscillator_is_ready(
    handle: *mut ProjectionOscillator,
) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_projection_oscillator_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_projection_oscillator_name(
    handle: *mut ProjectionOscillator,
) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_projection_oscillator_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_projection_oscillator_reset(handle: *mut ProjectionOscillator) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_projection_oscillator_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_projection_oscillator_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_projection_oscillator_free(handle: *mut ProjectionOscillator) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `Psar` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_psar_free`.
#[no_mangle]
pub extern "C" fn wickra_psar_new(af_start: f64, af_step: f64, af_max: f64) -> *mut Psar {
    match Psar::new(af_start, af_step, af_max) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one OHLCV candle; returns the output, or `NaN` during warmup / on a `NULL`
/// handle / if the candle is invalid (e.g. `high < low`).
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_psar_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_psar_update(
    handle: *mut Psar,
    open: f64,
    high: f64,
    low: f64,
    close: f64,
    volume: f64,
    timestamp: i64,
) -> f64 {
    let Some(ind) = handle.as_mut() else {
        return f64::NAN;
    };
    match Candle::new(open, high, low, close, volume, timestamp) {
        Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
        Err(_) => f64::NAN,
    }
}

/// Run over the OHLCV series (`open/high/low/close/volume/timestamp[0..n]`) into
/// `out[0..n]` (`NaN` at warmup or on an invalid candle).
///
/// # Safety
/// `handle` valid (from `wickra_psar_new`, not freed); every input pointer and `out`
/// cover `n` elements.
#[no_mangle]
pub unsafe extern "C" fn wickra_psar_batch(
    handle: *mut Psar,
    open: *const f64,
    high: *const f64,
    low: *const f64,
    close: *const f64,
    volume: *const f64,
    timestamp: *const i64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null()
        || open.is_null()
        || high.is_null()
        || low.is_null()
        || close.is_null()
        || volume.is_null()
        || timestamp.is_null()
        || out.is_null()
    {
        return;
    }
    let ind = &mut *handle;
    let opens = slice::from_raw_parts(open, n);
    let highs = slice::from_raw_parts(high, n);
    let lows = slice::from_raw_parts(low, n);
    let closes = slice::from_raw_parts(close, n);
    let volumes = slice::from_raw_parts(volume, n);
    let timestamps = slice::from_raw_parts(timestamp, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (idx, slot) in outputs.iter_mut().enumerate() {
        *slot = match Candle::new(
            opens[idx],
            highs[idx],
            lows[idx],
            closes[idx],
            volumes[idx],
            timestamps[idx],
        ) {
            Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
            Err(_) => f64::NAN,
        };
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_psar_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_psar_warmup_period(handle: *mut Psar) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_psar_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_psar_is_ready(handle: *mut Psar) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_psar_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_psar_name(handle: *mut Psar) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_psar_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_psar_reset(handle: *mut Psar) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_psar_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_psar_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_psar_free(handle: *mut Psar) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `Pvi` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_pvi_free`.
#[no_mangle]
pub extern "C" fn wickra_pvi_new() -> *mut Pvi {
    Box::into_raw(Box::new(Pvi::new()))
}

/// Feed one OHLCV candle; returns the output, or `NaN` during warmup / on a `NULL`
/// handle / if the candle is invalid (e.g. `high < low`).
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_pvi_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_pvi_update(
    handle: *mut Pvi,
    open: f64,
    high: f64,
    low: f64,
    close: f64,
    volume: f64,
    timestamp: i64,
) -> f64 {
    let Some(ind) = handle.as_mut() else {
        return f64::NAN;
    };
    match Candle::new(open, high, low, close, volume, timestamp) {
        Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
        Err(_) => f64::NAN,
    }
}

/// Run over the OHLCV series (`open/high/low/close/volume/timestamp[0..n]`) into
/// `out[0..n]` (`NaN` at warmup or on an invalid candle).
///
/// # Safety
/// `handle` valid (from `wickra_pvi_new`, not freed); every input pointer and `out`
/// cover `n` elements.
#[no_mangle]
pub unsafe extern "C" fn wickra_pvi_batch(
    handle: *mut Pvi,
    open: *const f64,
    high: *const f64,
    low: *const f64,
    close: *const f64,
    volume: *const f64,
    timestamp: *const i64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null()
        || open.is_null()
        || high.is_null()
        || low.is_null()
        || close.is_null()
        || volume.is_null()
        || timestamp.is_null()
        || out.is_null()
    {
        return;
    }
    let ind = &mut *handle;
    let opens = slice::from_raw_parts(open, n);
    let highs = slice::from_raw_parts(high, n);
    let lows = slice::from_raw_parts(low, n);
    let closes = slice::from_raw_parts(close, n);
    let volumes = slice::from_raw_parts(volume, n);
    let timestamps = slice::from_raw_parts(timestamp, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (idx, slot) in outputs.iter_mut().enumerate() {
        *slot = match Candle::new(
            opens[idx],
            highs[idx],
            lows[idx],
            closes[idx],
            volumes[idx],
            timestamps[idx],
        ) {
            Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
            Err(_) => f64::NAN,
        };
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_pvi_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_pvi_warmup_period(handle: *mut Pvi) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_pvi_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_pvi_is_ready(handle: *mut Pvi) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_pvi_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_pvi_name(handle: *mut Pvi) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_pvi_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_pvi_reset(handle: *mut Pvi) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_pvi_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_pvi_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_pvi_free(handle: *mut Pvi) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `Qstick` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_qstick_free`.
#[no_mangle]
pub extern "C" fn wickra_qstick_new(period: usize) -> *mut Qstick {
    match Qstick::new(period) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one OHLCV candle; returns the output, or `NaN` during warmup / on a `NULL`
/// handle / if the candle is invalid (e.g. `high < low`).
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_qstick_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_qstick_update(
    handle: *mut Qstick,
    open: f64,
    high: f64,
    low: f64,
    close: f64,
    volume: f64,
    timestamp: i64,
) -> f64 {
    let Some(ind) = handle.as_mut() else {
        return f64::NAN;
    };
    match Candle::new(open, high, low, close, volume, timestamp) {
        Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
        Err(_) => f64::NAN,
    }
}

/// Run over the OHLCV series (`open/high/low/close/volume/timestamp[0..n]`) into
/// `out[0..n]` (`NaN` at warmup or on an invalid candle).
///
/// # Safety
/// `handle` valid (from `wickra_qstick_new`, not freed); every input pointer and `out`
/// cover `n` elements.
#[no_mangle]
pub unsafe extern "C" fn wickra_qstick_batch(
    handle: *mut Qstick,
    open: *const f64,
    high: *const f64,
    low: *const f64,
    close: *const f64,
    volume: *const f64,
    timestamp: *const i64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null()
        || open.is_null()
        || high.is_null()
        || low.is_null()
        || close.is_null()
        || volume.is_null()
        || timestamp.is_null()
        || out.is_null()
    {
        return;
    }
    let ind = &mut *handle;
    let opens = slice::from_raw_parts(open, n);
    let highs = slice::from_raw_parts(high, n);
    let lows = slice::from_raw_parts(low, n);
    let closes = slice::from_raw_parts(close, n);
    let volumes = slice::from_raw_parts(volume, n);
    let timestamps = slice::from_raw_parts(timestamp, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (idx, slot) in outputs.iter_mut().enumerate() {
        *slot = match Candle::new(
            opens[idx],
            highs[idx],
            lows[idx],
            closes[idx],
            volumes[idx],
            timestamps[idx],
        ) {
            Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
            Err(_) => f64::NAN,
        };
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_qstick_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_qstick_warmup_period(handle: *mut Qstick) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_qstick_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_qstick_is_ready(handle: *mut Qstick) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_qstick_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_qstick_name(handle: *mut Qstick) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_qstick_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_qstick_reset(handle: *mut Qstick) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_qstick_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_qstick_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_qstick_free(handle: *mut Qstick) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `RectangleRange` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_rectangle_range_free`.
#[no_mangle]
pub extern "C" fn wickra_rectangle_range_new() -> *mut RectangleRange {
    Box::into_raw(Box::new(RectangleRange::new()))
}

/// Feed one OHLCV candle; returns the output, or `NaN` during warmup / on a `NULL`
/// handle / if the candle is invalid (e.g. `high < low`).
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_rectangle_range_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_rectangle_range_update(
    handle: *mut RectangleRange,
    open: f64,
    high: f64,
    low: f64,
    close: f64,
    volume: f64,
    timestamp: i64,
) -> f64 {
    let Some(ind) = handle.as_mut() else {
        return f64::NAN;
    };
    match Candle::new(open, high, low, close, volume, timestamp) {
        Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
        Err(_) => f64::NAN,
    }
}

/// Run over the OHLCV series (`open/high/low/close/volume/timestamp[0..n]`) into
/// `out[0..n]` (`NaN` at warmup or on an invalid candle).
///
/// # Safety
/// `handle` valid (from `wickra_rectangle_range_new`, not freed); every input pointer and `out`
/// cover `n` elements.
#[no_mangle]
pub unsafe extern "C" fn wickra_rectangle_range_batch(
    handle: *mut RectangleRange,
    open: *const f64,
    high: *const f64,
    low: *const f64,
    close: *const f64,
    volume: *const f64,
    timestamp: *const i64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null()
        || open.is_null()
        || high.is_null()
        || low.is_null()
        || close.is_null()
        || volume.is_null()
        || timestamp.is_null()
        || out.is_null()
    {
        return;
    }
    let ind = &mut *handle;
    let opens = slice::from_raw_parts(open, n);
    let highs = slice::from_raw_parts(high, n);
    let lows = slice::from_raw_parts(low, n);
    let closes = slice::from_raw_parts(close, n);
    let volumes = slice::from_raw_parts(volume, n);
    let timestamps = slice::from_raw_parts(timestamp, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (idx, slot) in outputs.iter_mut().enumerate() {
        *slot = match Candle::new(
            opens[idx],
            highs[idx],
            lows[idx],
            closes[idx],
            volumes[idx],
            timestamps[idx],
        ) {
            Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
            Err(_) => f64::NAN,
        };
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_rectangle_range_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_rectangle_range_warmup_period(
    handle: *mut RectangleRange,
) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_rectangle_range_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_rectangle_range_is_ready(handle: *mut RectangleRange) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_rectangle_range_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_rectangle_range_name(handle: *mut RectangleRange) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_rectangle_range_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_rectangle_range_reset(handle: *mut RectangleRange) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_rectangle_range_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_rectangle_range_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_rectangle_range_free(handle: *mut RectangleRange) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `RickshawMan` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_rickshaw_man_free`.
#[no_mangle]
pub extern "C" fn wickra_rickshaw_man_new() -> *mut RickshawMan {
    Box::into_raw(Box::new(RickshawMan::new()))
}

/// Feed one OHLCV candle; returns the output, or `NaN` during warmup / on a `NULL`
/// handle / if the candle is invalid (e.g. `high < low`).
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_rickshaw_man_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_rickshaw_man_update(
    handle: *mut RickshawMan,
    open: f64,
    high: f64,
    low: f64,
    close: f64,
    volume: f64,
    timestamp: i64,
) -> f64 {
    let Some(ind) = handle.as_mut() else {
        return f64::NAN;
    };
    match Candle::new(open, high, low, close, volume, timestamp) {
        Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
        Err(_) => f64::NAN,
    }
}

/// Run over the OHLCV series (`open/high/low/close/volume/timestamp[0..n]`) into
/// `out[0..n]` (`NaN` at warmup or on an invalid candle).
///
/// # Safety
/// `handle` valid (from `wickra_rickshaw_man_new`, not freed); every input pointer and `out`
/// cover `n` elements.
#[no_mangle]
pub unsafe extern "C" fn wickra_rickshaw_man_batch(
    handle: *mut RickshawMan,
    open: *const f64,
    high: *const f64,
    low: *const f64,
    close: *const f64,
    volume: *const f64,
    timestamp: *const i64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null()
        || open.is_null()
        || high.is_null()
        || low.is_null()
        || close.is_null()
        || volume.is_null()
        || timestamp.is_null()
        || out.is_null()
    {
        return;
    }
    let ind = &mut *handle;
    let opens = slice::from_raw_parts(open, n);
    let highs = slice::from_raw_parts(high, n);
    let lows = slice::from_raw_parts(low, n);
    let closes = slice::from_raw_parts(close, n);
    let volumes = slice::from_raw_parts(volume, n);
    let timestamps = slice::from_raw_parts(timestamp, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (idx, slot) in outputs.iter_mut().enumerate() {
        *slot = match Candle::new(
            opens[idx],
            highs[idx],
            lows[idx],
            closes[idx],
            volumes[idx],
            timestamps[idx],
        ) {
            Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
            Err(_) => f64::NAN,
        };
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_rickshaw_man_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_rickshaw_man_warmup_period(handle: *mut RickshawMan) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_rickshaw_man_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_rickshaw_man_is_ready(handle: *mut RickshawMan) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_rickshaw_man_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_rickshaw_man_name(handle: *mut RickshawMan) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_rickshaw_man_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_rickshaw_man_reset(handle: *mut RickshawMan) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_rickshaw_man_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_rickshaw_man_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_rickshaw_man_free(handle: *mut RickshawMan) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `RisingThreeMethods` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_rising_three_methods_free`.
#[no_mangle]
pub extern "C" fn wickra_rising_three_methods_new() -> *mut RisingThreeMethods {
    Box::into_raw(Box::new(RisingThreeMethods::new()))
}

/// Feed one OHLCV candle; returns the output, or `NaN` during warmup / on a `NULL`
/// handle / if the candle is invalid (e.g. `high < low`).
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_rising_three_methods_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_rising_three_methods_update(
    handle: *mut RisingThreeMethods,
    open: f64,
    high: f64,
    low: f64,
    close: f64,
    volume: f64,
    timestamp: i64,
) -> f64 {
    let Some(ind) = handle.as_mut() else {
        return f64::NAN;
    };
    match Candle::new(open, high, low, close, volume, timestamp) {
        Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
        Err(_) => f64::NAN,
    }
}

/// Run over the OHLCV series (`open/high/low/close/volume/timestamp[0..n]`) into
/// `out[0..n]` (`NaN` at warmup or on an invalid candle).
///
/// # Safety
/// `handle` valid (from `wickra_rising_three_methods_new`, not freed); every input pointer and `out`
/// cover `n` elements.
#[no_mangle]
pub unsafe extern "C" fn wickra_rising_three_methods_batch(
    handle: *mut RisingThreeMethods,
    open: *const f64,
    high: *const f64,
    low: *const f64,
    close: *const f64,
    volume: *const f64,
    timestamp: *const i64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null()
        || open.is_null()
        || high.is_null()
        || low.is_null()
        || close.is_null()
        || volume.is_null()
        || timestamp.is_null()
        || out.is_null()
    {
        return;
    }
    let ind = &mut *handle;
    let opens = slice::from_raw_parts(open, n);
    let highs = slice::from_raw_parts(high, n);
    let lows = slice::from_raw_parts(low, n);
    let closes = slice::from_raw_parts(close, n);
    let volumes = slice::from_raw_parts(volume, n);
    let timestamps = slice::from_raw_parts(timestamp, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (idx, slot) in outputs.iter_mut().enumerate() {
        *slot = match Candle::new(
            opens[idx],
            highs[idx],
            lows[idx],
            closes[idx],
            volumes[idx],
            timestamps[idx],
        ) {
            Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
            Err(_) => f64::NAN,
        };
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_rising_three_methods_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_rising_three_methods_warmup_period(
    handle: *mut RisingThreeMethods,
) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_rising_three_methods_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_rising_three_methods_is_ready(
    handle: *mut RisingThreeMethods,
) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_rising_three_methods_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_rising_three_methods_name(
    handle: *mut RisingThreeMethods,
) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_rising_three_methods_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_rising_three_methods_reset(handle: *mut RisingThreeMethods) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_rising_three_methods_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_rising_three_methods_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_rising_three_methods_free(handle: *mut RisingThreeMethods) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `RogersSatchellVolatility` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_rogers_satchell_volatility_free`.
#[no_mangle]
pub extern "C" fn wickra_rogers_satchell_volatility_new(
    period: usize,
    trading_periods: usize,
) -> *mut RogersSatchellVolatility {
    match RogersSatchellVolatility::new(period, trading_periods) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one OHLCV candle; returns the output, or `NaN` during warmup / on a `NULL`
/// handle / if the candle is invalid (e.g. `high < low`).
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_rogers_satchell_volatility_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_rogers_satchell_volatility_update(
    handle: *mut RogersSatchellVolatility,
    open: f64,
    high: f64,
    low: f64,
    close: f64,
    volume: f64,
    timestamp: i64,
) -> f64 {
    let Some(ind) = handle.as_mut() else {
        return f64::NAN;
    };
    match Candle::new(open, high, low, close, volume, timestamp) {
        Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
        Err(_) => f64::NAN,
    }
}

/// Run over the OHLCV series (`open/high/low/close/volume/timestamp[0..n]`) into
/// `out[0..n]` (`NaN` at warmup or on an invalid candle).
///
/// # Safety
/// `handle` valid (from `wickra_rogers_satchell_volatility_new`, not freed); every input pointer and `out`
/// cover `n` elements.
#[no_mangle]
pub unsafe extern "C" fn wickra_rogers_satchell_volatility_batch(
    handle: *mut RogersSatchellVolatility,
    open: *const f64,
    high: *const f64,
    low: *const f64,
    close: *const f64,
    volume: *const f64,
    timestamp: *const i64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null()
        || open.is_null()
        || high.is_null()
        || low.is_null()
        || close.is_null()
        || volume.is_null()
        || timestamp.is_null()
        || out.is_null()
    {
        return;
    }
    let ind = &mut *handle;
    let opens = slice::from_raw_parts(open, n);
    let highs = slice::from_raw_parts(high, n);
    let lows = slice::from_raw_parts(low, n);
    let closes = slice::from_raw_parts(close, n);
    let volumes = slice::from_raw_parts(volume, n);
    let timestamps = slice::from_raw_parts(timestamp, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (idx, slot) in outputs.iter_mut().enumerate() {
        *slot = match Candle::new(
            opens[idx],
            highs[idx],
            lows[idx],
            closes[idx],
            volumes[idx],
            timestamps[idx],
        ) {
            Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
            Err(_) => f64::NAN,
        };
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_rogers_satchell_volatility_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_rogers_satchell_volatility_warmup_period(
    handle: *mut RogersSatchellVolatility,
) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_rogers_satchell_volatility_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_rogers_satchell_volatility_is_ready(
    handle: *mut RogersSatchellVolatility,
) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_rogers_satchell_volatility_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_rogers_satchell_volatility_name(
    handle: *mut RogersSatchellVolatility,
) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_rogers_satchell_volatility_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_rogers_satchell_volatility_reset(
    handle: *mut RogersSatchellVolatility,
) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_rogers_satchell_volatility_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_rogers_satchell_volatility_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_rogers_satchell_volatility_free(
    handle: *mut RogersSatchellVolatility,
) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `Rvi` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_rvi_free`.
#[no_mangle]
pub extern "C" fn wickra_rvi_new(period: usize) -> *mut Rvi {
    match Rvi::new(period) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one OHLCV candle; returns the output, or `NaN` during warmup / on a `NULL`
/// handle / if the candle is invalid (e.g. `high < low`).
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_rvi_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_rvi_update(
    handle: *mut Rvi,
    open: f64,
    high: f64,
    low: f64,
    close: f64,
    volume: f64,
    timestamp: i64,
) -> f64 {
    let Some(ind) = handle.as_mut() else {
        return f64::NAN;
    };
    match Candle::new(open, high, low, close, volume, timestamp) {
        Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
        Err(_) => f64::NAN,
    }
}

/// Run over the OHLCV series (`open/high/low/close/volume/timestamp[0..n]`) into
/// `out[0..n]` (`NaN` at warmup or on an invalid candle).
///
/// # Safety
/// `handle` valid (from `wickra_rvi_new`, not freed); every input pointer and `out`
/// cover `n` elements.
#[no_mangle]
pub unsafe extern "C" fn wickra_rvi_batch(
    handle: *mut Rvi,
    open: *const f64,
    high: *const f64,
    low: *const f64,
    close: *const f64,
    volume: *const f64,
    timestamp: *const i64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null()
        || open.is_null()
        || high.is_null()
        || low.is_null()
        || close.is_null()
        || volume.is_null()
        || timestamp.is_null()
        || out.is_null()
    {
        return;
    }
    let ind = &mut *handle;
    let opens = slice::from_raw_parts(open, n);
    let highs = slice::from_raw_parts(high, n);
    let lows = slice::from_raw_parts(low, n);
    let closes = slice::from_raw_parts(close, n);
    let volumes = slice::from_raw_parts(volume, n);
    let timestamps = slice::from_raw_parts(timestamp, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (idx, slot) in outputs.iter_mut().enumerate() {
        *slot = match Candle::new(
            opens[idx],
            highs[idx],
            lows[idx],
            closes[idx],
            volumes[idx],
            timestamps[idx],
        ) {
            Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
            Err(_) => f64::NAN,
        };
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_rvi_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_rvi_warmup_period(handle: *mut Rvi) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_rvi_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_rvi_is_ready(handle: *mut Rvi) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_rvi_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_rvi_name(handle: *mut Rvi) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_rvi_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_rvi_reset(handle: *mut Rvi) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_rvi_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_rvi_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_rvi_free(handle: *mut Rvi) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `SarExt` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_sar_ext_free`.
#[no_mangle]
pub extern "C" fn wickra_sar_ext_new(
    start_value: f64,
    offset_on_reverse: f64,
    accel_init_long: f64,
    accel_long: f64,
    accel_max_long: f64,
    accel_init_short: f64,
    accel_short: f64,
    accel_max_short: f64,
) -> *mut SarExt {
    match SarExt::new(
        start_value,
        offset_on_reverse,
        accel_init_long,
        accel_long,
        accel_max_long,
        accel_init_short,
        accel_short,
        accel_max_short,
    ) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one OHLCV candle; returns the output, or `NaN` during warmup / on a `NULL`
/// handle / if the candle is invalid (e.g. `high < low`).
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_sar_ext_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_sar_ext_update(
    handle: *mut SarExt,
    open: f64,
    high: f64,
    low: f64,
    close: f64,
    volume: f64,
    timestamp: i64,
) -> f64 {
    let Some(ind) = handle.as_mut() else {
        return f64::NAN;
    };
    match Candle::new(open, high, low, close, volume, timestamp) {
        Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
        Err(_) => f64::NAN,
    }
}

/// Run over the OHLCV series (`open/high/low/close/volume/timestamp[0..n]`) into
/// `out[0..n]` (`NaN` at warmup or on an invalid candle).
///
/// # Safety
/// `handle` valid (from `wickra_sar_ext_new`, not freed); every input pointer and `out`
/// cover `n` elements.
#[no_mangle]
pub unsafe extern "C" fn wickra_sar_ext_batch(
    handle: *mut SarExt,
    open: *const f64,
    high: *const f64,
    low: *const f64,
    close: *const f64,
    volume: *const f64,
    timestamp: *const i64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null()
        || open.is_null()
        || high.is_null()
        || low.is_null()
        || close.is_null()
        || volume.is_null()
        || timestamp.is_null()
        || out.is_null()
    {
        return;
    }
    let ind = &mut *handle;
    let opens = slice::from_raw_parts(open, n);
    let highs = slice::from_raw_parts(high, n);
    let lows = slice::from_raw_parts(low, n);
    let closes = slice::from_raw_parts(close, n);
    let volumes = slice::from_raw_parts(volume, n);
    let timestamps = slice::from_raw_parts(timestamp, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (idx, slot) in outputs.iter_mut().enumerate() {
        *slot = match Candle::new(
            opens[idx],
            highs[idx],
            lows[idx],
            closes[idx],
            volumes[idx],
            timestamps[idx],
        ) {
            Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
            Err(_) => f64::NAN,
        };
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_sar_ext_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_sar_ext_warmup_period(handle: *mut SarExt) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_sar_ext_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_sar_ext_is_ready(handle: *mut SarExt) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_sar_ext_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_sar_ext_name(handle: *mut SarExt) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_sar_ext_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_sar_ext_reset(handle: *mut SarExt) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_sar_ext_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_sar_ext_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_sar_ext_free(handle: *mut SarExt) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `SeasonalZScore` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_seasonal_z_score_free`.
#[no_mangle]
pub extern "C" fn wickra_seasonal_z_score_new(utc_offset_minutes: i32) -> *mut SeasonalZScore {
    Box::into_raw(Box::new(SeasonalZScore::new(utc_offset_minutes)))
}

/// Feed one OHLCV candle; returns the output, or `NaN` during warmup / on a `NULL`
/// handle / if the candle is invalid (e.g. `high < low`).
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_seasonal_z_score_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_seasonal_z_score_update(
    handle: *mut SeasonalZScore,
    open: f64,
    high: f64,
    low: f64,
    close: f64,
    volume: f64,
    timestamp: i64,
) -> f64 {
    let Some(ind) = handle.as_mut() else {
        return f64::NAN;
    };
    match Candle::new(open, high, low, close, volume, timestamp) {
        Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
        Err(_) => f64::NAN,
    }
}

/// Run over the OHLCV series (`open/high/low/close/volume/timestamp[0..n]`) into
/// `out[0..n]` (`NaN` at warmup or on an invalid candle).
///
/// # Safety
/// `handle` valid (from `wickra_seasonal_z_score_new`, not freed); every input pointer and `out`
/// cover `n` elements.
#[no_mangle]
pub unsafe extern "C" fn wickra_seasonal_z_score_batch(
    handle: *mut SeasonalZScore,
    open: *const f64,
    high: *const f64,
    low: *const f64,
    close: *const f64,
    volume: *const f64,
    timestamp: *const i64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null()
        || open.is_null()
        || high.is_null()
        || low.is_null()
        || close.is_null()
        || volume.is_null()
        || timestamp.is_null()
        || out.is_null()
    {
        return;
    }
    let ind = &mut *handle;
    let opens = slice::from_raw_parts(open, n);
    let highs = slice::from_raw_parts(high, n);
    let lows = slice::from_raw_parts(low, n);
    let closes = slice::from_raw_parts(close, n);
    let volumes = slice::from_raw_parts(volume, n);
    let timestamps = slice::from_raw_parts(timestamp, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (idx, slot) in outputs.iter_mut().enumerate() {
        *slot = match Candle::new(
            opens[idx],
            highs[idx],
            lows[idx],
            closes[idx],
            volumes[idx],
            timestamps[idx],
        ) {
            Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
            Err(_) => f64::NAN,
        };
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_seasonal_z_score_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_seasonal_z_score_warmup_period(
    handle: *mut SeasonalZScore,
) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_seasonal_z_score_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_seasonal_z_score_is_ready(handle: *mut SeasonalZScore) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_seasonal_z_score_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_seasonal_z_score_name(
    handle: *mut SeasonalZScore,
) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_seasonal_z_score_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_seasonal_z_score_reset(handle: *mut SeasonalZScore) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_seasonal_z_score_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_seasonal_z_score_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_seasonal_z_score_free(handle: *mut SeasonalZScore) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `SeparatingLines` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_separating_lines_free`.
#[no_mangle]
pub extern "C" fn wickra_separating_lines_new() -> *mut SeparatingLines {
    Box::into_raw(Box::new(SeparatingLines::new()))
}

/// Feed one OHLCV candle; returns the output, or `NaN` during warmup / on a `NULL`
/// handle / if the candle is invalid (e.g. `high < low`).
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_separating_lines_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_separating_lines_update(
    handle: *mut SeparatingLines,
    open: f64,
    high: f64,
    low: f64,
    close: f64,
    volume: f64,
    timestamp: i64,
) -> f64 {
    let Some(ind) = handle.as_mut() else {
        return f64::NAN;
    };
    match Candle::new(open, high, low, close, volume, timestamp) {
        Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
        Err(_) => f64::NAN,
    }
}

/// Run over the OHLCV series (`open/high/low/close/volume/timestamp[0..n]`) into
/// `out[0..n]` (`NaN` at warmup or on an invalid candle).
///
/// # Safety
/// `handle` valid (from `wickra_separating_lines_new`, not freed); every input pointer and `out`
/// cover `n` elements.
#[no_mangle]
pub unsafe extern "C" fn wickra_separating_lines_batch(
    handle: *mut SeparatingLines,
    open: *const f64,
    high: *const f64,
    low: *const f64,
    close: *const f64,
    volume: *const f64,
    timestamp: *const i64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null()
        || open.is_null()
        || high.is_null()
        || low.is_null()
        || close.is_null()
        || volume.is_null()
        || timestamp.is_null()
        || out.is_null()
    {
        return;
    }
    let ind = &mut *handle;
    let opens = slice::from_raw_parts(open, n);
    let highs = slice::from_raw_parts(high, n);
    let lows = slice::from_raw_parts(low, n);
    let closes = slice::from_raw_parts(close, n);
    let volumes = slice::from_raw_parts(volume, n);
    let timestamps = slice::from_raw_parts(timestamp, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (idx, slot) in outputs.iter_mut().enumerate() {
        *slot = match Candle::new(
            opens[idx],
            highs[idx],
            lows[idx],
            closes[idx],
            volumes[idx],
            timestamps[idx],
        ) {
            Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
            Err(_) => f64::NAN,
        };
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_separating_lines_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_separating_lines_warmup_period(
    handle: *mut SeparatingLines,
) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_separating_lines_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_separating_lines_is_ready(handle: *mut SeparatingLines) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_separating_lines_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_separating_lines_name(
    handle: *mut SeparatingLines,
) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_separating_lines_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_separating_lines_reset(handle: *mut SeparatingLines) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_separating_lines_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_separating_lines_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_separating_lines_free(handle: *mut SeparatingLines) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `SessionVwap` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_session_vwap_free`.
#[no_mangle]
pub extern "C" fn wickra_session_vwap_new(utc_offset_minutes: i32) -> *mut SessionVwap {
    Box::into_raw(Box::new(SessionVwap::new(utc_offset_minutes)))
}

/// Feed one OHLCV candle; returns the output, or `NaN` during warmup / on a `NULL`
/// handle / if the candle is invalid (e.g. `high < low`).
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_session_vwap_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_session_vwap_update(
    handle: *mut SessionVwap,
    open: f64,
    high: f64,
    low: f64,
    close: f64,
    volume: f64,
    timestamp: i64,
) -> f64 {
    let Some(ind) = handle.as_mut() else {
        return f64::NAN;
    };
    match Candle::new(open, high, low, close, volume, timestamp) {
        Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
        Err(_) => f64::NAN,
    }
}

/// Run over the OHLCV series (`open/high/low/close/volume/timestamp[0..n]`) into
/// `out[0..n]` (`NaN` at warmup or on an invalid candle).
///
/// # Safety
/// `handle` valid (from `wickra_session_vwap_new`, not freed); every input pointer and `out`
/// cover `n` elements.
#[no_mangle]
pub unsafe extern "C" fn wickra_session_vwap_batch(
    handle: *mut SessionVwap,
    open: *const f64,
    high: *const f64,
    low: *const f64,
    close: *const f64,
    volume: *const f64,
    timestamp: *const i64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null()
        || open.is_null()
        || high.is_null()
        || low.is_null()
        || close.is_null()
        || volume.is_null()
        || timestamp.is_null()
        || out.is_null()
    {
        return;
    }
    let ind = &mut *handle;
    let opens = slice::from_raw_parts(open, n);
    let highs = slice::from_raw_parts(high, n);
    let lows = slice::from_raw_parts(low, n);
    let closes = slice::from_raw_parts(close, n);
    let volumes = slice::from_raw_parts(volume, n);
    let timestamps = slice::from_raw_parts(timestamp, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (idx, slot) in outputs.iter_mut().enumerate() {
        *slot = match Candle::new(
            opens[idx],
            highs[idx],
            lows[idx],
            closes[idx],
            volumes[idx],
            timestamps[idx],
        ) {
            Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
            Err(_) => f64::NAN,
        };
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_session_vwap_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_session_vwap_warmup_period(handle: *mut SessionVwap) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_session_vwap_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_session_vwap_is_ready(handle: *mut SessionVwap) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_session_vwap_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_session_vwap_name(handle: *mut SessionVwap) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_session_vwap_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_session_vwap_reset(handle: *mut SessionVwap) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_session_vwap_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_session_vwap_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_session_vwap_free(handle: *mut SessionVwap) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `Shark` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_shark_free`.
#[no_mangle]
pub extern "C" fn wickra_shark_new() -> *mut Shark {
    Box::into_raw(Box::new(Shark::new()))
}

/// Feed one OHLCV candle; returns the output, or `NaN` during warmup / on a `NULL`
/// handle / if the candle is invalid (e.g. `high < low`).
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_shark_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_shark_update(
    handle: *mut Shark,
    open: f64,
    high: f64,
    low: f64,
    close: f64,
    volume: f64,
    timestamp: i64,
) -> f64 {
    let Some(ind) = handle.as_mut() else {
        return f64::NAN;
    };
    match Candle::new(open, high, low, close, volume, timestamp) {
        Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
        Err(_) => f64::NAN,
    }
}

/// Run over the OHLCV series (`open/high/low/close/volume/timestamp[0..n]`) into
/// `out[0..n]` (`NaN` at warmup or on an invalid candle).
///
/// # Safety
/// `handle` valid (from `wickra_shark_new`, not freed); every input pointer and `out`
/// cover `n` elements.
#[no_mangle]
pub unsafe extern "C" fn wickra_shark_batch(
    handle: *mut Shark,
    open: *const f64,
    high: *const f64,
    low: *const f64,
    close: *const f64,
    volume: *const f64,
    timestamp: *const i64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null()
        || open.is_null()
        || high.is_null()
        || low.is_null()
        || close.is_null()
        || volume.is_null()
        || timestamp.is_null()
        || out.is_null()
    {
        return;
    }
    let ind = &mut *handle;
    let opens = slice::from_raw_parts(open, n);
    let highs = slice::from_raw_parts(high, n);
    let lows = slice::from_raw_parts(low, n);
    let closes = slice::from_raw_parts(close, n);
    let volumes = slice::from_raw_parts(volume, n);
    let timestamps = slice::from_raw_parts(timestamp, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (idx, slot) in outputs.iter_mut().enumerate() {
        *slot = match Candle::new(
            opens[idx],
            highs[idx],
            lows[idx],
            closes[idx],
            volumes[idx],
            timestamps[idx],
        ) {
            Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
            Err(_) => f64::NAN,
        };
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_shark_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_shark_warmup_period(handle: *mut Shark) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_shark_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_shark_is_ready(handle: *mut Shark) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_shark_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_shark_name(handle: *mut Shark) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_shark_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_shark_reset(handle: *mut Shark) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_shark_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_shark_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_shark_free(handle: *mut Shark) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `ShootingStar` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_shooting_star_free`.
#[no_mangle]
pub extern "C" fn wickra_shooting_star_new() -> *mut ShootingStar {
    Box::into_raw(Box::new(ShootingStar::new()))
}

/// Feed one OHLCV candle; returns the output, or `NaN` during warmup / on a `NULL`
/// handle / if the candle is invalid (e.g. `high < low`).
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_shooting_star_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_shooting_star_update(
    handle: *mut ShootingStar,
    open: f64,
    high: f64,
    low: f64,
    close: f64,
    volume: f64,
    timestamp: i64,
) -> f64 {
    let Some(ind) = handle.as_mut() else {
        return f64::NAN;
    };
    match Candle::new(open, high, low, close, volume, timestamp) {
        Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
        Err(_) => f64::NAN,
    }
}

/// Run over the OHLCV series (`open/high/low/close/volume/timestamp[0..n]`) into
/// `out[0..n]` (`NaN` at warmup or on an invalid candle).
///
/// # Safety
/// `handle` valid (from `wickra_shooting_star_new`, not freed); every input pointer and `out`
/// cover `n` elements.
#[no_mangle]
pub unsafe extern "C" fn wickra_shooting_star_batch(
    handle: *mut ShootingStar,
    open: *const f64,
    high: *const f64,
    low: *const f64,
    close: *const f64,
    volume: *const f64,
    timestamp: *const i64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null()
        || open.is_null()
        || high.is_null()
        || low.is_null()
        || close.is_null()
        || volume.is_null()
        || timestamp.is_null()
        || out.is_null()
    {
        return;
    }
    let ind = &mut *handle;
    let opens = slice::from_raw_parts(open, n);
    let highs = slice::from_raw_parts(high, n);
    let lows = slice::from_raw_parts(low, n);
    let closes = slice::from_raw_parts(close, n);
    let volumes = slice::from_raw_parts(volume, n);
    let timestamps = slice::from_raw_parts(timestamp, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (idx, slot) in outputs.iter_mut().enumerate() {
        *slot = match Candle::new(
            opens[idx],
            highs[idx],
            lows[idx],
            closes[idx],
            volumes[idx],
            timestamps[idx],
        ) {
            Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
            Err(_) => f64::NAN,
        };
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_shooting_star_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_shooting_star_warmup_period(handle: *mut ShootingStar) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_shooting_star_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_shooting_star_is_ready(handle: *mut ShootingStar) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_shooting_star_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_shooting_star_name(handle: *mut ShootingStar) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_shooting_star_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_shooting_star_reset(handle: *mut ShootingStar) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_shooting_star_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_shooting_star_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_shooting_star_free(handle: *mut ShootingStar) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `ShortLine` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_short_line_free`.
#[no_mangle]
pub extern "C" fn wickra_short_line_new() -> *mut ShortLine {
    Box::into_raw(Box::new(ShortLine::new()))
}

/// Feed one OHLCV candle; returns the output, or `NaN` during warmup / on a `NULL`
/// handle / if the candle is invalid (e.g. `high < low`).
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_short_line_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_short_line_update(
    handle: *mut ShortLine,
    open: f64,
    high: f64,
    low: f64,
    close: f64,
    volume: f64,
    timestamp: i64,
) -> f64 {
    let Some(ind) = handle.as_mut() else {
        return f64::NAN;
    };
    match Candle::new(open, high, low, close, volume, timestamp) {
        Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
        Err(_) => f64::NAN,
    }
}

/// Run over the OHLCV series (`open/high/low/close/volume/timestamp[0..n]`) into
/// `out[0..n]` (`NaN` at warmup or on an invalid candle).
///
/// # Safety
/// `handle` valid (from `wickra_short_line_new`, not freed); every input pointer and `out`
/// cover `n` elements.
#[no_mangle]
pub unsafe extern "C" fn wickra_short_line_batch(
    handle: *mut ShortLine,
    open: *const f64,
    high: *const f64,
    low: *const f64,
    close: *const f64,
    volume: *const f64,
    timestamp: *const i64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null()
        || open.is_null()
        || high.is_null()
        || low.is_null()
        || close.is_null()
        || volume.is_null()
        || timestamp.is_null()
        || out.is_null()
    {
        return;
    }
    let ind = &mut *handle;
    let opens = slice::from_raw_parts(open, n);
    let highs = slice::from_raw_parts(high, n);
    let lows = slice::from_raw_parts(low, n);
    let closes = slice::from_raw_parts(close, n);
    let volumes = slice::from_raw_parts(volume, n);
    let timestamps = slice::from_raw_parts(timestamp, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (idx, slot) in outputs.iter_mut().enumerate() {
        *slot = match Candle::new(
            opens[idx],
            highs[idx],
            lows[idx],
            closes[idx],
            volumes[idx],
            timestamps[idx],
        ) {
            Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
            Err(_) => f64::NAN,
        };
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_short_line_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_short_line_warmup_period(handle: *mut ShortLine) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_short_line_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_short_line_is_ready(handle: *mut ShortLine) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_short_line_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_short_line_name(handle: *mut ShortLine) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_short_line_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_short_line_reset(handle: *mut ShortLine) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_short_line_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_short_line_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_short_line_free(handle: *mut ShortLine) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `SinglePrints` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_single_prints_free`.
#[no_mangle]
pub extern "C" fn wickra_single_prints_new(period: usize, bins: usize) -> *mut SinglePrints {
    match SinglePrints::new(period, bins) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one OHLCV candle; returns the output, or `NaN` during warmup / on a `NULL`
/// handle / if the candle is invalid (e.g. `high < low`).
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_single_prints_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_single_prints_update(
    handle: *mut SinglePrints,
    open: f64,
    high: f64,
    low: f64,
    close: f64,
    volume: f64,
    timestamp: i64,
) -> f64 {
    let Some(ind) = handle.as_mut() else {
        return f64::NAN;
    };
    match Candle::new(open, high, low, close, volume, timestamp) {
        Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
        Err(_) => f64::NAN,
    }
}

/// Run over the OHLCV series (`open/high/low/close/volume/timestamp[0..n]`) into
/// `out[0..n]` (`NaN` at warmup or on an invalid candle).
///
/// # Safety
/// `handle` valid (from `wickra_single_prints_new`, not freed); every input pointer and `out`
/// cover `n` elements.
#[no_mangle]
pub unsafe extern "C" fn wickra_single_prints_batch(
    handle: *mut SinglePrints,
    open: *const f64,
    high: *const f64,
    low: *const f64,
    close: *const f64,
    volume: *const f64,
    timestamp: *const i64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null()
        || open.is_null()
        || high.is_null()
        || low.is_null()
        || close.is_null()
        || volume.is_null()
        || timestamp.is_null()
        || out.is_null()
    {
        return;
    }
    let ind = &mut *handle;
    let opens = slice::from_raw_parts(open, n);
    let highs = slice::from_raw_parts(high, n);
    let lows = slice::from_raw_parts(low, n);
    let closes = slice::from_raw_parts(close, n);
    let volumes = slice::from_raw_parts(volume, n);
    let timestamps = slice::from_raw_parts(timestamp, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (idx, slot) in outputs.iter_mut().enumerate() {
        *slot = match Candle::new(
            opens[idx],
            highs[idx],
            lows[idx],
            closes[idx],
            volumes[idx],
            timestamps[idx],
        ) {
            Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
            Err(_) => f64::NAN,
        };
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_single_prints_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_single_prints_warmup_period(handle: *mut SinglePrints) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_single_prints_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_single_prints_is_ready(handle: *mut SinglePrints) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_single_prints_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_single_prints_name(handle: *mut SinglePrints) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_single_prints_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_single_prints_reset(handle: *mut SinglePrints) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_single_prints_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_single_prints_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_single_prints_free(handle: *mut SinglePrints) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `Smi` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_smi_free`.
#[no_mangle]
pub extern "C" fn wickra_smi_new(period: usize, d_period: usize, d2_period: usize) -> *mut Smi {
    match Smi::new(period, d_period, d2_period) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one OHLCV candle; returns the output, or `NaN` during warmup / on a `NULL`
/// handle / if the candle is invalid (e.g. `high < low`).
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_smi_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_smi_update(
    handle: *mut Smi,
    open: f64,
    high: f64,
    low: f64,
    close: f64,
    volume: f64,
    timestamp: i64,
) -> f64 {
    let Some(ind) = handle.as_mut() else {
        return f64::NAN;
    };
    match Candle::new(open, high, low, close, volume, timestamp) {
        Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
        Err(_) => f64::NAN,
    }
}

/// Run over the OHLCV series (`open/high/low/close/volume/timestamp[0..n]`) into
/// `out[0..n]` (`NaN` at warmup or on an invalid candle).
///
/// # Safety
/// `handle` valid (from `wickra_smi_new`, not freed); every input pointer and `out`
/// cover `n` elements.
#[no_mangle]
pub unsafe extern "C" fn wickra_smi_batch(
    handle: *mut Smi,
    open: *const f64,
    high: *const f64,
    low: *const f64,
    close: *const f64,
    volume: *const f64,
    timestamp: *const i64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null()
        || open.is_null()
        || high.is_null()
        || low.is_null()
        || close.is_null()
        || volume.is_null()
        || timestamp.is_null()
        || out.is_null()
    {
        return;
    }
    let ind = &mut *handle;
    let opens = slice::from_raw_parts(open, n);
    let highs = slice::from_raw_parts(high, n);
    let lows = slice::from_raw_parts(low, n);
    let closes = slice::from_raw_parts(close, n);
    let volumes = slice::from_raw_parts(volume, n);
    let timestamps = slice::from_raw_parts(timestamp, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (idx, slot) in outputs.iter_mut().enumerate() {
        *slot = match Candle::new(
            opens[idx],
            highs[idx],
            lows[idx],
            closes[idx],
            volumes[idx],
            timestamps[idx],
        ) {
            Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
            Err(_) => f64::NAN,
        };
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_smi_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_smi_warmup_period(handle: *mut Smi) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_smi_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_smi_is_ready(handle: *mut Smi) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_smi_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_smi_name(handle: *mut Smi) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_smi_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_smi_reset(handle: *mut Smi) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_smi_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_smi_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_smi_free(handle: *mut Smi) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `SpinningTop` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_spinning_top_free`.
#[no_mangle]
pub extern "C" fn wickra_spinning_top_new() -> *mut SpinningTop {
    Box::into_raw(Box::new(SpinningTop::new()))
}

/// Feed one OHLCV candle; returns the output, or `NaN` during warmup / on a `NULL`
/// handle / if the candle is invalid (e.g. `high < low`).
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_spinning_top_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_spinning_top_update(
    handle: *mut SpinningTop,
    open: f64,
    high: f64,
    low: f64,
    close: f64,
    volume: f64,
    timestamp: i64,
) -> f64 {
    let Some(ind) = handle.as_mut() else {
        return f64::NAN;
    };
    match Candle::new(open, high, low, close, volume, timestamp) {
        Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
        Err(_) => f64::NAN,
    }
}

/// Run over the OHLCV series (`open/high/low/close/volume/timestamp[0..n]`) into
/// `out[0..n]` (`NaN` at warmup or on an invalid candle).
///
/// # Safety
/// `handle` valid (from `wickra_spinning_top_new`, not freed); every input pointer and `out`
/// cover `n` elements.
#[no_mangle]
pub unsafe extern "C" fn wickra_spinning_top_batch(
    handle: *mut SpinningTop,
    open: *const f64,
    high: *const f64,
    low: *const f64,
    close: *const f64,
    volume: *const f64,
    timestamp: *const i64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null()
        || open.is_null()
        || high.is_null()
        || low.is_null()
        || close.is_null()
        || volume.is_null()
        || timestamp.is_null()
        || out.is_null()
    {
        return;
    }
    let ind = &mut *handle;
    let opens = slice::from_raw_parts(open, n);
    let highs = slice::from_raw_parts(high, n);
    let lows = slice::from_raw_parts(low, n);
    let closes = slice::from_raw_parts(close, n);
    let volumes = slice::from_raw_parts(volume, n);
    let timestamps = slice::from_raw_parts(timestamp, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (idx, slot) in outputs.iter_mut().enumerate() {
        *slot = match Candle::new(
            opens[idx],
            highs[idx],
            lows[idx],
            closes[idx],
            volumes[idx],
            timestamps[idx],
        ) {
            Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
            Err(_) => f64::NAN,
        };
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_spinning_top_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_spinning_top_warmup_period(handle: *mut SpinningTop) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_spinning_top_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_spinning_top_is_ready(handle: *mut SpinningTop) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_spinning_top_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_spinning_top_name(handle: *mut SpinningTop) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_spinning_top_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_spinning_top_reset(handle: *mut SpinningTop) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_spinning_top_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_spinning_top_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_spinning_top_free(handle: *mut SpinningTop) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `StalledPattern` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_stalled_pattern_free`.
#[no_mangle]
pub extern "C" fn wickra_stalled_pattern_new() -> *mut StalledPattern {
    Box::into_raw(Box::new(StalledPattern::new()))
}

/// Feed one OHLCV candle; returns the output, or `NaN` during warmup / on a `NULL`
/// handle / if the candle is invalid (e.g. `high < low`).
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_stalled_pattern_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_stalled_pattern_update(
    handle: *mut StalledPattern,
    open: f64,
    high: f64,
    low: f64,
    close: f64,
    volume: f64,
    timestamp: i64,
) -> f64 {
    let Some(ind) = handle.as_mut() else {
        return f64::NAN;
    };
    match Candle::new(open, high, low, close, volume, timestamp) {
        Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
        Err(_) => f64::NAN,
    }
}

/// Run over the OHLCV series (`open/high/low/close/volume/timestamp[0..n]`) into
/// `out[0..n]` (`NaN` at warmup or on an invalid candle).
///
/// # Safety
/// `handle` valid (from `wickra_stalled_pattern_new`, not freed); every input pointer and `out`
/// cover `n` elements.
#[no_mangle]
pub unsafe extern "C" fn wickra_stalled_pattern_batch(
    handle: *mut StalledPattern,
    open: *const f64,
    high: *const f64,
    low: *const f64,
    close: *const f64,
    volume: *const f64,
    timestamp: *const i64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null()
        || open.is_null()
        || high.is_null()
        || low.is_null()
        || close.is_null()
        || volume.is_null()
        || timestamp.is_null()
        || out.is_null()
    {
        return;
    }
    let ind = &mut *handle;
    let opens = slice::from_raw_parts(open, n);
    let highs = slice::from_raw_parts(high, n);
    let lows = slice::from_raw_parts(low, n);
    let closes = slice::from_raw_parts(close, n);
    let volumes = slice::from_raw_parts(volume, n);
    let timestamps = slice::from_raw_parts(timestamp, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (idx, slot) in outputs.iter_mut().enumerate() {
        *slot = match Candle::new(
            opens[idx],
            highs[idx],
            lows[idx],
            closes[idx],
            volumes[idx],
            timestamps[idx],
        ) {
            Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
            Err(_) => f64::NAN,
        };
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_stalled_pattern_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_stalled_pattern_warmup_period(
    handle: *mut StalledPattern,
) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_stalled_pattern_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_stalled_pattern_is_ready(handle: *mut StalledPattern) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_stalled_pattern_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_stalled_pattern_name(handle: *mut StalledPattern) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_stalled_pattern_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_stalled_pattern_reset(handle: *mut StalledPattern) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_stalled_pattern_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_stalled_pattern_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_stalled_pattern_free(handle: *mut StalledPattern) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `StickSandwich` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_stick_sandwich_free`.
#[no_mangle]
pub extern "C" fn wickra_stick_sandwich_new() -> *mut StickSandwich {
    Box::into_raw(Box::new(StickSandwich::new()))
}

/// Feed one OHLCV candle; returns the output, or `NaN` during warmup / on a `NULL`
/// handle / if the candle is invalid (e.g. `high < low`).
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_stick_sandwich_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_stick_sandwich_update(
    handle: *mut StickSandwich,
    open: f64,
    high: f64,
    low: f64,
    close: f64,
    volume: f64,
    timestamp: i64,
) -> f64 {
    let Some(ind) = handle.as_mut() else {
        return f64::NAN;
    };
    match Candle::new(open, high, low, close, volume, timestamp) {
        Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
        Err(_) => f64::NAN,
    }
}

/// Run over the OHLCV series (`open/high/low/close/volume/timestamp[0..n]`) into
/// `out[0..n]` (`NaN` at warmup or on an invalid candle).
///
/// # Safety
/// `handle` valid (from `wickra_stick_sandwich_new`, not freed); every input pointer and `out`
/// cover `n` elements.
#[no_mangle]
pub unsafe extern "C" fn wickra_stick_sandwich_batch(
    handle: *mut StickSandwich,
    open: *const f64,
    high: *const f64,
    low: *const f64,
    close: *const f64,
    volume: *const f64,
    timestamp: *const i64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null()
        || open.is_null()
        || high.is_null()
        || low.is_null()
        || close.is_null()
        || volume.is_null()
        || timestamp.is_null()
        || out.is_null()
    {
        return;
    }
    let ind = &mut *handle;
    let opens = slice::from_raw_parts(open, n);
    let highs = slice::from_raw_parts(high, n);
    let lows = slice::from_raw_parts(low, n);
    let closes = slice::from_raw_parts(close, n);
    let volumes = slice::from_raw_parts(volume, n);
    let timestamps = slice::from_raw_parts(timestamp, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (idx, slot) in outputs.iter_mut().enumerate() {
        *slot = match Candle::new(
            opens[idx],
            highs[idx],
            lows[idx],
            closes[idx],
            volumes[idx],
            timestamps[idx],
        ) {
            Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
            Err(_) => f64::NAN,
        };
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_stick_sandwich_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_stick_sandwich_warmup_period(handle: *mut StickSandwich) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_stick_sandwich_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_stick_sandwich_is_ready(handle: *mut StickSandwich) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_stick_sandwich_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_stick_sandwich_name(handle: *mut StickSandwich) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_stick_sandwich_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_stick_sandwich_reset(handle: *mut StickSandwich) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_stick_sandwich_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_stick_sandwich_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_stick_sandwich_free(handle: *mut StickSandwich) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `StochasticCci` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_stochastic_cci_free`.
#[no_mangle]
pub extern "C" fn wickra_stochastic_cci_new(period: usize) -> *mut StochasticCci {
    match StochasticCci::new(period) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one OHLCV candle; returns the output, or `NaN` during warmup / on a `NULL`
/// handle / if the candle is invalid (e.g. `high < low`).
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_stochastic_cci_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_stochastic_cci_update(
    handle: *mut StochasticCci,
    open: f64,
    high: f64,
    low: f64,
    close: f64,
    volume: f64,
    timestamp: i64,
) -> f64 {
    let Some(ind) = handle.as_mut() else {
        return f64::NAN;
    };
    match Candle::new(open, high, low, close, volume, timestamp) {
        Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
        Err(_) => f64::NAN,
    }
}

/// Run over the OHLCV series (`open/high/low/close/volume/timestamp[0..n]`) into
/// `out[0..n]` (`NaN` at warmup or on an invalid candle).
///
/// # Safety
/// `handle` valid (from `wickra_stochastic_cci_new`, not freed); every input pointer and `out`
/// cover `n` elements.
#[no_mangle]
pub unsafe extern "C" fn wickra_stochastic_cci_batch(
    handle: *mut StochasticCci,
    open: *const f64,
    high: *const f64,
    low: *const f64,
    close: *const f64,
    volume: *const f64,
    timestamp: *const i64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null()
        || open.is_null()
        || high.is_null()
        || low.is_null()
        || close.is_null()
        || volume.is_null()
        || timestamp.is_null()
        || out.is_null()
    {
        return;
    }
    let ind = &mut *handle;
    let opens = slice::from_raw_parts(open, n);
    let highs = slice::from_raw_parts(high, n);
    let lows = slice::from_raw_parts(low, n);
    let closes = slice::from_raw_parts(close, n);
    let volumes = slice::from_raw_parts(volume, n);
    let timestamps = slice::from_raw_parts(timestamp, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (idx, slot) in outputs.iter_mut().enumerate() {
        *slot = match Candle::new(
            opens[idx],
            highs[idx],
            lows[idx],
            closes[idx],
            volumes[idx],
            timestamps[idx],
        ) {
            Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
            Err(_) => f64::NAN,
        };
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_stochastic_cci_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_stochastic_cci_warmup_period(handle: *mut StochasticCci) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_stochastic_cci_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_stochastic_cci_is_ready(handle: *mut StochasticCci) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_stochastic_cci_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_stochastic_cci_name(handle: *mut StochasticCci) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_stochastic_cci_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_stochastic_cci_reset(handle: *mut StochasticCci) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_stochastic_cci_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_stochastic_cci_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_stochastic_cci_free(handle: *mut StochasticCci) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `Takuri` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_takuri_free`.
#[no_mangle]
pub extern "C" fn wickra_takuri_new() -> *mut Takuri {
    Box::into_raw(Box::new(Takuri::new()))
}

/// Feed one OHLCV candle; returns the output, or `NaN` during warmup / on a `NULL`
/// handle / if the candle is invalid (e.g. `high < low`).
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_takuri_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_takuri_update(
    handle: *mut Takuri,
    open: f64,
    high: f64,
    low: f64,
    close: f64,
    volume: f64,
    timestamp: i64,
) -> f64 {
    let Some(ind) = handle.as_mut() else {
        return f64::NAN;
    };
    match Candle::new(open, high, low, close, volume, timestamp) {
        Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
        Err(_) => f64::NAN,
    }
}

/// Run over the OHLCV series (`open/high/low/close/volume/timestamp[0..n]`) into
/// `out[0..n]` (`NaN` at warmup or on an invalid candle).
///
/// # Safety
/// `handle` valid (from `wickra_takuri_new`, not freed); every input pointer and `out`
/// cover `n` elements.
#[no_mangle]
pub unsafe extern "C" fn wickra_takuri_batch(
    handle: *mut Takuri,
    open: *const f64,
    high: *const f64,
    low: *const f64,
    close: *const f64,
    volume: *const f64,
    timestamp: *const i64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null()
        || open.is_null()
        || high.is_null()
        || low.is_null()
        || close.is_null()
        || volume.is_null()
        || timestamp.is_null()
        || out.is_null()
    {
        return;
    }
    let ind = &mut *handle;
    let opens = slice::from_raw_parts(open, n);
    let highs = slice::from_raw_parts(high, n);
    let lows = slice::from_raw_parts(low, n);
    let closes = slice::from_raw_parts(close, n);
    let volumes = slice::from_raw_parts(volume, n);
    let timestamps = slice::from_raw_parts(timestamp, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (idx, slot) in outputs.iter_mut().enumerate() {
        *slot = match Candle::new(
            opens[idx],
            highs[idx],
            lows[idx],
            closes[idx],
            volumes[idx],
            timestamps[idx],
        ) {
            Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
            Err(_) => f64::NAN,
        };
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_takuri_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_takuri_warmup_period(handle: *mut Takuri) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_takuri_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_takuri_is_ready(handle: *mut Takuri) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_takuri_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_takuri_name(handle: *mut Takuri) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_takuri_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_takuri_reset(handle: *mut Takuri) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_takuri_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_takuri_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_takuri_free(handle: *mut Takuri) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `TasukiGap` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_tasuki_gap_free`.
#[no_mangle]
pub extern "C" fn wickra_tasuki_gap_new() -> *mut TasukiGap {
    Box::into_raw(Box::new(TasukiGap::new()))
}

/// Feed one OHLCV candle; returns the output, or `NaN` during warmup / on a `NULL`
/// handle / if the candle is invalid (e.g. `high < low`).
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_tasuki_gap_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_tasuki_gap_update(
    handle: *mut TasukiGap,
    open: f64,
    high: f64,
    low: f64,
    close: f64,
    volume: f64,
    timestamp: i64,
) -> f64 {
    let Some(ind) = handle.as_mut() else {
        return f64::NAN;
    };
    match Candle::new(open, high, low, close, volume, timestamp) {
        Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
        Err(_) => f64::NAN,
    }
}

/// Run over the OHLCV series (`open/high/low/close/volume/timestamp[0..n]`) into
/// `out[0..n]` (`NaN` at warmup or on an invalid candle).
///
/// # Safety
/// `handle` valid (from `wickra_tasuki_gap_new`, not freed); every input pointer and `out`
/// cover `n` elements.
#[no_mangle]
pub unsafe extern "C" fn wickra_tasuki_gap_batch(
    handle: *mut TasukiGap,
    open: *const f64,
    high: *const f64,
    low: *const f64,
    close: *const f64,
    volume: *const f64,
    timestamp: *const i64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null()
        || open.is_null()
        || high.is_null()
        || low.is_null()
        || close.is_null()
        || volume.is_null()
        || timestamp.is_null()
        || out.is_null()
    {
        return;
    }
    let ind = &mut *handle;
    let opens = slice::from_raw_parts(open, n);
    let highs = slice::from_raw_parts(high, n);
    let lows = slice::from_raw_parts(low, n);
    let closes = slice::from_raw_parts(close, n);
    let volumes = slice::from_raw_parts(volume, n);
    let timestamps = slice::from_raw_parts(timestamp, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (idx, slot) in outputs.iter_mut().enumerate() {
        *slot = match Candle::new(
            opens[idx],
            highs[idx],
            lows[idx],
            closes[idx],
            volumes[idx],
            timestamps[idx],
        ) {
            Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
            Err(_) => f64::NAN,
        };
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_tasuki_gap_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_tasuki_gap_warmup_period(handle: *mut TasukiGap) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_tasuki_gap_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_tasuki_gap_is_ready(handle: *mut TasukiGap) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_tasuki_gap_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_tasuki_gap_name(handle: *mut TasukiGap) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_tasuki_gap_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_tasuki_gap_reset(handle: *mut TasukiGap) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_tasuki_gap_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_tasuki_gap_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_tasuki_gap_free(handle: *mut TasukiGap) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `TdCamouflage` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_td_camouflage_free`.
#[no_mangle]
pub extern "C" fn wickra_td_camouflage_new() -> *mut TdCamouflage {
    Box::into_raw(Box::new(TdCamouflage::new()))
}

/// Feed one OHLCV candle; returns the output, or `NaN` during warmup / on a `NULL`
/// handle / if the candle is invalid (e.g. `high < low`).
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_td_camouflage_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_td_camouflage_update(
    handle: *mut TdCamouflage,
    open: f64,
    high: f64,
    low: f64,
    close: f64,
    volume: f64,
    timestamp: i64,
) -> f64 {
    let Some(ind) = handle.as_mut() else {
        return f64::NAN;
    };
    match Candle::new(open, high, low, close, volume, timestamp) {
        Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
        Err(_) => f64::NAN,
    }
}

/// Run over the OHLCV series (`open/high/low/close/volume/timestamp[0..n]`) into
/// `out[0..n]` (`NaN` at warmup or on an invalid candle).
///
/// # Safety
/// `handle` valid (from `wickra_td_camouflage_new`, not freed); every input pointer and `out`
/// cover `n` elements.
#[no_mangle]
pub unsafe extern "C" fn wickra_td_camouflage_batch(
    handle: *mut TdCamouflage,
    open: *const f64,
    high: *const f64,
    low: *const f64,
    close: *const f64,
    volume: *const f64,
    timestamp: *const i64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null()
        || open.is_null()
        || high.is_null()
        || low.is_null()
        || close.is_null()
        || volume.is_null()
        || timestamp.is_null()
        || out.is_null()
    {
        return;
    }
    let ind = &mut *handle;
    let opens = slice::from_raw_parts(open, n);
    let highs = slice::from_raw_parts(high, n);
    let lows = slice::from_raw_parts(low, n);
    let closes = slice::from_raw_parts(close, n);
    let volumes = slice::from_raw_parts(volume, n);
    let timestamps = slice::from_raw_parts(timestamp, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (idx, slot) in outputs.iter_mut().enumerate() {
        *slot = match Candle::new(
            opens[idx],
            highs[idx],
            lows[idx],
            closes[idx],
            volumes[idx],
            timestamps[idx],
        ) {
            Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
            Err(_) => f64::NAN,
        };
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_td_camouflage_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_td_camouflage_warmup_period(handle: *mut TdCamouflage) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_td_camouflage_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_td_camouflage_is_ready(handle: *mut TdCamouflage) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_td_camouflage_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_td_camouflage_name(handle: *mut TdCamouflage) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_td_camouflage_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_td_camouflage_reset(handle: *mut TdCamouflage) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_td_camouflage_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_td_camouflage_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_td_camouflage_free(handle: *mut TdCamouflage) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `TdClop` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_td_clop_free`.
#[no_mangle]
pub extern "C" fn wickra_td_clop_new() -> *mut TdClop {
    Box::into_raw(Box::new(TdClop::new()))
}

/// Feed one OHLCV candle; returns the output, or `NaN` during warmup / on a `NULL`
/// handle / if the candle is invalid (e.g. `high < low`).
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_td_clop_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_td_clop_update(
    handle: *mut TdClop,
    open: f64,
    high: f64,
    low: f64,
    close: f64,
    volume: f64,
    timestamp: i64,
) -> f64 {
    let Some(ind) = handle.as_mut() else {
        return f64::NAN;
    };
    match Candle::new(open, high, low, close, volume, timestamp) {
        Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
        Err(_) => f64::NAN,
    }
}

/// Run over the OHLCV series (`open/high/low/close/volume/timestamp[0..n]`) into
/// `out[0..n]` (`NaN` at warmup or on an invalid candle).
///
/// # Safety
/// `handle` valid (from `wickra_td_clop_new`, not freed); every input pointer and `out`
/// cover `n` elements.
#[no_mangle]
pub unsafe extern "C" fn wickra_td_clop_batch(
    handle: *mut TdClop,
    open: *const f64,
    high: *const f64,
    low: *const f64,
    close: *const f64,
    volume: *const f64,
    timestamp: *const i64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null()
        || open.is_null()
        || high.is_null()
        || low.is_null()
        || close.is_null()
        || volume.is_null()
        || timestamp.is_null()
        || out.is_null()
    {
        return;
    }
    let ind = &mut *handle;
    let opens = slice::from_raw_parts(open, n);
    let highs = slice::from_raw_parts(high, n);
    let lows = slice::from_raw_parts(low, n);
    let closes = slice::from_raw_parts(close, n);
    let volumes = slice::from_raw_parts(volume, n);
    let timestamps = slice::from_raw_parts(timestamp, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (idx, slot) in outputs.iter_mut().enumerate() {
        *slot = match Candle::new(
            opens[idx],
            highs[idx],
            lows[idx],
            closes[idx],
            volumes[idx],
            timestamps[idx],
        ) {
            Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
            Err(_) => f64::NAN,
        };
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_td_clop_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_td_clop_warmup_period(handle: *mut TdClop) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_td_clop_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_td_clop_is_ready(handle: *mut TdClop) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_td_clop_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_td_clop_name(handle: *mut TdClop) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_td_clop_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_td_clop_reset(handle: *mut TdClop) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_td_clop_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_td_clop_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_td_clop_free(handle: *mut TdClop) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `TdClopwin` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_td_clopwin_free`.
#[no_mangle]
pub extern "C" fn wickra_td_clopwin_new() -> *mut TdClopwin {
    Box::into_raw(Box::new(TdClopwin::new()))
}

/// Feed one OHLCV candle; returns the output, or `NaN` during warmup / on a `NULL`
/// handle / if the candle is invalid (e.g. `high < low`).
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_td_clopwin_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_td_clopwin_update(
    handle: *mut TdClopwin,
    open: f64,
    high: f64,
    low: f64,
    close: f64,
    volume: f64,
    timestamp: i64,
) -> f64 {
    let Some(ind) = handle.as_mut() else {
        return f64::NAN;
    };
    match Candle::new(open, high, low, close, volume, timestamp) {
        Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
        Err(_) => f64::NAN,
    }
}

/// Run over the OHLCV series (`open/high/low/close/volume/timestamp[0..n]`) into
/// `out[0..n]` (`NaN` at warmup or on an invalid candle).
///
/// # Safety
/// `handle` valid (from `wickra_td_clopwin_new`, not freed); every input pointer and `out`
/// cover `n` elements.
#[no_mangle]
pub unsafe extern "C" fn wickra_td_clopwin_batch(
    handle: *mut TdClopwin,
    open: *const f64,
    high: *const f64,
    low: *const f64,
    close: *const f64,
    volume: *const f64,
    timestamp: *const i64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null()
        || open.is_null()
        || high.is_null()
        || low.is_null()
        || close.is_null()
        || volume.is_null()
        || timestamp.is_null()
        || out.is_null()
    {
        return;
    }
    let ind = &mut *handle;
    let opens = slice::from_raw_parts(open, n);
    let highs = slice::from_raw_parts(high, n);
    let lows = slice::from_raw_parts(low, n);
    let closes = slice::from_raw_parts(close, n);
    let volumes = slice::from_raw_parts(volume, n);
    let timestamps = slice::from_raw_parts(timestamp, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (idx, slot) in outputs.iter_mut().enumerate() {
        *slot = match Candle::new(
            opens[idx],
            highs[idx],
            lows[idx],
            closes[idx],
            volumes[idx],
            timestamps[idx],
        ) {
            Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
            Err(_) => f64::NAN,
        };
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_td_clopwin_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_td_clopwin_warmup_period(handle: *mut TdClopwin) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_td_clopwin_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_td_clopwin_is_ready(handle: *mut TdClopwin) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_td_clopwin_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_td_clopwin_name(handle: *mut TdClopwin) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_td_clopwin_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_td_clopwin_reset(handle: *mut TdClopwin) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_td_clopwin_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_td_clopwin_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_td_clopwin_free(handle: *mut TdClopwin) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `TdCombo` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_td_combo_free`.
#[no_mangle]
pub extern "C" fn wickra_td_combo_new(
    setup_lookback: usize,
    setup_target: usize,
    countdown_lookback: usize,
    countdown_target: usize,
) -> *mut TdCombo {
    match TdCombo::new(
        setup_lookback,
        setup_target,
        countdown_lookback,
        countdown_target,
    ) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one OHLCV candle; returns the output, or `NaN` during warmup / on a `NULL`
/// handle / if the candle is invalid (e.g. `high < low`).
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_td_combo_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_td_combo_update(
    handle: *mut TdCombo,
    open: f64,
    high: f64,
    low: f64,
    close: f64,
    volume: f64,
    timestamp: i64,
) -> f64 {
    let Some(ind) = handle.as_mut() else {
        return f64::NAN;
    };
    match Candle::new(open, high, low, close, volume, timestamp) {
        Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
        Err(_) => f64::NAN,
    }
}

/// Run over the OHLCV series (`open/high/low/close/volume/timestamp[0..n]`) into
/// `out[0..n]` (`NaN` at warmup or on an invalid candle).
///
/// # Safety
/// `handle` valid (from `wickra_td_combo_new`, not freed); every input pointer and `out`
/// cover `n` elements.
#[no_mangle]
pub unsafe extern "C" fn wickra_td_combo_batch(
    handle: *mut TdCombo,
    open: *const f64,
    high: *const f64,
    low: *const f64,
    close: *const f64,
    volume: *const f64,
    timestamp: *const i64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null()
        || open.is_null()
        || high.is_null()
        || low.is_null()
        || close.is_null()
        || volume.is_null()
        || timestamp.is_null()
        || out.is_null()
    {
        return;
    }
    let ind = &mut *handle;
    let opens = slice::from_raw_parts(open, n);
    let highs = slice::from_raw_parts(high, n);
    let lows = slice::from_raw_parts(low, n);
    let closes = slice::from_raw_parts(close, n);
    let volumes = slice::from_raw_parts(volume, n);
    let timestamps = slice::from_raw_parts(timestamp, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (idx, slot) in outputs.iter_mut().enumerate() {
        *slot = match Candle::new(
            opens[idx],
            highs[idx],
            lows[idx],
            closes[idx],
            volumes[idx],
            timestamps[idx],
        ) {
            Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
            Err(_) => f64::NAN,
        };
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_td_combo_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_td_combo_warmup_period(handle: *mut TdCombo) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_td_combo_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_td_combo_is_ready(handle: *mut TdCombo) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_td_combo_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_td_combo_name(handle: *mut TdCombo) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_td_combo_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_td_combo_reset(handle: *mut TdCombo) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_td_combo_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_td_combo_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_td_combo_free(handle: *mut TdCombo) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `TdCountdown` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_td_countdown_free`.
#[no_mangle]
pub extern "C" fn wickra_td_countdown_new(
    setup_lookback: usize,
    setup_target: usize,
    countdown_lookback: usize,
    countdown_target: usize,
) -> *mut TdCountdown {
    match TdCountdown::new(
        setup_lookback,
        setup_target,
        countdown_lookback,
        countdown_target,
    ) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one OHLCV candle; returns the output, or `NaN` during warmup / on a `NULL`
/// handle / if the candle is invalid (e.g. `high < low`).
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_td_countdown_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_td_countdown_update(
    handle: *mut TdCountdown,
    open: f64,
    high: f64,
    low: f64,
    close: f64,
    volume: f64,
    timestamp: i64,
) -> f64 {
    let Some(ind) = handle.as_mut() else {
        return f64::NAN;
    };
    match Candle::new(open, high, low, close, volume, timestamp) {
        Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
        Err(_) => f64::NAN,
    }
}

/// Run over the OHLCV series (`open/high/low/close/volume/timestamp[0..n]`) into
/// `out[0..n]` (`NaN` at warmup or on an invalid candle).
///
/// # Safety
/// `handle` valid (from `wickra_td_countdown_new`, not freed); every input pointer and `out`
/// cover `n` elements.
#[no_mangle]
pub unsafe extern "C" fn wickra_td_countdown_batch(
    handle: *mut TdCountdown,
    open: *const f64,
    high: *const f64,
    low: *const f64,
    close: *const f64,
    volume: *const f64,
    timestamp: *const i64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null()
        || open.is_null()
        || high.is_null()
        || low.is_null()
        || close.is_null()
        || volume.is_null()
        || timestamp.is_null()
        || out.is_null()
    {
        return;
    }
    let ind = &mut *handle;
    let opens = slice::from_raw_parts(open, n);
    let highs = slice::from_raw_parts(high, n);
    let lows = slice::from_raw_parts(low, n);
    let closes = slice::from_raw_parts(close, n);
    let volumes = slice::from_raw_parts(volume, n);
    let timestamps = slice::from_raw_parts(timestamp, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (idx, slot) in outputs.iter_mut().enumerate() {
        *slot = match Candle::new(
            opens[idx],
            highs[idx],
            lows[idx],
            closes[idx],
            volumes[idx],
            timestamps[idx],
        ) {
            Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
            Err(_) => f64::NAN,
        };
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_td_countdown_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_td_countdown_warmup_period(handle: *mut TdCountdown) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_td_countdown_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_td_countdown_is_ready(handle: *mut TdCountdown) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_td_countdown_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_td_countdown_name(handle: *mut TdCountdown) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_td_countdown_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_td_countdown_reset(handle: *mut TdCountdown) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_td_countdown_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_td_countdown_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_td_countdown_free(handle: *mut TdCountdown) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `TdDeMarker` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_td_de_marker_free`.
#[no_mangle]
pub extern "C" fn wickra_td_de_marker_new(period: usize) -> *mut TdDeMarker {
    match TdDeMarker::new(period) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one OHLCV candle; returns the output, or `NaN` during warmup / on a `NULL`
/// handle / if the candle is invalid (e.g. `high < low`).
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_td_de_marker_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_td_de_marker_update(
    handle: *mut TdDeMarker,
    open: f64,
    high: f64,
    low: f64,
    close: f64,
    volume: f64,
    timestamp: i64,
) -> f64 {
    let Some(ind) = handle.as_mut() else {
        return f64::NAN;
    };
    match Candle::new(open, high, low, close, volume, timestamp) {
        Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
        Err(_) => f64::NAN,
    }
}

/// Run over the OHLCV series (`open/high/low/close/volume/timestamp[0..n]`) into
/// `out[0..n]` (`NaN` at warmup or on an invalid candle).
///
/// # Safety
/// `handle` valid (from `wickra_td_de_marker_new`, not freed); every input pointer and `out`
/// cover `n` elements.
#[no_mangle]
pub unsafe extern "C" fn wickra_td_de_marker_batch(
    handle: *mut TdDeMarker,
    open: *const f64,
    high: *const f64,
    low: *const f64,
    close: *const f64,
    volume: *const f64,
    timestamp: *const i64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null()
        || open.is_null()
        || high.is_null()
        || low.is_null()
        || close.is_null()
        || volume.is_null()
        || timestamp.is_null()
        || out.is_null()
    {
        return;
    }
    let ind = &mut *handle;
    let opens = slice::from_raw_parts(open, n);
    let highs = slice::from_raw_parts(high, n);
    let lows = slice::from_raw_parts(low, n);
    let closes = slice::from_raw_parts(close, n);
    let volumes = slice::from_raw_parts(volume, n);
    let timestamps = slice::from_raw_parts(timestamp, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (idx, slot) in outputs.iter_mut().enumerate() {
        *slot = match Candle::new(
            opens[idx],
            highs[idx],
            lows[idx],
            closes[idx],
            volumes[idx],
            timestamps[idx],
        ) {
            Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
            Err(_) => f64::NAN,
        };
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_td_de_marker_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_td_de_marker_warmup_period(handle: *mut TdDeMarker) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_td_de_marker_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_td_de_marker_is_ready(handle: *mut TdDeMarker) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_td_de_marker_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_td_de_marker_name(handle: *mut TdDeMarker) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_td_de_marker_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_td_de_marker_reset(handle: *mut TdDeMarker) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_td_de_marker_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_td_de_marker_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_td_de_marker_free(handle: *mut TdDeMarker) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `TdDifferential` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_td_differential_free`.
#[no_mangle]
pub extern "C" fn wickra_td_differential_new() -> *mut TdDifferential {
    Box::into_raw(Box::new(TdDifferential::new()))
}

/// Feed one OHLCV candle; returns the output, or `NaN` during warmup / on a `NULL`
/// handle / if the candle is invalid (e.g. `high < low`).
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_td_differential_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_td_differential_update(
    handle: *mut TdDifferential,
    open: f64,
    high: f64,
    low: f64,
    close: f64,
    volume: f64,
    timestamp: i64,
) -> f64 {
    let Some(ind) = handle.as_mut() else {
        return f64::NAN;
    };
    match Candle::new(open, high, low, close, volume, timestamp) {
        Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
        Err(_) => f64::NAN,
    }
}

/// Run over the OHLCV series (`open/high/low/close/volume/timestamp[0..n]`) into
/// `out[0..n]` (`NaN` at warmup or on an invalid candle).
///
/// # Safety
/// `handle` valid (from `wickra_td_differential_new`, not freed); every input pointer and `out`
/// cover `n` elements.
#[no_mangle]
pub unsafe extern "C" fn wickra_td_differential_batch(
    handle: *mut TdDifferential,
    open: *const f64,
    high: *const f64,
    low: *const f64,
    close: *const f64,
    volume: *const f64,
    timestamp: *const i64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null()
        || open.is_null()
        || high.is_null()
        || low.is_null()
        || close.is_null()
        || volume.is_null()
        || timestamp.is_null()
        || out.is_null()
    {
        return;
    }
    let ind = &mut *handle;
    let opens = slice::from_raw_parts(open, n);
    let highs = slice::from_raw_parts(high, n);
    let lows = slice::from_raw_parts(low, n);
    let closes = slice::from_raw_parts(close, n);
    let volumes = slice::from_raw_parts(volume, n);
    let timestamps = slice::from_raw_parts(timestamp, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (idx, slot) in outputs.iter_mut().enumerate() {
        *slot = match Candle::new(
            opens[idx],
            highs[idx],
            lows[idx],
            closes[idx],
            volumes[idx],
            timestamps[idx],
        ) {
            Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
            Err(_) => f64::NAN,
        };
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_td_differential_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_td_differential_warmup_period(
    handle: *mut TdDifferential,
) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_td_differential_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_td_differential_is_ready(handle: *mut TdDifferential) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_td_differential_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_td_differential_name(handle: *mut TdDifferential) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_td_differential_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_td_differential_reset(handle: *mut TdDifferential) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_td_differential_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_td_differential_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_td_differential_free(handle: *mut TdDifferential) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `TdDWave` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_td_d_wave_free`.
#[no_mangle]
pub extern "C" fn wickra_td_d_wave_new(strength: usize) -> *mut TdDWave {
    match TdDWave::new(strength) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one OHLCV candle; returns the output, or `NaN` during warmup / on a `NULL`
/// handle / if the candle is invalid (e.g. `high < low`).
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_td_d_wave_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_td_d_wave_update(
    handle: *mut TdDWave,
    open: f64,
    high: f64,
    low: f64,
    close: f64,
    volume: f64,
    timestamp: i64,
) -> f64 {
    let Some(ind) = handle.as_mut() else {
        return f64::NAN;
    };
    match Candle::new(open, high, low, close, volume, timestamp) {
        Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
        Err(_) => f64::NAN,
    }
}

/// Run over the OHLCV series (`open/high/low/close/volume/timestamp[0..n]`) into
/// `out[0..n]` (`NaN` at warmup or on an invalid candle).
///
/// # Safety
/// `handle` valid (from `wickra_td_d_wave_new`, not freed); every input pointer and `out`
/// cover `n` elements.
#[no_mangle]
pub unsafe extern "C" fn wickra_td_d_wave_batch(
    handle: *mut TdDWave,
    open: *const f64,
    high: *const f64,
    low: *const f64,
    close: *const f64,
    volume: *const f64,
    timestamp: *const i64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null()
        || open.is_null()
        || high.is_null()
        || low.is_null()
        || close.is_null()
        || volume.is_null()
        || timestamp.is_null()
        || out.is_null()
    {
        return;
    }
    let ind = &mut *handle;
    let opens = slice::from_raw_parts(open, n);
    let highs = slice::from_raw_parts(high, n);
    let lows = slice::from_raw_parts(low, n);
    let closes = slice::from_raw_parts(close, n);
    let volumes = slice::from_raw_parts(volume, n);
    let timestamps = slice::from_raw_parts(timestamp, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (idx, slot) in outputs.iter_mut().enumerate() {
        *slot = match Candle::new(
            opens[idx],
            highs[idx],
            lows[idx],
            closes[idx],
            volumes[idx],
            timestamps[idx],
        ) {
            Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
            Err(_) => f64::NAN,
        };
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_td_d_wave_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_td_d_wave_warmup_period(handle: *mut TdDWave) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_td_d_wave_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_td_d_wave_is_ready(handle: *mut TdDWave) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_td_d_wave_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_td_d_wave_name(handle: *mut TdDWave) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_td_d_wave_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_td_d_wave_reset(handle: *mut TdDWave) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_td_d_wave_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_td_d_wave_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_td_d_wave_free(handle: *mut TdDWave) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `TdOpen` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_td_open_free`.
#[no_mangle]
pub extern "C" fn wickra_td_open_new() -> *mut TdOpen {
    Box::into_raw(Box::new(TdOpen::new()))
}

/// Feed one OHLCV candle; returns the output, or `NaN` during warmup / on a `NULL`
/// handle / if the candle is invalid (e.g. `high < low`).
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_td_open_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_td_open_update(
    handle: *mut TdOpen,
    open: f64,
    high: f64,
    low: f64,
    close: f64,
    volume: f64,
    timestamp: i64,
) -> f64 {
    let Some(ind) = handle.as_mut() else {
        return f64::NAN;
    };
    match Candle::new(open, high, low, close, volume, timestamp) {
        Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
        Err(_) => f64::NAN,
    }
}

/// Run over the OHLCV series (`open/high/low/close/volume/timestamp[0..n]`) into
/// `out[0..n]` (`NaN` at warmup or on an invalid candle).
///
/// # Safety
/// `handle` valid (from `wickra_td_open_new`, not freed); every input pointer and `out`
/// cover `n` elements.
#[no_mangle]
pub unsafe extern "C" fn wickra_td_open_batch(
    handle: *mut TdOpen,
    open: *const f64,
    high: *const f64,
    low: *const f64,
    close: *const f64,
    volume: *const f64,
    timestamp: *const i64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null()
        || open.is_null()
        || high.is_null()
        || low.is_null()
        || close.is_null()
        || volume.is_null()
        || timestamp.is_null()
        || out.is_null()
    {
        return;
    }
    let ind = &mut *handle;
    let opens = slice::from_raw_parts(open, n);
    let highs = slice::from_raw_parts(high, n);
    let lows = slice::from_raw_parts(low, n);
    let closes = slice::from_raw_parts(close, n);
    let volumes = slice::from_raw_parts(volume, n);
    let timestamps = slice::from_raw_parts(timestamp, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (idx, slot) in outputs.iter_mut().enumerate() {
        *slot = match Candle::new(
            opens[idx],
            highs[idx],
            lows[idx],
            closes[idx],
            volumes[idx],
            timestamps[idx],
        ) {
            Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
            Err(_) => f64::NAN,
        };
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_td_open_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_td_open_warmup_period(handle: *mut TdOpen) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_td_open_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_td_open_is_ready(handle: *mut TdOpen) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_td_open_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_td_open_name(handle: *mut TdOpen) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_td_open_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_td_open_reset(handle: *mut TdOpen) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_td_open_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_td_open_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_td_open_free(handle: *mut TdOpen) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `TdPressure` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_td_pressure_free`.
#[no_mangle]
pub extern "C" fn wickra_td_pressure_new(period: usize) -> *mut TdPressure {
    match TdPressure::new(period) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one OHLCV candle; returns the output, or `NaN` during warmup / on a `NULL`
/// handle / if the candle is invalid (e.g. `high < low`).
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_td_pressure_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_td_pressure_update(
    handle: *mut TdPressure,
    open: f64,
    high: f64,
    low: f64,
    close: f64,
    volume: f64,
    timestamp: i64,
) -> f64 {
    let Some(ind) = handle.as_mut() else {
        return f64::NAN;
    };
    match Candle::new(open, high, low, close, volume, timestamp) {
        Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
        Err(_) => f64::NAN,
    }
}

/// Run over the OHLCV series (`open/high/low/close/volume/timestamp[0..n]`) into
/// `out[0..n]` (`NaN` at warmup or on an invalid candle).
///
/// # Safety
/// `handle` valid (from `wickra_td_pressure_new`, not freed); every input pointer and `out`
/// cover `n` elements.
#[no_mangle]
pub unsafe extern "C" fn wickra_td_pressure_batch(
    handle: *mut TdPressure,
    open: *const f64,
    high: *const f64,
    low: *const f64,
    close: *const f64,
    volume: *const f64,
    timestamp: *const i64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null()
        || open.is_null()
        || high.is_null()
        || low.is_null()
        || close.is_null()
        || volume.is_null()
        || timestamp.is_null()
        || out.is_null()
    {
        return;
    }
    let ind = &mut *handle;
    let opens = slice::from_raw_parts(open, n);
    let highs = slice::from_raw_parts(high, n);
    let lows = slice::from_raw_parts(low, n);
    let closes = slice::from_raw_parts(close, n);
    let volumes = slice::from_raw_parts(volume, n);
    let timestamps = slice::from_raw_parts(timestamp, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (idx, slot) in outputs.iter_mut().enumerate() {
        *slot = match Candle::new(
            opens[idx],
            highs[idx],
            lows[idx],
            closes[idx],
            volumes[idx],
            timestamps[idx],
        ) {
            Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
            Err(_) => f64::NAN,
        };
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_td_pressure_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_td_pressure_warmup_period(handle: *mut TdPressure) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_td_pressure_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_td_pressure_is_ready(handle: *mut TdPressure) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_td_pressure_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_td_pressure_name(handle: *mut TdPressure) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_td_pressure_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_td_pressure_reset(handle: *mut TdPressure) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_td_pressure_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_td_pressure_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_td_pressure_free(handle: *mut TdPressure) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `TdPropulsion` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_td_propulsion_free`.
#[no_mangle]
pub extern "C" fn wickra_td_propulsion_new() -> *mut TdPropulsion {
    Box::into_raw(Box::new(TdPropulsion::new()))
}

/// Feed one OHLCV candle; returns the output, or `NaN` during warmup / on a `NULL`
/// handle / if the candle is invalid (e.g. `high < low`).
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_td_propulsion_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_td_propulsion_update(
    handle: *mut TdPropulsion,
    open: f64,
    high: f64,
    low: f64,
    close: f64,
    volume: f64,
    timestamp: i64,
) -> f64 {
    let Some(ind) = handle.as_mut() else {
        return f64::NAN;
    };
    match Candle::new(open, high, low, close, volume, timestamp) {
        Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
        Err(_) => f64::NAN,
    }
}

/// Run over the OHLCV series (`open/high/low/close/volume/timestamp[0..n]`) into
/// `out[0..n]` (`NaN` at warmup or on an invalid candle).
///
/// # Safety
/// `handle` valid (from `wickra_td_propulsion_new`, not freed); every input pointer and `out`
/// cover `n` elements.
#[no_mangle]
pub unsafe extern "C" fn wickra_td_propulsion_batch(
    handle: *mut TdPropulsion,
    open: *const f64,
    high: *const f64,
    low: *const f64,
    close: *const f64,
    volume: *const f64,
    timestamp: *const i64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null()
        || open.is_null()
        || high.is_null()
        || low.is_null()
        || close.is_null()
        || volume.is_null()
        || timestamp.is_null()
        || out.is_null()
    {
        return;
    }
    let ind = &mut *handle;
    let opens = slice::from_raw_parts(open, n);
    let highs = slice::from_raw_parts(high, n);
    let lows = slice::from_raw_parts(low, n);
    let closes = slice::from_raw_parts(close, n);
    let volumes = slice::from_raw_parts(volume, n);
    let timestamps = slice::from_raw_parts(timestamp, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (idx, slot) in outputs.iter_mut().enumerate() {
        *slot = match Candle::new(
            opens[idx],
            highs[idx],
            lows[idx],
            closes[idx],
            volumes[idx],
            timestamps[idx],
        ) {
            Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
            Err(_) => f64::NAN,
        };
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_td_propulsion_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_td_propulsion_warmup_period(handle: *mut TdPropulsion) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_td_propulsion_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_td_propulsion_is_ready(handle: *mut TdPropulsion) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_td_propulsion_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_td_propulsion_name(handle: *mut TdPropulsion) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_td_propulsion_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_td_propulsion_reset(handle: *mut TdPropulsion) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_td_propulsion_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_td_propulsion_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_td_propulsion_free(handle: *mut TdPropulsion) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `TdRei` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_td_rei_free`.
#[no_mangle]
pub extern "C" fn wickra_td_rei_new(period: usize) -> *mut TdRei {
    match TdRei::new(period) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one OHLCV candle; returns the output, or `NaN` during warmup / on a `NULL`
/// handle / if the candle is invalid (e.g. `high < low`).
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_td_rei_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_td_rei_update(
    handle: *mut TdRei,
    open: f64,
    high: f64,
    low: f64,
    close: f64,
    volume: f64,
    timestamp: i64,
) -> f64 {
    let Some(ind) = handle.as_mut() else {
        return f64::NAN;
    };
    match Candle::new(open, high, low, close, volume, timestamp) {
        Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
        Err(_) => f64::NAN,
    }
}

/// Run over the OHLCV series (`open/high/low/close/volume/timestamp[0..n]`) into
/// `out[0..n]` (`NaN` at warmup or on an invalid candle).
///
/// # Safety
/// `handle` valid (from `wickra_td_rei_new`, not freed); every input pointer and `out`
/// cover `n` elements.
#[no_mangle]
pub unsafe extern "C" fn wickra_td_rei_batch(
    handle: *mut TdRei,
    open: *const f64,
    high: *const f64,
    low: *const f64,
    close: *const f64,
    volume: *const f64,
    timestamp: *const i64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null()
        || open.is_null()
        || high.is_null()
        || low.is_null()
        || close.is_null()
        || volume.is_null()
        || timestamp.is_null()
        || out.is_null()
    {
        return;
    }
    let ind = &mut *handle;
    let opens = slice::from_raw_parts(open, n);
    let highs = slice::from_raw_parts(high, n);
    let lows = slice::from_raw_parts(low, n);
    let closes = slice::from_raw_parts(close, n);
    let volumes = slice::from_raw_parts(volume, n);
    let timestamps = slice::from_raw_parts(timestamp, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (idx, slot) in outputs.iter_mut().enumerate() {
        *slot = match Candle::new(
            opens[idx],
            highs[idx],
            lows[idx],
            closes[idx],
            volumes[idx],
            timestamps[idx],
        ) {
            Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
            Err(_) => f64::NAN,
        };
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_td_rei_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_td_rei_warmup_period(handle: *mut TdRei) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_td_rei_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_td_rei_is_ready(handle: *mut TdRei) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_td_rei_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_td_rei_name(handle: *mut TdRei) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_td_rei_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_td_rei_reset(handle: *mut TdRei) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_td_rei_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_td_rei_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_td_rei_free(handle: *mut TdRei) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `TdSetup` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_td_setup_free`.
#[no_mangle]
pub extern "C" fn wickra_td_setup_new(lookback: usize, target: usize) -> *mut TdSetup {
    match TdSetup::new(lookback, target) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one OHLCV candle; returns the output, or `NaN` during warmup / on a `NULL`
/// handle / if the candle is invalid (e.g. `high < low`).
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_td_setup_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_td_setup_update(
    handle: *mut TdSetup,
    open: f64,
    high: f64,
    low: f64,
    close: f64,
    volume: f64,
    timestamp: i64,
) -> f64 {
    let Some(ind) = handle.as_mut() else {
        return f64::NAN;
    };
    match Candle::new(open, high, low, close, volume, timestamp) {
        Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
        Err(_) => f64::NAN,
    }
}

/// Run over the OHLCV series (`open/high/low/close/volume/timestamp[0..n]`) into
/// `out[0..n]` (`NaN` at warmup or on an invalid candle).
///
/// # Safety
/// `handle` valid (from `wickra_td_setup_new`, not freed); every input pointer and `out`
/// cover `n` elements.
#[no_mangle]
pub unsafe extern "C" fn wickra_td_setup_batch(
    handle: *mut TdSetup,
    open: *const f64,
    high: *const f64,
    low: *const f64,
    close: *const f64,
    volume: *const f64,
    timestamp: *const i64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null()
        || open.is_null()
        || high.is_null()
        || low.is_null()
        || close.is_null()
        || volume.is_null()
        || timestamp.is_null()
        || out.is_null()
    {
        return;
    }
    let ind = &mut *handle;
    let opens = slice::from_raw_parts(open, n);
    let highs = slice::from_raw_parts(high, n);
    let lows = slice::from_raw_parts(low, n);
    let closes = slice::from_raw_parts(close, n);
    let volumes = slice::from_raw_parts(volume, n);
    let timestamps = slice::from_raw_parts(timestamp, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (idx, slot) in outputs.iter_mut().enumerate() {
        *slot = match Candle::new(
            opens[idx],
            highs[idx],
            lows[idx],
            closes[idx],
            volumes[idx],
            timestamps[idx],
        ) {
            Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
            Err(_) => f64::NAN,
        };
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_td_setup_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_td_setup_warmup_period(handle: *mut TdSetup) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_td_setup_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_td_setup_is_ready(handle: *mut TdSetup) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_td_setup_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_td_setup_name(handle: *mut TdSetup) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_td_setup_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_td_setup_reset(handle: *mut TdSetup) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_td_setup_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_td_setup_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_td_setup_free(handle: *mut TdSetup) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `TdTrap` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_td_trap_free`.
#[no_mangle]
pub extern "C" fn wickra_td_trap_new() -> *mut TdTrap {
    Box::into_raw(Box::new(TdTrap::new()))
}

/// Feed one OHLCV candle; returns the output, or `NaN` during warmup / on a `NULL`
/// handle / if the candle is invalid (e.g. `high < low`).
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_td_trap_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_td_trap_update(
    handle: *mut TdTrap,
    open: f64,
    high: f64,
    low: f64,
    close: f64,
    volume: f64,
    timestamp: i64,
) -> f64 {
    let Some(ind) = handle.as_mut() else {
        return f64::NAN;
    };
    match Candle::new(open, high, low, close, volume, timestamp) {
        Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
        Err(_) => f64::NAN,
    }
}

/// Run over the OHLCV series (`open/high/low/close/volume/timestamp[0..n]`) into
/// `out[0..n]` (`NaN` at warmup or on an invalid candle).
///
/// # Safety
/// `handle` valid (from `wickra_td_trap_new`, not freed); every input pointer and `out`
/// cover `n` elements.
#[no_mangle]
pub unsafe extern "C" fn wickra_td_trap_batch(
    handle: *mut TdTrap,
    open: *const f64,
    high: *const f64,
    low: *const f64,
    close: *const f64,
    volume: *const f64,
    timestamp: *const i64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null()
        || open.is_null()
        || high.is_null()
        || low.is_null()
        || close.is_null()
        || volume.is_null()
        || timestamp.is_null()
        || out.is_null()
    {
        return;
    }
    let ind = &mut *handle;
    let opens = slice::from_raw_parts(open, n);
    let highs = slice::from_raw_parts(high, n);
    let lows = slice::from_raw_parts(low, n);
    let closes = slice::from_raw_parts(close, n);
    let volumes = slice::from_raw_parts(volume, n);
    let timestamps = slice::from_raw_parts(timestamp, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (idx, slot) in outputs.iter_mut().enumerate() {
        *slot = match Candle::new(
            opens[idx],
            highs[idx],
            lows[idx],
            closes[idx],
            volumes[idx],
            timestamps[idx],
        ) {
            Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
            Err(_) => f64::NAN,
        };
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_td_trap_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_td_trap_warmup_period(handle: *mut TdTrap) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_td_trap_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_td_trap_is_ready(handle: *mut TdTrap) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_td_trap_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_td_trap_name(handle: *mut TdTrap) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_td_trap_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_td_trap_reset(handle: *mut TdTrap) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_td_trap_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_td_trap_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_td_trap_free(handle: *mut TdTrap) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `ThreeDrives` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_three_drives_free`.
#[no_mangle]
pub extern "C" fn wickra_three_drives_new() -> *mut ThreeDrives {
    Box::into_raw(Box::new(ThreeDrives::new()))
}

/// Feed one OHLCV candle; returns the output, or `NaN` during warmup / on a `NULL`
/// handle / if the candle is invalid (e.g. `high < low`).
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_three_drives_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_three_drives_update(
    handle: *mut ThreeDrives,
    open: f64,
    high: f64,
    low: f64,
    close: f64,
    volume: f64,
    timestamp: i64,
) -> f64 {
    let Some(ind) = handle.as_mut() else {
        return f64::NAN;
    };
    match Candle::new(open, high, low, close, volume, timestamp) {
        Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
        Err(_) => f64::NAN,
    }
}

/// Run over the OHLCV series (`open/high/low/close/volume/timestamp[0..n]`) into
/// `out[0..n]` (`NaN` at warmup or on an invalid candle).
///
/// # Safety
/// `handle` valid (from `wickra_three_drives_new`, not freed); every input pointer and `out`
/// cover `n` elements.
#[no_mangle]
pub unsafe extern "C" fn wickra_three_drives_batch(
    handle: *mut ThreeDrives,
    open: *const f64,
    high: *const f64,
    low: *const f64,
    close: *const f64,
    volume: *const f64,
    timestamp: *const i64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null()
        || open.is_null()
        || high.is_null()
        || low.is_null()
        || close.is_null()
        || volume.is_null()
        || timestamp.is_null()
        || out.is_null()
    {
        return;
    }
    let ind = &mut *handle;
    let opens = slice::from_raw_parts(open, n);
    let highs = slice::from_raw_parts(high, n);
    let lows = slice::from_raw_parts(low, n);
    let closes = slice::from_raw_parts(close, n);
    let volumes = slice::from_raw_parts(volume, n);
    let timestamps = slice::from_raw_parts(timestamp, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (idx, slot) in outputs.iter_mut().enumerate() {
        *slot = match Candle::new(
            opens[idx],
            highs[idx],
            lows[idx],
            closes[idx],
            volumes[idx],
            timestamps[idx],
        ) {
            Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
            Err(_) => f64::NAN,
        };
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_three_drives_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_three_drives_warmup_period(handle: *mut ThreeDrives) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_three_drives_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_three_drives_is_ready(handle: *mut ThreeDrives) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_three_drives_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_three_drives_name(handle: *mut ThreeDrives) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_three_drives_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_three_drives_reset(handle: *mut ThreeDrives) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_three_drives_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_three_drives_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_three_drives_free(handle: *mut ThreeDrives) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `ThreeInside` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_three_inside_free`.
#[no_mangle]
pub extern "C" fn wickra_three_inside_new() -> *mut ThreeInside {
    Box::into_raw(Box::new(ThreeInside::new()))
}

/// Feed one OHLCV candle; returns the output, or `NaN` during warmup / on a `NULL`
/// handle / if the candle is invalid (e.g. `high < low`).
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_three_inside_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_three_inside_update(
    handle: *mut ThreeInside,
    open: f64,
    high: f64,
    low: f64,
    close: f64,
    volume: f64,
    timestamp: i64,
) -> f64 {
    let Some(ind) = handle.as_mut() else {
        return f64::NAN;
    };
    match Candle::new(open, high, low, close, volume, timestamp) {
        Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
        Err(_) => f64::NAN,
    }
}

/// Run over the OHLCV series (`open/high/low/close/volume/timestamp[0..n]`) into
/// `out[0..n]` (`NaN` at warmup or on an invalid candle).
///
/// # Safety
/// `handle` valid (from `wickra_three_inside_new`, not freed); every input pointer and `out`
/// cover `n` elements.
#[no_mangle]
pub unsafe extern "C" fn wickra_three_inside_batch(
    handle: *mut ThreeInside,
    open: *const f64,
    high: *const f64,
    low: *const f64,
    close: *const f64,
    volume: *const f64,
    timestamp: *const i64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null()
        || open.is_null()
        || high.is_null()
        || low.is_null()
        || close.is_null()
        || volume.is_null()
        || timestamp.is_null()
        || out.is_null()
    {
        return;
    }
    let ind = &mut *handle;
    let opens = slice::from_raw_parts(open, n);
    let highs = slice::from_raw_parts(high, n);
    let lows = slice::from_raw_parts(low, n);
    let closes = slice::from_raw_parts(close, n);
    let volumes = slice::from_raw_parts(volume, n);
    let timestamps = slice::from_raw_parts(timestamp, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (idx, slot) in outputs.iter_mut().enumerate() {
        *slot = match Candle::new(
            opens[idx],
            highs[idx],
            lows[idx],
            closes[idx],
            volumes[idx],
            timestamps[idx],
        ) {
            Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
            Err(_) => f64::NAN,
        };
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_three_inside_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_three_inside_warmup_period(handle: *mut ThreeInside) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_three_inside_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_three_inside_is_ready(handle: *mut ThreeInside) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_three_inside_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_three_inside_name(handle: *mut ThreeInside) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_three_inside_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_three_inside_reset(handle: *mut ThreeInside) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_three_inside_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_three_inside_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_three_inside_free(handle: *mut ThreeInside) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `ThreeLineBreak` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_three_line_break_free`.
#[no_mangle]
pub extern "C" fn wickra_three_line_break_new(lines: usize) -> *mut ThreeLineBreak {
    match ThreeLineBreak::new(lines) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one OHLCV candle; returns the output, or `NaN` during warmup / on a `NULL`
/// handle / if the candle is invalid (e.g. `high < low`).
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_three_line_break_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_three_line_break_update(
    handle: *mut ThreeLineBreak,
    open: f64,
    high: f64,
    low: f64,
    close: f64,
    volume: f64,
    timestamp: i64,
) -> f64 {
    let Some(ind) = handle.as_mut() else {
        return f64::NAN;
    };
    match Candle::new(open, high, low, close, volume, timestamp) {
        Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
        Err(_) => f64::NAN,
    }
}

/// Run over the OHLCV series (`open/high/low/close/volume/timestamp[0..n]`) into
/// `out[0..n]` (`NaN` at warmup or on an invalid candle).
///
/// # Safety
/// `handle` valid (from `wickra_three_line_break_new`, not freed); every input pointer and `out`
/// cover `n` elements.
#[no_mangle]
pub unsafe extern "C" fn wickra_three_line_break_batch(
    handle: *mut ThreeLineBreak,
    open: *const f64,
    high: *const f64,
    low: *const f64,
    close: *const f64,
    volume: *const f64,
    timestamp: *const i64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null()
        || open.is_null()
        || high.is_null()
        || low.is_null()
        || close.is_null()
        || volume.is_null()
        || timestamp.is_null()
        || out.is_null()
    {
        return;
    }
    let ind = &mut *handle;
    let opens = slice::from_raw_parts(open, n);
    let highs = slice::from_raw_parts(high, n);
    let lows = slice::from_raw_parts(low, n);
    let closes = slice::from_raw_parts(close, n);
    let volumes = slice::from_raw_parts(volume, n);
    let timestamps = slice::from_raw_parts(timestamp, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (idx, slot) in outputs.iter_mut().enumerate() {
        *slot = match Candle::new(
            opens[idx],
            highs[idx],
            lows[idx],
            closes[idx],
            volumes[idx],
            timestamps[idx],
        ) {
            Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
            Err(_) => f64::NAN,
        };
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_three_line_break_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_three_line_break_warmup_period(
    handle: *mut ThreeLineBreak,
) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_three_line_break_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_three_line_break_is_ready(handle: *mut ThreeLineBreak) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_three_line_break_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_three_line_break_name(
    handle: *mut ThreeLineBreak,
) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_three_line_break_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_three_line_break_reset(handle: *mut ThreeLineBreak) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_three_line_break_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_three_line_break_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_three_line_break_free(handle: *mut ThreeLineBreak) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `ThreeLineStrike` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_three_line_strike_free`.
#[no_mangle]
pub extern "C" fn wickra_three_line_strike_new() -> *mut ThreeLineStrike {
    Box::into_raw(Box::new(ThreeLineStrike::new()))
}

/// Feed one OHLCV candle; returns the output, or `NaN` during warmup / on a `NULL`
/// handle / if the candle is invalid (e.g. `high < low`).
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_three_line_strike_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_three_line_strike_update(
    handle: *mut ThreeLineStrike,
    open: f64,
    high: f64,
    low: f64,
    close: f64,
    volume: f64,
    timestamp: i64,
) -> f64 {
    let Some(ind) = handle.as_mut() else {
        return f64::NAN;
    };
    match Candle::new(open, high, low, close, volume, timestamp) {
        Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
        Err(_) => f64::NAN,
    }
}

/// Run over the OHLCV series (`open/high/low/close/volume/timestamp[0..n]`) into
/// `out[0..n]` (`NaN` at warmup or on an invalid candle).
///
/// # Safety
/// `handle` valid (from `wickra_three_line_strike_new`, not freed); every input pointer and `out`
/// cover `n` elements.
#[no_mangle]
pub unsafe extern "C" fn wickra_three_line_strike_batch(
    handle: *mut ThreeLineStrike,
    open: *const f64,
    high: *const f64,
    low: *const f64,
    close: *const f64,
    volume: *const f64,
    timestamp: *const i64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null()
        || open.is_null()
        || high.is_null()
        || low.is_null()
        || close.is_null()
        || volume.is_null()
        || timestamp.is_null()
        || out.is_null()
    {
        return;
    }
    let ind = &mut *handle;
    let opens = slice::from_raw_parts(open, n);
    let highs = slice::from_raw_parts(high, n);
    let lows = slice::from_raw_parts(low, n);
    let closes = slice::from_raw_parts(close, n);
    let volumes = slice::from_raw_parts(volume, n);
    let timestamps = slice::from_raw_parts(timestamp, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (idx, slot) in outputs.iter_mut().enumerate() {
        *slot = match Candle::new(
            opens[idx],
            highs[idx],
            lows[idx],
            closes[idx],
            volumes[idx],
            timestamps[idx],
        ) {
            Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
            Err(_) => f64::NAN,
        };
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_three_line_strike_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_three_line_strike_warmup_period(
    handle: *mut ThreeLineStrike,
) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_three_line_strike_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_three_line_strike_is_ready(handle: *mut ThreeLineStrike) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_three_line_strike_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_three_line_strike_name(
    handle: *mut ThreeLineStrike,
) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_three_line_strike_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_three_line_strike_reset(handle: *mut ThreeLineStrike) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_three_line_strike_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_three_line_strike_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_three_line_strike_free(handle: *mut ThreeLineStrike) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `ThreeOutside` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_three_outside_free`.
#[no_mangle]
pub extern "C" fn wickra_three_outside_new() -> *mut ThreeOutside {
    Box::into_raw(Box::new(ThreeOutside::new()))
}

/// Feed one OHLCV candle; returns the output, or `NaN` during warmup / on a `NULL`
/// handle / if the candle is invalid (e.g. `high < low`).
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_three_outside_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_three_outside_update(
    handle: *mut ThreeOutside,
    open: f64,
    high: f64,
    low: f64,
    close: f64,
    volume: f64,
    timestamp: i64,
) -> f64 {
    let Some(ind) = handle.as_mut() else {
        return f64::NAN;
    };
    match Candle::new(open, high, low, close, volume, timestamp) {
        Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
        Err(_) => f64::NAN,
    }
}

/// Run over the OHLCV series (`open/high/low/close/volume/timestamp[0..n]`) into
/// `out[0..n]` (`NaN` at warmup or on an invalid candle).
///
/// # Safety
/// `handle` valid (from `wickra_three_outside_new`, not freed); every input pointer and `out`
/// cover `n` elements.
#[no_mangle]
pub unsafe extern "C" fn wickra_three_outside_batch(
    handle: *mut ThreeOutside,
    open: *const f64,
    high: *const f64,
    low: *const f64,
    close: *const f64,
    volume: *const f64,
    timestamp: *const i64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null()
        || open.is_null()
        || high.is_null()
        || low.is_null()
        || close.is_null()
        || volume.is_null()
        || timestamp.is_null()
        || out.is_null()
    {
        return;
    }
    let ind = &mut *handle;
    let opens = slice::from_raw_parts(open, n);
    let highs = slice::from_raw_parts(high, n);
    let lows = slice::from_raw_parts(low, n);
    let closes = slice::from_raw_parts(close, n);
    let volumes = slice::from_raw_parts(volume, n);
    let timestamps = slice::from_raw_parts(timestamp, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (idx, slot) in outputs.iter_mut().enumerate() {
        *slot = match Candle::new(
            opens[idx],
            highs[idx],
            lows[idx],
            closes[idx],
            volumes[idx],
            timestamps[idx],
        ) {
            Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
            Err(_) => f64::NAN,
        };
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_three_outside_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_three_outside_warmup_period(handle: *mut ThreeOutside) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_three_outside_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_three_outside_is_ready(handle: *mut ThreeOutside) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_three_outside_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_three_outside_name(handle: *mut ThreeOutside) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_three_outside_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_three_outside_reset(handle: *mut ThreeOutside) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_three_outside_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_three_outside_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_three_outside_free(handle: *mut ThreeOutside) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `ThreeSoldiersOrCrows` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_three_soldiers_or_crows_free`.
#[no_mangle]
pub extern "C" fn wickra_three_soldiers_or_crows_new() -> *mut ThreeSoldiersOrCrows {
    Box::into_raw(Box::new(ThreeSoldiersOrCrows::new()))
}

/// Feed one OHLCV candle; returns the output, or `NaN` during warmup / on a `NULL`
/// handle / if the candle is invalid (e.g. `high < low`).
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_three_soldiers_or_crows_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_three_soldiers_or_crows_update(
    handle: *mut ThreeSoldiersOrCrows,
    open: f64,
    high: f64,
    low: f64,
    close: f64,
    volume: f64,
    timestamp: i64,
) -> f64 {
    let Some(ind) = handle.as_mut() else {
        return f64::NAN;
    };
    match Candle::new(open, high, low, close, volume, timestamp) {
        Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
        Err(_) => f64::NAN,
    }
}

/// Run over the OHLCV series (`open/high/low/close/volume/timestamp[0..n]`) into
/// `out[0..n]` (`NaN` at warmup or on an invalid candle).
///
/// # Safety
/// `handle` valid (from `wickra_three_soldiers_or_crows_new`, not freed); every input pointer and `out`
/// cover `n` elements.
#[no_mangle]
pub unsafe extern "C" fn wickra_three_soldiers_or_crows_batch(
    handle: *mut ThreeSoldiersOrCrows,
    open: *const f64,
    high: *const f64,
    low: *const f64,
    close: *const f64,
    volume: *const f64,
    timestamp: *const i64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null()
        || open.is_null()
        || high.is_null()
        || low.is_null()
        || close.is_null()
        || volume.is_null()
        || timestamp.is_null()
        || out.is_null()
    {
        return;
    }
    let ind = &mut *handle;
    let opens = slice::from_raw_parts(open, n);
    let highs = slice::from_raw_parts(high, n);
    let lows = slice::from_raw_parts(low, n);
    let closes = slice::from_raw_parts(close, n);
    let volumes = slice::from_raw_parts(volume, n);
    let timestamps = slice::from_raw_parts(timestamp, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (idx, slot) in outputs.iter_mut().enumerate() {
        *slot = match Candle::new(
            opens[idx],
            highs[idx],
            lows[idx],
            closes[idx],
            volumes[idx],
            timestamps[idx],
        ) {
            Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
            Err(_) => f64::NAN,
        };
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_three_soldiers_or_crows_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_three_soldiers_or_crows_warmup_period(
    handle: *mut ThreeSoldiersOrCrows,
) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_three_soldiers_or_crows_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_three_soldiers_or_crows_is_ready(
    handle: *mut ThreeSoldiersOrCrows,
) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_three_soldiers_or_crows_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_three_soldiers_or_crows_name(
    handle: *mut ThreeSoldiersOrCrows,
) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_three_soldiers_or_crows_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_three_soldiers_or_crows_reset(handle: *mut ThreeSoldiersOrCrows) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_three_soldiers_or_crows_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_three_soldiers_or_crows_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_three_soldiers_or_crows_free(handle: *mut ThreeSoldiersOrCrows) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `ThreeStarsInSouth` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_three_stars_in_south_free`.
#[no_mangle]
pub extern "C" fn wickra_three_stars_in_south_new() -> *mut ThreeStarsInSouth {
    Box::into_raw(Box::new(ThreeStarsInSouth::new()))
}

/// Feed one OHLCV candle; returns the output, or `NaN` during warmup / on a `NULL`
/// handle / if the candle is invalid (e.g. `high < low`).
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_three_stars_in_south_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_three_stars_in_south_update(
    handle: *mut ThreeStarsInSouth,
    open: f64,
    high: f64,
    low: f64,
    close: f64,
    volume: f64,
    timestamp: i64,
) -> f64 {
    let Some(ind) = handle.as_mut() else {
        return f64::NAN;
    };
    match Candle::new(open, high, low, close, volume, timestamp) {
        Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
        Err(_) => f64::NAN,
    }
}

/// Run over the OHLCV series (`open/high/low/close/volume/timestamp[0..n]`) into
/// `out[0..n]` (`NaN` at warmup or on an invalid candle).
///
/// # Safety
/// `handle` valid (from `wickra_three_stars_in_south_new`, not freed); every input pointer and `out`
/// cover `n` elements.
#[no_mangle]
pub unsafe extern "C" fn wickra_three_stars_in_south_batch(
    handle: *mut ThreeStarsInSouth,
    open: *const f64,
    high: *const f64,
    low: *const f64,
    close: *const f64,
    volume: *const f64,
    timestamp: *const i64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null()
        || open.is_null()
        || high.is_null()
        || low.is_null()
        || close.is_null()
        || volume.is_null()
        || timestamp.is_null()
        || out.is_null()
    {
        return;
    }
    let ind = &mut *handle;
    let opens = slice::from_raw_parts(open, n);
    let highs = slice::from_raw_parts(high, n);
    let lows = slice::from_raw_parts(low, n);
    let closes = slice::from_raw_parts(close, n);
    let volumes = slice::from_raw_parts(volume, n);
    let timestamps = slice::from_raw_parts(timestamp, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (idx, slot) in outputs.iter_mut().enumerate() {
        *slot = match Candle::new(
            opens[idx],
            highs[idx],
            lows[idx],
            closes[idx],
            volumes[idx],
            timestamps[idx],
        ) {
            Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
            Err(_) => f64::NAN,
        };
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_three_stars_in_south_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_three_stars_in_south_warmup_period(
    handle: *mut ThreeStarsInSouth,
) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_three_stars_in_south_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_three_stars_in_south_is_ready(
    handle: *mut ThreeStarsInSouth,
) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_three_stars_in_south_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_three_stars_in_south_name(
    handle: *mut ThreeStarsInSouth,
) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_three_stars_in_south_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_three_stars_in_south_reset(handle: *mut ThreeStarsInSouth) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_three_stars_in_south_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_three_stars_in_south_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_three_stars_in_south_free(handle: *mut ThreeStarsInSouth) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `Thrusting` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_thrusting_free`.
#[no_mangle]
pub extern "C" fn wickra_thrusting_new() -> *mut Thrusting {
    Box::into_raw(Box::new(Thrusting::new()))
}

/// Feed one OHLCV candle; returns the output, or `NaN` during warmup / on a `NULL`
/// handle / if the candle is invalid (e.g. `high < low`).
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_thrusting_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_thrusting_update(
    handle: *mut Thrusting,
    open: f64,
    high: f64,
    low: f64,
    close: f64,
    volume: f64,
    timestamp: i64,
) -> f64 {
    let Some(ind) = handle.as_mut() else {
        return f64::NAN;
    };
    match Candle::new(open, high, low, close, volume, timestamp) {
        Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
        Err(_) => f64::NAN,
    }
}

/// Run over the OHLCV series (`open/high/low/close/volume/timestamp[0..n]`) into
/// `out[0..n]` (`NaN` at warmup or on an invalid candle).
///
/// # Safety
/// `handle` valid (from `wickra_thrusting_new`, not freed); every input pointer and `out`
/// cover `n` elements.
#[no_mangle]
pub unsafe extern "C" fn wickra_thrusting_batch(
    handle: *mut Thrusting,
    open: *const f64,
    high: *const f64,
    low: *const f64,
    close: *const f64,
    volume: *const f64,
    timestamp: *const i64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null()
        || open.is_null()
        || high.is_null()
        || low.is_null()
        || close.is_null()
        || volume.is_null()
        || timestamp.is_null()
        || out.is_null()
    {
        return;
    }
    let ind = &mut *handle;
    let opens = slice::from_raw_parts(open, n);
    let highs = slice::from_raw_parts(high, n);
    let lows = slice::from_raw_parts(low, n);
    let closes = slice::from_raw_parts(close, n);
    let volumes = slice::from_raw_parts(volume, n);
    let timestamps = slice::from_raw_parts(timestamp, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (idx, slot) in outputs.iter_mut().enumerate() {
        *slot = match Candle::new(
            opens[idx],
            highs[idx],
            lows[idx],
            closes[idx],
            volumes[idx],
            timestamps[idx],
        ) {
            Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
            Err(_) => f64::NAN,
        };
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_thrusting_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_thrusting_warmup_period(handle: *mut Thrusting) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_thrusting_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_thrusting_is_ready(handle: *mut Thrusting) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_thrusting_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_thrusting_name(handle: *mut Thrusting) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_thrusting_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_thrusting_reset(handle: *mut Thrusting) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_thrusting_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_thrusting_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_thrusting_free(handle: *mut Thrusting) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `TimeBasedStop` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_time_based_stop_free`.
#[no_mangle]
pub extern "C" fn wickra_time_based_stop_new(max_bars: usize) -> *mut TimeBasedStop {
    match TimeBasedStop::new(max_bars) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one OHLCV candle; returns the output, or `NaN` during warmup / on a `NULL`
/// handle / if the candle is invalid (e.g. `high < low`).
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_time_based_stop_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_time_based_stop_update(
    handle: *mut TimeBasedStop,
    open: f64,
    high: f64,
    low: f64,
    close: f64,
    volume: f64,
    timestamp: i64,
) -> f64 {
    let Some(ind) = handle.as_mut() else {
        return f64::NAN;
    };
    match Candle::new(open, high, low, close, volume, timestamp) {
        Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
        Err(_) => f64::NAN,
    }
}

/// Run over the OHLCV series (`open/high/low/close/volume/timestamp[0..n]`) into
/// `out[0..n]` (`NaN` at warmup or on an invalid candle).
///
/// # Safety
/// `handle` valid (from `wickra_time_based_stop_new`, not freed); every input pointer and `out`
/// cover `n` elements.
#[no_mangle]
pub unsafe extern "C" fn wickra_time_based_stop_batch(
    handle: *mut TimeBasedStop,
    open: *const f64,
    high: *const f64,
    low: *const f64,
    close: *const f64,
    volume: *const f64,
    timestamp: *const i64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null()
        || open.is_null()
        || high.is_null()
        || low.is_null()
        || close.is_null()
        || volume.is_null()
        || timestamp.is_null()
        || out.is_null()
    {
        return;
    }
    let ind = &mut *handle;
    let opens = slice::from_raw_parts(open, n);
    let highs = slice::from_raw_parts(high, n);
    let lows = slice::from_raw_parts(low, n);
    let closes = slice::from_raw_parts(close, n);
    let volumes = slice::from_raw_parts(volume, n);
    let timestamps = slice::from_raw_parts(timestamp, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (idx, slot) in outputs.iter_mut().enumerate() {
        *slot = match Candle::new(
            opens[idx],
            highs[idx],
            lows[idx],
            closes[idx],
            volumes[idx],
            timestamps[idx],
        ) {
            Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
            Err(_) => f64::NAN,
        };
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_time_based_stop_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_time_based_stop_warmup_period(handle: *mut TimeBasedStop) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_time_based_stop_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_time_based_stop_is_ready(handle: *mut TimeBasedStop) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_time_based_stop_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_time_based_stop_name(handle: *mut TimeBasedStop) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_time_based_stop_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_time_based_stop_reset(handle: *mut TimeBasedStop) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_time_based_stop_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_time_based_stop_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_time_based_stop_free(handle: *mut TimeBasedStop) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `TowerTopBottom` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_tower_top_bottom_free`.
#[no_mangle]
pub extern "C" fn wickra_tower_top_bottom_new() -> *mut TowerTopBottom {
    Box::into_raw(Box::new(TowerTopBottom::new()))
}

/// Feed one OHLCV candle; returns the output, or `NaN` during warmup / on a `NULL`
/// handle / if the candle is invalid (e.g. `high < low`).
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_tower_top_bottom_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_tower_top_bottom_update(
    handle: *mut TowerTopBottom,
    open: f64,
    high: f64,
    low: f64,
    close: f64,
    volume: f64,
    timestamp: i64,
) -> f64 {
    let Some(ind) = handle.as_mut() else {
        return f64::NAN;
    };
    match Candle::new(open, high, low, close, volume, timestamp) {
        Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
        Err(_) => f64::NAN,
    }
}

/// Run over the OHLCV series (`open/high/low/close/volume/timestamp[0..n]`) into
/// `out[0..n]` (`NaN` at warmup or on an invalid candle).
///
/// # Safety
/// `handle` valid (from `wickra_tower_top_bottom_new`, not freed); every input pointer and `out`
/// cover `n` elements.
#[no_mangle]
pub unsafe extern "C" fn wickra_tower_top_bottom_batch(
    handle: *mut TowerTopBottom,
    open: *const f64,
    high: *const f64,
    low: *const f64,
    close: *const f64,
    volume: *const f64,
    timestamp: *const i64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null()
        || open.is_null()
        || high.is_null()
        || low.is_null()
        || close.is_null()
        || volume.is_null()
        || timestamp.is_null()
        || out.is_null()
    {
        return;
    }
    let ind = &mut *handle;
    let opens = slice::from_raw_parts(open, n);
    let highs = slice::from_raw_parts(high, n);
    let lows = slice::from_raw_parts(low, n);
    let closes = slice::from_raw_parts(close, n);
    let volumes = slice::from_raw_parts(volume, n);
    let timestamps = slice::from_raw_parts(timestamp, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (idx, slot) in outputs.iter_mut().enumerate() {
        *slot = match Candle::new(
            opens[idx],
            highs[idx],
            lows[idx],
            closes[idx],
            volumes[idx],
            timestamps[idx],
        ) {
            Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
            Err(_) => f64::NAN,
        };
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_tower_top_bottom_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_tower_top_bottom_warmup_period(
    handle: *mut TowerTopBottom,
) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_tower_top_bottom_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_tower_top_bottom_is_ready(handle: *mut TowerTopBottom) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_tower_top_bottom_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_tower_top_bottom_name(
    handle: *mut TowerTopBottom,
) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_tower_top_bottom_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_tower_top_bottom_reset(handle: *mut TowerTopBottom) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_tower_top_bottom_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_tower_top_bottom_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_tower_top_bottom_free(handle: *mut TowerTopBottom) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `TradeVolumeIndex` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_trade_volume_index_free`.
#[no_mangle]
pub extern "C" fn wickra_trade_volume_index_new(min_tick: f64) -> *mut TradeVolumeIndex {
    match TradeVolumeIndex::new(min_tick) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one OHLCV candle; returns the output, or `NaN` during warmup / on a `NULL`
/// handle / if the candle is invalid (e.g. `high < low`).
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_trade_volume_index_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_trade_volume_index_update(
    handle: *mut TradeVolumeIndex,
    open: f64,
    high: f64,
    low: f64,
    close: f64,
    volume: f64,
    timestamp: i64,
) -> f64 {
    let Some(ind) = handle.as_mut() else {
        return f64::NAN;
    };
    match Candle::new(open, high, low, close, volume, timestamp) {
        Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
        Err(_) => f64::NAN,
    }
}

/// Run over the OHLCV series (`open/high/low/close/volume/timestamp[0..n]`) into
/// `out[0..n]` (`NaN` at warmup or on an invalid candle).
///
/// # Safety
/// `handle` valid (from `wickra_trade_volume_index_new`, not freed); every input pointer and `out`
/// cover `n` elements.
#[no_mangle]
pub unsafe extern "C" fn wickra_trade_volume_index_batch(
    handle: *mut TradeVolumeIndex,
    open: *const f64,
    high: *const f64,
    low: *const f64,
    close: *const f64,
    volume: *const f64,
    timestamp: *const i64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null()
        || open.is_null()
        || high.is_null()
        || low.is_null()
        || close.is_null()
        || volume.is_null()
        || timestamp.is_null()
        || out.is_null()
    {
        return;
    }
    let ind = &mut *handle;
    let opens = slice::from_raw_parts(open, n);
    let highs = slice::from_raw_parts(high, n);
    let lows = slice::from_raw_parts(low, n);
    let closes = slice::from_raw_parts(close, n);
    let volumes = slice::from_raw_parts(volume, n);
    let timestamps = slice::from_raw_parts(timestamp, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (idx, slot) in outputs.iter_mut().enumerate() {
        *slot = match Candle::new(
            opens[idx],
            highs[idx],
            lows[idx],
            closes[idx],
            volumes[idx],
            timestamps[idx],
        ) {
            Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
            Err(_) => f64::NAN,
        };
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_trade_volume_index_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_trade_volume_index_warmup_period(
    handle: *mut TradeVolumeIndex,
) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_trade_volume_index_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_trade_volume_index_is_ready(handle: *mut TradeVolumeIndex) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_trade_volume_index_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_trade_volume_index_name(
    handle: *mut TradeVolumeIndex,
) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_trade_volume_index_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_trade_volume_index_reset(handle: *mut TradeVolumeIndex) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_trade_volume_index_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_trade_volume_index_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_trade_volume_index_free(handle: *mut TradeVolumeIndex) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `Triangle` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_triangle_free`.
#[no_mangle]
pub extern "C" fn wickra_triangle_new() -> *mut Triangle {
    Box::into_raw(Box::new(Triangle::new()))
}

/// Feed one OHLCV candle; returns the output, or `NaN` during warmup / on a `NULL`
/// handle / if the candle is invalid (e.g. `high < low`).
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_triangle_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_triangle_update(
    handle: *mut Triangle,
    open: f64,
    high: f64,
    low: f64,
    close: f64,
    volume: f64,
    timestamp: i64,
) -> f64 {
    let Some(ind) = handle.as_mut() else {
        return f64::NAN;
    };
    match Candle::new(open, high, low, close, volume, timestamp) {
        Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
        Err(_) => f64::NAN,
    }
}

/// Run over the OHLCV series (`open/high/low/close/volume/timestamp[0..n]`) into
/// `out[0..n]` (`NaN` at warmup or on an invalid candle).
///
/// # Safety
/// `handle` valid (from `wickra_triangle_new`, not freed); every input pointer and `out`
/// cover `n` elements.
#[no_mangle]
pub unsafe extern "C" fn wickra_triangle_batch(
    handle: *mut Triangle,
    open: *const f64,
    high: *const f64,
    low: *const f64,
    close: *const f64,
    volume: *const f64,
    timestamp: *const i64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null()
        || open.is_null()
        || high.is_null()
        || low.is_null()
        || close.is_null()
        || volume.is_null()
        || timestamp.is_null()
        || out.is_null()
    {
        return;
    }
    let ind = &mut *handle;
    let opens = slice::from_raw_parts(open, n);
    let highs = slice::from_raw_parts(high, n);
    let lows = slice::from_raw_parts(low, n);
    let closes = slice::from_raw_parts(close, n);
    let volumes = slice::from_raw_parts(volume, n);
    let timestamps = slice::from_raw_parts(timestamp, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (idx, slot) in outputs.iter_mut().enumerate() {
        *slot = match Candle::new(
            opens[idx],
            highs[idx],
            lows[idx],
            closes[idx],
            volumes[idx],
            timestamps[idx],
        ) {
            Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
            Err(_) => f64::NAN,
        };
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_triangle_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_triangle_warmup_period(handle: *mut Triangle) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_triangle_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_triangle_is_ready(handle: *mut Triangle) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_triangle_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_triangle_name(handle: *mut Triangle) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_triangle_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_triangle_reset(handle: *mut Triangle) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_triangle_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_triangle_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_triangle_free(handle: *mut Triangle) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `TripleTopBottom` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_triple_top_bottom_free`.
#[no_mangle]
pub extern "C" fn wickra_triple_top_bottom_new() -> *mut TripleTopBottom {
    Box::into_raw(Box::new(TripleTopBottom::new()))
}

/// Feed one OHLCV candle; returns the output, or `NaN` during warmup / on a `NULL`
/// handle / if the candle is invalid (e.g. `high < low`).
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_triple_top_bottom_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_triple_top_bottom_update(
    handle: *mut TripleTopBottom,
    open: f64,
    high: f64,
    low: f64,
    close: f64,
    volume: f64,
    timestamp: i64,
) -> f64 {
    let Some(ind) = handle.as_mut() else {
        return f64::NAN;
    };
    match Candle::new(open, high, low, close, volume, timestamp) {
        Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
        Err(_) => f64::NAN,
    }
}

/// Run over the OHLCV series (`open/high/low/close/volume/timestamp[0..n]`) into
/// `out[0..n]` (`NaN` at warmup or on an invalid candle).
///
/// # Safety
/// `handle` valid (from `wickra_triple_top_bottom_new`, not freed); every input pointer and `out`
/// cover `n` elements.
#[no_mangle]
pub unsafe extern "C" fn wickra_triple_top_bottom_batch(
    handle: *mut TripleTopBottom,
    open: *const f64,
    high: *const f64,
    low: *const f64,
    close: *const f64,
    volume: *const f64,
    timestamp: *const i64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null()
        || open.is_null()
        || high.is_null()
        || low.is_null()
        || close.is_null()
        || volume.is_null()
        || timestamp.is_null()
        || out.is_null()
    {
        return;
    }
    let ind = &mut *handle;
    let opens = slice::from_raw_parts(open, n);
    let highs = slice::from_raw_parts(high, n);
    let lows = slice::from_raw_parts(low, n);
    let closes = slice::from_raw_parts(close, n);
    let volumes = slice::from_raw_parts(volume, n);
    let timestamps = slice::from_raw_parts(timestamp, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (idx, slot) in outputs.iter_mut().enumerate() {
        *slot = match Candle::new(
            opens[idx],
            highs[idx],
            lows[idx],
            closes[idx],
            volumes[idx],
            timestamps[idx],
        ) {
            Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
            Err(_) => f64::NAN,
        };
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_triple_top_bottom_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_triple_top_bottom_warmup_period(
    handle: *mut TripleTopBottom,
) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_triple_top_bottom_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_triple_top_bottom_is_ready(handle: *mut TripleTopBottom) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_triple_top_bottom_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_triple_top_bottom_name(
    handle: *mut TripleTopBottom,
) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_triple_top_bottom_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_triple_top_bottom_reset(handle: *mut TripleTopBottom) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_triple_top_bottom_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_triple_top_bottom_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_triple_top_bottom_free(handle: *mut TripleTopBottom) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `Tristar` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_tristar_free`.
#[no_mangle]
pub extern "C" fn wickra_tristar_new() -> *mut Tristar {
    Box::into_raw(Box::new(Tristar::new()))
}

/// Feed one OHLCV candle; returns the output, or `NaN` during warmup / on a `NULL`
/// handle / if the candle is invalid (e.g. `high < low`).
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_tristar_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_tristar_update(
    handle: *mut Tristar,
    open: f64,
    high: f64,
    low: f64,
    close: f64,
    volume: f64,
    timestamp: i64,
) -> f64 {
    let Some(ind) = handle.as_mut() else {
        return f64::NAN;
    };
    match Candle::new(open, high, low, close, volume, timestamp) {
        Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
        Err(_) => f64::NAN,
    }
}

/// Run over the OHLCV series (`open/high/low/close/volume/timestamp[0..n]`) into
/// `out[0..n]` (`NaN` at warmup or on an invalid candle).
///
/// # Safety
/// `handle` valid (from `wickra_tristar_new`, not freed); every input pointer and `out`
/// cover `n` elements.
#[no_mangle]
pub unsafe extern "C" fn wickra_tristar_batch(
    handle: *mut Tristar,
    open: *const f64,
    high: *const f64,
    low: *const f64,
    close: *const f64,
    volume: *const f64,
    timestamp: *const i64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null()
        || open.is_null()
        || high.is_null()
        || low.is_null()
        || close.is_null()
        || volume.is_null()
        || timestamp.is_null()
        || out.is_null()
    {
        return;
    }
    let ind = &mut *handle;
    let opens = slice::from_raw_parts(open, n);
    let highs = slice::from_raw_parts(high, n);
    let lows = slice::from_raw_parts(low, n);
    let closes = slice::from_raw_parts(close, n);
    let volumes = slice::from_raw_parts(volume, n);
    let timestamps = slice::from_raw_parts(timestamp, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (idx, slot) in outputs.iter_mut().enumerate() {
        *slot = match Candle::new(
            opens[idx],
            highs[idx],
            lows[idx],
            closes[idx],
            volumes[idx],
            timestamps[idx],
        ) {
            Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
            Err(_) => f64::NAN,
        };
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_tristar_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_tristar_warmup_period(handle: *mut Tristar) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_tristar_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_tristar_is_ready(handle: *mut Tristar) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_tristar_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_tristar_name(handle: *mut Tristar) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_tristar_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_tristar_reset(handle: *mut Tristar) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_tristar_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_tristar_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_tristar_free(handle: *mut Tristar) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `TrueRange` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_true_range_free`.
#[no_mangle]
pub extern "C" fn wickra_true_range_new() -> *mut TrueRange {
    Box::into_raw(Box::new(TrueRange::new()))
}

/// Feed one OHLCV candle; returns the output, or `NaN` during warmup / on a `NULL`
/// handle / if the candle is invalid (e.g. `high < low`).
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_true_range_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_true_range_update(
    handle: *mut TrueRange,
    open: f64,
    high: f64,
    low: f64,
    close: f64,
    volume: f64,
    timestamp: i64,
) -> f64 {
    let Some(ind) = handle.as_mut() else {
        return f64::NAN;
    };
    match Candle::new(open, high, low, close, volume, timestamp) {
        Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
        Err(_) => f64::NAN,
    }
}

/// Run over the OHLCV series (`open/high/low/close/volume/timestamp[0..n]`) into
/// `out[0..n]` (`NaN` at warmup or on an invalid candle).
///
/// # Safety
/// `handle` valid (from `wickra_true_range_new`, not freed); every input pointer and `out`
/// cover `n` elements.
#[no_mangle]
pub unsafe extern "C" fn wickra_true_range_batch(
    handle: *mut TrueRange,
    open: *const f64,
    high: *const f64,
    low: *const f64,
    close: *const f64,
    volume: *const f64,
    timestamp: *const i64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null()
        || open.is_null()
        || high.is_null()
        || low.is_null()
        || close.is_null()
        || volume.is_null()
        || timestamp.is_null()
        || out.is_null()
    {
        return;
    }
    let ind = &mut *handle;
    let opens = slice::from_raw_parts(open, n);
    let highs = slice::from_raw_parts(high, n);
    let lows = slice::from_raw_parts(low, n);
    let closes = slice::from_raw_parts(close, n);
    let volumes = slice::from_raw_parts(volume, n);
    let timestamps = slice::from_raw_parts(timestamp, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (idx, slot) in outputs.iter_mut().enumerate() {
        *slot = match Candle::new(
            opens[idx],
            highs[idx],
            lows[idx],
            closes[idx],
            volumes[idx],
            timestamps[idx],
        ) {
            Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
            Err(_) => f64::NAN,
        };
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_true_range_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_true_range_warmup_period(handle: *mut TrueRange) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_true_range_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_true_range_is_ready(handle: *mut TrueRange) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_true_range_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_true_range_name(handle: *mut TrueRange) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_true_range_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_true_range_reset(handle: *mut TrueRange) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_true_range_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_true_range_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_true_range_free(handle: *mut TrueRange) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `Tsv` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_tsv_free`.
#[no_mangle]
pub extern "C" fn wickra_tsv_new(period: usize) -> *mut Tsv {
    match Tsv::new(period) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one OHLCV candle; returns the output, or `NaN` during warmup / on a `NULL`
/// handle / if the candle is invalid (e.g. `high < low`).
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_tsv_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_tsv_update(
    handle: *mut Tsv,
    open: f64,
    high: f64,
    low: f64,
    close: f64,
    volume: f64,
    timestamp: i64,
) -> f64 {
    let Some(ind) = handle.as_mut() else {
        return f64::NAN;
    };
    match Candle::new(open, high, low, close, volume, timestamp) {
        Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
        Err(_) => f64::NAN,
    }
}

/// Run over the OHLCV series (`open/high/low/close/volume/timestamp[0..n]`) into
/// `out[0..n]` (`NaN` at warmup or on an invalid candle).
///
/// # Safety
/// `handle` valid (from `wickra_tsv_new`, not freed); every input pointer and `out`
/// cover `n` elements.
#[no_mangle]
pub unsafe extern "C" fn wickra_tsv_batch(
    handle: *mut Tsv,
    open: *const f64,
    high: *const f64,
    low: *const f64,
    close: *const f64,
    volume: *const f64,
    timestamp: *const i64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null()
        || open.is_null()
        || high.is_null()
        || low.is_null()
        || close.is_null()
        || volume.is_null()
        || timestamp.is_null()
        || out.is_null()
    {
        return;
    }
    let ind = &mut *handle;
    let opens = slice::from_raw_parts(open, n);
    let highs = slice::from_raw_parts(high, n);
    let lows = slice::from_raw_parts(low, n);
    let closes = slice::from_raw_parts(close, n);
    let volumes = slice::from_raw_parts(volume, n);
    let timestamps = slice::from_raw_parts(timestamp, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (idx, slot) in outputs.iter_mut().enumerate() {
        *slot = match Candle::new(
            opens[idx],
            highs[idx],
            lows[idx],
            closes[idx],
            volumes[idx],
            timestamps[idx],
        ) {
            Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
            Err(_) => f64::NAN,
        };
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_tsv_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_tsv_warmup_period(handle: *mut Tsv) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_tsv_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_tsv_is_ready(handle: *mut Tsv) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_tsv_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_tsv_name(handle: *mut Tsv) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_tsv_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_tsv_reset(handle: *mut Tsv) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_tsv_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_tsv_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_tsv_free(handle: *mut Tsv) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `TtmTrend` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_ttm_trend_free`.
#[no_mangle]
pub extern "C" fn wickra_ttm_trend_new(period: usize) -> *mut TtmTrend {
    match TtmTrend::new(period) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one OHLCV candle; returns the output, or `NaN` during warmup / on a `NULL`
/// handle / if the candle is invalid (e.g. `high < low`).
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_ttm_trend_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_ttm_trend_update(
    handle: *mut TtmTrend,
    open: f64,
    high: f64,
    low: f64,
    close: f64,
    volume: f64,
    timestamp: i64,
) -> f64 {
    let Some(ind) = handle.as_mut() else {
        return f64::NAN;
    };
    match Candle::new(open, high, low, close, volume, timestamp) {
        Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
        Err(_) => f64::NAN,
    }
}

/// Run over the OHLCV series (`open/high/low/close/volume/timestamp[0..n]`) into
/// `out[0..n]` (`NaN` at warmup or on an invalid candle).
///
/// # Safety
/// `handle` valid (from `wickra_ttm_trend_new`, not freed); every input pointer and `out`
/// cover `n` elements.
#[no_mangle]
pub unsafe extern "C" fn wickra_ttm_trend_batch(
    handle: *mut TtmTrend,
    open: *const f64,
    high: *const f64,
    low: *const f64,
    close: *const f64,
    volume: *const f64,
    timestamp: *const i64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null()
        || open.is_null()
        || high.is_null()
        || low.is_null()
        || close.is_null()
        || volume.is_null()
        || timestamp.is_null()
        || out.is_null()
    {
        return;
    }
    let ind = &mut *handle;
    let opens = slice::from_raw_parts(open, n);
    let highs = slice::from_raw_parts(high, n);
    let lows = slice::from_raw_parts(low, n);
    let closes = slice::from_raw_parts(close, n);
    let volumes = slice::from_raw_parts(volume, n);
    let timestamps = slice::from_raw_parts(timestamp, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (idx, slot) in outputs.iter_mut().enumerate() {
        *slot = match Candle::new(
            opens[idx],
            highs[idx],
            lows[idx],
            closes[idx],
            volumes[idx],
            timestamps[idx],
        ) {
            Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
            Err(_) => f64::NAN,
        };
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_ttm_trend_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_ttm_trend_warmup_period(handle: *mut TtmTrend) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_ttm_trend_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_ttm_trend_is_ready(handle: *mut TtmTrend) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_ttm_trend_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_ttm_trend_name(handle: *mut TtmTrend) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_ttm_trend_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_ttm_trend_reset(handle: *mut TtmTrend) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_ttm_trend_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_ttm_trend_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_ttm_trend_free(handle: *mut TtmTrend) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `TurnOfMonth` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_turn_of_month_free`.
#[no_mangle]
pub extern "C" fn wickra_turn_of_month_new(
    n_first: u32,
    n_last: u32,
    utc_offset_minutes: i32,
) -> *mut TurnOfMonth {
    match TurnOfMonth::new(n_first, n_last, utc_offset_minutes) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one OHLCV candle; returns the output, or `NaN` during warmup / on a `NULL`
/// handle / if the candle is invalid (e.g. `high < low`).
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_turn_of_month_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_turn_of_month_update(
    handle: *mut TurnOfMonth,
    open: f64,
    high: f64,
    low: f64,
    close: f64,
    volume: f64,
    timestamp: i64,
) -> f64 {
    let Some(ind) = handle.as_mut() else {
        return f64::NAN;
    };
    match Candle::new(open, high, low, close, volume, timestamp) {
        Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
        Err(_) => f64::NAN,
    }
}

/// Run over the OHLCV series (`open/high/low/close/volume/timestamp[0..n]`) into
/// `out[0..n]` (`NaN` at warmup or on an invalid candle).
///
/// # Safety
/// `handle` valid (from `wickra_turn_of_month_new`, not freed); every input pointer and `out`
/// cover `n` elements.
#[no_mangle]
pub unsafe extern "C" fn wickra_turn_of_month_batch(
    handle: *mut TurnOfMonth,
    open: *const f64,
    high: *const f64,
    low: *const f64,
    close: *const f64,
    volume: *const f64,
    timestamp: *const i64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null()
        || open.is_null()
        || high.is_null()
        || low.is_null()
        || close.is_null()
        || volume.is_null()
        || timestamp.is_null()
        || out.is_null()
    {
        return;
    }
    let ind = &mut *handle;
    let opens = slice::from_raw_parts(open, n);
    let highs = slice::from_raw_parts(high, n);
    let lows = slice::from_raw_parts(low, n);
    let closes = slice::from_raw_parts(close, n);
    let volumes = slice::from_raw_parts(volume, n);
    let timestamps = slice::from_raw_parts(timestamp, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (idx, slot) in outputs.iter_mut().enumerate() {
        *slot = match Candle::new(
            opens[idx],
            highs[idx],
            lows[idx],
            closes[idx],
            volumes[idx],
            timestamps[idx],
        ) {
            Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
            Err(_) => f64::NAN,
        };
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_turn_of_month_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_turn_of_month_warmup_period(handle: *mut TurnOfMonth) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_turn_of_month_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_turn_of_month_is_ready(handle: *mut TurnOfMonth) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_turn_of_month_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_turn_of_month_name(handle: *mut TurnOfMonth) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_turn_of_month_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_turn_of_month_reset(handle: *mut TurnOfMonth) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_turn_of_month_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_turn_of_month_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_turn_of_month_free(handle: *mut TurnOfMonth) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `Tweezer` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_tweezer_free`.
#[no_mangle]
pub extern "C" fn wickra_tweezer_new() -> *mut Tweezer {
    Box::into_raw(Box::new(Tweezer::new()))
}

/// Feed one OHLCV candle; returns the output, or `NaN` during warmup / on a `NULL`
/// handle / if the candle is invalid (e.g. `high < low`).
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_tweezer_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_tweezer_update(
    handle: *mut Tweezer,
    open: f64,
    high: f64,
    low: f64,
    close: f64,
    volume: f64,
    timestamp: i64,
) -> f64 {
    let Some(ind) = handle.as_mut() else {
        return f64::NAN;
    };
    match Candle::new(open, high, low, close, volume, timestamp) {
        Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
        Err(_) => f64::NAN,
    }
}

/// Run over the OHLCV series (`open/high/low/close/volume/timestamp[0..n]`) into
/// `out[0..n]` (`NaN` at warmup or on an invalid candle).
///
/// # Safety
/// `handle` valid (from `wickra_tweezer_new`, not freed); every input pointer and `out`
/// cover `n` elements.
#[no_mangle]
pub unsafe extern "C" fn wickra_tweezer_batch(
    handle: *mut Tweezer,
    open: *const f64,
    high: *const f64,
    low: *const f64,
    close: *const f64,
    volume: *const f64,
    timestamp: *const i64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null()
        || open.is_null()
        || high.is_null()
        || low.is_null()
        || close.is_null()
        || volume.is_null()
        || timestamp.is_null()
        || out.is_null()
    {
        return;
    }
    let ind = &mut *handle;
    let opens = slice::from_raw_parts(open, n);
    let highs = slice::from_raw_parts(high, n);
    let lows = slice::from_raw_parts(low, n);
    let closes = slice::from_raw_parts(close, n);
    let volumes = slice::from_raw_parts(volume, n);
    let timestamps = slice::from_raw_parts(timestamp, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (idx, slot) in outputs.iter_mut().enumerate() {
        *slot = match Candle::new(
            opens[idx],
            highs[idx],
            lows[idx],
            closes[idx],
            volumes[idx],
            timestamps[idx],
        ) {
            Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
            Err(_) => f64::NAN,
        };
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_tweezer_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_tweezer_warmup_period(handle: *mut Tweezer) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_tweezer_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_tweezer_is_ready(handle: *mut Tweezer) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_tweezer_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_tweezer_name(handle: *mut Tweezer) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_tweezer_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_tweezer_reset(handle: *mut Tweezer) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_tweezer_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_tweezer_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_tweezer_free(handle: *mut Tweezer) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `TwiggsMoneyFlow` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_twiggs_money_flow_free`.
#[no_mangle]
pub extern "C" fn wickra_twiggs_money_flow_new(period: usize) -> *mut TwiggsMoneyFlow {
    match TwiggsMoneyFlow::new(period) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one OHLCV candle; returns the output, or `NaN` during warmup / on a `NULL`
/// handle / if the candle is invalid (e.g. `high < low`).
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_twiggs_money_flow_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_twiggs_money_flow_update(
    handle: *mut TwiggsMoneyFlow,
    open: f64,
    high: f64,
    low: f64,
    close: f64,
    volume: f64,
    timestamp: i64,
) -> f64 {
    let Some(ind) = handle.as_mut() else {
        return f64::NAN;
    };
    match Candle::new(open, high, low, close, volume, timestamp) {
        Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
        Err(_) => f64::NAN,
    }
}

/// Run over the OHLCV series (`open/high/low/close/volume/timestamp[0..n]`) into
/// `out[0..n]` (`NaN` at warmup or on an invalid candle).
///
/// # Safety
/// `handle` valid (from `wickra_twiggs_money_flow_new`, not freed); every input pointer and `out`
/// cover `n` elements.
#[no_mangle]
pub unsafe extern "C" fn wickra_twiggs_money_flow_batch(
    handle: *mut TwiggsMoneyFlow,
    open: *const f64,
    high: *const f64,
    low: *const f64,
    close: *const f64,
    volume: *const f64,
    timestamp: *const i64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null()
        || open.is_null()
        || high.is_null()
        || low.is_null()
        || close.is_null()
        || volume.is_null()
        || timestamp.is_null()
        || out.is_null()
    {
        return;
    }
    let ind = &mut *handle;
    let opens = slice::from_raw_parts(open, n);
    let highs = slice::from_raw_parts(high, n);
    let lows = slice::from_raw_parts(low, n);
    let closes = slice::from_raw_parts(close, n);
    let volumes = slice::from_raw_parts(volume, n);
    let timestamps = slice::from_raw_parts(timestamp, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (idx, slot) in outputs.iter_mut().enumerate() {
        *slot = match Candle::new(
            opens[idx],
            highs[idx],
            lows[idx],
            closes[idx],
            volumes[idx],
            timestamps[idx],
        ) {
            Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
            Err(_) => f64::NAN,
        };
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_twiggs_money_flow_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_twiggs_money_flow_warmup_period(
    handle: *mut TwiggsMoneyFlow,
) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_twiggs_money_flow_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_twiggs_money_flow_is_ready(handle: *mut TwiggsMoneyFlow) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_twiggs_money_flow_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_twiggs_money_flow_name(
    handle: *mut TwiggsMoneyFlow,
) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_twiggs_money_flow_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_twiggs_money_flow_reset(handle: *mut TwiggsMoneyFlow) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_twiggs_money_flow_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_twiggs_money_flow_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_twiggs_money_flow_free(handle: *mut TwiggsMoneyFlow) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `TwoCrows` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_two_crows_free`.
#[no_mangle]
pub extern "C" fn wickra_two_crows_new() -> *mut TwoCrows {
    Box::into_raw(Box::new(TwoCrows::new()))
}

/// Feed one OHLCV candle; returns the output, or `NaN` during warmup / on a `NULL`
/// handle / if the candle is invalid (e.g. `high < low`).
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_two_crows_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_two_crows_update(
    handle: *mut TwoCrows,
    open: f64,
    high: f64,
    low: f64,
    close: f64,
    volume: f64,
    timestamp: i64,
) -> f64 {
    let Some(ind) = handle.as_mut() else {
        return f64::NAN;
    };
    match Candle::new(open, high, low, close, volume, timestamp) {
        Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
        Err(_) => f64::NAN,
    }
}

/// Run over the OHLCV series (`open/high/low/close/volume/timestamp[0..n]`) into
/// `out[0..n]` (`NaN` at warmup or on an invalid candle).
///
/// # Safety
/// `handle` valid (from `wickra_two_crows_new`, not freed); every input pointer and `out`
/// cover `n` elements.
#[no_mangle]
pub unsafe extern "C" fn wickra_two_crows_batch(
    handle: *mut TwoCrows,
    open: *const f64,
    high: *const f64,
    low: *const f64,
    close: *const f64,
    volume: *const f64,
    timestamp: *const i64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null()
        || open.is_null()
        || high.is_null()
        || low.is_null()
        || close.is_null()
        || volume.is_null()
        || timestamp.is_null()
        || out.is_null()
    {
        return;
    }
    let ind = &mut *handle;
    let opens = slice::from_raw_parts(open, n);
    let highs = slice::from_raw_parts(high, n);
    let lows = slice::from_raw_parts(low, n);
    let closes = slice::from_raw_parts(close, n);
    let volumes = slice::from_raw_parts(volume, n);
    let timestamps = slice::from_raw_parts(timestamp, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (idx, slot) in outputs.iter_mut().enumerate() {
        *slot = match Candle::new(
            opens[idx],
            highs[idx],
            lows[idx],
            closes[idx],
            volumes[idx],
            timestamps[idx],
        ) {
            Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
            Err(_) => f64::NAN,
        };
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_two_crows_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_two_crows_warmup_period(handle: *mut TwoCrows) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_two_crows_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_two_crows_is_ready(handle: *mut TwoCrows) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_two_crows_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_two_crows_name(handle: *mut TwoCrows) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_two_crows_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_two_crows_reset(handle: *mut TwoCrows) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_two_crows_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_two_crows_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_two_crows_free(handle: *mut TwoCrows) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `TypicalPrice` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_typical_price_free`.
#[no_mangle]
pub extern "C" fn wickra_typical_price_new() -> *mut TypicalPrice {
    Box::into_raw(Box::new(TypicalPrice::new()))
}

/// Feed one OHLCV candle; returns the output, or `NaN` during warmup / on a `NULL`
/// handle / if the candle is invalid (e.g. `high < low`).
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_typical_price_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_typical_price_update(
    handle: *mut TypicalPrice,
    open: f64,
    high: f64,
    low: f64,
    close: f64,
    volume: f64,
    timestamp: i64,
) -> f64 {
    let Some(ind) = handle.as_mut() else {
        return f64::NAN;
    };
    match Candle::new(open, high, low, close, volume, timestamp) {
        Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
        Err(_) => f64::NAN,
    }
}

/// Run over the OHLCV series (`open/high/low/close/volume/timestamp[0..n]`) into
/// `out[0..n]` (`NaN` at warmup or on an invalid candle).
///
/// # Safety
/// `handle` valid (from `wickra_typical_price_new`, not freed); every input pointer and `out`
/// cover `n` elements.
#[no_mangle]
pub unsafe extern "C" fn wickra_typical_price_batch(
    handle: *mut TypicalPrice,
    open: *const f64,
    high: *const f64,
    low: *const f64,
    close: *const f64,
    volume: *const f64,
    timestamp: *const i64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null()
        || open.is_null()
        || high.is_null()
        || low.is_null()
        || close.is_null()
        || volume.is_null()
        || timestamp.is_null()
        || out.is_null()
    {
        return;
    }
    let ind = &mut *handle;
    let opens = slice::from_raw_parts(open, n);
    let highs = slice::from_raw_parts(high, n);
    let lows = slice::from_raw_parts(low, n);
    let closes = slice::from_raw_parts(close, n);
    let volumes = slice::from_raw_parts(volume, n);
    let timestamps = slice::from_raw_parts(timestamp, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (idx, slot) in outputs.iter_mut().enumerate() {
        *slot = match Candle::new(
            opens[idx],
            highs[idx],
            lows[idx],
            closes[idx],
            volumes[idx],
            timestamps[idx],
        ) {
            Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
            Err(_) => f64::NAN,
        };
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_typical_price_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_typical_price_warmup_period(handle: *mut TypicalPrice) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_typical_price_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_typical_price_is_ready(handle: *mut TypicalPrice) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_typical_price_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_typical_price_name(handle: *mut TypicalPrice) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_typical_price_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_typical_price_reset(handle: *mut TypicalPrice) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_typical_price_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_typical_price_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_typical_price_free(handle: *mut TypicalPrice) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `UltimateOscillator` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_ultimate_oscillator_free`.
#[no_mangle]
pub extern "C" fn wickra_ultimate_oscillator_new(
    short: usize,
    mid: usize,
    long: usize,
) -> *mut UltimateOscillator {
    match UltimateOscillator::new(short, mid, long) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one OHLCV candle; returns the output, or `NaN` during warmup / on a `NULL`
/// handle / if the candle is invalid (e.g. `high < low`).
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_ultimate_oscillator_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_ultimate_oscillator_update(
    handle: *mut UltimateOscillator,
    open: f64,
    high: f64,
    low: f64,
    close: f64,
    volume: f64,
    timestamp: i64,
) -> f64 {
    let Some(ind) = handle.as_mut() else {
        return f64::NAN;
    };
    match Candle::new(open, high, low, close, volume, timestamp) {
        Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
        Err(_) => f64::NAN,
    }
}

/// Run over the OHLCV series (`open/high/low/close/volume/timestamp[0..n]`) into
/// `out[0..n]` (`NaN` at warmup or on an invalid candle).
///
/// # Safety
/// `handle` valid (from `wickra_ultimate_oscillator_new`, not freed); every input pointer and `out`
/// cover `n` elements.
#[no_mangle]
pub unsafe extern "C" fn wickra_ultimate_oscillator_batch(
    handle: *mut UltimateOscillator,
    open: *const f64,
    high: *const f64,
    low: *const f64,
    close: *const f64,
    volume: *const f64,
    timestamp: *const i64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null()
        || open.is_null()
        || high.is_null()
        || low.is_null()
        || close.is_null()
        || volume.is_null()
        || timestamp.is_null()
        || out.is_null()
    {
        return;
    }
    let ind = &mut *handle;
    let opens = slice::from_raw_parts(open, n);
    let highs = slice::from_raw_parts(high, n);
    let lows = slice::from_raw_parts(low, n);
    let closes = slice::from_raw_parts(close, n);
    let volumes = slice::from_raw_parts(volume, n);
    let timestamps = slice::from_raw_parts(timestamp, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (idx, slot) in outputs.iter_mut().enumerate() {
        *slot = match Candle::new(
            opens[idx],
            highs[idx],
            lows[idx],
            closes[idx],
            volumes[idx],
            timestamps[idx],
        ) {
            Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
            Err(_) => f64::NAN,
        };
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_ultimate_oscillator_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_ultimate_oscillator_warmup_period(
    handle: *mut UltimateOscillator,
) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_ultimate_oscillator_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_ultimate_oscillator_is_ready(
    handle: *mut UltimateOscillator,
) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_ultimate_oscillator_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_ultimate_oscillator_name(
    handle: *mut UltimateOscillator,
) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_ultimate_oscillator_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_ultimate_oscillator_reset(handle: *mut UltimateOscillator) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_ultimate_oscillator_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_ultimate_oscillator_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_ultimate_oscillator_free(handle: *mut UltimateOscillator) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `UniqueThreeRiver` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_unique_three_river_free`.
#[no_mangle]
pub extern "C" fn wickra_unique_three_river_new() -> *mut UniqueThreeRiver {
    Box::into_raw(Box::new(UniqueThreeRiver::new()))
}

/// Feed one OHLCV candle; returns the output, or `NaN` during warmup / on a `NULL`
/// handle / if the candle is invalid (e.g. `high < low`).
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_unique_three_river_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_unique_three_river_update(
    handle: *mut UniqueThreeRiver,
    open: f64,
    high: f64,
    low: f64,
    close: f64,
    volume: f64,
    timestamp: i64,
) -> f64 {
    let Some(ind) = handle.as_mut() else {
        return f64::NAN;
    };
    match Candle::new(open, high, low, close, volume, timestamp) {
        Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
        Err(_) => f64::NAN,
    }
}

/// Run over the OHLCV series (`open/high/low/close/volume/timestamp[0..n]`) into
/// `out[0..n]` (`NaN` at warmup or on an invalid candle).
///
/// # Safety
/// `handle` valid (from `wickra_unique_three_river_new`, not freed); every input pointer and `out`
/// cover `n` elements.
#[no_mangle]
pub unsafe extern "C" fn wickra_unique_three_river_batch(
    handle: *mut UniqueThreeRiver,
    open: *const f64,
    high: *const f64,
    low: *const f64,
    close: *const f64,
    volume: *const f64,
    timestamp: *const i64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null()
        || open.is_null()
        || high.is_null()
        || low.is_null()
        || close.is_null()
        || volume.is_null()
        || timestamp.is_null()
        || out.is_null()
    {
        return;
    }
    let ind = &mut *handle;
    let opens = slice::from_raw_parts(open, n);
    let highs = slice::from_raw_parts(high, n);
    let lows = slice::from_raw_parts(low, n);
    let closes = slice::from_raw_parts(close, n);
    let volumes = slice::from_raw_parts(volume, n);
    let timestamps = slice::from_raw_parts(timestamp, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (idx, slot) in outputs.iter_mut().enumerate() {
        *slot = match Candle::new(
            opens[idx],
            highs[idx],
            lows[idx],
            closes[idx],
            volumes[idx],
            timestamps[idx],
        ) {
            Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
            Err(_) => f64::NAN,
        };
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_unique_three_river_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_unique_three_river_warmup_period(
    handle: *mut UniqueThreeRiver,
) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_unique_three_river_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_unique_three_river_is_ready(handle: *mut UniqueThreeRiver) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_unique_three_river_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_unique_three_river_name(
    handle: *mut UniqueThreeRiver,
) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_unique_three_river_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_unique_three_river_reset(handle: *mut UniqueThreeRiver) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_unique_three_river_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_unique_three_river_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_unique_three_river_free(handle: *mut UniqueThreeRiver) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `UpsideGapThreeMethods` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_upside_gap_three_methods_free`.
#[no_mangle]
pub extern "C" fn wickra_upside_gap_three_methods_new() -> *mut UpsideGapThreeMethods {
    Box::into_raw(Box::new(UpsideGapThreeMethods::new()))
}

/// Feed one OHLCV candle; returns the output, or `NaN` during warmup / on a `NULL`
/// handle / if the candle is invalid (e.g. `high < low`).
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_upside_gap_three_methods_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_upside_gap_three_methods_update(
    handle: *mut UpsideGapThreeMethods,
    open: f64,
    high: f64,
    low: f64,
    close: f64,
    volume: f64,
    timestamp: i64,
) -> f64 {
    let Some(ind) = handle.as_mut() else {
        return f64::NAN;
    };
    match Candle::new(open, high, low, close, volume, timestamp) {
        Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
        Err(_) => f64::NAN,
    }
}

/// Run over the OHLCV series (`open/high/low/close/volume/timestamp[0..n]`) into
/// `out[0..n]` (`NaN` at warmup or on an invalid candle).
///
/// # Safety
/// `handle` valid (from `wickra_upside_gap_three_methods_new`, not freed); every input pointer and `out`
/// cover `n` elements.
#[no_mangle]
pub unsafe extern "C" fn wickra_upside_gap_three_methods_batch(
    handle: *mut UpsideGapThreeMethods,
    open: *const f64,
    high: *const f64,
    low: *const f64,
    close: *const f64,
    volume: *const f64,
    timestamp: *const i64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null()
        || open.is_null()
        || high.is_null()
        || low.is_null()
        || close.is_null()
        || volume.is_null()
        || timestamp.is_null()
        || out.is_null()
    {
        return;
    }
    let ind = &mut *handle;
    let opens = slice::from_raw_parts(open, n);
    let highs = slice::from_raw_parts(high, n);
    let lows = slice::from_raw_parts(low, n);
    let closes = slice::from_raw_parts(close, n);
    let volumes = slice::from_raw_parts(volume, n);
    let timestamps = slice::from_raw_parts(timestamp, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (idx, slot) in outputs.iter_mut().enumerate() {
        *slot = match Candle::new(
            opens[idx],
            highs[idx],
            lows[idx],
            closes[idx],
            volumes[idx],
            timestamps[idx],
        ) {
            Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
            Err(_) => f64::NAN,
        };
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_upside_gap_three_methods_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_upside_gap_three_methods_warmup_period(
    handle: *mut UpsideGapThreeMethods,
) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_upside_gap_three_methods_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_upside_gap_three_methods_is_ready(
    handle: *mut UpsideGapThreeMethods,
) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_upside_gap_three_methods_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_upside_gap_three_methods_name(
    handle: *mut UpsideGapThreeMethods,
) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_upside_gap_three_methods_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_upside_gap_three_methods_reset(handle: *mut UpsideGapThreeMethods) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_upside_gap_three_methods_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_upside_gap_three_methods_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_upside_gap_three_methods_free(handle: *mut UpsideGapThreeMethods) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `UpsideGapTwoCrows` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_upside_gap_two_crows_free`.
#[no_mangle]
pub extern "C" fn wickra_upside_gap_two_crows_new() -> *mut UpsideGapTwoCrows {
    Box::into_raw(Box::new(UpsideGapTwoCrows::new()))
}

/// Feed one OHLCV candle; returns the output, or `NaN` during warmup / on a `NULL`
/// handle / if the candle is invalid (e.g. `high < low`).
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_upside_gap_two_crows_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_upside_gap_two_crows_update(
    handle: *mut UpsideGapTwoCrows,
    open: f64,
    high: f64,
    low: f64,
    close: f64,
    volume: f64,
    timestamp: i64,
) -> f64 {
    let Some(ind) = handle.as_mut() else {
        return f64::NAN;
    };
    match Candle::new(open, high, low, close, volume, timestamp) {
        Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
        Err(_) => f64::NAN,
    }
}

/// Run over the OHLCV series (`open/high/low/close/volume/timestamp[0..n]`) into
/// `out[0..n]` (`NaN` at warmup or on an invalid candle).
///
/// # Safety
/// `handle` valid (from `wickra_upside_gap_two_crows_new`, not freed); every input pointer and `out`
/// cover `n` elements.
#[no_mangle]
pub unsafe extern "C" fn wickra_upside_gap_two_crows_batch(
    handle: *mut UpsideGapTwoCrows,
    open: *const f64,
    high: *const f64,
    low: *const f64,
    close: *const f64,
    volume: *const f64,
    timestamp: *const i64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null()
        || open.is_null()
        || high.is_null()
        || low.is_null()
        || close.is_null()
        || volume.is_null()
        || timestamp.is_null()
        || out.is_null()
    {
        return;
    }
    let ind = &mut *handle;
    let opens = slice::from_raw_parts(open, n);
    let highs = slice::from_raw_parts(high, n);
    let lows = slice::from_raw_parts(low, n);
    let closes = slice::from_raw_parts(close, n);
    let volumes = slice::from_raw_parts(volume, n);
    let timestamps = slice::from_raw_parts(timestamp, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (idx, slot) in outputs.iter_mut().enumerate() {
        *slot = match Candle::new(
            opens[idx],
            highs[idx],
            lows[idx],
            closes[idx],
            volumes[idx],
            timestamps[idx],
        ) {
            Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
            Err(_) => f64::NAN,
        };
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_upside_gap_two_crows_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_upside_gap_two_crows_warmup_period(
    handle: *mut UpsideGapTwoCrows,
) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_upside_gap_two_crows_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_upside_gap_two_crows_is_ready(
    handle: *mut UpsideGapTwoCrows,
) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_upside_gap_two_crows_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_upside_gap_two_crows_name(
    handle: *mut UpsideGapTwoCrows,
) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_upside_gap_two_crows_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_upside_gap_two_crows_reset(handle: *mut UpsideGapTwoCrows) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_upside_gap_two_crows_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_upside_gap_two_crows_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_upside_gap_two_crows_free(handle: *mut UpsideGapTwoCrows) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `VolatilityRatio` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_volatility_ratio_free`.
#[no_mangle]
pub extern "C" fn wickra_volatility_ratio_new(period: usize) -> *mut VolatilityRatio {
    match VolatilityRatio::new(period) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one OHLCV candle; returns the output, or `NaN` during warmup / on a `NULL`
/// handle / if the candle is invalid (e.g. `high < low`).
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_volatility_ratio_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_volatility_ratio_update(
    handle: *mut VolatilityRatio,
    open: f64,
    high: f64,
    low: f64,
    close: f64,
    volume: f64,
    timestamp: i64,
) -> f64 {
    let Some(ind) = handle.as_mut() else {
        return f64::NAN;
    };
    match Candle::new(open, high, low, close, volume, timestamp) {
        Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
        Err(_) => f64::NAN,
    }
}

/// Run over the OHLCV series (`open/high/low/close/volume/timestamp[0..n]`) into
/// `out[0..n]` (`NaN` at warmup or on an invalid candle).
///
/// # Safety
/// `handle` valid (from `wickra_volatility_ratio_new`, not freed); every input pointer and `out`
/// cover `n` elements.
#[no_mangle]
pub unsafe extern "C" fn wickra_volatility_ratio_batch(
    handle: *mut VolatilityRatio,
    open: *const f64,
    high: *const f64,
    low: *const f64,
    close: *const f64,
    volume: *const f64,
    timestamp: *const i64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null()
        || open.is_null()
        || high.is_null()
        || low.is_null()
        || close.is_null()
        || volume.is_null()
        || timestamp.is_null()
        || out.is_null()
    {
        return;
    }
    let ind = &mut *handle;
    let opens = slice::from_raw_parts(open, n);
    let highs = slice::from_raw_parts(high, n);
    let lows = slice::from_raw_parts(low, n);
    let closes = slice::from_raw_parts(close, n);
    let volumes = slice::from_raw_parts(volume, n);
    let timestamps = slice::from_raw_parts(timestamp, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (idx, slot) in outputs.iter_mut().enumerate() {
        *slot = match Candle::new(
            opens[idx],
            highs[idx],
            lows[idx],
            closes[idx],
            volumes[idx],
            timestamps[idx],
        ) {
            Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
            Err(_) => f64::NAN,
        };
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_volatility_ratio_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_volatility_ratio_warmup_period(
    handle: *mut VolatilityRatio,
) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_volatility_ratio_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_volatility_ratio_is_ready(handle: *mut VolatilityRatio) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_volatility_ratio_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_volatility_ratio_name(
    handle: *mut VolatilityRatio,
) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_volatility_ratio_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_volatility_ratio_reset(handle: *mut VolatilityRatio) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_volatility_ratio_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_volatility_ratio_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_volatility_ratio_free(handle: *mut VolatilityRatio) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `VoltyStop` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_volty_stop_free`.
#[no_mangle]
pub extern "C" fn wickra_volty_stop_new(atr_period: usize, multiplier: f64) -> *mut VoltyStop {
    match VoltyStop::new(atr_period, multiplier) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one OHLCV candle; returns the output, or `NaN` during warmup / on a `NULL`
/// handle / if the candle is invalid (e.g. `high < low`).
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_volty_stop_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_volty_stop_update(
    handle: *mut VoltyStop,
    open: f64,
    high: f64,
    low: f64,
    close: f64,
    volume: f64,
    timestamp: i64,
) -> f64 {
    let Some(ind) = handle.as_mut() else {
        return f64::NAN;
    };
    match Candle::new(open, high, low, close, volume, timestamp) {
        Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
        Err(_) => f64::NAN,
    }
}

/// Run over the OHLCV series (`open/high/low/close/volume/timestamp[0..n]`) into
/// `out[0..n]` (`NaN` at warmup or on an invalid candle).
///
/// # Safety
/// `handle` valid (from `wickra_volty_stop_new`, not freed); every input pointer and `out`
/// cover `n` elements.
#[no_mangle]
pub unsafe extern "C" fn wickra_volty_stop_batch(
    handle: *mut VoltyStop,
    open: *const f64,
    high: *const f64,
    low: *const f64,
    close: *const f64,
    volume: *const f64,
    timestamp: *const i64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null()
        || open.is_null()
        || high.is_null()
        || low.is_null()
        || close.is_null()
        || volume.is_null()
        || timestamp.is_null()
        || out.is_null()
    {
        return;
    }
    let ind = &mut *handle;
    let opens = slice::from_raw_parts(open, n);
    let highs = slice::from_raw_parts(high, n);
    let lows = slice::from_raw_parts(low, n);
    let closes = slice::from_raw_parts(close, n);
    let volumes = slice::from_raw_parts(volume, n);
    let timestamps = slice::from_raw_parts(timestamp, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (idx, slot) in outputs.iter_mut().enumerate() {
        *slot = match Candle::new(
            opens[idx],
            highs[idx],
            lows[idx],
            closes[idx],
            volumes[idx],
            timestamps[idx],
        ) {
            Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
            Err(_) => f64::NAN,
        };
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_volty_stop_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_volty_stop_warmup_period(handle: *mut VoltyStop) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_volty_stop_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_volty_stop_is_ready(handle: *mut VoltyStop) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_volty_stop_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_volty_stop_name(handle: *mut VoltyStop) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_volty_stop_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_volty_stop_reset(handle: *mut VoltyStop) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_volty_stop_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_volty_stop_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_volty_stop_free(handle: *mut VoltyStop) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `VolumeOscillator` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_volume_oscillator_free`.
#[no_mangle]
pub extern "C" fn wickra_volume_oscillator_new(fast: usize, slow: usize) -> *mut VolumeOscillator {
    match VolumeOscillator::new(fast, slow) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one OHLCV candle; returns the output, or `NaN` during warmup / on a `NULL`
/// handle / if the candle is invalid (e.g. `high < low`).
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_volume_oscillator_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_volume_oscillator_update(
    handle: *mut VolumeOscillator,
    open: f64,
    high: f64,
    low: f64,
    close: f64,
    volume: f64,
    timestamp: i64,
) -> f64 {
    let Some(ind) = handle.as_mut() else {
        return f64::NAN;
    };
    match Candle::new(open, high, low, close, volume, timestamp) {
        Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
        Err(_) => f64::NAN,
    }
}

/// Run over the OHLCV series (`open/high/low/close/volume/timestamp[0..n]`) into
/// `out[0..n]` (`NaN` at warmup or on an invalid candle).
///
/// # Safety
/// `handle` valid (from `wickra_volume_oscillator_new`, not freed); every input pointer and `out`
/// cover `n` elements.
#[no_mangle]
pub unsafe extern "C" fn wickra_volume_oscillator_batch(
    handle: *mut VolumeOscillator,
    open: *const f64,
    high: *const f64,
    low: *const f64,
    close: *const f64,
    volume: *const f64,
    timestamp: *const i64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null()
        || open.is_null()
        || high.is_null()
        || low.is_null()
        || close.is_null()
        || volume.is_null()
        || timestamp.is_null()
        || out.is_null()
    {
        return;
    }
    let ind = &mut *handle;
    let opens = slice::from_raw_parts(open, n);
    let highs = slice::from_raw_parts(high, n);
    let lows = slice::from_raw_parts(low, n);
    let closes = slice::from_raw_parts(close, n);
    let volumes = slice::from_raw_parts(volume, n);
    let timestamps = slice::from_raw_parts(timestamp, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (idx, slot) in outputs.iter_mut().enumerate() {
        *slot = match Candle::new(
            opens[idx],
            highs[idx],
            lows[idx],
            closes[idx],
            volumes[idx],
            timestamps[idx],
        ) {
            Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
            Err(_) => f64::NAN,
        };
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_volume_oscillator_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_volume_oscillator_warmup_period(
    handle: *mut VolumeOscillator,
) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_volume_oscillator_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_volume_oscillator_is_ready(handle: *mut VolumeOscillator) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_volume_oscillator_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_volume_oscillator_name(
    handle: *mut VolumeOscillator,
) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_volume_oscillator_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_volume_oscillator_reset(handle: *mut VolumeOscillator) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_volume_oscillator_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_volume_oscillator_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_volume_oscillator_free(handle: *mut VolumeOscillator) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `VolumeRsi` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_volume_rsi_free`.
#[no_mangle]
pub extern "C" fn wickra_volume_rsi_new(period: usize) -> *mut VolumeRsi {
    match VolumeRsi::new(period) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one OHLCV candle; returns the output, or `NaN` during warmup / on a `NULL`
/// handle / if the candle is invalid (e.g. `high < low`).
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_volume_rsi_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_volume_rsi_update(
    handle: *mut VolumeRsi,
    open: f64,
    high: f64,
    low: f64,
    close: f64,
    volume: f64,
    timestamp: i64,
) -> f64 {
    let Some(ind) = handle.as_mut() else {
        return f64::NAN;
    };
    match Candle::new(open, high, low, close, volume, timestamp) {
        Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
        Err(_) => f64::NAN,
    }
}

/// Run over the OHLCV series (`open/high/low/close/volume/timestamp[0..n]`) into
/// `out[0..n]` (`NaN` at warmup or on an invalid candle).
///
/// # Safety
/// `handle` valid (from `wickra_volume_rsi_new`, not freed); every input pointer and `out`
/// cover `n` elements.
#[no_mangle]
pub unsafe extern "C" fn wickra_volume_rsi_batch(
    handle: *mut VolumeRsi,
    open: *const f64,
    high: *const f64,
    low: *const f64,
    close: *const f64,
    volume: *const f64,
    timestamp: *const i64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null()
        || open.is_null()
        || high.is_null()
        || low.is_null()
        || close.is_null()
        || volume.is_null()
        || timestamp.is_null()
        || out.is_null()
    {
        return;
    }
    let ind = &mut *handle;
    let opens = slice::from_raw_parts(open, n);
    let highs = slice::from_raw_parts(high, n);
    let lows = slice::from_raw_parts(low, n);
    let closes = slice::from_raw_parts(close, n);
    let volumes = slice::from_raw_parts(volume, n);
    let timestamps = slice::from_raw_parts(timestamp, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (idx, slot) in outputs.iter_mut().enumerate() {
        *slot = match Candle::new(
            opens[idx],
            highs[idx],
            lows[idx],
            closes[idx],
            volumes[idx],
            timestamps[idx],
        ) {
            Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
            Err(_) => f64::NAN,
        };
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_volume_rsi_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_volume_rsi_warmup_period(handle: *mut VolumeRsi) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_volume_rsi_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_volume_rsi_is_ready(handle: *mut VolumeRsi) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_volume_rsi_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_volume_rsi_name(handle: *mut VolumeRsi) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_volume_rsi_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_volume_rsi_reset(handle: *mut VolumeRsi) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_volume_rsi_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_volume_rsi_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_volume_rsi_free(handle: *mut VolumeRsi) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `VolumePriceTrend` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_volume_price_trend_free`.
#[no_mangle]
pub extern "C" fn wickra_volume_price_trend_new() -> *mut VolumePriceTrend {
    Box::into_raw(Box::new(VolumePriceTrend::new()))
}

/// Feed one OHLCV candle; returns the output, or `NaN` during warmup / on a `NULL`
/// handle / if the candle is invalid (e.g. `high < low`).
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_volume_price_trend_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_volume_price_trend_update(
    handle: *mut VolumePriceTrend,
    open: f64,
    high: f64,
    low: f64,
    close: f64,
    volume: f64,
    timestamp: i64,
) -> f64 {
    let Some(ind) = handle.as_mut() else {
        return f64::NAN;
    };
    match Candle::new(open, high, low, close, volume, timestamp) {
        Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
        Err(_) => f64::NAN,
    }
}

/// Run over the OHLCV series (`open/high/low/close/volume/timestamp[0..n]`) into
/// `out[0..n]` (`NaN` at warmup or on an invalid candle).
///
/// # Safety
/// `handle` valid (from `wickra_volume_price_trend_new`, not freed); every input pointer and `out`
/// cover `n` elements.
#[no_mangle]
pub unsafe extern "C" fn wickra_volume_price_trend_batch(
    handle: *mut VolumePriceTrend,
    open: *const f64,
    high: *const f64,
    low: *const f64,
    close: *const f64,
    volume: *const f64,
    timestamp: *const i64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null()
        || open.is_null()
        || high.is_null()
        || low.is_null()
        || close.is_null()
        || volume.is_null()
        || timestamp.is_null()
        || out.is_null()
    {
        return;
    }
    let ind = &mut *handle;
    let opens = slice::from_raw_parts(open, n);
    let highs = slice::from_raw_parts(high, n);
    let lows = slice::from_raw_parts(low, n);
    let closes = slice::from_raw_parts(close, n);
    let volumes = slice::from_raw_parts(volume, n);
    let timestamps = slice::from_raw_parts(timestamp, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (idx, slot) in outputs.iter_mut().enumerate() {
        *slot = match Candle::new(
            opens[idx],
            highs[idx],
            lows[idx],
            closes[idx],
            volumes[idx],
            timestamps[idx],
        ) {
            Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
            Err(_) => f64::NAN,
        };
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_volume_price_trend_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_volume_price_trend_warmup_period(
    handle: *mut VolumePriceTrend,
) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_volume_price_trend_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_volume_price_trend_is_ready(handle: *mut VolumePriceTrend) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_volume_price_trend_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_volume_price_trend_name(
    handle: *mut VolumePriceTrend,
) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_volume_price_trend_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_volume_price_trend_reset(handle: *mut VolumePriceTrend) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_volume_price_trend_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_volume_price_trend_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_volume_price_trend_free(handle: *mut VolumePriceTrend) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `Vwap` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_vwap_free`.
#[no_mangle]
pub extern "C" fn wickra_vwap_new() -> *mut Vwap {
    Box::into_raw(Box::new(Vwap::new()))
}

/// Feed one OHLCV candle; returns the output, or `NaN` during warmup / on a `NULL`
/// handle / if the candle is invalid (e.g. `high < low`).
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_vwap_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_vwap_update(
    handle: *mut Vwap,
    open: f64,
    high: f64,
    low: f64,
    close: f64,
    volume: f64,
    timestamp: i64,
) -> f64 {
    let Some(ind) = handle.as_mut() else {
        return f64::NAN;
    };
    match Candle::new(open, high, low, close, volume, timestamp) {
        Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
        Err(_) => f64::NAN,
    }
}

/// Run over the OHLCV series (`open/high/low/close/volume/timestamp[0..n]`) into
/// `out[0..n]` (`NaN` at warmup or on an invalid candle).
///
/// # Safety
/// `handle` valid (from `wickra_vwap_new`, not freed); every input pointer and `out`
/// cover `n` elements.
#[no_mangle]
pub unsafe extern "C" fn wickra_vwap_batch(
    handle: *mut Vwap,
    open: *const f64,
    high: *const f64,
    low: *const f64,
    close: *const f64,
    volume: *const f64,
    timestamp: *const i64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null()
        || open.is_null()
        || high.is_null()
        || low.is_null()
        || close.is_null()
        || volume.is_null()
        || timestamp.is_null()
        || out.is_null()
    {
        return;
    }
    let ind = &mut *handle;
    let opens = slice::from_raw_parts(open, n);
    let highs = slice::from_raw_parts(high, n);
    let lows = slice::from_raw_parts(low, n);
    let closes = slice::from_raw_parts(close, n);
    let volumes = slice::from_raw_parts(volume, n);
    let timestamps = slice::from_raw_parts(timestamp, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (idx, slot) in outputs.iter_mut().enumerate() {
        *slot = match Candle::new(
            opens[idx],
            highs[idx],
            lows[idx],
            closes[idx],
            volumes[idx],
            timestamps[idx],
        ) {
            Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
            Err(_) => f64::NAN,
        };
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_vwap_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_vwap_warmup_period(handle: *mut Vwap) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_vwap_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_vwap_is_ready(handle: *mut Vwap) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_vwap_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_vwap_name(handle: *mut Vwap) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_vwap_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_vwap_reset(handle: *mut Vwap) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_vwap_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_vwap_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_vwap_free(handle: *mut Vwap) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `RollingVwap` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_rolling_vwap_free`.
#[no_mangle]
pub extern "C" fn wickra_rolling_vwap_new(period: usize) -> *mut RollingVwap {
    match RollingVwap::new(period) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one OHLCV candle; returns the output, or `NaN` during warmup / on a `NULL`
/// handle / if the candle is invalid (e.g. `high < low`).
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_rolling_vwap_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_rolling_vwap_update(
    handle: *mut RollingVwap,
    open: f64,
    high: f64,
    low: f64,
    close: f64,
    volume: f64,
    timestamp: i64,
) -> f64 {
    let Some(ind) = handle.as_mut() else {
        return f64::NAN;
    };
    match Candle::new(open, high, low, close, volume, timestamp) {
        Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
        Err(_) => f64::NAN,
    }
}

/// Run over the OHLCV series (`open/high/low/close/volume/timestamp[0..n]`) into
/// `out[0..n]` (`NaN` at warmup or on an invalid candle).
///
/// # Safety
/// `handle` valid (from `wickra_rolling_vwap_new`, not freed); every input pointer and `out`
/// cover `n` elements.
#[no_mangle]
pub unsafe extern "C" fn wickra_rolling_vwap_batch(
    handle: *mut RollingVwap,
    open: *const f64,
    high: *const f64,
    low: *const f64,
    close: *const f64,
    volume: *const f64,
    timestamp: *const i64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null()
        || open.is_null()
        || high.is_null()
        || low.is_null()
        || close.is_null()
        || volume.is_null()
        || timestamp.is_null()
        || out.is_null()
    {
        return;
    }
    let ind = &mut *handle;
    let opens = slice::from_raw_parts(open, n);
    let highs = slice::from_raw_parts(high, n);
    let lows = slice::from_raw_parts(low, n);
    let closes = slice::from_raw_parts(close, n);
    let volumes = slice::from_raw_parts(volume, n);
    let timestamps = slice::from_raw_parts(timestamp, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (idx, slot) in outputs.iter_mut().enumerate() {
        *slot = match Candle::new(
            opens[idx],
            highs[idx],
            lows[idx],
            closes[idx],
            volumes[idx],
            timestamps[idx],
        ) {
            Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
            Err(_) => f64::NAN,
        };
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_rolling_vwap_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_rolling_vwap_warmup_period(handle: *mut RollingVwap) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_rolling_vwap_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_rolling_vwap_is_ready(handle: *mut RollingVwap) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_rolling_vwap_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_rolling_vwap_name(handle: *mut RollingVwap) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_rolling_vwap_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_rolling_vwap_reset(handle: *mut RollingVwap) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_rolling_vwap_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_rolling_vwap_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_rolling_vwap_free(handle: *mut RollingVwap) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `Vwma` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_vwma_free`.
#[no_mangle]
pub extern "C" fn wickra_vwma_new(period: usize) -> *mut Vwma {
    match Vwma::new(period) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one OHLCV candle; returns the output, or `NaN` during warmup / on a `NULL`
/// handle / if the candle is invalid (e.g. `high < low`).
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_vwma_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_vwma_update(
    handle: *mut Vwma,
    open: f64,
    high: f64,
    low: f64,
    close: f64,
    volume: f64,
    timestamp: i64,
) -> f64 {
    let Some(ind) = handle.as_mut() else {
        return f64::NAN;
    };
    match Candle::new(open, high, low, close, volume, timestamp) {
        Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
        Err(_) => f64::NAN,
    }
}

/// Run over the OHLCV series (`open/high/low/close/volume/timestamp[0..n]`) into
/// `out[0..n]` (`NaN` at warmup or on an invalid candle).
///
/// # Safety
/// `handle` valid (from `wickra_vwma_new`, not freed); every input pointer and `out`
/// cover `n` elements.
#[no_mangle]
pub unsafe extern "C" fn wickra_vwma_batch(
    handle: *mut Vwma,
    open: *const f64,
    high: *const f64,
    low: *const f64,
    close: *const f64,
    volume: *const f64,
    timestamp: *const i64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null()
        || open.is_null()
        || high.is_null()
        || low.is_null()
        || close.is_null()
        || volume.is_null()
        || timestamp.is_null()
        || out.is_null()
    {
        return;
    }
    let ind = &mut *handle;
    let opens = slice::from_raw_parts(open, n);
    let highs = slice::from_raw_parts(high, n);
    let lows = slice::from_raw_parts(low, n);
    let closes = slice::from_raw_parts(close, n);
    let volumes = slice::from_raw_parts(volume, n);
    let timestamps = slice::from_raw_parts(timestamp, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (idx, slot) in outputs.iter_mut().enumerate() {
        *slot = match Candle::new(
            opens[idx],
            highs[idx],
            lows[idx],
            closes[idx],
            volumes[idx],
            timestamps[idx],
        ) {
            Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
            Err(_) => f64::NAN,
        };
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_vwma_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_vwma_warmup_period(handle: *mut Vwma) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_vwma_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_vwma_is_ready(handle: *mut Vwma) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_vwma_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_vwma_name(handle: *mut Vwma) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_vwma_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_vwma_reset(handle: *mut Vwma) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_vwma_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_vwma_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_vwma_free(handle: *mut Vwma) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `Vzo` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_vzo_free`.
#[no_mangle]
pub extern "C" fn wickra_vzo_new(period: usize) -> *mut Vzo {
    match Vzo::new(period) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one OHLCV candle; returns the output, or `NaN` during warmup / on a `NULL`
/// handle / if the candle is invalid (e.g. `high < low`).
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_vzo_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_vzo_update(
    handle: *mut Vzo,
    open: f64,
    high: f64,
    low: f64,
    close: f64,
    volume: f64,
    timestamp: i64,
) -> f64 {
    let Some(ind) = handle.as_mut() else {
        return f64::NAN;
    };
    match Candle::new(open, high, low, close, volume, timestamp) {
        Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
        Err(_) => f64::NAN,
    }
}

/// Run over the OHLCV series (`open/high/low/close/volume/timestamp[0..n]`) into
/// `out[0..n]` (`NaN` at warmup or on an invalid candle).
///
/// # Safety
/// `handle` valid (from `wickra_vzo_new`, not freed); every input pointer and `out`
/// cover `n` elements.
#[no_mangle]
pub unsafe extern "C" fn wickra_vzo_batch(
    handle: *mut Vzo,
    open: *const f64,
    high: *const f64,
    low: *const f64,
    close: *const f64,
    volume: *const f64,
    timestamp: *const i64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null()
        || open.is_null()
        || high.is_null()
        || low.is_null()
        || close.is_null()
        || volume.is_null()
        || timestamp.is_null()
        || out.is_null()
    {
        return;
    }
    let ind = &mut *handle;
    let opens = slice::from_raw_parts(open, n);
    let highs = slice::from_raw_parts(high, n);
    let lows = slice::from_raw_parts(low, n);
    let closes = slice::from_raw_parts(close, n);
    let volumes = slice::from_raw_parts(volume, n);
    let timestamps = slice::from_raw_parts(timestamp, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (idx, slot) in outputs.iter_mut().enumerate() {
        *slot = match Candle::new(
            opens[idx],
            highs[idx],
            lows[idx],
            closes[idx],
            volumes[idx],
            timestamps[idx],
        ) {
            Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
            Err(_) => f64::NAN,
        };
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_vzo_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_vzo_warmup_period(handle: *mut Vzo) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_vzo_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_vzo_is_ready(handle: *mut Vzo) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_vzo_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_vzo_name(handle: *mut Vzo) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_vzo_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_vzo_reset(handle: *mut Vzo) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_vzo_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_vzo_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_vzo_free(handle: *mut Vzo) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `Wad` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_wad_free`.
#[no_mangle]
pub extern "C" fn wickra_wad_new() -> *mut Wad {
    Box::into_raw(Box::new(Wad::new()))
}

/// Feed one OHLCV candle; returns the output, or `NaN` during warmup / on a `NULL`
/// handle / if the candle is invalid (e.g. `high < low`).
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_wad_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_wad_update(
    handle: *mut Wad,
    open: f64,
    high: f64,
    low: f64,
    close: f64,
    volume: f64,
    timestamp: i64,
) -> f64 {
    let Some(ind) = handle.as_mut() else {
        return f64::NAN;
    };
    match Candle::new(open, high, low, close, volume, timestamp) {
        Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
        Err(_) => f64::NAN,
    }
}

/// Run over the OHLCV series (`open/high/low/close/volume/timestamp[0..n]`) into
/// `out[0..n]` (`NaN` at warmup or on an invalid candle).
///
/// # Safety
/// `handle` valid (from `wickra_wad_new`, not freed); every input pointer and `out`
/// cover `n` elements.
#[no_mangle]
pub unsafe extern "C" fn wickra_wad_batch(
    handle: *mut Wad,
    open: *const f64,
    high: *const f64,
    low: *const f64,
    close: *const f64,
    volume: *const f64,
    timestamp: *const i64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null()
        || open.is_null()
        || high.is_null()
        || low.is_null()
        || close.is_null()
        || volume.is_null()
        || timestamp.is_null()
        || out.is_null()
    {
        return;
    }
    let ind = &mut *handle;
    let opens = slice::from_raw_parts(open, n);
    let highs = slice::from_raw_parts(high, n);
    let lows = slice::from_raw_parts(low, n);
    let closes = slice::from_raw_parts(close, n);
    let volumes = slice::from_raw_parts(volume, n);
    let timestamps = slice::from_raw_parts(timestamp, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (idx, slot) in outputs.iter_mut().enumerate() {
        *slot = match Candle::new(
            opens[idx],
            highs[idx],
            lows[idx],
            closes[idx],
            volumes[idx],
            timestamps[idx],
        ) {
            Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
            Err(_) => f64::NAN,
        };
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_wad_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_wad_warmup_period(handle: *mut Wad) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_wad_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_wad_is_ready(handle: *mut Wad) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_wad_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_wad_name(handle: *mut Wad) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_wad_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_wad_reset(handle: *mut Wad) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_wad_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_wad_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_wad_free(handle: *mut Wad) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `Wedge` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_wedge_free`.
#[no_mangle]
pub extern "C" fn wickra_wedge_new() -> *mut Wedge {
    Box::into_raw(Box::new(Wedge::new()))
}

/// Feed one OHLCV candle; returns the output, or `NaN` during warmup / on a `NULL`
/// handle / if the candle is invalid (e.g. `high < low`).
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_wedge_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_wedge_update(
    handle: *mut Wedge,
    open: f64,
    high: f64,
    low: f64,
    close: f64,
    volume: f64,
    timestamp: i64,
) -> f64 {
    let Some(ind) = handle.as_mut() else {
        return f64::NAN;
    };
    match Candle::new(open, high, low, close, volume, timestamp) {
        Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
        Err(_) => f64::NAN,
    }
}

/// Run over the OHLCV series (`open/high/low/close/volume/timestamp[0..n]`) into
/// `out[0..n]` (`NaN` at warmup or on an invalid candle).
///
/// # Safety
/// `handle` valid (from `wickra_wedge_new`, not freed); every input pointer and `out`
/// cover `n` elements.
#[no_mangle]
pub unsafe extern "C" fn wickra_wedge_batch(
    handle: *mut Wedge,
    open: *const f64,
    high: *const f64,
    low: *const f64,
    close: *const f64,
    volume: *const f64,
    timestamp: *const i64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null()
        || open.is_null()
        || high.is_null()
        || low.is_null()
        || close.is_null()
        || volume.is_null()
        || timestamp.is_null()
        || out.is_null()
    {
        return;
    }
    let ind = &mut *handle;
    let opens = slice::from_raw_parts(open, n);
    let highs = slice::from_raw_parts(high, n);
    let lows = slice::from_raw_parts(low, n);
    let closes = slice::from_raw_parts(close, n);
    let volumes = slice::from_raw_parts(volume, n);
    let timestamps = slice::from_raw_parts(timestamp, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (idx, slot) in outputs.iter_mut().enumerate() {
        *slot = match Candle::new(
            opens[idx],
            highs[idx],
            lows[idx],
            closes[idx],
            volumes[idx],
            timestamps[idx],
        ) {
            Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
            Err(_) => f64::NAN,
        };
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_wedge_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_wedge_warmup_period(handle: *mut Wedge) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_wedge_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_wedge_is_ready(handle: *mut Wedge) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_wedge_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_wedge_name(handle: *mut Wedge) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_wedge_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_wedge_reset(handle: *mut Wedge) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_wedge_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_wedge_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_wedge_free(handle: *mut Wedge) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `WeightedClose` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_weighted_close_free`.
#[no_mangle]
pub extern "C" fn wickra_weighted_close_new() -> *mut WeightedClose {
    Box::into_raw(Box::new(WeightedClose::new()))
}

/// Feed one OHLCV candle; returns the output, or `NaN` during warmup / on a `NULL`
/// handle / if the candle is invalid (e.g. `high < low`).
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_weighted_close_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_weighted_close_update(
    handle: *mut WeightedClose,
    open: f64,
    high: f64,
    low: f64,
    close: f64,
    volume: f64,
    timestamp: i64,
) -> f64 {
    let Some(ind) = handle.as_mut() else {
        return f64::NAN;
    };
    match Candle::new(open, high, low, close, volume, timestamp) {
        Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
        Err(_) => f64::NAN,
    }
}

/// Run over the OHLCV series (`open/high/low/close/volume/timestamp[0..n]`) into
/// `out[0..n]` (`NaN` at warmup or on an invalid candle).
///
/// # Safety
/// `handle` valid (from `wickra_weighted_close_new`, not freed); every input pointer and `out`
/// cover `n` elements.
#[no_mangle]
pub unsafe extern "C" fn wickra_weighted_close_batch(
    handle: *mut WeightedClose,
    open: *const f64,
    high: *const f64,
    low: *const f64,
    close: *const f64,
    volume: *const f64,
    timestamp: *const i64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null()
        || open.is_null()
        || high.is_null()
        || low.is_null()
        || close.is_null()
        || volume.is_null()
        || timestamp.is_null()
        || out.is_null()
    {
        return;
    }
    let ind = &mut *handle;
    let opens = slice::from_raw_parts(open, n);
    let highs = slice::from_raw_parts(high, n);
    let lows = slice::from_raw_parts(low, n);
    let closes = slice::from_raw_parts(close, n);
    let volumes = slice::from_raw_parts(volume, n);
    let timestamps = slice::from_raw_parts(timestamp, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (idx, slot) in outputs.iter_mut().enumerate() {
        *slot = match Candle::new(
            opens[idx],
            highs[idx],
            lows[idx],
            closes[idx],
            volumes[idx],
            timestamps[idx],
        ) {
            Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
            Err(_) => f64::NAN,
        };
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_weighted_close_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_weighted_close_warmup_period(handle: *mut WeightedClose) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_weighted_close_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_weighted_close_is_ready(handle: *mut WeightedClose) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_weighted_close_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_weighted_close_name(handle: *mut WeightedClose) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_weighted_close_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_weighted_close_reset(handle: *mut WeightedClose) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_weighted_close_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_weighted_close_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_weighted_close_free(handle: *mut WeightedClose) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `WickRatio` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_wick_ratio_free`.
#[no_mangle]
pub extern "C" fn wickra_wick_ratio_new() -> *mut WickRatio {
    Box::into_raw(Box::new(WickRatio::new()))
}

/// Feed one OHLCV candle; returns the output, or `NaN` during warmup / on a `NULL`
/// handle / if the candle is invalid (e.g. `high < low`).
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_wick_ratio_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_wick_ratio_update(
    handle: *mut WickRatio,
    open: f64,
    high: f64,
    low: f64,
    close: f64,
    volume: f64,
    timestamp: i64,
) -> f64 {
    let Some(ind) = handle.as_mut() else {
        return f64::NAN;
    };
    match Candle::new(open, high, low, close, volume, timestamp) {
        Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
        Err(_) => f64::NAN,
    }
}

/// Run over the OHLCV series (`open/high/low/close/volume/timestamp[0..n]`) into
/// `out[0..n]` (`NaN` at warmup or on an invalid candle).
///
/// # Safety
/// `handle` valid (from `wickra_wick_ratio_new`, not freed); every input pointer and `out`
/// cover `n` elements.
#[no_mangle]
pub unsafe extern "C" fn wickra_wick_ratio_batch(
    handle: *mut WickRatio,
    open: *const f64,
    high: *const f64,
    low: *const f64,
    close: *const f64,
    volume: *const f64,
    timestamp: *const i64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null()
        || open.is_null()
        || high.is_null()
        || low.is_null()
        || close.is_null()
        || volume.is_null()
        || timestamp.is_null()
        || out.is_null()
    {
        return;
    }
    let ind = &mut *handle;
    let opens = slice::from_raw_parts(open, n);
    let highs = slice::from_raw_parts(high, n);
    let lows = slice::from_raw_parts(low, n);
    let closes = slice::from_raw_parts(close, n);
    let volumes = slice::from_raw_parts(volume, n);
    let timestamps = slice::from_raw_parts(timestamp, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (idx, slot) in outputs.iter_mut().enumerate() {
        *slot = match Candle::new(
            opens[idx],
            highs[idx],
            lows[idx],
            closes[idx],
            volumes[idx],
            timestamps[idx],
        ) {
            Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
            Err(_) => f64::NAN,
        };
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_wick_ratio_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_wick_ratio_warmup_period(handle: *mut WickRatio) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_wick_ratio_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_wick_ratio_is_ready(handle: *mut WickRatio) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_wick_ratio_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_wick_ratio_name(handle: *mut WickRatio) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_wick_ratio_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_wick_ratio_reset(handle: *mut WickRatio) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_wick_ratio_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_wick_ratio_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_wick_ratio_free(handle: *mut WickRatio) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `WilliamsR` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_williams_r_free`.
#[no_mangle]
pub extern "C" fn wickra_williams_r_new(period: usize) -> *mut WilliamsR {
    match WilliamsR::new(period) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one OHLCV candle; returns the output, or `NaN` during warmup / on a `NULL`
/// handle / if the candle is invalid (e.g. `high < low`).
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_williams_r_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_williams_r_update(
    handle: *mut WilliamsR,
    open: f64,
    high: f64,
    low: f64,
    close: f64,
    volume: f64,
    timestamp: i64,
) -> f64 {
    let Some(ind) = handle.as_mut() else {
        return f64::NAN;
    };
    match Candle::new(open, high, low, close, volume, timestamp) {
        Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
        Err(_) => f64::NAN,
    }
}

/// Run over the OHLCV series (`open/high/low/close/volume/timestamp[0..n]`) into
/// `out[0..n]` (`NaN` at warmup or on an invalid candle).
///
/// # Safety
/// `handle` valid (from `wickra_williams_r_new`, not freed); every input pointer and `out`
/// cover `n` elements.
#[no_mangle]
pub unsafe extern "C" fn wickra_williams_r_batch(
    handle: *mut WilliamsR,
    open: *const f64,
    high: *const f64,
    low: *const f64,
    close: *const f64,
    volume: *const f64,
    timestamp: *const i64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null()
        || open.is_null()
        || high.is_null()
        || low.is_null()
        || close.is_null()
        || volume.is_null()
        || timestamp.is_null()
        || out.is_null()
    {
        return;
    }
    let ind = &mut *handle;
    let opens = slice::from_raw_parts(open, n);
    let highs = slice::from_raw_parts(high, n);
    let lows = slice::from_raw_parts(low, n);
    let closes = slice::from_raw_parts(close, n);
    let volumes = slice::from_raw_parts(volume, n);
    let timestamps = slice::from_raw_parts(timestamp, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (idx, slot) in outputs.iter_mut().enumerate() {
        *slot = match Candle::new(
            opens[idx],
            highs[idx],
            lows[idx],
            closes[idx],
            volumes[idx],
            timestamps[idx],
        ) {
            Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
            Err(_) => f64::NAN,
        };
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_williams_r_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_williams_r_warmup_period(handle: *mut WilliamsR) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_williams_r_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_williams_r_is_ready(handle: *mut WilliamsR) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_williams_r_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_williams_r_name(handle: *mut WilliamsR) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_williams_r_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_williams_r_reset(handle: *mut WilliamsR) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_williams_r_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_williams_r_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_williams_r_free(handle: *mut WilliamsR) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `YangZhangVolatility` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_yang_zhang_volatility_free`.
#[no_mangle]
pub extern "C" fn wickra_yang_zhang_volatility_new(
    period: usize,
    trading_periods: usize,
) -> *mut YangZhangVolatility {
    match YangZhangVolatility::new(period, trading_periods) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one OHLCV candle; returns the output, or `NaN` during warmup / on a `NULL`
/// handle / if the candle is invalid (e.g. `high < low`).
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_yang_zhang_volatility_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_yang_zhang_volatility_update(
    handle: *mut YangZhangVolatility,
    open: f64,
    high: f64,
    low: f64,
    close: f64,
    volume: f64,
    timestamp: i64,
) -> f64 {
    let Some(ind) = handle.as_mut() else {
        return f64::NAN;
    };
    match Candle::new(open, high, low, close, volume, timestamp) {
        Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
        Err(_) => f64::NAN,
    }
}

/// Run over the OHLCV series (`open/high/low/close/volume/timestamp[0..n]`) into
/// `out[0..n]` (`NaN` at warmup or on an invalid candle).
///
/// # Safety
/// `handle` valid (from `wickra_yang_zhang_volatility_new`, not freed); every input pointer and `out`
/// cover `n` elements.
#[no_mangle]
pub unsafe extern "C" fn wickra_yang_zhang_volatility_batch(
    handle: *mut YangZhangVolatility,
    open: *const f64,
    high: *const f64,
    low: *const f64,
    close: *const f64,
    volume: *const f64,
    timestamp: *const i64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null()
        || open.is_null()
        || high.is_null()
        || low.is_null()
        || close.is_null()
        || volume.is_null()
        || timestamp.is_null()
        || out.is_null()
    {
        return;
    }
    let ind = &mut *handle;
    let opens = slice::from_raw_parts(open, n);
    let highs = slice::from_raw_parts(high, n);
    let lows = slice::from_raw_parts(low, n);
    let closes = slice::from_raw_parts(close, n);
    let volumes = slice::from_raw_parts(volume, n);
    let timestamps = slice::from_raw_parts(timestamp, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (idx, slot) in outputs.iter_mut().enumerate() {
        *slot = match Candle::new(
            opens[idx],
            highs[idx],
            lows[idx],
            closes[idx],
            volumes[idx],
            timestamps[idx],
        ) {
            Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
            Err(_) => f64::NAN,
        };
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_yang_zhang_volatility_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_yang_zhang_volatility_warmup_period(
    handle: *mut YangZhangVolatility,
) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_yang_zhang_volatility_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_yang_zhang_volatility_is_ready(
    handle: *mut YangZhangVolatility,
) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_yang_zhang_volatility_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_yang_zhang_volatility_name(
    handle: *mut YangZhangVolatility,
) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_yang_zhang_volatility_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_yang_zhang_volatility_reset(handle: *mut YangZhangVolatility) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_yang_zhang_volatility_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_yang_zhang_volatility_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_yang_zhang_volatility_free(handle: *mut YangZhangVolatility) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `YoyoExit` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_yoyo_exit_free`.
#[no_mangle]
pub extern "C" fn wickra_yoyo_exit_new(atr_period: usize, multiplier: f64) -> *mut YoyoExit {
    match YoyoExit::new(atr_period, multiplier) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one OHLCV candle; returns the output, or `NaN` during warmup / on a `NULL`
/// handle / if the candle is invalid (e.g. `high < low`).
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_yoyo_exit_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_yoyo_exit_update(
    handle: *mut YoyoExit,
    open: f64,
    high: f64,
    low: f64,
    close: f64,
    volume: f64,
    timestamp: i64,
) -> f64 {
    let Some(ind) = handle.as_mut() else {
        return f64::NAN;
    };
    match Candle::new(open, high, low, close, volume, timestamp) {
        Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
        Err(_) => f64::NAN,
    }
}

/// Run over the OHLCV series (`open/high/low/close/volume/timestamp[0..n]`) into
/// `out[0..n]` (`NaN` at warmup or on an invalid candle).
///
/// # Safety
/// `handle` valid (from `wickra_yoyo_exit_new`, not freed); every input pointer and `out`
/// cover `n` elements.
#[no_mangle]
pub unsafe extern "C" fn wickra_yoyo_exit_batch(
    handle: *mut YoyoExit,
    open: *const f64,
    high: *const f64,
    low: *const f64,
    close: *const f64,
    volume: *const f64,
    timestamp: *const i64,
    out: *mut f64,
    n: usize,
) {
    if handle.is_null()
        || open.is_null()
        || high.is_null()
        || low.is_null()
        || close.is_null()
        || volume.is_null()
        || timestamp.is_null()
        || out.is_null()
    {
        return;
    }
    let ind = &mut *handle;
    let opens = slice::from_raw_parts(open, n);
    let highs = slice::from_raw_parts(high, n);
    let lows = slice::from_raw_parts(low, n);
    let closes = slice::from_raw_parts(close, n);
    let volumes = slice::from_raw_parts(volume, n);
    let timestamps = slice::from_raw_parts(timestamp, n);
    let outputs = slice::from_raw_parts_mut(out, n);
    for (idx, slot) in outputs.iter_mut().enumerate() {
        *slot = match Candle::new(
            opens[idx],
            highs[idx],
            lows[idx],
            closes[idx],
            volumes[idx],
            timestamps[idx],
        ) {
            Ok(candle) => ind.update(candle).unwrap_or(f64::NAN),
            Err(_) => f64::NAN,
        };
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_yoyo_exit_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_yoyo_exit_warmup_period(handle: *mut YoyoExit) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_yoyo_exit_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_yoyo_exit_is_ready(handle: *mut YoyoExit) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_yoyo_exit_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_yoyo_exit_name(handle: *mut YoyoExit) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_yoyo_exit_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_yoyo_exit_reset(handle: *mut YoyoExit) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_yoyo_exit_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_yoyo_exit_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_yoyo_exit_free(handle: *mut YoyoExit) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

// ===== Trade-input indicators (microstructure) =====

/// Create a `AmihudIlliquidity` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_amihud_illiquidity_free`.
#[no_mangle]
pub extern "C" fn wickra_amihud_illiquidity_new(period: usize) -> *mut AmihudIlliquidity {
    match AmihudIlliquidity::new(period) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one trade; returns the output, or `NaN` during warmup / on a `NULL` handle /
/// if the trade is invalid (non-positive price, negative size).
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_amihud_illiquidity_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_amihud_illiquidity_update(
    handle: *mut AmihudIlliquidity,
    price: f64,
    size: f64,
    is_buy: bool,
    timestamp: i64,
) -> f64 {
    let Some(ind) = handle.as_mut() else {
        return f64::NAN;
    };
    let side = if is_buy { Side::Buy } else { Side::Sell };
    match Trade::new(price, size, side, timestamp) {
        Ok(trade) => ind.update(trade).unwrap_or(f64::NAN),
        Err(_) => f64::NAN,
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_amihud_illiquidity_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_amihud_illiquidity_warmup_period(
    handle: *mut AmihudIlliquidity,
) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_amihud_illiquidity_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_amihud_illiquidity_is_ready(
    handle: *mut AmihudIlliquidity,
) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_amihud_illiquidity_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_amihud_illiquidity_name(
    handle: *mut AmihudIlliquidity,
) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_amihud_illiquidity_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_amihud_illiquidity_reset(handle: *mut AmihudIlliquidity) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_amihud_illiquidity_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_amihud_illiquidity_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_amihud_illiquidity_free(handle: *mut AmihudIlliquidity) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `CumulativeVolumeDelta` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_cumulative_volume_delta_free`.
#[no_mangle]
pub extern "C" fn wickra_cumulative_volume_delta_new() -> *mut CumulativeVolumeDelta {
    Box::into_raw(Box::new(CumulativeVolumeDelta::new()))
}

/// Feed one trade; returns the output, or `NaN` during warmup / on a `NULL` handle /
/// if the trade is invalid (non-positive price, negative size).
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_cumulative_volume_delta_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_cumulative_volume_delta_update(
    handle: *mut CumulativeVolumeDelta,
    price: f64,
    size: f64,
    is_buy: bool,
    timestamp: i64,
) -> f64 {
    let Some(ind) = handle.as_mut() else {
        return f64::NAN;
    };
    let side = if is_buy { Side::Buy } else { Side::Sell };
    match Trade::new(price, size, side, timestamp) {
        Ok(trade) => ind.update(trade).unwrap_or(f64::NAN),
        Err(_) => f64::NAN,
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_cumulative_volume_delta_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_cumulative_volume_delta_warmup_period(
    handle: *mut CumulativeVolumeDelta,
) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_cumulative_volume_delta_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_cumulative_volume_delta_is_ready(
    handle: *mut CumulativeVolumeDelta,
) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_cumulative_volume_delta_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_cumulative_volume_delta_name(
    handle: *mut CumulativeVolumeDelta,
) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_cumulative_volume_delta_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_cumulative_volume_delta_reset(handle: *mut CumulativeVolumeDelta) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_cumulative_volume_delta_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_cumulative_volume_delta_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_cumulative_volume_delta_free(handle: *mut CumulativeVolumeDelta) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `Pin` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_pin_free`.
#[no_mangle]
pub extern "C" fn wickra_pin_new(window: usize) -> *mut Pin {
    match Pin::new(window) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one trade; returns the output, or `NaN` during warmup / on a `NULL` handle /
/// if the trade is invalid (non-positive price, negative size).
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_pin_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_pin_update(
    handle: *mut Pin,
    price: f64,
    size: f64,
    is_buy: bool,
    timestamp: i64,
) -> f64 {
    let Some(ind) = handle.as_mut() else {
        return f64::NAN;
    };
    let side = if is_buy { Side::Buy } else { Side::Sell };
    match Trade::new(price, size, side, timestamp) {
        Ok(trade) => ind.update(trade).unwrap_or(f64::NAN),
        Err(_) => f64::NAN,
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_pin_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_pin_warmup_period(handle: *mut Pin) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_pin_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_pin_is_ready(handle: *mut Pin) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_pin_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_pin_name(handle: *mut Pin) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_pin_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_pin_reset(handle: *mut Pin) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_pin_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_pin_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_pin_free(handle: *mut Pin) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `RollMeasure` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_roll_measure_free`.
#[no_mangle]
pub extern "C" fn wickra_roll_measure_new(period: usize) -> *mut RollMeasure {
    match RollMeasure::new(period) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one trade; returns the output, or `NaN` during warmup / on a `NULL` handle /
/// if the trade is invalid (non-positive price, negative size).
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_roll_measure_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_roll_measure_update(
    handle: *mut RollMeasure,
    price: f64,
    size: f64,
    is_buy: bool,
    timestamp: i64,
) -> f64 {
    let Some(ind) = handle.as_mut() else {
        return f64::NAN;
    };
    let side = if is_buy { Side::Buy } else { Side::Sell };
    match Trade::new(price, size, side, timestamp) {
        Ok(trade) => ind.update(trade).unwrap_or(f64::NAN),
        Err(_) => f64::NAN,
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_roll_measure_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_roll_measure_warmup_period(handle: *mut RollMeasure) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_roll_measure_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_roll_measure_is_ready(handle: *mut RollMeasure) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_roll_measure_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_roll_measure_name(handle: *mut RollMeasure) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_roll_measure_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_roll_measure_reset(handle: *mut RollMeasure) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_roll_measure_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_roll_measure_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_roll_measure_free(handle: *mut RollMeasure) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `SignedVolume` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_signed_volume_free`.
#[no_mangle]
pub extern "C" fn wickra_signed_volume_new() -> *mut SignedVolume {
    Box::into_raw(Box::new(SignedVolume::new()))
}

/// Feed one trade; returns the output, or `NaN` during warmup / on a `NULL` handle /
/// if the trade is invalid (non-positive price, negative size).
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_signed_volume_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_signed_volume_update(
    handle: *mut SignedVolume,
    price: f64,
    size: f64,
    is_buy: bool,
    timestamp: i64,
) -> f64 {
    let Some(ind) = handle.as_mut() else {
        return f64::NAN;
    };
    let side = if is_buy { Side::Buy } else { Side::Sell };
    match Trade::new(price, size, side, timestamp) {
        Ok(trade) => ind.update(trade).unwrap_or(f64::NAN),
        Err(_) => f64::NAN,
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_signed_volume_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_signed_volume_warmup_period(handle: *mut SignedVolume) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_signed_volume_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_signed_volume_is_ready(handle: *mut SignedVolume) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_signed_volume_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_signed_volume_name(handle: *mut SignedVolume) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_signed_volume_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_signed_volume_reset(handle: *mut SignedVolume) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_signed_volume_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_signed_volume_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_signed_volume_free(handle: *mut SignedVolume) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `TradeImbalance` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_trade_imbalance_free`.
#[no_mangle]
pub extern "C" fn wickra_trade_imbalance_new(window: usize) -> *mut TradeImbalance {
    match TradeImbalance::new(window) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one trade; returns the output, or `NaN` during warmup / on a `NULL` handle /
/// if the trade is invalid (non-positive price, negative size).
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_trade_imbalance_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_trade_imbalance_update(
    handle: *mut TradeImbalance,
    price: f64,
    size: f64,
    is_buy: bool,
    timestamp: i64,
) -> f64 {
    let Some(ind) = handle.as_mut() else {
        return f64::NAN;
    };
    let side = if is_buy { Side::Buy } else { Side::Sell };
    match Trade::new(price, size, side, timestamp) {
        Ok(trade) => ind.update(trade).unwrap_or(f64::NAN),
        Err(_) => f64::NAN,
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_trade_imbalance_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_trade_imbalance_warmup_period(
    handle: *mut TradeImbalance,
) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_trade_imbalance_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_trade_imbalance_is_ready(handle: *mut TradeImbalance) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_trade_imbalance_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_trade_imbalance_name(handle: *mut TradeImbalance) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_trade_imbalance_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_trade_imbalance_reset(handle: *mut TradeImbalance) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_trade_imbalance_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_trade_imbalance_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_trade_imbalance_free(handle: *mut TradeImbalance) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `TradeSignAutocorrelation` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_trade_sign_autocorrelation_free`.
#[no_mangle]
pub extern "C" fn wickra_trade_sign_autocorrelation_new(
    period: usize,
) -> *mut TradeSignAutocorrelation {
    match TradeSignAutocorrelation::new(period) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one trade; returns the output, or `NaN` during warmup / on a `NULL` handle /
/// if the trade is invalid (non-positive price, negative size).
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_trade_sign_autocorrelation_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_trade_sign_autocorrelation_update(
    handle: *mut TradeSignAutocorrelation,
    price: f64,
    size: f64,
    is_buy: bool,
    timestamp: i64,
) -> f64 {
    let Some(ind) = handle.as_mut() else {
        return f64::NAN;
    };
    let side = if is_buy { Side::Buy } else { Side::Sell };
    match Trade::new(price, size, side, timestamp) {
        Ok(trade) => ind.update(trade).unwrap_or(f64::NAN),
        Err(_) => f64::NAN,
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_trade_sign_autocorrelation_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_trade_sign_autocorrelation_warmup_period(
    handle: *mut TradeSignAutocorrelation,
) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_trade_sign_autocorrelation_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_trade_sign_autocorrelation_is_ready(
    handle: *mut TradeSignAutocorrelation,
) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_trade_sign_autocorrelation_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_trade_sign_autocorrelation_name(
    handle: *mut TradeSignAutocorrelation,
) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_trade_sign_autocorrelation_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_trade_sign_autocorrelation_reset(
    handle: *mut TradeSignAutocorrelation,
) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_trade_sign_autocorrelation_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_trade_sign_autocorrelation_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_trade_sign_autocorrelation_free(
    handle: *mut TradeSignAutocorrelation,
) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `Vpin` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_vpin_free`.
#[no_mangle]
pub extern "C" fn wickra_vpin_new(bucket_volume: f64, num_buckets: usize) -> *mut Vpin {
    match Vpin::new(bucket_volume, num_buckets) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one trade; returns the output, or `NaN` during warmup / on a `NULL` handle /
/// if the trade is invalid (non-positive price, negative size).
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_vpin_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_vpin_update(
    handle: *mut Vpin,
    price: f64,
    size: f64,
    is_buy: bool,
    timestamp: i64,
) -> f64 {
    let Some(ind) = handle.as_mut() else {
        return f64::NAN;
    };
    let side = if is_buy { Side::Buy } else { Side::Sell };
    match Trade::new(price, size, side, timestamp) {
        Ok(trade) => ind.update(trade).unwrap_or(f64::NAN),
        Err(_) => f64::NAN,
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_vpin_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_vpin_warmup_period(handle: *mut Vpin) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_vpin_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_vpin_is_ready(handle: *mut Vpin) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_vpin_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_vpin_name(handle: *mut Vpin) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_vpin_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_vpin_reset(handle: *mut Vpin) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_vpin_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_vpin_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_vpin_free(handle: *mut Vpin) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

// ===== Trade+quote-input indicators (microstructure) =====

/// Create a `EffectiveSpread` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_effective_spread_free`.
#[no_mangle]
pub extern "C" fn wickra_effective_spread_new() -> *mut EffectiveSpread {
    Box::into_raw(Box::new(EffectiveSpread::new()))
}

/// Feed one trade with the prevailing mid price; returns the output, or `NaN` during
/// warmup / on a `NULL` handle / if the trade or mid is invalid.
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_effective_spread_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_effective_spread_update(
    handle: *mut EffectiveSpread,
    price: f64,
    size: f64,
    is_buy: bool,
    timestamp: i64,
    mid: f64,
) -> f64 {
    let Some(ind) = handle.as_mut() else {
        return f64::NAN;
    };
    let side = if is_buy { Side::Buy } else { Side::Sell };
    let Ok(trade) = Trade::new(price, size, side, timestamp) else {
        return f64::NAN;
    };
    match TradeQuote::new(trade, mid) {
        Ok(quote) => ind.update(quote).unwrap_or(f64::NAN),
        Err(_) => f64::NAN,
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_effective_spread_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_effective_spread_warmup_period(
    handle: *mut EffectiveSpread,
) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_effective_spread_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_effective_spread_is_ready(handle: *mut EffectiveSpread) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_effective_spread_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_effective_spread_name(
    handle: *mut EffectiveSpread,
) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_effective_spread_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_effective_spread_reset(handle: *mut EffectiveSpread) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_effective_spread_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_effective_spread_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_effective_spread_free(handle: *mut EffectiveSpread) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `KylesLambda` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_kyles_lambda_free`.
#[no_mangle]
pub extern "C" fn wickra_kyles_lambda_new(window: usize) -> *mut KylesLambda {
    match KylesLambda::new(window) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one trade with the prevailing mid price; returns the output, or `NaN` during
/// warmup / on a `NULL` handle / if the trade or mid is invalid.
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_kyles_lambda_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_kyles_lambda_update(
    handle: *mut KylesLambda,
    price: f64,
    size: f64,
    is_buy: bool,
    timestamp: i64,
    mid: f64,
) -> f64 {
    let Some(ind) = handle.as_mut() else {
        return f64::NAN;
    };
    let side = if is_buy { Side::Buy } else { Side::Sell };
    let Ok(trade) = Trade::new(price, size, side, timestamp) else {
        return f64::NAN;
    };
    match TradeQuote::new(trade, mid) {
        Ok(quote) => ind.update(quote).unwrap_or(f64::NAN),
        Err(_) => f64::NAN,
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_kyles_lambda_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_kyles_lambda_warmup_period(handle: *mut KylesLambda) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_kyles_lambda_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_kyles_lambda_is_ready(handle: *mut KylesLambda) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_kyles_lambda_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_kyles_lambda_name(handle: *mut KylesLambda) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_kyles_lambda_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_kyles_lambda_reset(handle: *mut KylesLambda) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_kyles_lambda_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_kyles_lambda_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_kyles_lambda_free(handle: *mut KylesLambda) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `RealizedSpread` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_realized_spread_free`.
#[no_mangle]
pub extern "C" fn wickra_realized_spread_new(horizon: usize) -> *mut RealizedSpread {
    match RealizedSpread::new(horizon) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one trade with the prevailing mid price; returns the output, or `NaN` during
/// warmup / on a `NULL` handle / if the trade or mid is invalid.
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_realized_spread_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_realized_spread_update(
    handle: *mut RealizedSpread,
    price: f64,
    size: f64,
    is_buy: bool,
    timestamp: i64,
    mid: f64,
) -> f64 {
    let Some(ind) = handle.as_mut() else {
        return f64::NAN;
    };
    let side = if is_buy { Side::Buy } else { Side::Sell };
    let Ok(trade) = Trade::new(price, size, side, timestamp) else {
        return f64::NAN;
    };
    match TradeQuote::new(trade, mid) {
        Ok(quote) => ind.update(quote).unwrap_or(f64::NAN),
        Err(_) => f64::NAN,
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_realized_spread_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_realized_spread_warmup_period(
    handle: *mut RealizedSpread,
) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_realized_spread_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_realized_spread_is_ready(handle: *mut RealizedSpread) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_realized_spread_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_realized_spread_name(handle: *mut RealizedSpread) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_realized_spread_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_realized_spread_reset(handle: *mut RealizedSpread) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_realized_spread_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_realized_spread_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_realized_spread_free(handle: *mut RealizedSpread) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

// ===== Derivatives-tick-input indicators =====

/// Create a `CalendarSpread` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_calendar_spread_free`.
#[no_mangle]
pub extern "C" fn wickra_calendar_spread_new() -> *mut CalendarSpread {
    Box::into_raw(Box::new(CalendarSpread::new()))
}

/// Feed one derivatives tick; returns the output, or `NaN` during warmup / on a
/// `NULL` handle / if the tick is invalid.
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_calendar_spread_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_calendar_spread_update(
    handle: *mut CalendarSpread,
    funding_rate: f64,
    mark_price: f64,
    index_price: f64,
    futures_price: f64,
    open_interest: f64,
    long_size: f64,
    short_size: f64,
    taker_buy_volume: f64,
    taker_sell_volume: f64,
    long_liquidation: f64,
    short_liquidation: f64,
    timestamp: i64,
) -> f64 {
    let Some(ind) = handle.as_mut() else {
        return f64::NAN;
    };
    match DerivativesTick::new(
        funding_rate,
        mark_price,
        index_price,
        futures_price,
        open_interest,
        long_size,
        short_size,
        taker_buy_volume,
        taker_sell_volume,
        long_liquidation,
        short_liquidation,
        timestamp,
    ) {
        Ok(tick) => ind.update(tick).unwrap_or(f64::NAN),
        Err(_) => f64::NAN,
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_calendar_spread_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_calendar_spread_warmup_period(
    handle: *mut CalendarSpread,
) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_calendar_spread_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_calendar_spread_is_ready(handle: *mut CalendarSpread) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_calendar_spread_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_calendar_spread_name(handle: *mut CalendarSpread) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_calendar_spread_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_calendar_spread_reset(handle: *mut CalendarSpread) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_calendar_spread_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_calendar_spread_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_calendar_spread_free(handle: *mut CalendarSpread) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `EstimatedLeverageRatio` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_estimated_leverage_ratio_free`.
#[no_mangle]
pub extern "C" fn wickra_estimated_leverage_ratio_new() -> *mut EstimatedLeverageRatio {
    Box::into_raw(Box::new(EstimatedLeverageRatio::new()))
}

/// Feed one derivatives tick; returns the output, or `NaN` during warmup / on a
/// `NULL` handle / if the tick is invalid.
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_estimated_leverage_ratio_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_estimated_leverage_ratio_update(
    handle: *mut EstimatedLeverageRatio,
    funding_rate: f64,
    mark_price: f64,
    index_price: f64,
    futures_price: f64,
    open_interest: f64,
    long_size: f64,
    short_size: f64,
    taker_buy_volume: f64,
    taker_sell_volume: f64,
    long_liquidation: f64,
    short_liquidation: f64,
    timestamp: i64,
) -> f64 {
    let Some(ind) = handle.as_mut() else {
        return f64::NAN;
    };
    match DerivativesTick::new(
        funding_rate,
        mark_price,
        index_price,
        futures_price,
        open_interest,
        long_size,
        short_size,
        taker_buy_volume,
        taker_sell_volume,
        long_liquidation,
        short_liquidation,
        timestamp,
    ) {
        Ok(tick) => ind.update(tick).unwrap_or(f64::NAN),
        Err(_) => f64::NAN,
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_estimated_leverage_ratio_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_estimated_leverage_ratio_warmup_period(
    handle: *mut EstimatedLeverageRatio,
) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_estimated_leverage_ratio_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_estimated_leverage_ratio_is_ready(
    handle: *mut EstimatedLeverageRatio,
) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_estimated_leverage_ratio_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_estimated_leverage_ratio_name(
    handle: *mut EstimatedLeverageRatio,
) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_estimated_leverage_ratio_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_estimated_leverage_ratio_reset(
    handle: *mut EstimatedLeverageRatio,
) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_estimated_leverage_ratio_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_estimated_leverage_ratio_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_estimated_leverage_ratio_free(handle: *mut EstimatedLeverageRatio) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `FundingBasis` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_funding_basis_free`.
#[no_mangle]
pub extern "C" fn wickra_funding_basis_new() -> *mut FundingBasis {
    Box::into_raw(Box::new(FundingBasis::new()))
}

/// Feed one derivatives tick; returns the output, or `NaN` during warmup / on a
/// `NULL` handle / if the tick is invalid.
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_funding_basis_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_funding_basis_update(
    handle: *mut FundingBasis,
    funding_rate: f64,
    mark_price: f64,
    index_price: f64,
    futures_price: f64,
    open_interest: f64,
    long_size: f64,
    short_size: f64,
    taker_buy_volume: f64,
    taker_sell_volume: f64,
    long_liquidation: f64,
    short_liquidation: f64,
    timestamp: i64,
) -> f64 {
    let Some(ind) = handle.as_mut() else {
        return f64::NAN;
    };
    match DerivativesTick::new(
        funding_rate,
        mark_price,
        index_price,
        futures_price,
        open_interest,
        long_size,
        short_size,
        taker_buy_volume,
        taker_sell_volume,
        long_liquidation,
        short_liquidation,
        timestamp,
    ) {
        Ok(tick) => ind.update(tick).unwrap_or(f64::NAN),
        Err(_) => f64::NAN,
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_funding_basis_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_funding_basis_warmup_period(handle: *mut FundingBasis) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_funding_basis_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_funding_basis_is_ready(handle: *mut FundingBasis) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_funding_basis_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_funding_basis_name(handle: *mut FundingBasis) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_funding_basis_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_funding_basis_reset(handle: *mut FundingBasis) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_funding_basis_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_funding_basis_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_funding_basis_free(handle: *mut FundingBasis) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `FundingImpliedApr` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_funding_implied_apr_free`.
#[no_mangle]
pub extern "C" fn wickra_funding_implied_apr_new(
    intervals_per_year: f64,
) -> *mut FundingImpliedApr {
    match FundingImpliedApr::new(intervals_per_year) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one derivatives tick; returns the output, or `NaN` during warmup / on a
/// `NULL` handle / if the tick is invalid.
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_funding_implied_apr_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_funding_implied_apr_update(
    handle: *mut FundingImpliedApr,
    funding_rate: f64,
    mark_price: f64,
    index_price: f64,
    futures_price: f64,
    open_interest: f64,
    long_size: f64,
    short_size: f64,
    taker_buy_volume: f64,
    taker_sell_volume: f64,
    long_liquidation: f64,
    short_liquidation: f64,
    timestamp: i64,
) -> f64 {
    let Some(ind) = handle.as_mut() else {
        return f64::NAN;
    };
    match DerivativesTick::new(
        funding_rate,
        mark_price,
        index_price,
        futures_price,
        open_interest,
        long_size,
        short_size,
        taker_buy_volume,
        taker_sell_volume,
        long_liquidation,
        short_liquidation,
        timestamp,
    ) {
        Ok(tick) => ind.update(tick).unwrap_or(f64::NAN),
        Err(_) => f64::NAN,
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_funding_implied_apr_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_funding_implied_apr_warmup_period(
    handle: *mut FundingImpliedApr,
) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_funding_implied_apr_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_funding_implied_apr_is_ready(
    handle: *mut FundingImpliedApr,
) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_funding_implied_apr_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_funding_implied_apr_name(
    handle: *mut FundingImpliedApr,
) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_funding_implied_apr_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_funding_implied_apr_reset(handle: *mut FundingImpliedApr) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_funding_implied_apr_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_funding_implied_apr_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_funding_implied_apr_free(handle: *mut FundingImpliedApr) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `FundingRate` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_funding_rate_free`.
#[no_mangle]
pub extern "C" fn wickra_funding_rate_new() -> *mut FundingRate {
    Box::into_raw(Box::new(FundingRate::new()))
}

/// Feed one derivatives tick; returns the output, or `NaN` during warmup / on a
/// `NULL` handle / if the tick is invalid.
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_funding_rate_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_funding_rate_update(
    handle: *mut FundingRate,
    funding_rate: f64,
    mark_price: f64,
    index_price: f64,
    futures_price: f64,
    open_interest: f64,
    long_size: f64,
    short_size: f64,
    taker_buy_volume: f64,
    taker_sell_volume: f64,
    long_liquidation: f64,
    short_liquidation: f64,
    timestamp: i64,
) -> f64 {
    let Some(ind) = handle.as_mut() else {
        return f64::NAN;
    };
    match DerivativesTick::new(
        funding_rate,
        mark_price,
        index_price,
        futures_price,
        open_interest,
        long_size,
        short_size,
        taker_buy_volume,
        taker_sell_volume,
        long_liquidation,
        short_liquidation,
        timestamp,
    ) {
        Ok(tick) => ind.update(tick).unwrap_or(f64::NAN),
        Err(_) => f64::NAN,
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_funding_rate_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_funding_rate_warmup_period(handle: *mut FundingRate) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_funding_rate_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_funding_rate_is_ready(handle: *mut FundingRate) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_funding_rate_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_funding_rate_name(handle: *mut FundingRate) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_funding_rate_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_funding_rate_reset(handle: *mut FundingRate) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_funding_rate_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_funding_rate_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_funding_rate_free(handle: *mut FundingRate) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `FundingRateMean` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_funding_rate_mean_free`.
#[no_mangle]
pub extern "C" fn wickra_funding_rate_mean_new(window: usize) -> *mut FundingRateMean {
    match FundingRateMean::new(window) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one derivatives tick; returns the output, or `NaN` during warmup / on a
/// `NULL` handle / if the tick is invalid.
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_funding_rate_mean_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_funding_rate_mean_update(
    handle: *mut FundingRateMean,
    funding_rate: f64,
    mark_price: f64,
    index_price: f64,
    futures_price: f64,
    open_interest: f64,
    long_size: f64,
    short_size: f64,
    taker_buy_volume: f64,
    taker_sell_volume: f64,
    long_liquidation: f64,
    short_liquidation: f64,
    timestamp: i64,
) -> f64 {
    let Some(ind) = handle.as_mut() else {
        return f64::NAN;
    };
    match DerivativesTick::new(
        funding_rate,
        mark_price,
        index_price,
        futures_price,
        open_interest,
        long_size,
        short_size,
        taker_buy_volume,
        taker_sell_volume,
        long_liquidation,
        short_liquidation,
        timestamp,
    ) {
        Ok(tick) => ind.update(tick).unwrap_or(f64::NAN),
        Err(_) => f64::NAN,
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_funding_rate_mean_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_funding_rate_mean_warmup_period(
    handle: *mut FundingRateMean,
) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_funding_rate_mean_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_funding_rate_mean_is_ready(handle: *mut FundingRateMean) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_funding_rate_mean_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_funding_rate_mean_name(
    handle: *mut FundingRateMean,
) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_funding_rate_mean_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_funding_rate_mean_reset(handle: *mut FundingRateMean) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_funding_rate_mean_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_funding_rate_mean_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_funding_rate_mean_free(handle: *mut FundingRateMean) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `FundingRateZScore` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_funding_rate_z_score_free`.
#[no_mangle]
pub extern "C" fn wickra_funding_rate_z_score_new(window: usize) -> *mut FundingRateZScore {
    match FundingRateZScore::new(window) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one derivatives tick; returns the output, or `NaN` during warmup / on a
/// `NULL` handle / if the tick is invalid.
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_funding_rate_z_score_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_funding_rate_z_score_update(
    handle: *mut FundingRateZScore,
    funding_rate: f64,
    mark_price: f64,
    index_price: f64,
    futures_price: f64,
    open_interest: f64,
    long_size: f64,
    short_size: f64,
    taker_buy_volume: f64,
    taker_sell_volume: f64,
    long_liquidation: f64,
    short_liquidation: f64,
    timestamp: i64,
) -> f64 {
    let Some(ind) = handle.as_mut() else {
        return f64::NAN;
    };
    match DerivativesTick::new(
        funding_rate,
        mark_price,
        index_price,
        futures_price,
        open_interest,
        long_size,
        short_size,
        taker_buy_volume,
        taker_sell_volume,
        long_liquidation,
        short_liquidation,
        timestamp,
    ) {
        Ok(tick) => ind.update(tick).unwrap_or(f64::NAN),
        Err(_) => f64::NAN,
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_funding_rate_z_score_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_funding_rate_z_score_warmup_period(
    handle: *mut FundingRateZScore,
) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_funding_rate_z_score_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_funding_rate_z_score_is_ready(
    handle: *mut FundingRateZScore,
) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_funding_rate_z_score_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_funding_rate_z_score_name(
    handle: *mut FundingRateZScore,
) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_funding_rate_z_score_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_funding_rate_z_score_reset(handle: *mut FundingRateZScore) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_funding_rate_z_score_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_funding_rate_z_score_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_funding_rate_z_score_free(handle: *mut FundingRateZScore) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `LongShortRatio` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_long_short_ratio_free`.
#[no_mangle]
pub extern "C" fn wickra_long_short_ratio_new() -> *mut LongShortRatio {
    Box::into_raw(Box::new(LongShortRatio::new()))
}

/// Feed one derivatives tick; returns the output, or `NaN` during warmup / on a
/// `NULL` handle / if the tick is invalid.
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_long_short_ratio_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_long_short_ratio_update(
    handle: *mut LongShortRatio,
    funding_rate: f64,
    mark_price: f64,
    index_price: f64,
    futures_price: f64,
    open_interest: f64,
    long_size: f64,
    short_size: f64,
    taker_buy_volume: f64,
    taker_sell_volume: f64,
    long_liquidation: f64,
    short_liquidation: f64,
    timestamp: i64,
) -> f64 {
    let Some(ind) = handle.as_mut() else {
        return f64::NAN;
    };
    match DerivativesTick::new(
        funding_rate,
        mark_price,
        index_price,
        futures_price,
        open_interest,
        long_size,
        short_size,
        taker_buy_volume,
        taker_sell_volume,
        long_liquidation,
        short_liquidation,
        timestamp,
    ) {
        Ok(tick) => ind.update(tick).unwrap_or(f64::NAN),
        Err(_) => f64::NAN,
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_long_short_ratio_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_long_short_ratio_warmup_period(
    handle: *mut LongShortRatio,
) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_long_short_ratio_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_long_short_ratio_is_ready(handle: *mut LongShortRatio) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_long_short_ratio_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_long_short_ratio_name(
    handle: *mut LongShortRatio,
) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_long_short_ratio_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_long_short_ratio_reset(handle: *mut LongShortRatio) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_long_short_ratio_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_long_short_ratio_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_long_short_ratio_free(handle: *mut LongShortRatio) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `OpenInterestDelta` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_open_interest_delta_free`.
#[no_mangle]
pub extern "C" fn wickra_open_interest_delta_new() -> *mut OpenInterestDelta {
    Box::into_raw(Box::new(OpenInterestDelta::new()))
}

/// Feed one derivatives tick; returns the output, or `NaN` during warmup / on a
/// `NULL` handle / if the tick is invalid.
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_open_interest_delta_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_open_interest_delta_update(
    handle: *mut OpenInterestDelta,
    funding_rate: f64,
    mark_price: f64,
    index_price: f64,
    futures_price: f64,
    open_interest: f64,
    long_size: f64,
    short_size: f64,
    taker_buy_volume: f64,
    taker_sell_volume: f64,
    long_liquidation: f64,
    short_liquidation: f64,
    timestamp: i64,
) -> f64 {
    let Some(ind) = handle.as_mut() else {
        return f64::NAN;
    };
    match DerivativesTick::new(
        funding_rate,
        mark_price,
        index_price,
        futures_price,
        open_interest,
        long_size,
        short_size,
        taker_buy_volume,
        taker_sell_volume,
        long_liquidation,
        short_liquidation,
        timestamp,
    ) {
        Ok(tick) => ind.update(tick).unwrap_or(f64::NAN),
        Err(_) => f64::NAN,
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_open_interest_delta_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_open_interest_delta_warmup_period(
    handle: *mut OpenInterestDelta,
) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_open_interest_delta_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_open_interest_delta_is_ready(
    handle: *mut OpenInterestDelta,
) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_open_interest_delta_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_open_interest_delta_name(
    handle: *mut OpenInterestDelta,
) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_open_interest_delta_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_open_interest_delta_reset(handle: *mut OpenInterestDelta) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_open_interest_delta_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_open_interest_delta_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_open_interest_delta_free(handle: *mut OpenInterestDelta) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `OIPriceDivergence` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_oi_price_divergence_free`.
#[no_mangle]
pub extern "C" fn wickra_oi_price_divergence_new(window: usize) -> *mut OIPriceDivergence {
    match OIPriceDivergence::new(window) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one derivatives tick; returns the output, or `NaN` during warmup / on a
/// `NULL` handle / if the tick is invalid.
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_oi_price_divergence_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_oi_price_divergence_update(
    handle: *mut OIPriceDivergence,
    funding_rate: f64,
    mark_price: f64,
    index_price: f64,
    futures_price: f64,
    open_interest: f64,
    long_size: f64,
    short_size: f64,
    taker_buy_volume: f64,
    taker_sell_volume: f64,
    long_liquidation: f64,
    short_liquidation: f64,
    timestamp: i64,
) -> f64 {
    let Some(ind) = handle.as_mut() else {
        return f64::NAN;
    };
    match DerivativesTick::new(
        funding_rate,
        mark_price,
        index_price,
        futures_price,
        open_interest,
        long_size,
        short_size,
        taker_buy_volume,
        taker_sell_volume,
        long_liquidation,
        short_liquidation,
        timestamp,
    ) {
        Ok(tick) => ind.update(tick).unwrap_or(f64::NAN),
        Err(_) => f64::NAN,
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_oi_price_divergence_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_oi_price_divergence_warmup_period(
    handle: *mut OIPriceDivergence,
) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_oi_price_divergence_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_oi_price_divergence_is_ready(
    handle: *mut OIPriceDivergence,
) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_oi_price_divergence_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_oi_price_divergence_name(
    handle: *mut OIPriceDivergence,
) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_oi_price_divergence_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_oi_price_divergence_reset(handle: *mut OIPriceDivergence) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_oi_price_divergence_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_oi_price_divergence_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_oi_price_divergence_free(handle: *mut OIPriceDivergence) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `OiToVolumeRatio` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_oi_to_volume_ratio_free`.
#[no_mangle]
pub extern "C" fn wickra_oi_to_volume_ratio_new() -> *mut OiToVolumeRatio {
    Box::into_raw(Box::new(OiToVolumeRatio::new()))
}

/// Feed one derivatives tick; returns the output, or `NaN` during warmup / on a
/// `NULL` handle / if the tick is invalid.
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_oi_to_volume_ratio_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_oi_to_volume_ratio_update(
    handle: *mut OiToVolumeRatio,
    funding_rate: f64,
    mark_price: f64,
    index_price: f64,
    futures_price: f64,
    open_interest: f64,
    long_size: f64,
    short_size: f64,
    taker_buy_volume: f64,
    taker_sell_volume: f64,
    long_liquidation: f64,
    short_liquidation: f64,
    timestamp: i64,
) -> f64 {
    let Some(ind) = handle.as_mut() else {
        return f64::NAN;
    };
    match DerivativesTick::new(
        funding_rate,
        mark_price,
        index_price,
        futures_price,
        open_interest,
        long_size,
        short_size,
        taker_buy_volume,
        taker_sell_volume,
        long_liquidation,
        short_liquidation,
        timestamp,
    ) {
        Ok(tick) => ind.update(tick).unwrap_or(f64::NAN),
        Err(_) => f64::NAN,
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_oi_to_volume_ratio_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_oi_to_volume_ratio_warmup_period(
    handle: *mut OiToVolumeRatio,
) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_oi_to_volume_ratio_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_oi_to_volume_ratio_is_ready(handle: *mut OiToVolumeRatio) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_oi_to_volume_ratio_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_oi_to_volume_ratio_name(
    handle: *mut OiToVolumeRatio,
) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_oi_to_volume_ratio_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_oi_to_volume_ratio_reset(handle: *mut OiToVolumeRatio) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_oi_to_volume_ratio_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_oi_to_volume_ratio_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_oi_to_volume_ratio_free(handle: *mut OiToVolumeRatio) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `OIWeighted` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_oi_weighted_free`.
#[no_mangle]
pub extern "C" fn wickra_oi_weighted_new() -> *mut OIWeighted {
    Box::into_raw(Box::new(OIWeighted::new()))
}

/// Feed one derivatives tick; returns the output, or `NaN` during warmup / on a
/// `NULL` handle / if the tick is invalid.
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_oi_weighted_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_oi_weighted_update(
    handle: *mut OIWeighted,
    funding_rate: f64,
    mark_price: f64,
    index_price: f64,
    futures_price: f64,
    open_interest: f64,
    long_size: f64,
    short_size: f64,
    taker_buy_volume: f64,
    taker_sell_volume: f64,
    long_liquidation: f64,
    short_liquidation: f64,
    timestamp: i64,
) -> f64 {
    let Some(ind) = handle.as_mut() else {
        return f64::NAN;
    };
    match DerivativesTick::new(
        funding_rate,
        mark_price,
        index_price,
        futures_price,
        open_interest,
        long_size,
        short_size,
        taker_buy_volume,
        taker_sell_volume,
        long_liquidation,
        short_liquidation,
        timestamp,
    ) {
        Ok(tick) => ind.update(tick).unwrap_or(f64::NAN),
        Err(_) => f64::NAN,
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_oi_weighted_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_oi_weighted_warmup_period(handle: *mut OIWeighted) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_oi_weighted_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_oi_weighted_is_ready(handle: *mut OIWeighted) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_oi_weighted_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_oi_weighted_name(handle: *mut OIWeighted) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_oi_weighted_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_oi_weighted_reset(handle: *mut OIWeighted) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_oi_weighted_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_oi_weighted_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_oi_weighted_free(handle: *mut OIWeighted) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `OpenInterestMomentum` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_open_interest_momentum_free`.
#[no_mangle]
pub extern "C" fn wickra_open_interest_momentum_new(period: usize) -> *mut OpenInterestMomentum {
    match OpenInterestMomentum::new(period) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one derivatives tick; returns the output, or `NaN` during warmup / on a
/// `NULL` handle / if the tick is invalid.
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_open_interest_momentum_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_open_interest_momentum_update(
    handle: *mut OpenInterestMomentum,
    funding_rate: f64,
    mark_price: f64,
    index_price: f64,
    futures_price: f64,
    open_interest: f64,
    long_size: f64,
    short_size: f64,
    taker_buy_volume: f64,
    taker_sell_volume: f64,
    long_liquidation: f64,
    short_liquidation: f64,
    timestamp: i64,
) -> f64 {
    let Some(ind) = handle.as_mut() else {
        return f64::NAN;
    };
    match DerivativesTick::new(
        funding_rate,
        mark_price,
        index_price,
        futures_price,
        open_interest,
        long_size,
        short_size,
        taker_buy_volume,
        taker_sell_volume,
        long_liquidation,
        short_liquidation,
        timestamp,
    ) {
        Ok(tick) => ind.update(tick).unwrap_or(f64::NAN),
        Err(_) => f64::NAN,
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_open_interest_momentum_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_open_interest_momentum_warmup_period(
    handle: *mut OpenInterestMomentum,
) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_open_interest_momentum_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_open_interest_momentum_is_ready(
    handle: *mut OpenInterestMomentum,
) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_open_interest_momentum_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_open_interest_momentum_name(
    handle: *mut OpenInterestMomentum,
) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_open_interest_momentum_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_open_interest_momentum_reset(handle: *mut OpenInterestMomentum) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_open_interest_momentum_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_open_interest_momentum_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_open_interest_momentum_free(handle: *mut OpenInterestMomentum) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `PerpetualPremiumIndex` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_perpetual_premium_index_free`.
#[no_mangle]
pub extern "C" fn wickra_perpetual_premium_index_new() -> *mut PerpetualPremiumIndex {
    Box::into_raw(Box::new(PerpetualPremiumIndex::new()))
}

/// Feed one derivatives tick; returns the output, or `NaN` during warmup / on a
/// `NULL` handle / if the tick is invalid.
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_perpetual_premium_index_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_perpetual_premium_index_update(
    handle: *mut PerpetualPremiumIndex,
    funding_rate: f64,
    mark_price: f64,
    index_price: f64,
    futures_price: f64,
    open_interest: f64,
    long_size: f64,
    short_size: f64,
    taker_buy_volume: f64,
    taker_sell_volume: f64,
    long_liquidation: f64,
    short_liquidation: f64,
    timestamp: i64,
) -> f64 {
    let Some(ind) = handle.as_mut() else {
        return f64::NAN;
    };
    match DerivativesTick::new(
        funding_rate,
        mark_price,
        index_price,
        futures_price,
        open_interest,
        long_size,
        short_size,
        taker_buy_volume,
        taker_sell_volume,
        long_liquidation,
        short_liquidation,
        timestamp,
    ) {
        Ok(tick) => ind.update(tick).unwrap_or(f64::NAN),
        Err(_) => f64::NAN,
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_perpetual_premium_index_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_perpetual_premium_index_warmup_period(
    handle: *mut PerpetualPremiumIndex,
) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_perpetual_premium_index_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_perpetual_premium_index_is_ready(
    handle: *mut PerpetualPremiumIndex,
) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_perpetual_premium_index_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_perpetual_premium_index_name(
    handle: *mut PerpetualPremiumIndex,
) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_perpetual_premium_index_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_perpetual_premium_index_reset(handle: *mut PerpetualPremiumIndex) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_perpetual_premium_index_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_perpetual_premium_index_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_perpetual_premium_index_free(handle: *mut PerpetualPremiumIndex) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `TakerBuySellRatio` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_taker_buy_sell_ratio_free`.
#[no_mangle]
pub extern "C" fn wickra_taker_buy_sell_ratio_new() -> *mut TakerBuySellRatio {
    Box::into_raw(Box::new(TakerBuySellRatio::new()))
}

/// Feed one derivatives tick; returns the output, or `NaN` during warmup / on a
/// `NULL` handle / if the tick is invalid.
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_taker_buy_sell_ratio_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_taker_buy_sell_ratio_update(
    handle: *mut TakerBuySellRatio,
    funding_rate: f64,
    mark_price: f64,
    index_price: f64,
    futures_price: f64,
    open_interest: f64,
    long_size: f64,
    short_size: f64,
    taker_buy_volume: f64,
    taker_sell_volume: f64,
    long_liquidation: f64,
    short_liquidation: f64,
    timestamp: i64,
) -> f64 {
    let Some(ind) = handle.as_mut() else {
        return f64::NAN;
    };
    match DerivativesTick::new(
        funding_rate,
        mark_price,
        index_price,
        futures_price,
        open_interest,
        long_size,
        short_size,
        taker_buy_volume,
        taker_sell_volume,
        long_liquidation,
        short_liquidation,
        timestamp,
    ) {
        Ok(tick) => ind.update(tick).unwrap_or(f64::NAN),
        Err(_) => f64::NAN,
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_taker_buy_sell_ratio_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_taker_buy_sell_ratio_warmup_period(
    handle: *mut TakerBuySellRatio,
) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_taker_buy_sell_ratio_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_taker_buy_sell_ratio_is_ready(
    handle: *mut TakerBuySellRatio,
) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_taker_buy_sell_ratio_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_taker_buy_sell_ratio_name(
    handle: *mut TakerBuySellRatio,
) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_taker_buy_sell_ratio_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_taker_buy_sell_ratio_reset(handle: *mut TakerBuySellRatio) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_taker_buy_sell_ratio_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_taker_buy_sell_ratio_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_taker_buy_sell_ratio_free(handle: *mut TakerBuySellRatio) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `TermStructureBasis` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_term_structure_basis_free`.
#[no_mangle]
pub extern "C" fn wickra_term_structure_basis_new() -> *mut TermStructureBasis {
    Box::into_raw(Box::new(TermStructureBasis::new()))
}

/// Feed one derivatives tick; returns the output, or `NaN` during warmup / on a
/// `NULL` handle / if the tick is invalid.
///
/// # Safety
/// `handle` must be a valid pointer from `wickra_term_structure_basis_new` (not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_term_structure_basis_update(
    handle: *mut TermStructureBasis,
    funding_rate: f64,
    mark_price: f64,
    index_price: f64,
    futures_price: f64,
    open_interest: f64,
    long_size: f64,
    short_size: f64,
    taker_buy_volume: f64,
    taker_sell_volume: f64,
    long_liquidation: f64,
    short_liquidation: f64,
    timestamp: i64,
) -> f64 {
    let Some(ind) = handle.as_mut() else {
        return f64::NAN;
    };
    match DerivativesTick::new(
        funding_rate,
        mark_price,
        index_price,
        futures_price,
        open_interest,
        long_size,
        short_size,
        taker_buy_volume,
        taker_sell_volume,
        long_liquidation,
        short_liquidation,
        timestamp,
    ) {
        Ok(tick) => ind.update(tick).unwrap_or(f64::NAN),
        Err(_) => f64::NAN,
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_term_structure_basis_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_term_structure_basis_warmup_period(
    handle: *mut TermStructureBasis,
) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_term_structure_basis_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_term_structure_basis_is_ready(
    handle: *mut TermStructureBasis,
) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_term_structure_basis_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_term_structure_basis_name(
    handle: *mut TermStructureBasis,
) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_term_structure_basis_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_term_structure_basis_reset(handle: *mut TermStructureBasis) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_term_structure_basis_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_term_structure_basis_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_term_structure_basis_free(handle: *mut TermStructureBasis) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

// ===== Order-book-input indicators (microstructure) =====

/// Create a `DepthSlope` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_depth_slope_free`.
#[no_mangle]
pub extern "C" fn wickra_depth_slope_new() -> *mut DepthSlope {
    Box::into_raw(Box::new(DepthSlope::new()))
}

/// Feed one order-book snapshot as parallel bid/ask price+size arrays; returns the
/// output, or `NaN` during warmup / on a `NULL` handle / if a level or the book is invalid.
///
/// # Safety
/// `handle` valid (from `wickra_depth_slope_new`, not freed) or `NULL`. `bid_price`/`bid_size`
/// each cover `n_bids`; `ask_price`/`ask_size` each cover `n_asks`.
#[no_mangle]
pub unsafe extern "C" fn wickra_depth_slope_update(
    handle: *mut DepthSlope,
    bid_price: *const f64,
    bid_size: *const f64,
    n_bids: usize,
    ask_price: *const f64,
    ask_size: *const f64,
    n_asks: usize,
) -> f64 {
    let Some(ind) = handle.as_mut() else {
        return f64::NAN;
    };
    if bid_price.is_null() || bid_size.is_null() || ask_price.is_null() || ask_size.is_null() {
        return f64::NAN;
    }
    let bid_px = slice::from_raw_parts(bid_price, n_bids);
    let bid_sz = slice::from_raw_parts(bid_size, n_bids);
    let ask_px = slice::from_raw_parts(ask_price, n_asks);
    let ask_sz = slice::from_raw_parts(ask_size, n_asks);
    let mut bids = Vec::with_capacity(n_bids);
    for (&px, &sz) in bid_px.iter().zip(bid_sz) {
        let Ok(level) = Level::new(px, sz) else {
            return f64::NAN;
        };
        bids.push(level);
    }
    let mut asks = Vec::with_capacity(n_asks);
    for (&px, &sz) in ask_px.iter().zip(ask_sz) {
        let Ok(level) = Level::new(px, sz) else {
            return f64::NAN;
        };
        asks.push(level);
    }
    match OrderBook::new(bids, asks) {
        Ok(book) => ind.update(book).unwrap_or(f64::NAN),
        Err(_) => f64::NAN,
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_depth_slope_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_depth_slope_warmup_period(handle: *mut DepthSlope) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_depth_slope_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_depth_slope_is_ready(handle: *mut DepthSlope) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_depth_slope_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_depth_slope_name(handle: *mut DepthSlope) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_depth_slope_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_depth_slope_reset(handle: *mut DepthSlope) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_depth_slope_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_depth_slope_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_depth_slope_free(handle: *mut DepthSlope) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `Microprice` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_microprice_free`.
#[no_mangle]
pub extern "C" fn wickra_microprice_new() -> *mut Microprice {
    Box::into_raw(Box::new(Microprice::new()))
}

/// Feed one order-book snapshot as parallel bid/ask price+size arrays; returns the
/// output, or `NaN` during warmup / on a `NULL` handle / if a level or the book is invalid.
///
/// # Safety
/// `handle` valid (from `wickra_microprice_new`, not freed) or `NULL`. `bid_price`/`bid_size`
/// each cover `n_bids`; `ask_price`/`ask_size` each cover `n_asks`.
#[no_mangle]
pub unsafe extern "C" fn wickra_microprice_update(
    handle: *mut Microprice,
    bid_price: *const f64,
    bid_size: *const f64,
    n_bids: usize,
    ask_price: *const f64,
    ask_size: *const f64,
    n_asks: usize,
) -> f64 {
    let Some(ind) = handle.as_mut() else {
        return f64::NAN;
    };
    if bid_price.is_null() || bid_size.is_null() || ask_price.is_null() || ask_size.is_null() {
        return f64::NAN;
    }
    let bid_px = slice::from_raw_parts(bid_price, n_bids);
    let bid_sz = slice::from_raw_parts(bid_size, n_bids);
    let ask_px = slice::from_raw_parts(ask_price, n_asks);
    let ask_sz = slice::from_raw_parts(ask_size, n_asks);
    let mut bids = Vec::with_capacity(n_bids);
    for (&px, &sz) in bid_px.iter().zip(bid_sz) {
        let Ok(level) = Level::new(px, sz) else {
            return f64::NAN;
        };
        bids.push(level);
    }
    let mut asks = Vec::with_capacity(n_asks);
    for (&px, &sz) in ask_px.iter().zip(ask_sz) {
        let Ok(level) = Level::new(px, sz) else {
            return f64::NAN;
        };
        asks.push(level);
    }
    match OrderBook::new(bids, asks) {
        Ok(book) => ind.update(book).unwrap_or(f64::NAN),
        Err(_) => f64::NAN,
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_microprice_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_microprice_warmup_period(handle: *mut Microprice) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_microprice_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_microprice_is_ready(handle: *mut Microprice) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_microprice_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_microprice_name(handle: *mut Microprice) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_microprice_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_microprice_reset(handle: *mut Microprice) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_microprice_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_microprice_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_microprice_free(handle: *mut Microprice) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `OrderBookImbalanceFull` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_order_book_imbalance_full_free`.
#[no_mangle]
pub extern "C" fn wickra_order_book_imbalance_full_new() -> *mut OrderBookImbalanceFull {
    Box::into_raw(Box::new(OrderBookImbalanceFull::new()))
}

/// Feed one order-book snapshot as parallel bid/ask price+size arrays; returns the
/// output, or `NaN` during warmup / on a `NULL` handle / if a level or the book is invalid.
///
/// # Safety
/// `handle` valid (from `wickra_order_book_imbalance_full_new`, not freed) or `NULL`. `bid_price`/`bid_size`
/// each cover `n_bids`; `ask_price`/`ask_size` each cover `n_asks`.
#[no_mangle]
pub unsafe extern "C" fn wickra_order_book_imbalance_full_update(
    handle: *mut OrderBookImbalanceFull,
    bid_price: *const f64,
    bid_size: *const f64,
    n_bids: usize,
    ask_price: *const f64,
    ask_size: *const f64,
    n_asks: usize,
) -> f64 {
    let Some(ind) = handle.as_mut() else {
        return f64::NAN;
    };
    if bid_price.is_null() || bid_size.is_null() || ask_price.is_null() || ask_size.is_null() {
        return f64::NAN;
    }
    let bid_px = slice::from_raw_parts(bid_price, n_bids);
    let bid_sz = slice::from_raw_parts(bid_size, n_bids);
    let ask_px = slice::from_raw_parts(ask_price, n_asks);
    let ask_sz = slice::from_raw_parts(ask_size, n_asks);
    let mut bids = Vec::with_capacity(n_bids);
    for (&px, &sz) in bid_px.iter().zip(bid_sz) {
        let Ok(level) = Level::new(px, sz) else {
            return f64::NAN;
        };
        bids.push(level);
    }
    let mut asks = Vec::with_capacity(n_asks);
    for (&px, &sz) in ask_px.iter().zip(ask_sz) {
        let Ok(level) = Level::new(px, sz) else {
            return f64::NAN;
        };
        asks.push(level);
    }
    match OrderBook::new(bids, asks) {
        Ok(book) => ind.update(book).unwrap_or(f64::NAN),
        Err(_) => f64::NAN,
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_order_book_imbalance_full_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_order_book_imbalance_full_warmup_period(
    handle: *mut OrderBookImbalanceFull,
) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_order_book_imbalance_full_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_order_book_imbalance_full_is_ready(
    handle: *mut OrderBookImbalanceFull,
) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_order_book_imbalance_full_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_order_book_imbalance_full_name(
    handle: *mut OrderBookImbalanceFull,
) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_order_book_imbalance_full_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_order_book_imbalance_full_reset(
    handle: *mut OrderBookImbalanceFull,
) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_order_book_imbalance_full_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_order_book_imbalance_full_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_order_book_imbalance_full_free(
    handle: *mut OrderBookImbalanceFull,
) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `OrderBookImbalanceTop1` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_order_book_imbalance_top1_free`.
#[no_mangle]
pub extern "C" fn wickra_order_book_imbalance_top1_new() -> *mut OrderBookImbalanceTop1 {
    Box::into_raw(Box::new(OrderBookImbalanceTop1::new()))
}

/// Feed one order-book snapshot as parallel bid/ask price+size arrays; returns the
/// output, or `NaN` during warmup / on a `NULL` handle / if a level or the book is invalid.
///
/// # Safety
/// `handle` valid (from `wickra_order_book_imbalance_top1_new`, not freed) or `NULL`. `bid_price`/`bid_size`
/// each cover `n_bids`; `ask_price`/`ask_size` each cover `n_asks`.
#[no_mangle]
pub unsafe extern "C" fn wickra_order_book_imbalance_top1_update(
    handle: *mut OrderBookImbalanceTop1,
    bid_price: *const f64,
    bid_size: *const f64,
    n_bids: usize,
    ask_price: *const f64,
    ask_size: *const f64,
    n_asks: usize,
) -> f64 {
    let Some(ind) = handle.as_mut() else {
        return f64::NAN;
    };
    if bid_price.is_null() || bid_size.is_null() || ask_price.is_null() || ask_size.is_null() {
        return f64::NAN;
    }
    let bid_px = slice::from_raw_parts(bid_price, n_bids);
    let bid_sz = slice::from_raw_parts(bid_size, n_bids);
    let ask_px = slice::from_raw_parts(ask_price, n_asks);
    let ask_sz = slice::from_raw_parts(ask_size, n_asks);
    let mut bids = Vec::with_capacity(n_bids);
    for (&px, &sz) in bid_px.iter().zip(bid_sz) {
        let Ok(level) = Level::new(px, sz) else {
            return f64::NAN;
        };
        bids.push(level);
    }
    let mut asks = Vec::with_capacity(n_asks);
    for (&px, &sz) in ask_px.iter().zip(ask_sz) {
        let Ok(level) = Level::new(px, sz) else {
            return f64::NAN;
        };
        asks.push(level);
    }
    match OrderBook::new(bids, asks) {
        Ok(book) => ind.update(book).unwrap_or(f64::NAN),
        Err(_) => f64::NAN,
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_order_book_imbalance_top1_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_order_book_imbalance_top1_warmup_period(
    handle: *mut OrderBookImbalanceTop1,
) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_order_book_imbalance_top1_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_order_book_imbalance_top1_is_ready(
    handle: *mut OrderBookImbalanceTop1,
) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_order_book_imbalance_top1_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_order_book_imbalance_top1_name(
    handle: *mut OrderBookImbalanceTop1,
) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_order_book_imbalance_top1_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_order_book_imbalance_top1_reset(
    handle: *mut OrderBookImbalanceTop1,
) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_order_book_imbalance_top1_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_order_book_imbalance_top1_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_order_book_imbalance_top1_free(
    handle: *mut OrderBookImbalanceTop1,
) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `OrderBookImbalanceTopN` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_order_book_imbalance_top_n_free`.
#[no_mangle]
pub extern "C" fn wickra_order_book_imbalance_top_n_new(
    levels: usize,
) -> *mut OrderBookImbalanceTopN {
    match OrderBookImbalanceTopN::new(levels) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one order-book snapshot as parallel bid/ask price+size arrays; returns the
/// output, or `NaN` during warmup / on a `NULL` handle / if a level or the book is invalid.
///
/// # Safety
/// `handle` valid (from `wickra_order_book_imbalance_top_n_new`, not freed) or `NULL`. `bid_price`/`bid_size`
/// each cover `n_bids`; `ask_price`/`ask_size` each cover `n_asks`.
#[no_mangle]
pub unsafe extern "C" fn wickra_order_book_imbalance_top_n_update(
    handle: *mut OrderBookImbalanceTopN,
    bid_price: *const f64,
    bid_size: *const f64,
    n_bids: usize,
    ask_price: *const f64,
    ask_size: *const f64,
    n_asks: usize,
) -> f64 {
    let Some(ind) = handle.as_mut() else {
        return f64::NAN;
    };
    if bid_price.is_null() || bid_size.is_null() || ask_price.is_null() || ask_size.is_null() {
        return f64::NAN;
    }
    let bid_px = slice::from_raw_parts(bid_price, n_bids);
    let bid_sz = slice::from_raw_parts(bid_size, n_bids);
    let ask_px = slice::from_raw_parts(ask_price, n_asks);
    let ask_sz = slice::from_raw_parts(ask_size, n_asks);
    let mut bids = Vec::with_capacity(n_bids);
    for (&px, &sz) in bid_px.iter().zip(bid_sz) {
        let Ok(level) = Level::new(px, sz) else {
            return f64::NAN;
        };
        bids.push(level);
    }
    let mut asks = Vec::with_capacity(n_asks);
    for (&px, &sz) in ask_px.iter().zip(ask_sz) {
        let Ok(level) = Level::new(px, sz) else {
            return f64::NAN;
        };
        asks.push(level);
    }
    match OrderBook::new(bids, asks) {
        Ok(book) => ind.update(book).unwrap_or(f64::NAN),
        Err(_) => f64::NAN,
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_order_book_imbalance_top_n_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_order_book_imbalance_top_n_warmup_period(
    handle: *mut OrderBookImbalanceTopN,
) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_order_book_imbalance_top_n_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_order_book_imbalance_top_n_is_ready(
    handle: *mut OrderBookImbalanceTopN,
) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_order_book_imbalance_top_n_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_order_book_imbalance_top_n_name(
    handle: *mut OrderBookImbalanceTopN,
) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_order_book_imbalance_top_n_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_order_book_imbalance_top_n_reset(
    handle: *mut OrderBookImbalanceTopN,
) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_order_book_imbalance_top_n_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_order_book_imbalance_top_n_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_order_book_imbalance_top_n_free(
    handle: *mut OrderBookImbalanceTopN,
) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `OrderFlowImbalance` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_order_flow_imbalance_free`.
#[no_mangle]
pub extern "C" fn wickra_order_flow_imbalance_new(period: usize) -> *mut OrderFlowImbalance {
    match OrderFlowImbalance::new(period) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one order-book snapshot as parallel bid/ask price+size arrays; returns the
/// output, or `NaN` during warmup / on a `NULL` handle / if a level or the book is invalid.
///
/// # Safety
/// `handle` valid (from `wickra_order_flow_imbalance_new`, not freed) or `NULL`. `bid_price`/`bid_size`
/// each cover `n_bids`; `ask_price`/`ask_size` each cover `n_asks`.
#[no_mangle]
pub unsafe extern "C" fn wickra_order_flow_imbalance_update(
    handle: *mut OrderFlowImbalance,
    bid_price: *const f64,
    bid_size: *const f64,
    n_bids: usize,
    ask_price: *const f64,
    ask_size: *const f64,
    n_asks: usize,
) -> f64 {
    let Some(ind) = handle.as_mut() else {
        return f64::NAN;
    };
    if bid_price.is_null() || bid_size.is_null() || ask_price.is_null() || ask_size.is_null() {
        return f64::NAN;
    }
    let bid_px = slice::from_raw_parts(bid_price, n_bids);
    let bid_sz = slice::from_raw_parts(bid_size, n_bids);
    let ask_px = slice::from_raw_parts(ask_price, n_asks);
    let ask_sz = slice::from_raw_parts(ask_size, n_asks);
    let mut bids = Vec::with_capacity(n_bids);
    for (&px, &sz) in bid_px.iter().zip(bid_sz) {
        let Ok(level) = Level::new(px, sz) else {
            return f64::NAN;
        };
        bids.push(level);
    }
    let mut asks = Vec::with_capacity(n_asks);
    for (&px, &sz) in ask_px.iter().zip(ask_sz) {
        let Ok(level) = Level::new(px, sz) else {
            return f64::NAN;
        };
        asks.push(level);
    }
    match OrderBook::new(bids, asks) {
        Ok(book) => ind.update(book).unwrap_or(f64::NAN),
        Err(_) => f64::NAN,
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_order_flow_imbalance_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_order_flow_imbalance_warmup_period(
    handle: *mut OrderFlowImbalance,
) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_order_flow_imbalance_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_order_flow_imbalance_is_ready(
    handle: *mut OrderFlowImbalance,
) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_order_flow_imbalance_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_order_flow_imbalance_name(
    handle: *mut OrderFlowImbalance,
) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_order_flow_imbalance_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_order_flow_imbalance_reset(handle: *mut OrderFlowImbalance) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_order_flow_imbalance_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_order_flow_imbalance_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_order_flow_imbalance_free(handle: *mut OrderFlowImbalance) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `QuotedSpread` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_quoted_spread_free`.
#[no_mangle]
pub extern "C" fn wickra_quoted_spread_new() -> *mut QuotedSpread {
    Box::into_raw(Box::new(QuotedSpread::new()))
}

/// Feed one order-book snapshot as parallel bid/ask price+size arrays; returns the
/// output, or `NaN` during warmup / on a `NULL` handle / if a level or the book is invalid.
///
/// # Safety
/// `handle` valid (from `wickra_quoted_spread_new`, not freed) or `NULL`. `bid_price`/`bid_size`
/// each cover `n_bids`; `ask_price`/`ask_size` each cover `n_asks`.
#[no_mangle]
pub unsafe extern "C" fn wickra_quoted_spread_update(
    handle: *mut QuotedSpread,
    bid_price: *const f64,
    bid_size: *const f64,
    n_bids: usize,
    ask_price: *const f64,
    ask_size: *const f64,
    n_asks: usize,
) -> f64 {
    let Some(ind) = handle.as_mut() else {
        return f64::NAN;
    };
    if bid_price.is_null() || bid_size.is_null() || ask_price.is_null() || ask_size.is_null() {
        return f64::NAN;
    }
    let bid_px = slice::from_raw_parts(bid_price, n_bids);
    let bid_sz = slice::from_raw_parts(bid_size, n_bids);
    let ask_px = slice::from_raw_parts(ask_price, n_asks);
    let ask_sz = slice::from_raw_parts(ask_size, n_asks);
    let mut bids = Vec::with_capacity(n_bids);
    for (&px, &sz) in bid_px.iter().zip(bid_sz) {
        let Ok(level) = Level::new(px, sz) else {
            return f64::NAN;
        };
        bids.push(level);
    }
    let mut asks = Vec::with_capacity(n_asks);
    for (&px, &sz) in ask_px.iter().zip(ask_sz) {
        let Ok(level) = Level::new(px, sz) else {
            return f64::NAN;
        };
        asks.push(level);
    }
    match OrderBook::new(bids, asks) {
        Ok(book) => ind.update(book).unwrap_or(f64::NAN),
        Err(_) => f64::NAN,
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_quoted_spread_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_quoted_spread_warmup_period(handle: *mut QuotedSpread) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_quoted_spread_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_quoted_spread_is_ready(handle: *mut QuotedSpread) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_quoted_spread_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_quoted_spread_name(handle: *mut QuotedSpread) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_quoted_spread_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_quoted_spread_reset(handle: *mut QuotedSpread) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_quoted_spread_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_quoted_spread_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_quoted_spread_free(handle: *mut QuotedSpread) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

// ===== Cross-section-input indicators (market breadth) =====

/// Create a `AbsoluteBreadthIndex` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_absolute_breadth_index_free`.
#[no_mangle]
pub extern "C" fn wickra_absolute_breadth_index_new() -> *mut AbsoluteBreadthIndex {
    Box::into_raw(Box::new(AbsoluteBreadthIndex::new()))
}

/// Feed one cross-sectional snapshot as parallel per-member arrays; returns the output,
/// or `NaN` during warmup / on a `NULL` handle / if the snapshot is invalid.
///
/// # Safety
/// `handle` valid (from `wickra_absolute_breadth_index_new`, not freed) or `NULL`. Every member array
/// covers `n` elements.
#[no_mangle]
#[allow(clippy::needless_range_loop)]
pub unsafe extern "C" fn wickra_absolute_breadth_index_update(
    handle: *mut AbsoluteBreadthIndex,
    change: *const f64,
    volume: *const f64,
    new_high: *const bool,
    new_low: *const bool,
    above_ma: *const bool,
    on_buy_signal: *const bool,
    n: usize,
    timestamp: i64,
) -> f64 {
    let Some(ind) = handle.as_mut() else {
        return f64::NAN;
    };
    if change.is_null()
        || volume.is_null()
        || new_high.is_null()
        || new_low.is_null()
        || above_ma.is_null()
        || on_buy_signal.is_null()
    {
        return f64::NAN;
    }
    let changes = slice::from_raw_parts(change, n);
    let volumes = slice::from_raw_parts(volume, n);
    let new_highs = slice::from_raw_parts(new_high, n);
    let new_lows = slice::from_raw_parts(new_low, n);
    let above_mas = slice::from_raw_parts(above_ma, n);
    let on_buy_signals = slice::from_raw_parts(on_buy_signal, n);
    let mut members = Vec::with_capacity(n);
    for idx in 0..n {
        let mut member = Member::new(changes[idx], volumes[idx], new_highs[idx], new_lows[idx]);
        member.above_ma = above_mas[idx];
        member.on_buy_signal = on_buy_signals[idx];
        members.push(member);
    }
    match CrossSection::new(members, timestamp) {
        Ok(snapshot) => ind.update(snapshot).unwrap_or(f64::NAN),
        Err(_) => f64::NAN,
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_absolute_breadth_index_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_absolute_breadth_index_warmup_period(
    handle: *mut AbsoluteBreadthIndex,
) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_absolute_breadth_index_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_absolute_breadth_index_is_ready(
    handle: *mut AbsoluteBreadthIndex,
) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_absolute_breadth_index_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_absolute_breadth_index_name(
    handle: *mut AbsoluteBreadthIndex,
) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_absolute_breadth_index_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_absolute_breadth_index_reset(handle: *mut AbsoluteBreadthIndex) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_absolute_breadth_index_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_absolute_breadth_index_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_absolute_breadth_index_free(handle: *mut AbsoluteBreadthIndex) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `AdVolumeLine` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_ad_volume_line_free`.
#[no_mangle]
pub extern "C" fn wickra_ad_volume_line_new() -> *mut AdVolumeLine {
    Box::into_raw(Box::new(AdVolumeLine::new()))
}

/// Feed one cross-sectional snapshot as parallel per-member arrays; returns the output,
/// or `NaN` during warmup / on a `NULL` handle / if the snapshot is invalid.
///
/// # Safety
/// `handle` valid (from `wickra_ad_volume_line_new`, not freed) or `NULL`. Every member array
/// covers `n` elements.
#[no_mangle]
#[allow(clippy::needless_range_loop)]
pub unsafe extern "C" fn wickra_ad_volume_line_update(
    handle: *mut AdVolumeLine,
    change: *const f64,
    volume: *const f64,
    new_high: *const bool,
    new_low: *const bool,
    above_ma: *const bool,
    on_buy_signal: *const bool,
    n: usize,
    timestamp: i64,
) -> f64 {
    let Some(ind) = handle.as_mut() else {
        return f64::NAN;
    };
    if change.is_null()
        || volume.is_null()
        || new_high.is_null()
        || new_low.is_null()
        || above_ma.is_null()
        || on_buy_signal.is_null()
    {
        return f64::NAN;
    }
    let changes = slice::from_raw_parts(change, n);
    let volumes = slice::from_raw_parts(volume, n);
    let new_highs = slice::from_raw_parts(new_high, n);
    let new_lows = slice::from_raw_parts(new_low, n);
    let above_mas = slice::from_raw_parts(above_ma, n);
    let on_buy_signals = slice::from_raw_parts(on_buy_signal, n);
    let mut members = Vec::with_capacity(n);
    for idx in 0..n {
        let mut member = Member::new(changes[idx], volumes[idx], new_highs[idx], new_lows[idx]);
        member.above_ma = above_mas[idx];
        member.on_buy_signal = on_buy_signals[idx];
        members.push(member);
    }
    match CrossSection::new(members, timestamp) {
        Ok(snapshot) => ind.update(snapshot).unwrap_or(f64::NAN),
        Err(_) => f64::NAN,
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_ad_volume_line_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_ad_volume_line_warmup_period(handle: *mut AdVolumeLine) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_ad_volume_line_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_ad_volume_line_is_ready(handle: *mut AdVolumeLine) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_ad_volume_line_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_ad_volume_line_name(handle: *mut AdVolumeLine) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_ad_volume_line_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_ad_volume_line_reset(handle: *mut AdVolumeLine) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_ad_volume_line_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_ad_volume_line_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_ad_volume_line_free(handle: *mut AdVolumeLine) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `AdvanceDecline` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_advance_decline_free`.
#[no_mangle]
pub extern "C" fn wickra_advance_decline_new() -> *mut AdvanceDecline {
    Box::into_raw(Box::new(AdvanceDecline::new()))
}

/// Feed one cross-sectional snapshot as parallel per-member arrays; returns the output,
/// or `NaN` during warmup / on a `NULL` handle / if the snapshot is invalid.
///
/// # Safety
/// `handle` valid (from `wickra_advance_decline_new`, not freed) or `NULL`. Every member array
/// covers `n` elements.
#[no_mangle]
#[allow(clippy::needless_range_loop)]
pub unsafe extern "C" fn wickra_advance_decline_update(
    handle: *mut AdvanceDecline,
    change: *const f64,
    volume: *const f64,
    new_high: *const bool,
    new_low: *const bool,
    above_ma: *const bool,
    on_buy_signal: *const bool,
    n: usize,
    timestamp: i64,
) -> f64 {
    let Some(ind) = handle.as_mut() else {
        return f64::NAN;
    };
    if change.is_null()
        || volume.is_null()
        || new_high.is_null()
        || new_low.is_null()
        || above_ma.is_null()
        || on_buy_signal.is_null()
    {
        return f64::NAN;
    }
    let changes = slice::from_raw_parts(change, n);
    let volumes = slice::from_raw_parts(volume, n);
    let new_highs = slice::from_raw_parts(new_high, n);
    let new_lows = slice::from_raw_parts(new_low, n);
    let above_mas = slice::from_raw_parts(above_ma, n);
    let on_buy_signals = slice::from_raw_parts(on_buy_signal, n);
    let mut members = Vec::with_capacity(n);
    for idx in 0..n {
        let mut member = Member::new(changes[idx], volumes[idx], new_highs[idx], new_lows[idx]);
        member.above_ma = above_mas[idx];
        member.on_buy_signal = on_buy_signals[idx];
        members.push(member);
    }
    match CrossSection::new(members, timestamp) {
        Ok(snapshot) => ind.update(snapshot).unwrap_or(f64::NAN),
        Err(_) => f64::NAN,
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_advance_decline_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_advance_decline_warmup_period(
    handle: *mut AdvanceDecline,
) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_advance_decline_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_advance_decline_is_ready(handle: *mut AdvanceDecline) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_advance_decline_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_advance_decline_name(handle: *mut AdvanceDecline) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_advance_decline_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_advance_decline_reset(handle: *mut AdvanceDecline) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_advance_decline_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_advance_decline_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_advance_decline_free(handle: *mut AdvanceDecline) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `AdvanceDeclineRatio` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_advance_decline_ratio_free`.
#[no_mangle]
pub extern "C" fn wickra_advance_decline_ratio_new() -> *mut AdvanceDeclineRatio {
    Box::into_raw(Box::new(AdvanceDeclineRatio::new()))
}

/// Feed one cross-sectional snapshot as parallel per-member arrays; returns the output,
/// or `NaN` during warmup / on a `NULL` handle / if the snapshot is invalid.
///
/// # Safety
/// `handle` valid (from `wickra_advance_decline_ratio_new`, not freed) or `NULL`. Every member array
/// covers `n` elements.
#[no_mangle]
#[allow(clippy::needless_range_loop)]
pub unsafe extern "C" fn wickra_advance_decline_ratio_update(
    handle: *mut AdvanceDeclineRatio,
    change: *const f64,
    volume: *const f64,
    new_high: *const bool,
    new_low: *const bool,
    above_ma: *const bool,
    on_buy_signal: *const bool,
    n: usize,
    timestamp: i64,
) -> f64 {
    let Some(ind) = handle.as_mut() else {
        return f64::NAN;
    };
    if change.is_null()
        || volume.is_null()
        || new_high.is_null()
        || new_low.is_null()
        || above_ma.is_null()
        || on_buy_signal.is_null()
    {
        return f64::NAN;
    }
    let changes = slice::from_raw_parts(change, n);
    let volumes = slice::from_raw_parts(volume, n);
    let new_highs = slice::from_raw_parts(new_high, n);
    let new_lows = slice::from_raw_parts(new_low, n);
    let above_mas = slice::from_raw_parts(above_ma, n);
    let on_buy_signals = slice::from_raw_parts(on_buy_signal, n);
    let mut members = Vec::with_capacity(n);
    for idx in 0..n {
        let mut member = Member::new(changes[idx], volumes[idx], new_highs[idx], new_lows[idx]);
        member.above_ma = above_mas[idx];
        member.on_buy_signal = on_buy_signals[idx];
        members.push(member);
    }
    match CrossSection::new(members, timestamp) {
        Ok(snapshot) => ind.update(snapshot).unwrap_or(f64::NAN),
        Err(_) => f64::NAN,
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_advance_decline_ratio_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_advance_decline_ratio_warmup_period(
    handle: *mut AdvanceDeclineRatio,
) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_advance_decline_ratio_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_advance_decline_ratio_is_ready(
    handle: *mut AdvanceDeclineRatio,
) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_advance_decline_ratio_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_advance_decline_ratio_name(
    handle: *mut AdvanceDeclineRatio,
) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_advance_decline_ratio_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_advance_decline_ratio_reset(handle: *mut AdvanceDeclineRatio) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_advance_decline_ratio_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_advance_decline_ratio_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_advance_decline_ratio_free(handle: *mut AdvanceDeclineRatio) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `BreadthThrust` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_breadth_thrust_free`.
#[no_mangle]
pub extern "C" fn wickra_breadth_thrust_new(period: usize) -> *mut BreadthThrust {
    match BreadthThrust::new(period) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one cross-sectional snapshot as parallel per-member arrays; returns the output,
/// or `NaN` during warmup / on a `NULL` handle / if the snapshot is invalid.
///
/// # Safety
/// `handle` valid (from `wickra_breadth_thrust_new`, not freed) or `NULL`. Every member array
/// covers `n` elements.
#[no_mangle]
#[allow(clippy::needless_range_loop)]
pub unsafe extern "C" fn wickra_breadth_thrust_update(
    handle: *mut BreadthThrust,
    change: *const f64,
    volume: *const f64,
    new_high: *const bool,
    new_low: *const bool,
    above_ma: *const bool,
    on_buy_signal: *const bool,
    n: usize,
    timestamp: i64,
) -> f64 {
    let Some(ind) = handle.as_mut() else {
        return f64::NAN;
    };
    if change.is_null()
        || volume.is_null()
        || new_high.is_null()
        || new_low.is_null()
        || above_ma.is_null()
        || on_buy_signal.is_null()
    {
        return f64::NAN;
    }
    let changes = slice::from_raw_parts(change, n);
    let volumes = slice::from_raw_parts(volume, n);
    let new_highs = slice::from_raw_parts(new_high, n);
    let new_lows = slice::from_raw_parts(new_low, n);
    let above_mas = slice::from_raw_parts(above_ma, n);
    let on_buy_signals = slice::from_raw_parts(on_buy_signal, n);
    let mut members = Vec::with_capacity(n);
    for idx in 0..n {
        let mut member = Member::new(changes[idx], volumes[idx], new_highs[idx], new_lows[idx]);
        member.above_ma = above_mas[idx];
        member.on_buy_signal = on_buy_signals[idx];
        members.push(member);
    }
    match CrossSection::new(members, timestamp) {
        Ok(snapshot) => ind.update(snapshot).unwrap_or(f64::NAN),
        Err(_) => f64::NAN,
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_breadth_thrust_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_breadth_thrust_warmup_period(handle: *mut BreadthThrust) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_breadth_thrust_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_breadth_thrust_is_ready(handle: *mut BreadthThrust) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_breadth_thrust_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_breadth_thrust_name(handle: *mut BreadthThrust) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_breadth_thrust_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_breadth_thrust_reset(handle: *mut BreadthThrust) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_breadth_thrust_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_breadth_thrust_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_breadth_thrust_free(handle: *mut BreadthThrust) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `BullishPercentIndex` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_bullish_percent_index_free`.
#[no_mangle]
pub extern "C" fn wickra_bullish_percent_index_new() -> *mut BullishPercentIndex {
    Box::into_raw(Box::new(BullishPercentIndex::new()))
}

/// Feed one cross-sectional snapshot as parallel per-member arrays; returns the output,
/// or `NaN` during warmup / on a `NULL` handle / if the snapshot is invalid.
///
/// # Safety
/// `handle` valid (from `wickra_bullish_percent_index_new`, not freed) or `NULL`. Every member array
/// covers `n` elements.
#[no_mangle]
#[allow(clippy::needless_range_loop)]
pub unsafe extern "C" fn wickra_bullish_percent_index_update(
    handle: *mut BullishPercentIndex,
    change: *const f64,
    volume: *const f64,
    new_high: *const bool,
    new_low: *const bool,
    above_ma: *const bool,
    on_buy_signal: *const bool,
    n: usize,
    timestamp: i64,
) -> f64 {
    let Some(ind) = handle.as_mut() else {
        return f64::NAN;
    };
    if change.is_null()
        || volume.is_null()
        || new_high.is_null()
        || new_low.is_null()
        || above_ma.is_null()
        || on_buy_signal.is_null()
    {
        return f64::NAN;
    }
    let changes = slice::from_raw_parts(change, n);
    let volumes = slice::from_raw_parts(volume, n);
    let new_highs = slice::from_raw_parts(new_high, n);
    let new_lows = slice::from_raw_parts(new_low, n);
    let above_mas = slice::from_raw_parts(above_ma, n);
    let on_buy_signals = slice::from_raw_parts(on_buy_signal, n);
    let mut members = Vec::with_capacity(n);
    for idx in 0..n {
        let mut member = Member::new(changes[idx], volumes[idx], new_highs[idx], new_lows[idx]);
        member.above_ma = above_mas[idx];
        member.on_buy_signal = on_buy_signals[idx];
        members.push(member);
    }
    match CrossSection::new(members, timestamp) {
        Ok(snapshot) => ind.update(snapshot).unwrap_or(f64::NAN),
        Err(_) => f64::NAN,
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_bullish_percent_index_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_bullish_percent_index_warmup_period(
    handle: *mut BullishPercentIndex,
) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_bullish_percent_index_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_bullish_percent_index_is_ready(
    handle: *mut BullishPercentIndex,
) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_bullish_percent_index_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_bullish_percent_index_name(
    handle: *mut BullishPercentIndex,
) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_bullish_percent_index_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_bullish_percent_index_reset(handle: *mut BullishPercentIndex) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_bullish_percent_index_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_bullish_percent_index_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_bullish_percent_index_free(handle: *mut BullishPercentIndex) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `CumulativeVolumeIndex` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_cumulative_volume_index_free`.
#[no_mangle]
pub extern "C" fn wickra_cumulative_volume_index_new() -> *mut CumulativeVolumeIndex {
    Box::into_raw(Box::new(CumulativeVolumeIndex::new()))
}

/// Feed one cross-sectional snapshot as parallel per-member arrays; returns the output,
/// or `NaN` during warmup / on a `NULL` handle / if the snapshot is invalid.
///
/// # Safety
/// `handle` valid (from `wickra_cumulative_volume_index_new`, not freed) or `NULL`. Every member array
/// covers `n` elements.
#[no_mangle]
#[allow(clippy::needless_range_loop)]
pub unsafe extern "C" fn wickra_cumulative_volume_index_update(
    handle: *mut CumulativeVolumeIndex,
    change: *const f64,
    volume: *const f64,
    new_high: *const bool,
    new_low: *const bool,
    above_ma: *const bool,
    on_buy_signal: *const bool,
    n: usize,
    timestamp: i64,
) -> f64 {
    let Some(ind) = handle.as_mut() else {
        return f64::NAN;
    };
    if change.is_null()
        || volume.is_null()
        || new_high.is_null()
        || new_low.is_null()
        || above_ma.is_null()
        || on_buy_signal.is_null()
    {
        return f64::NAN;
    }
    let changes = slice::from_raw_parts(change, n);
    let volumes = slice::from_raw_parts(volume, n);
    let new_highs = slice::from_raw_parts(new_high, n);
    let new_lows = slice::from_raw_parts(new_low, n);
    let above_mas = slice::from_raw_parts(above_ma, n);
    let on_buy_signals = slice::from_raw_parts(on_buy_signal, n);
    let mut members = Vec::with_capacity(n);
    for idx in 0..n {
        let mut member = Member::new(changes[idx], volumes[idx], new_highs[idx], new_lows[idx]);
        member.above_ma = above_mas[idx];
        member.on_buy_signal = on_buy_signals[idx];
        members.push(member);
    }
    match CrossSection::new(members, timestamp) {
        Ok(snapshot) => ind.update(snapshot).unwrap_or(f64::NAN),
        Err(_) => f64::NAN,
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_cumulative_volume_index_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_cumulative_volume_index_warmup_period(
    handle: *mut CumulativeVolumeIndex,
) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_cumulative_volume_index_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_cumulative_volume_index_is_ready(
    handle: *mut CumulativeVolumeIndex,
) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_cumulative_volume_index_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_cumulative_volume_index_name(
    handle: *mut CumulativeVolumeIndex,
) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_cumulative_volume_index_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_cumulative_volume_index_reset(handle: *mut CumulativeVolumeIndex) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_cumulative_volume_index_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_cumulative_volume_index_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_cumulative_volume_index_free(handle: *mut CumulativeVolumeIndex) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `HighLowIndex` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_high_low_index_free`.
#[no_mangle]
pub extern "C" fn wickra_high_low_index_new(period: usize) -> *mut HighLowIndex {
    match HighLowIndex::new(period) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one cross-sectional snapshot as parallel per-member arrays; returns the output,
/// or `NaN` during warmup / on a `NULL` handle / if the snapshot is invalid.
///
/// # Safety
/// `handle` valid (from `wickra_high_low_index_new`, not freed) or `NULL`. Every member array
/// covers `n` elements.
#[no_mangle]
#[allow(clippy::needless_range_loop)]
pub unsafe extern "C" fn wickra_high_low_index_update(
    handle: *mut HighLowIndex,
    change: *const f64,
    volume: *const f64,
    new_high: *const bool,
    new_low: *const bool,
    above_ma: *const bool,
    on_buy_signal: *const bool,
    n: usize,
    timestamp: i64,
) -> f64 {
    let Some(ind) = handle.as_mut() else {
        return f64::NAN;
    };
    if change.is_null()
        || volume.is_null()
        || new_high.is_null()
        || new_low.is_null()
        || above_ma.is_null()
        || on_buy_signal.is_null()
    {
        return f64::NAN;
    }
    let changes = slice::from_raw_parts(change, n);
    let volumes = slice::from_raw_parts(volume, n);
    let new_highs = slice::from_raw_parts(new_high, n);
    let new_lows = slice::from_raw_parts(new_low, n);
    let above_mas = slice::from_raw_parts(above_ma, n);
    let on_buy_signals = slice::from_raw_parts(on_buy_signal, n);
    let mut members = Vec::with_capacity(n);
    for idx in 0..n {
        let mut member = Member::new(changes[idx], volumes[idx], new_highs[idx], new_lows[idx]);
        member.above_ma = above_mas[idx];
        member.on_buy_signal = on_buy_signals[idx];
        members.push(member);
    }
    match CrossSection::new(members, timestamp) {
        Ok(snapshot) => ind.update(snapshot).unwrap_or(f64::NAN),
        Err(_) => f64::NAN,
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_high_low_index_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_high_low_index_warmup_period(handle: *mut HighLowIndex) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_high_low_index_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_high_low_index_is_ready(handle: *mut HighLowIndex) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_high_low_index_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_high_low_index_name(handle: *mut HighLowIndex) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_high_low_index_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_high_low_index_reset(handle: *mut HighLowIndex) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_high_low_index_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_high_low_index_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_high_low_index_free(handle: *mut HighLowIndex) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `McClellanOscillator` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_mc_clellan_oscillator_free`.
#[no_mangle]
pub extern "C" fn wickra_mc_clellan_oscillator_new() -> *mut McClellanOscillator {
    Box::into_raw(Box::new(McClellanOscillator::new()))
}

/// Feed one cross-sectional snapshot as parallel per-member arrays; returns the output,
/// or `NaN` during warmup / on a `NULL` handle / if the snapshot is invalid.
///
/// # Safety
/// `handle` valid (from `wickra_mc_clellan_oscillator_new`, not freed) or `NULL`. Every member array
/// covers `n` elements.
#[no_mangle]
#[allow(clippy::needless_range_loop)]
pub unsafe extern "C" fn wickra_mc_clellan_oscillator_update(
    handle: *mut McClellanOscillator,
    change: *const f64,
    volume: *const f64,
    new_high: *const bool,
    new_low: *const bool,
    above_ma: *const bool,
    on_buy_signal: *const bool,
    n: usize,
    timestamp: i64,
) -> f64 {
    let Some(ind) = handle.as_mut() else {
        return f64::NAN;
    };
    if change.is_null()
        || volume.is_null()
        || new_high.is_null()
        || new_low.is_null()
        || above_ma.is_null()
        || on_buy_signal.is_null()
    {
        return f64::NAN;
    }
    let changes = slice::from_raw_parts(change, n);
    let volumes = slice::from_raw_parts(volume, n);
    let new_highs = slice::from_raw_parts(new_high, n);
    let new_lows = slice::from_raw_parts(new_low, n);
    let above_mas = slice::from_raw_parts(above_ma, n);
    let on_buy_signals = slice::from_raw_parts(on_buy_signal, n);
    let mut members = Vec::with_capacity(n);
    for idx in 0..n {
        let mut member = Member::new(changes[idx], volumes[idx], new_highs[idx], new_lows[idx]);
        member.above_ma = above_mas[idx];
        member.on_buy_signal = on_buy_signals[idx];
        members.push(member);
    }
    match CrossSection::new(members, timestamp) {
        Ok(snapshot) => ind.update(snapshot).unwrap_or(f64::NAN),
        Err(_) => f64::NAN,
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_mc_clellan_oscillator_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_mc_clellan_oscillator_warmup_period(
    handle: *mut McClellanOscillator,
) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_mc_clellan_oscillator_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_mc_clellan_oscillator_is_ready(
    handle: *mut McClellanOscillator,
) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_mc_clellan_oscillator_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_mc_clellan_oscillator_name(
    handle: *mut McClellanOscillator,
) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_mc_clellan_oscillator_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_mc_clellan_oscillator_reset(handle: *mut McClellanOscillator) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_mc_clellan_oscillator_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_mc_clellan_oscillator_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_mc_clellan_oscillator_free(handle: *mut McClellanOscillator) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `McClellanSummationIndex` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_mc_clellan_summation_index_free`.
#[no_mangle]
pub extern "C" fn wickra_mc_clellan_summation_index_new() -> *mut McClellanSummationIndex {
    Box::into_raw(Box::new(McClellanSummationIndex::new()))
}

/// Feed one cross-sectional snapshot as parallel per-member arrays; returns the output,
/// or `NaN` during warmup / on a `NULL` handle / if the snapshot is invalid.
///
/// # Safety
/// `handle` valid (from `wickra_mc_clellan_summation_index_new`, not freed) or `NULL`. Every member array
/// covers `n` elements.
#[no_mangle]
#[allow(clippy::needless_range_loop)]
pub unsafe extern "C" fn wickra_mc_clellan_summation_index_update(
    handle: *mut McClellanSummationIndex,
    change: *const f64,
    volume: *const f64,
    new_high: *const bool,
    new_low: *const bool,
    above_ma: *const bool,
    on_buy_signal: *const bool,
    n: usize,
    timestamp: i64,
) -> f64 {
    let Some(ind) = handle.as_mut() else {
        return f64::NAN;
    };
    if change.is_null()
        || volume.is_null()
        || new_high.is_null()
        || new_low.is_null()
        || above_ma.is_null()
        || on_buy_signal.is_null()
    {
        return f64::NAN;
    }
    let changes = slice::from_raw_parts(change, n);
    let volumes = slice::from_raw_parts(volume, n);
    let new_highs = slice::from_raw_parts(new_high, n);
    let new_lows = slice::from_raw_parts(new_low, n);
    let above_mas = slice::from_raw_parts(above_ma, n);
    let on_buy_signals = slice::from_raw_parts(on_buy_signal, n);
    let mut members = Vec::with_capacity(n);
    for idx in 0..n {
        let mut member = Member::new(changes[idx], volumes[idx], new_highs[idx], new_lows[idx]);
        member.above_ma = above_mas[idx];
        member.on_buy_signal = on_buy_signals[idx];
        members.push(member);
    }
    match CrossSection::new(members, timestamp) {
        Ok(snapshot) => ind.update(snapshot).unwrap_or(f64::NAN),
        Err(_) => f64::NAN,
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_mc_clellan_summation_index_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_mc_clellan_summation_index_warmup_period(
    handle: *mut McClellanSummationIndex,
) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_mc_clellan_summation_index_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_mc_clellan_summation_index_is_ready(
    handle: *mut McClellanSummationIndex,
) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_mc_clellan_summation_index_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_mc_clellan_summation_index_name(
    handle: *mut McClellanSummationIndex,
) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_mc_clellan_summation_index_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_mc_clellan_summation_index_reset(
    handle: *mut McClellanSummationIndex,
) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_mc_clellan_summation_index_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_mc_clellan_summation_index_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_mc_clellan_summation_index_free(
    handle: *mut McClellanSummationIndex,
) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `NewHighsNewLows` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_new_highs_new_lows_free`.
#[no_mangle]
pub extern "C" fn wickra_new_highs_new_lows_new() -> *mut NewHighsNewLows {
    Box::into_raw(Box::new(NewHighsNewLows::new()))
}

/// Feed one cross-sectional snapshot as parallel per-member arrays; returns the output,
/// or `NaN` during warmup / on a `NULL` handle / if the snapshot is invalid.
///
/// # Safety
/// `handle` valid (from `wickra_new_highs_new_lows_new`, not freed) or `NULL`. Every member array
/// covers `n` elements.
#[no_mangle]
#[allow(clippy::needless_range_loop)]
pub unsafe extern "C" fn wickra_new_highs_new_lows_update(
    handle: *mut NewHighsNewLows,
    change: *const f64,
    volume: *const f64,
    new_high: *const bool,
    new_low: *const bool,
    above_ma: *const bool,
    on_buy_signal: *const bool,
    n: usize,
    timestamp: i64,
) -> f64 {
    let Some(ind) = handle.as_mut() else {
        return f64::NAN;
    };
    if change.is_null()
        || volume.is_null()
        || new_high.is_null()
        || new_low.is_null()
        || above_ma.is_null()
        || on_buy_signal.is_null()
    {
        return f64::NAN;
    }
    let changes = slice::from_raw_parts(change, n);
    let volumes = slice::from_raw_parts(volume, n);
    let new_highs = slice::from_raw_parts(new_high, n);
    let new_lows = slice::from_raw_parts(new_low, n);
    let above_mas = slice::from_raw_parts(above_ma, n);
    let on_buy_signals = slice::from_raw_parts(on_buy_signal, n);
    let mut members = Vec::with_capacity(n);
    for idx in 0..n {
        let mut member = Member::new(changes[idx], volumes[idx], new_highs[idx], new_lows[idx]);
        member.above_ma = above_mas[idx];
        member.on_buy_signal = on_buy_signals[idx];
        members.push(member);
    }
    match CrossSection::new(members, timestamp) {
        Ok(snapshot) => ind.update(snapshot).unwrap_or(f64::NAN),
        Err(_) => f64::NAN,
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_new_highs_new_lows_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_new_highs_new_lows_warmup_period(
    handle: *mut NewHighsNewLows,
) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_new_highs_new_lows_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_new_highs_new_lows_is_ready(handle: *mut NewHighsNewLows) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_new_highs_new_lows_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_new_highs_new_lows_name(
    handle: *mut NewHighsNewLows,
) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_new_highs_new_lows_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_new_highs_new_lows_reset(handle: *mut NewHighsNewLows) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_new_highs_new_lows_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_new_highs_new_lows_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_new_highs_new_lows_free(handle: *mut NewHighsNewLows) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `PercentAboveMa` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_percent_above_ma_free`.
#[no_mangle]
pub extern "C" fn wickra_percent_above_ma_new() -> *mut PercentAboveMa {
    Box::into_raw(Box::new(PercentAboveMa::new()))
}

/// Feed one cross-sectional snapshot as parallel per-member arrays; returns the output,
/// or `NaN` during warmup / on a `NULL` handle / if the snapshot is invalid.
///
/// # Safety
/// `handle` valid (from `wickra_percent_above_ma_new`, not freed) or `NULL`. Every member array
/// covers `n` elements.
#[no_mangle]
#[allow(clippy::needless_range_loop)]
pub unsafe extern "C" fn wickra_percent_above_ma_update(
    handle: *mut PercentAboveMa,
    change: *const f64,
    volume: *const f64,
    new_high: *const bool,
    new_low: *const bool,
    above_ma: *const bool,
    on_buy_signal: *const bool,
    n: usize,
    timestamp: i64,
) -> f64 {
    let Some(ind) = handle.as_mut() else {
        return f64::NAN;
    };
    if change.is_null()
        || volume.is_null()
        || new_high.is_null()
        || new_low.is_null()
        || above_ma.is_null()
        || on_buy_signal.is_null()
    {
        return f64::NAN;
    }
    let changes = slice::from_raw_parts(change, n);
    let volumes = slice::from_raw_parts(volume, n);
    let new_highs = slice::from_raw_parts(new_high, n);
    let new_lows = slice::from_raw_parts(new_low, n);
    let above_mas = slice::from_raw_parts(above_ma, n);
    let on_buy_signals = slice::from_raw_parts(on_buy_signal, n);
    let mut members = Vec::with_capacity(n);
    for idx in 0..n {
        let mut member = Member::new(changes[idx], volumes[idx], new_highs[idx], new_lows[idx]);
        member.above_ma = above_mas[idx];
        member.on_buy_signal = on_buy_signals[idx];
        members.push(member);
    }
    match CrossSection::new(members, timestamp) {
        Ok(snapshot) => ind.update(snapshot).unwrap_or(f64::NAN),
        Err(_) => f64::NAN,
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_percent_above_ma_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_percent_above_ma_warmup_period(
    handle: *mut PercentAboveMa,
) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_percent_above_ma_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_percent_above_ma_is_ready(handle: *mut PercentAboveMa) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_percent_above_ma_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_percent_above_ma_name(
    handle: *mut PercentAboveMa,
) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_percent_above_ma_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_percent_above_ma_reset(handle: *mut PercentAboveMa) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_percent_above_ma_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_percent_above_ma_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_percent_above_ma_free(handle: *mut PercentAboveMa) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `TickIndex` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_tick_index_free`.
#[no_mangle]
pub extern "C" fn wickra_tick_index_new() -> *mut TickIndex {
    Box::into_raw(Box::new(TickIndex::new()))
}

/// Feed one cross-sectional snapshot as parallel per-member arrays; returns the output,
/// or `NaN` during warmup / on a `NULL` handle / if the snapshot is invalid.
///
/// # Safety
/// `handle` valid (from `wickra_tick_index_new`, not freed) or `NULL`. Every member array
/// covers `n` elements.
#[no_mangle]
#[allow(clippy::needless_range_loop)]
pub unsafe extern "C" fn wickra_tick_index_update(
    handle: *mut TickIndex,
    change: *const f64,
    volume: *const f64,
    new_high: *const bool,
    new_low: *const bool,
    above_ma: *const bool,
    on_buy_signal: *const bool,
    n: usize,
    timestamp: i64,
) -> f64 {
    let Some(ind) = handle.as_mut() else {
        return f64::NAN;
    };
    if change.is_null()
        || volume.is_null()
        || new_high.is_null()
        || new_low.is_null()
        || above_ma.is_null()
        || on_buy_signal.is_null()
    {
        return f64::NAN;
    }
    let changes = slice::from_raw_parts(change, n);
    let volumes = slice::from_raw_parts(volume, n);
    let new_highs = slice::from_raw_parts(new_high, n);
    let new_lows = slice::from_raw_parts(new_low, n);
    let above_mas = slice::from_raw_parts(above_ma, n);
    let on_buy_signals = slice::from_raw_parts(on_buy_signal, n);
    let mut members = Vec::with_capacity(n);
    for idx in 0..n {
        let mut member = Member::new(changes[idx], volumes[idx], new_highs[idx], new_lows[idx]);
        member.above_ma = above_mas[idx];
        member.on_buy_signal = on_buy_signals[idx];
        members.push(member);
    }
    match CrossSection::new(members, timestamp) {
        Ok(snapshot) => ind.update(snapshot).unwrap_or(f64::NAN),
        Err(_) => f64::NAN,
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_tick_index_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_tick_index_warmup_period(handle: *mut TickIndex) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_tick_index_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_tick_index_is_ready(handle: *mut TickIndex) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_tick_index_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_tick_index_name(handle: *mut TickIndex) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_tick_index_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_tick_index_reset(handle: *mut TickIndex) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_tick_index_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_tick_index_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_tick_index_free(handle: *mut TickIndex) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `Trin` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_trin_free`.
#[no_mangle]
pub extern "C" fn wickra_trin_new() -> *mut Trin {
    Box::into_raw(Box::new(Trin::new()))
}

/// Feed one cross-sectional snapshot as parallel per-member arrays; returns the output,
/// or `NaN` during warmup / on a `NULL` handle / if the snapshot is invalid.
///
/// # Safety
/// `handle` valid (from `wickra_trin_new`, not freed) or `NULL`. Every member array
/// covers `n` elements.
#[no_mangle]
#[allow(clippy::needless_range_loop)]
pub unsafe extern "C" fn wickra_trin_update(
    handle: *mut Trin,
    change: *const f64,
    volume: *const f64,
    new_high: *const bool,
    new_low: *const bool,
    above_ma: *const bool,
    on_buy_signal: *const bool,
    n: usize,
    timestamp: i64,
) -> f64 {
    let Some(ind) = handle.as_mut() else {
        return f64::NAN;
    };
    if change.is_null()
        || volume.is_null()
        || new_high.is_null()
        || new_low.is_null()
        || above_ma.is_null()
        || on_buy_signal.is_null()
    {
        return f64::NAN;
    }
    let changes = slice::from_raw_parts(change, n);
    let volumes = slice::from_raw_parts(volume, n);
    let new_highs = slice::from_raw_parts(new_high, n);
    let new_lows = slice::from_raw_parts(new_low, n);
    let above_mas = slice::from_raw_parts(above_ma, n);
    let on_buy_signals = slice::from_raw_parts(on_buy_signal, n);
    let mut members = Vec::with_capacity(n);
    for idx in 0..n {
        let mut member = Member::new(changes[idx], volumes[idx], new_highs[idx], new_lows[idx]);
        member.above_ma = above_mas[idx];
        member.on_buy_signal = on_buy_signals[idx];
        members.push(member);
    }
    match CrossSection::new(members, timestamp) {
        Ok(snapshot) => ind.update(snapshot).unwrap_or(f64::NAN),
        Err(_) => f64::NAN,
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_trin_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_trin_warmup_period(handle: *mut Trin) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_trin_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_trin_is_ready(handle: *mut Trin) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_trin_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_trin_name(handle: *mut Trin) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_trin_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_trin_reset(handle: *mut Trin) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_trin_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_trin_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_trin_free(handle: *mut Trin) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `UpDownVolumeRatio` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_up_down_volume_ratio_free`.
#[no_mangle]
pub extern "C" fn wickra_up_down_volume_ratio_new() -> *mut UpDownVolumeRatio {
    Box::into_raw(Box::new(UpDownVolumeRatio::new()))
}

/// Feed one cross-sectional snapshot as parallel per-member arrays; returns the output,
/// or `NaN` during warmup / on a `NULL` handle / if the snapshot is invalid.
///
/// # Safety
/// `handle` valid (from `wickra_up_down_volume_ratio_new`, not freed) or `NULL`. Every member array
/// covers `n` elements.
#[no_mangle]
#[allow(clippy::needless_range_loop)]
pub unsafe extern "C" fn wickra_up_down_volume_ratio_update(
    handle: *mut UpDownVolumeRatio,
    change: *const f64,
    volume: *const f64,
    new_high: *const bool,
    new_low: *const bool,
    above_ma: *const bool,
    on_buy_signal: *const bool,
    n: usize,
    timestamp: i64,
) -> f64 {
    let Some(ind) = handle.as_mut() else {
        return f64::NAN;
    };
    if change.is_null()
        || volume.is_null()
        || new_high.is_null()
        || new_low.is_null()
        || above_ma.is_null()
        || on_buy_signal.is_null()
    {
        return f64::NAN;
    }
    let changes = slice::from_raw_parts(change, n);
    let volumes = slice::from_raw_parts(volume, n);
    let new_highs = slice::from_raw_parts(new_high, n);
    let new_lows = slice::from_raw_parts(new_low, n);
    let above_mas = slice::from_raw_parts(above_ma, n);
    let on_buy_signals = slice::from_raw_parts(on_buy_signal, n);
    let mut members = Vec::with_capacity(n);
    for idx in 0..n {
        let mut member = Member::new(changes[idx], volumes[idx], new_highs[idx], new_lows[idx]);
        member.above_ma = above_mas[idx];
        member.on_buy_signal = on_buy_signals[idx];
        members.push(member);
    }
    match CrossSection::new(members, timestamp) {
        Ok(snapshot) => ind.update(snapshot).unwrap_or(f64::NAN),
        Err(_) => f64::NAN,
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_up_down_volume_ratio_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_up_down_volume_ratio_warmup_period(
    handle: *mut UpDownVolumeRatio,
) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_up_down_volume_ratio_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_up_down_volume_ratio_is_ready(
    handle: *mut UpDownVolumeRatio,
) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_up_down_volume_ratio_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_up_down_volume_ratio_name(
    handle: *mut UpDownVolumeRatio,
) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_up_down_volume_ratio_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_up_down_volume_ratio_reset(handle: *mut UpDownVolumeRatio) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_up_down_volume_ratio_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_up_down_volume_ratio_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_up_down_volume_ratio_free(handle: *mut UpDownVolumeRatio) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

// ===== Multi-output indicators (fixed-size struct via out-param) =====

/// C-ABI view of `AccelerationBandsOutput` (fixed-size; `Option<f64>` fields surface as `NaN`).
#[repr(C)]
#[derive(Clone, Copy, Debug)]
pub struct WickraAccelerationBandsOutput {
    pub upper: f64,
    pub middle: f64,
    pub lower: f64,
}

/// C-ABI view of `AdxOutput` (fixed-size; `Option<f64>` fields surface as `NaN`).
#[repr(C)]
#[derive(Clone, Copy, Debug)]
pub struct WickraAdxOutput {
    pub plus_di: f64,
    pub minus_di: f64,
    pub adx: f64,
}

/// C-ABI view of `AlligatorOutput` (fixed-size; `Option<f64>` fields surface as `NaN`).
#[repr(C)]
#[derive(Clone, Copy, Debug)]
pub struct WickraAlligatorOutput {
    pub jaw: f64,
    pub teeth: f64,
    pub lips: f64,
}

/// C-ABI view of `AndrewsPitchforkOutput` (fixed-size; `Option<f64>` fields surface as `NaN`).
#[repr(C)]
#[derive(Clone, Copy, Debug)]
pub struct WickraAndrewsPitchforkOutput {
    pub median: f64,
    pub upper: f64,
    pub lower: f64,
}

/// C-ABI view of `AroonOutput` (fixed-size; `Option<f64>` fields surface as `NaN`).
#[repr(C)]
#[derive(Clone, Copy, Debug)]
pub struct WickraAroonOutput {
    pub up: f64,
    pub down: f64,
}

/// C-ABI view of `AtrBandsOutput` (fixed-size; `Option<f64>` fields surface as `NaN`).
#[repr(C)]
#[derive(Clone, Copy, Debug)]
pub struct WickraAtrBandsOutput {
    pub upper: f64,
    pub middle: f64,
    pub lower: f64,
}

/// C-ABI view of `AtrRatchetOutput` (fixed-size; `Option<f64>` fields surface as `NaN`).
#[repr(C)]
#[derive(Clone, Copy, Debug)]
pub struct WickraAtrRatchetOutput {
    pub value: f64,
    pub direction: f64,
}

/// C-ABI view of `AutoFibOutput` (fixed-size; `Option<f64>` fields surface as `NaN`).
#[repr(C)]
#[derive(Clone, Copy, Debug)]
pub struct WickraAutoFibOutput {
    pub level_0: f64,
    pub level_236: f64,
    pub level_382: f64,
    pub level_500: f64,
    pub level_618: f64,
    pub level_786: f64,
    pub level_1000: f64,
}

/// C-ABI view of `BollingerOutput` (fixed-size; `Option<f64>` fields surface as `NaN`).
#[repr(C)]
#[derive(Clone, Copy, Debug)]
pub struct WickraBollingerOutput {
    pub upper: f64,
    pub middle: f64,
    pub lower: f64,
    pub stddev: f64,
}

/// C-ABI view of `BomarBandsOutput` (fixed-size; `Option<f64>` fields surface as `NaN`).
#[repr(C)]
#[derive(Clone, Copy, Debug)]
pub struct WickraBomarBandsOutput {
    pub upper: f64,
    pub middle: f64,
    pub lower: f64,
}

/// C-ABI view of `CamarillaPivotsOutput` (fixed-size; `Option<f64>` fields surface as `NaN`).
#[repr(C)]
#[derive(Clone, Copy, Debug)]
pub struct WickraCamarillaPivotsOutput {
    pub pp: f64,
    pub r1: f64,
    pub r2: f64,
    pub r3: f64,
    pub r4: f64,
    pub s1: f64,
    pub s2: f64,
    pub s3: f64,
    pub s4: f64,
}

/// C-ABI view of `CandleVolumeOutput` (fixed-size; `Option<f64>` fields surface as `NaN`).
#[repr(C)]
#[derive(Clone, Copy, Debug)]
pub struct WickraCandleVolumeOutput {
    pub body: f64,
    pub width: f64,
}

/// C-ABI view of `CentralPivotRangeOutput` (fixed-size; `Option<f64>` fields surface as `NaN`).
#[repr(C)]
#[derive(Clone, Copy, Debug)]
pub struct WickraCentralPivotRangeOutput {
    pub pivot: f64,
    pub tc: f64,
    pub bc: f64,
}

/// C-ABI view of `ChandeKrollStopOutput` (fixed-size; `Option<f64>` fields surface as `NaN`).
#[repr(C)]
#[derive(Clone, Copy, Debug)]
pub struct WickraChandeKrollStopOutput {
    pub stop_long: f64,
    pub stop_short: f64,
}

/// C-ABI view of `ChandelierExitOutput` (fixed-size; `Option<f64>` fields surface as `NaN`).
#[repr(C)]
#[derive(Clone, Copy, Debug)]
pub struct WickraChandelierExitOutput {
    pub long_stop: f64,
    pub short_stop: f64,
}

/// C-ABI view of `ClassicPivotsOutput` (fixed-size; `Option<f64>` fields surface as `NaN`).
#[repr(C)]
#[derive(Clone, Copy, Debug)]
pub struct WickraClassicPivotsOutput {
    pub pp: f64,
    pub r1: f64,
    pub r2: f64,
    pub r3: f64,
    pub s1: f64,
    pub s2: f64,
    pub s3: f64,
}

/// C-ABI view of `CointegrationOutput` (fixed-size; `Option<f64>` fields surface as `NaN`).
#[repr(C)]
#[derive(Clone, Copy, Debug)]
pub struct WickraCointegrationOutput {
    pub hedge_ratio: f64,
    pub spread: f64,
    pub adf_stat: f64,
}

/// C-ABI view of `CompositeProfileOutput` (fixed-size; `Option<f64>` fields surface as `NaN`).
#[repr(C)]
#[derive(Clone, Copy, Debug)]
pub struct WickraCompositeProfileOutput {
    pub poc: f64,
    pub vah: f64,
    pub val: f64,
}

/// C-ABI view of `DemarkPivotsOutput` (fixed-size; `Option<f64>` fields surface as `NaN`).
#[repr(C)]
#[derive(Clone, Copy, Debug)]
pub struct WickraDemarkPivotsOutput {
    pub pp: f64,
    pub r1: f64,
    pub s1: f64,
}

/// C-ABI view of `DonchianOutput` (fixed-size; `Option<f64>` fields surface as `NaN`).
#[repr(C)]
#[derive(Clone, Copy, Debug)]
pub struct WickraDonchianOutput {
    pub upper: f64,
    pub middle: f64,
    pub lower: f64,
}

/// C-ABI view of `DonchianStopOutput` (fixed-size; `Option<f64>` fields surface as `NaN`).
#[repr(C)]
#[derive(Clone, Copy, Debug)]
pub struct WickraDonchianStopOutput {
    pub stop_long: f64,
    pub stop_short: f64,
}

/// C-ABI view of `DoubleBollingerOutput` (fixed-size; `Option<f64>` fields surface as `NaN`).
#[repr(C)]
#[derive(Clone, Copy, Debug)]
pub struct WickraDoubleBollingerOutput {
    pub upper_outer: f64,
    pub upper_inner: f64,
    pub middle: f64,
    pub lower_inner: f64,
    pub lower_outer: f64,
}

/// C-ABI view of `ElderRayOutput` (fixed-size; `Option<f64>` fields surface as `NaN`).
#[repr(C)]
#[derive(Clone, Copy, Debug)]
pub struct WickraElderRayOutput {
    pub bull_power: f64,
    pub bear_power: f64,
}

/// C-ABI view of `ElderSafeZoneOutput` (fixed-size; `Option<f64>` fields surface as `NaN`).
#[repr(C)]
#[derive(Clone, Copy, Debug)]
pub struct WickraElderSafeZoneOutput {
    pub value: f64,
    pub direction: f64,
}

/// C-ABI view of `EquivolumeOutput` (fixed-size; `Option<f64>` fields surface as `NaN`).
#[repr(C)]
#[derive(Clone, Copy, Debug)]
pub struct WickraEquivolumeOutput {
    pub height: f64,
    pub width: f64,
}

/// C-ABI view of `FibArcsOutput` (fixed-size; `Option<f64>` fields surface as `NaN`).
#[repr(C)]
#[derive(Clone, Copy, Debug)]
pub struct WickraFibArcsOutput {
    pub arc_382: f64,
    pub arc_500: f64,
    pub arc_618: f64,
}

/// C-ABI view of `FibChannelOutput` (fixed-size; `Option<f64>` fields surface as `NaN`).
#[repr(C)]
#[derive(Clone, Copy, Debug)]
pub struct WickraFibChannelOutput {
    pub base: f64,
    pub level_618: f64,
    pub level_1000: f64,
    pub level_1618: f64,
}

/// C-ABI view of `FibConfluenceOutput` (fixed-size; `Option<f64>` fields surface as `NaN`).
#[repr(C)]
#[derive(Clone, Copy, Debug)]
pub struct WickraFibConfluenceOutput {
    pub price: f64,
    pub strength: f64,
}

/// C-ABI view of `FibExtensionOutput` (fixed-size; `Option<f64>` fields surface as `NaN`).
#[repr(C)]
#[derive(Clone, Copy, Debug)]
pub struct WickraFibExtensionOutput {
    pub level_1272: f64,
    pub level_1414: f64,
    pub level_1618: f64,
    pub level_2000: f64,
    pub level_2618: f64,
}

/// C-ABI view of `FibFanOutput` (fixed-size; `Option<f64>` fields surface as `NaN`).
#[repr(C)]
#[derive(Clone, Copy, Debug)]
pub struct WickraFibFanOutput {
    pub fan_382: f64,
    pub fan_500: f64,
    pub fan_618: f64,
}

/// C-ABI view of `FibProjectionOutput` (fixed-size; `Option<f64>` fields surface as `NaN`).
#[repr(C)]
#[derive(Clone, Copy, Debug)]
pub struct WickraFibProjectionOutput {
    pub level_618: f64,
    pub level_1000: f64,
    pub level_1618: f64,
    pub level_2618: f64,
}

/// C-ABI view of `FibRetracementOutput` (fixed-size; `Option<f64>` fields surface as `NaN`).
#[repr(C)]
#[derive(Clone, Copy, Debug)]
pub struct WickraFibRetracementOutput {
    pub level_0: f64,
    pub level_236: f64,
    pub level_382: f64,
    pub level_500: f64,
    pub level_618: f64,
    pub level_786: f64,
    pub level_1000: f64,
}

/// C-ABI view of `FibTimeZonesOutput` (fixed-size; `Option<f64>` fields surface as `NaN`).
#[repr(C)]
#[derive(Clone, Copy, Debug)]
pub struct WickraFibTimeZonesOutput {
    pub on_zone: f64,
    pub bars_to_next: f64,
}

/// C-ABI view of `FibonacciPivotsOutput` (fixed-size; `Option<f64>` fields surface as `NaN`).
#[repr(C)]
#[derive(Clone, Copy, Debug)]
pub struct WickraFibonacciPivotsOutput {
    pub pp: f64,
    pub r1: f64,
    pub r2: f64,
    pub r3: f64,
    pub s1: f64,
    pub s2: f64,
    pub s3: f64,
}

/// C-ABI view of `FractalChaosBandsOutput` (fixed-size; `Option<f64>` fields surface as `NaN`).
#[repr(C)]
#[derive(Clone, Copy, Debug)]
pub struct WickraFractalChaosBandsOutput {
    pub upper: f64,
    pub lower: f64,
}

/// C-ABI view of `GatorOscillatorOutput` (fixed-size; `Option<f64>` fields surface as `NaN`).
#[repr(C)]
#[derive(Clone, Copy, Debug)]
pub struct WickraGatorOscillatorOutput {
    pub upper: f64,
    pub lower: f64,
}

/// C-ABI view of `GoldenPocketOutput` (fixed-size; `Option<f64>` fields surface as `NaN`).
#[repr(C)]
#[derive(Clone, Copy, Debug)]
pub struct WickraGoldenPocketOutput {
    pub low: f64,
    pub mid: f64,
    pub high: f64,
}

/// C-ABI view of `HeikinAshiOutput` (fixed-size; `Option<f64>` fields surface as `NaN`).
#[repr(C)]
#[derive(Clone, Copy, Debug)]
pub struct WickraHeikinAshiOutput {
    pub open: f64,
    pub high: f64,
    pub low: f64,
    pub close: f64,
}

/// C-ABI view of `HighLowVolumeNodesOutput` (fixed-size; `Option<f64>` fields surface as `NaN`).
#[repr(C)]
#[derive(Clone, Copy, Debug)]
pub struct WickraHighLowVolumeNodesOutput {
    pub hvn: f64,
    pub lvn: f64,
}

/// C-ABI view of `HtPhasorOutput` (fixed-size; `Option<f64>` fields surface as `NaN`).
#[repr(C)]
#[derive(Clone, Copy, Debug)]
pub struct WickraHtPhasorOutput {
    pub inphase: f64,
    pub quadrature: f64,
}

/// C-ABI view of `HurstChannelOutput` (fixed-size; `Option<f64>` fields surface as `NaN`).
#[repr(C)]
#[derive(Clone, Copy, Debug)]
pub struct WickraHurstChannelOutput {
    pub upper: f64,
    pub middle: f64,
    pub lower: f64,
}

/// C-ABI view of `IchimokuOutput` (fixed-size; `Option<f64>` fields surface as `NaN`).
#[repr(C)]
#[derive(Clone, Copy, Debug)]
pub struct WickraIchimokuOutput {
    pub tenkan: f64,
    pub kijun: f64,
    pub senkou_a: f64,
    pub senkou_b: f64,
    pub chikou: f64,
}

/// C-ABI view of `InitialBalanceOutput` (fixed-size; `Option<f64>` fields surface as `NaN`).
#[repr(C)]
#[derive(Clone, Copy, Debug)]
pub struct WickraInitialBalanceOutput {
    pub high: f64,
    pub low: f64,
}

/// C-ABI view of `KalmanHedgeRatioOutput` (fixed-size; `Option<f64>` fields surface as `NaN`).
#[repr(C)]
#[derive(Clone, Copy, Debug)]
pub struct WickraKalmanHedgeRatioOutput {
    pub hedge_ratio: f64,
    pub intercept: f64,
    pub spread: f64,
}

/// C-ABI view of `KaseDevStopOutput` (fixed-size; `Option<f64>` fields surface as `NaN`).
#[repr(C)]
#[derive(Clone, Copy, Debug)]
pub struct WickraKaseDevStopOutput {
    pub value: f64,
    pub direction: f64,
}

/// C-ABI view of `KasePermissionStochasticOutput` (fixed-size; `Option<f64>` fields surface as `NaN`).
#[repr(C)]
#[derive(Clone, Copy, Debug)]
pub struct WickraKasePermissionStochasticOutput {
    pub fast: f64,
    pub slow: f64,
}

/// C-ABI view of `KeltnerOutput` (fixed-size; `Option<f64>` fields surface as `NaN`).
#[repr(C)]
#[derive(Clone, Copy, Debug)]
pub struct WickraKeltnerOutput {
    pub upper: f64,
    pub middle: f64,
    pub lower: f64,
}

/// C-ABI view of `KstOutput` (fixed-size; `Option<f64>` fields surface as `NaN`).
#[repr(C)]
#[derive(Clone, Copy, Debug)]
pub struct WickraKstOutput {
    pub kst: f64,
    pub signal: f64,
}

/// C-ABI view of `LeadLagCrossCorrelationOutput` (fixed-size; `Option<f64>` fields surface as `NaN`).
#[repr(C)]
#[derive(Clone, Copy, Debug)]
pub struct WickraLeadLagCrossCorrelationOutput {
    pub lag: i64,
    pub correlation: f64,
}

/// C-ABI view of `LinRegChannelOutput` (fixed-size; `Option<f64>` fields surface as `NaN`).
#[repr(C)]
#[derive(Clone, Copy, Debug)]
pub struct WickraLinRegChannelOutput {
    pub upper: f64,
    pub middle: f64,
    pub lower: f64,
}

/// C-ABI view of `LiquidationFeaturesOutput` (fixed-size; `Option<f64>` fields surface as `NaN`).
#[repr(C)]
#[derive(Clone, Copy, Debug)]
pub struct WickraLiquidationFeaturesOutput {
    pub long: f64,
    pub short: f64,
    pub net: f64,
    pub total: f64,
    pub imbalance: f64,
}

/// C-ABI view of `MaEnvelopeOutput` (fixed-size; `Option<f64>` fields surface as `NaN`).
#[repr(C)]
#[derive(Clone, Copy, Debug)]
pub struct WickraMaEnvelopeOutput {
    pub upper: f64,
    pub middle: f64,
    pub lower: f64,
}

/// C-ABI view of `MacdOutput` (fixed-size; `Option<f64>` fields surface as `NaN`).
#[repr(C)]
#[derive(Clone, Copy, Debug)]
pub struct WickraMacdOutput {
    pub macd: f64,
    pub signal: f64,
    pub histogram: f64,
}

/// C-ABI view of `MamaOutput` (fixed-size; `Option<f64>` fields surface as `NaN`).
#[repr(C)]
#[derive(Clone, Copy, Debug)]
pub struct WickraMamaOutput {
    pub mama: f64,
    pub fama: f64,
}

/// C-ABI view of `MedianChannelOutput` (fixed-size; `Option<f64>` fields surface as `NaN`).
#[repr(C)]
#[derive(Clone, Copy, Debug)]
pub struct WickraMedianChannelOutput {
    pub upper: f64,
    pub middle: f64,
    pub lower: f64,
}

/// C-ABI view of `ModifiedMaStopOutput` (fixed-size; `Option<f64>` fields surface as `NaN`).
#[repr(C)]
#[derive(Clone, Copy, Debug)]
pub struct WickraModifiedMaStopOutput {
    pub value: f64,
    pub direction: f64,
}

/// C-ABI view of `MurreyMathLinesOutput` (fixed-size; `Option<f64>` fields surface as `NaN`).
#[repr(C)]
#[derive(Clone, Copy, Debug)]
pub struct WickraMurreyMathLinesOutput {
    pub mm8_8: f64,
    pub mm7_8: f64,
    pub mm6_8: f64,
    pub mm5_8: f64,
    pub mm4_8: f64,
    pub mm3_8: f64,
    pub mm2_8: f64,
    pub mm1_8: f64,
    pub mm0_8: f64,
}

/// C-ABI view of `NrtrOutput` (fixed-size; `Option<f64>` fields surface as `NaN`).
#[repr(C)]
#[derive(Clone, Copy, Debug)]
pub struct WickraNrtrOutput {
    pub value: f64,
    pub direction: f64,
}

/// C-ABI view of `OpeningRangeOutput` (fixed-size; `Option<f64>` fields surface as `NaN`).
#[repr(C)]
#[derive(Clone, Copy, Debug)]
pub struct WickraOpeningRangeOutput {
    pub high: f64,
    pub low: f64,
    pub breakout_distance: f64,
}

/// C-ABI view of `OvernightIntradayReturnOutput` (fixed-size; `Option<f64>` fields surface as `NaN`).
#[repr(C)]
#[derive(Clone, Copy, Debug)]
pub struct WickraOvernightIntradayReturnOutput {
    pub overnight: f64,
    pub intraday: f64,
}

/// C-ABI view of `ProjectionBandsOutput` (fixed-size; `Option<f64>` fields surface as `NaN`).
#[repr(C)]
#[derive(Clone, Copy, Debug)]
pub struct WickraProjectionBandsOutput {
    pub upper: f64,
    pub middle: f64,
    pub lower: f64,
}

/// C-ABI view of `QqeOutput` (fixed-size; `Option<f64>` fields surface as `NaN`).
#[repr(C)]
#[derive(Clone, Copy, Debug)]
pub struct WickraQqeOutput {
    pub rsi_ma: f64,
    pub trailing_line: f64,
}

/// C-ABI view of `QuartileBandsOutput` (fixed-size; `Option<f64>` fields surface as `NaN`).
#[repr(C)]
#[derive(Clone, Copy, Debug)]
pub struct WickraQuartileBandsOutput {
    pub upper: f64,
    pub middle: f64,
    pub lower: f64,
}

/// C-ABI view of `RelativeStrengthOutput` (fixed-size; `Option<f64>` fields surface as `NaN`).
#[repr(C)]
#[derive(Clone, Copy, Debug)]
pub struct WickraRelativeStrengthOutput {
    pub ratio: f64,
    pub ratio_ma: f64,
    pub ratio_rsi: f64,
}

/// C-ABI view of `RwiOutput` (fixed-size; `Option<f64>` fields surface as `NaN`).
#[repr(C)]
#[derive(Clone, Copy, Debug)]
pub struct WickraRwiOutput {
    pub high: f64,
    pub low: f64,
}

/// C-ABI view of `SessionHighLowOutput` (fixed-size; `Option<f64>` fields surface as `NaN`).
#[repr(C)]
#[derive(Clone, Copy, Debug)]
pub struct WickraSessionHighLowOutput {
    pub high: f64,
    pub low: f64,
}

/// C-ABI view of `SessionRangeOutput` (fixed-size; `Option<f64>` fields surface as `NaN`).
#[repr(C)]
#[derive(Clone, Copy, Debug)]
pub struct WickraSessionRangeOutput {
    pub asia: f64,
    pub eu: f64,
    pub us: f64,
}

/// C-ABI view of `SmoothedHeikinAshiOutput` (fixed-size; `Option<f64>` fields surface as `NaN`).
#[repr(C)]
#[derive(Clone, Copy, Debug)]
pub struct WickraSmoothedHeikinAshiOutput {
    pub open: f64,
    pub high: f64,
    pub low: f64,
    pub close: f64,
}

/// C-ABI view of `SpreadBollingerBandsOutput` (fixed-size; `Option<f64>` fields surface as `NaN`).
#[repr(C)]
#[derive(Clone, Copy, Debug)]
pub struct WickraSpreadBollingerBandsOutput {
    pub middle: f64,
    pub upper: f64,
    pub lower: f64,
    pub percent_b: f64,
}

/// C-ABI view of `StandardErrorBandsOutput` (fixed-size; `Option<f64>` fields surface as `NaN`).
#[repr(C)]
#[derive(Clone, Copy, Debug)]
pub struct WickraStandardErrorBandsOutput {
    pub upper: f64,
    pub middle: f64,
    pub lower: f64,
}

/// C-ABI view of `StarcBandsOutput` (fixed-size; `Option<f64>` fields surface as `NaN`).
#[repr(C)]
#[derive(Clone, Copy, Debug)]
pub struct WickraStarcBandsOutput {
    pub upper: f64,
    pub middle: f64,
    pub lower: f64,
}

/// C-ABI view of `StochasticOutput` (fixed-size; `Option<f64>` fields surface as `NaN`).
#[repr(C)]
#[derive(Clone, Copy, Debug)]
pub struct WickraStochasticOutput {
    pub k: f64,
    pub d: f64,
}

/// C-ABI view of `SuperTrendOutput` (fixed-size; `Option<f64>` fields surface as `NaN`).
#[repr(C)]
#[derive(Clone, Copy, Debug)]
pub struct WickraSuperTrendOutput {
    pub value: f64,
    pub direction: f64,
}

/// C-ABI view of `TdLinesOutput` (fixed-size; `Option<f64>` fields surface as `NaN`).
#[repr(C)]
#[derive(Clone, Copy, Debug)]
pub struct WickraTdLinesOutput {
    pub resistance: f64,
    pub support: f64,
}

/// C-ABI view of `TdMovingAverageOutput` (fixed-size; `Option<f64>` fields surface as `NaN`).
#[repr(C)]
#[derive(Clone, Copy, Debug)]
pub struct WickraTdMovingAverageOutput {
    pub st1: f64,
    pub st2: f64,
}

/// C-ABI view of `TdRangeProjectionOutput` (fixed-size; `Option<f64>` fields surface as `NaN`).
#[repr(C)]
#[derive(Clone, Copy, Debug)]
pub struct WickraTdRangeProjectionOutput {
    pub high: f64,
    pub low: f64,
}

/// C-ABI view of `TdRiskLevelOutput` (fixed-size; `Option<f64>` fields surface as `NaN`).
#[repr(C)]
#[derive(Clone, Copy, Debug)]
pub struct WickraTdRiskLevelOutput {
    pub buy_risk: f64,
    pub sell_risk: f64,
}

/// C-ABI view of `TdSequentialOutput` (fixed-size; `Option<f64>` fields surface as `NaN`).
#[repr(C)]
#[derive(Clone, Copy, Debug)]
pub struct WickraTdSequentialOutput {
    pub setup: f64,
    pub countdown: f64,
    pub direction: f64,
}

/// C-ABI view of `TtmSqueezeOutput` (fixed-size; `Option<f64>` fields surface as `NaN`).
#[repr(C)]
#[derive(Clone, Copy, Debug)]
pub struct WickraTtmSqueezeOutput {
    pub squeeze: f64,
    pub momentum: f64,
}

/// C-ABI view of `ValueAreaOutput` (fixed-size; `Option<f64>` fields surface as `NaN`).
#[repr(C)]
#[derive(Clone, Copy, Debug)]
pub struct WickraValueAreaOutput {
    pub poc: f64,
    pub vah: f64,
    pub val: f64,
}

/// C-ABI view of `VolatilityConeOutput` (fixed-size; `Option<f64>` fields surface as `NaN`).
#[repr(C)]
#[derive(Clone, Copy, Debug)]
pub struct WickraVolatilityConeOutput {
    pub current: f64,
    pub min: f64,
    pub median: f64,
    pub max: f64,
    pub percentile: f64,
}

/// C-ABI view of `VolumeWeightedMacdOutput` (fixed-size; `Option<f64>` fields surface as `NaN`).
#[repr(C)]
#[derive(Clone, Copy, Debug)]
pub struct WickraVolumeWeightedMacdOutput {
    pub macd: f64,
    pub signal: f64,
    pub histogram: f64,
}

/// C-ABI view of `VolumeWeightedSrOutput` (fixed-size; `Option<f64>` fields surface as `NaN`).
#[repr(C)]
#[derive(Clone, Copy, Debug)]
pub struct WickraVolumeWeightedSrOutput {
    pub support: f64,
    pub resistance: f64,
}

/// C-ABI view of `VortexOutput` (fixed-size; `Option<f64>` fields surface as `NaN`).
#[repr(C)]
#[derive(Clone, Copy, Debug)]
pub struct WickraVortexOutput {
    pub plus: f64,
    pub minus: f64,
}

/// C-ABI view of `VwapStdDevBandsOutput` (fixed-size; `Option<f64>` fields surface as `NaN`).
#[repr(C)]
#[derive(Clone, Copy, Debug)]
pub struct WickraVwapStdDevBandsOutput {
    pub upper: f64,
    pub middle: f64,
    pub lower: f64,
    pub stddev: f64,
}

/// C-ABI view of `WaveTrendOutput` (fixed-size; `Option<f64>` fields surface as `NaN`).
#[repr(C)]
#[derive(Clone, Copy, Debug)]
pub struct WickraWaveTrendOutput {
    pub wt1: f64,
    pub wt2: f64,
}

/// C-ABI view of `WilliamsFractalsOutput` (fixed-size; `Option<f64>` fields surface as `NaN`).
#[repr(C)]
#[derive(Clone, Copy, Debug)]
pub struct WickraWilliamsFractalsOutput {
    pub up: f64,
    pub down: f64,
}

/// C-ABI view of `WoodiePivotsOutput` (fixed-size; `Option<f64>` fields surface as `NaN`).
#[repr(C)]
#[derive(Clone, Copy, Debug)]
pub struct WickraWoodiePivotsOutput {
    pub pp: f64,
    pub r1: f64,
    pub r2: f64,
    pub s1: f64,
    pub s2: f64,
}

/// C-ABI view of `ZeroLagMacdOutput` (fixed-size; `Option<f64>` fields surface as `NaN`).
#[repr(C)]
#[derive(Clone, Copy, Debug)]
pub struct WickraZeroLagMacdOutput {
    pub macd: f64,
    pub signal: f64,
    pub histogram: f64,
}

/// C-ABI view of `ZigZagOutput` (fixed-size; `Option<f64>` fields surface as `NaN`).
#[repr(C)]
#[derive(Clone, Copy, Debug)]
pub struct WickraZigZagOutput {
    pub swing: f64,
    pub direction: f64,
}

/// Create a `AccelerationBands` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_acceleration_bands_free`.
#[no_mangle]
pub extern "C" fn wickra_acceleration_bands_new(
    period: usize,
    factor: f64,
) -> *mut AccelerationBands {
    match AccelerationBands::new(period, factor) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one input; on `true` the freshly computed multi-field output is written to
/// `*out`. Returns `false` during warmup, on a `NULL` handle / `out`, or on invalid input.
///
/// # Safety
/// `handle` (from `wickra_acceleration_bands_new`, not freed) and `out` must be valid or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_acceleration_bands_update(
    handle: *mut AccelerationBands,
    open: f64,
    high: f64,
    low: f64,
    close: f64,
    volume: f64,
    timestamp: i64,
    out: *mut WickraAccelerationBandsOutput,
) -> bool {
    let Some(ind) = handle.as_mut() else {
        return false;
    };
    if out.is_null() {
        return false;
    }
    let Ok(input) = Candle::new(open, high, low, close, volume, timestamp) else {
        return false;
    };
    match ind.update(input) {
        Some(out_val) => {
            *out = WickraAccelerationBandsOutput {
                upper: out_val.upper,
                middle: out_val.middle,
                lower: out_val.lower,
            };
            true
        }
        None => false,
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_acceleration_bands_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_acceleration_bands_warmup_period(
    handle: *mut AccelerationBands,
) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_acceleration_bands_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_acceleration_bands_is_ready(
    handle: *mut AccelerationBands,
) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_acceleration_bands_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_acceleration_bands_name(
    handle: *mut AccelerationBands,
) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_acceleration_bands_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_acceleration_bands_reset(handle: *mut AccelerationBands) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_acceleration_bands_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_acceleration_bands_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_acceleration_bands_free(handle: *mut AccelerationBands) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `Adx` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_adx_free`.
#[no_mangle]
pub extern "C" fn wickra_adx_new(period: usize) -> *mut Adx {
    match Adx::new(period) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one input; on `true` the freshly computed multi-field output is written to
/// `*out`. Returns `false` during warmup, on a `NULL` handle / `out`, or on invalid input.
///
/// # Safety
/// `handle` (from `wickra_adx_new`, not freed) and `out` must be valid or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_adx_update(
    handle: *mut Adx,
    open: f64,
    high: f64,
    low: f64,
    close: f64,
    volume: f64,
    timestamp: i64,
    out: *mut WickraAdxOutput,
) -> bool {
    let Some(ind) = handle.as_mut() else {
        return false;
    };
    if out.is_null() {
        return false;
    }
    let Ok(input) = Candle::new(open, high, low, close, volume, timestamp) else {
        return false;
    };
    match ind.update(input) {
        Some(out_val) => {
            *out = WickraAdxOutput {
                plus_di: out_val.plus_di,
                minus_di: out_val.minus_di,
                adx: out_val.adx,
            };
            true
        }
        None => false,
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_adx_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_adx_warmup_period(handle: *mut Adx) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_adx_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_adx_is_ready(handle: *mut Adx) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_adx_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_adx_name(handle: *mut Adx) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_adx_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_adx_reset(handle: *mut Adx) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_adx_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_adx_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_adx_free(handle: *mut Adx) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `Alligator` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_alligator_free`.
#[no_mangle]
pub extern "C" fn wickra_alligator_new(
    jaw_period: usize,
    teeth_period: usize,
    lips_period: usize,
) -> *mut Alligator {
    match Alligator::new(jaw_period, teeth_period, lips_period) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one input; on `true` the freshly computed multi-field output is written to
/// `*out`. Returns `false` during warmup, on a `NULL` handle / `out`, or on invalid input.
///
/// # Safety
/// `handle` (from `wickra_alligator_new`, not freed) and `out` must be valid or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_alligator_update(
    handle: *mut Alligator,
    open: f64,
    high: f64,
    low: f64,
    close: f64,
    volume: f64,
    timestamp: i64,
    out: *mut WickraAlligatorOutput,
) -> bool {
    let Some(ind) = handle.as_mut() else {
        return false;
    };
    if out.is_null() {
        return false;
    }
    let Ok(input) = Candle::new(open, high, low, close, volume, timestamp) else {
        return false;
    };
    match ind.update(input) {
        Some(out_val) => {
            *out = WickraAlligatorOutput {
                jaw: out_val.jaw,
                teeth: out_val.teeth,
                lips: out_val.lips,
            };
            true
        }
        None => false,
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_alligator_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_alligator_warmup_period(handle: *mut Alligator) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_alligator_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_alligator_is_ready(handle: *mut Alligator) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_alligator_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_alligator_name(handle: *mut Alligator) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_alligator_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_alligator_reset(handle: *mut Alligator) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_alligator_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_alligator_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_alligator_free(handle: *mut Alligator) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `AndrewsPitchfork` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_andrews_pitchfork_free`.
#[no_mangle]
pub extern "C" fn wickra_andrews_pitchfork_new(strength: usize) -> *mut AndrewsPitchfork {
    match AndrewsPitchfork::new(strength) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one input; on `true` the freshly computed multi-field output is written to
/// `*out`. Returns `false` during warmup, on a `NULL` handle / `out`, or on invalid input.
///
/// # Safety
/// `handle` (from `wickra_andrews_pitchfork_new`, not freed) and `out` must be valid or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_andrews_pitchfork_update(
    handle: *mut AndrewsPitchfork,
    open: f64,
    high: f64,
    low: f64,
    close: f64,
    volume: f64,
    timestamp: i64,
    out: *mut WickraAndrewsPitchforkOutput,
) -> bool {
    let Some(ind) = handle.as_mut() else {
        return false;
    };
    if out.is_null() {
        return false;
    }
    let Ok(input) = Candle::new(open, high, low, close, volume, timestamp) else {
        return false;
    };
    match ind.update(input) {
        Some(out_val) => {
            *out = WickraAndrewsPitchforkOutput {
                median: out_val.median,
                upper: out_val.upper,
                lower: out_val.lower,
            };
            true
        }
        None => false,
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_andrews_pitchfork_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_andrews_pitchfork_warmup_period(
    handle: *mut AndrewsPitchfork,
) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_andrews_pitchfork_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_andrews_pitchfork_is_ready(handle: *mut AndrewsPitchfork) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_andrews_pitchfork_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_andrews_pitchfork_name(
    handle: *mut AndrewsPitchfork,
) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_andrews_pitchfork_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_andrews_pitchfork_reset(handle: *mut AndrewsPitchfork) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_andrews_pitchfork_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_andrews_pitchfork_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_andrews_pitchfork_free(handle: *mut AndrewsPitchfork) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `Aroon` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_aroon_free`.
#[no_mangle]
pub extern "C" fn wickra_aroon_new(period: usize) -> *mut Aroon {
    match Aroon::new(period) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one input; on `true` the freshly computed multi-field output is written to
/// `*out`. Returns `false` during warmup, on a `NULL` handle / `out`, or on invalid input.
///
/// # Safety
/// `handle` (from `wickra_aroon_new`, not freed) and `out` must be valid or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_aroon_update(
    handle: *mut Aroon,
    open: f64,
    high: f64,
    low: f64,
    close: f64,
    volume: f64,
    timestamp: i64,
    out: *mut WickraAroonOutput,
) -> bool {
    let Some(ind) = handle.as_mut() else {
        return false;
    };
    if out.is_null() {
        return false;
    }
    let Ok(input) = Candle::new(open, high, low, close, volume, timestamp) else {
        return false;
    };
    match ind.update(input) {
        Some(out_val) => {
            *out = WickraAroonOutput {
                up: out_val.up,
                down: out_val.down,
            };
            true
        }
        None => false,
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_aroon_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_aroon_warmup_period(handle: *mut Aroon) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_aroon_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_aroon_is_ready(handle: *mut Aroon) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_aroon_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_aroon_name(handle: *mut Aroon) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_aroon_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_aroon_reset(handle: *mut Aroon) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_aroon_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_aroon_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_aroon_free(handle: *mut Aroon) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `AtrBands` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_atr_bands_free`.
#[no_mangle]
pub extern "C" fn wickra_atr_bands_new(period: usize, multiplier: f64) -> *mut AtrBands {
    match AtrBands::new(period, multiplier) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one input; on `true` the freshly computed multi-field output is written to
/// `*out`. Returns `false` during warmup, on a `NULL` handle / `out`, or on invalid input.
///
/// # Safety
/// `handle` (from `wickra_atr_bands_new`, not freed) and `out` must be valid or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_atr_bands_update(
    handle: *mut AtrBands,
    open: f64,
    high: f64,
    low: f64,
    close: f64,
    volume: f64,
    timestamp: i64,
    out: *mut WickraAtrBandsOutput,
) -> bool {
    let Some(ind) = handle.as_mut() else {
        return false;
    };
    if out.is_null() {
        return false;
    }
    let Ok(input) = Candle::new(open, high, low, close, volume, timestamp) else {
        return false;
    };
    match ind.update(input) {
        Some(out_val) => {
            *out = WickraAtrBandsOutput {
                upper: out_val.upper,
                middle: out_val.middle,
                lower: out_val.lower,
            };
            true
        }
        None => false,
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_atr_bands_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_atr_bands_warmup_period(handle: *mut AtrBands) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_atr_bands_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_atr_bands_is_ready(handle: *mut AtrBands) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_atr_bands_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_atr_bands_name(handle: *mut AtrBands) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_atr_bands_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_atr_bands_reset(handle: *mut AtrBands) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_atr_bands_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_atr_bands_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_atr_bands_free(handle: *mut AtrBands) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `AtrRatchet` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_atr_ratchet_free`.
#[no_mangle]
pub extern "C" fn wickra_atr_ratchet_new(
    atr_period: usize,
    start_mult: f64,
    increment: f64,
) -> *mut AtrRatchet {
    match AtrRatchet::new(atr_period, start_mult, increment) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one input; on `true` the freshly computed multi-field output is written to
/// `*out`. Returns `false` during warmup, on a `NULL` handle / `out`, or on invalid input.
///
/// # Safety
/// `handle` (from `wickra_atr_ratchet_new`, not freed) and `out` must be valid or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_atr_ratchet_update(
    handle: *mut AtrRatchet,
    open: f64,
    high: f64,
    low: f64,
    close: f64,
    volume: f64,
    timestamp: i64,
    out: *mut WickraAtrRatchetOutput,
) -> bool {
    let Some(ind) = handle.as_mut() else {
        return false;
    };
    if out.is_null() {
        return false;
    }
    let Ok(input) = Candle::new(open, high, low, close, volume, timestamp) else {
        return false;
    };
    match ind.update(input) {
        Some(out_val) => {
            *out = WickraAtrRatchetOutput {
                value: out_val.value,
                direction: out_val.direction,
            };
            true
        }
        None => false,
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_atr_ratchet_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_atr_ratchet_warmup_period(handle: *mut AtrRatchet) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_atr_ratchet_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_atr_ratchet_is_ready(handle: *mut AtrRatchet) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_atr_ratchet_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_atr_ratchet_name(handle: *mut AtrRatchet) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_atr_ratchet_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_atr_ratchet_reset(handle: *mut AtrRatchet) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_atr_ratchet_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_atr_ratchet_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_atr_ratchet_free(handle: *mut AtrRatchet) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `AutoFib` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_auto_fib_free`.
#[no_mangle]
pub extern "C" fn wickra_auto_fib_new() -> *mut AutoFib {
    Box::into_raw(Box::new(AutoFib::new()))
}

/// Feed one input; on `true` the freshly computed multi-field output is written to
/// `*out`. Returns `false` during warmup, on a `NULL` handle / `out`, or on invalid input.
///
/// # Safety
/// `handle` (from `wickra_auto_fib_new`, not freed) and `out` must be valid or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_auto_fib_update(
    handle: *mut AutoFib,
    open: f64,
    high: f64,
    low: f64,
    close: f64,
    volume: f64,
    timestamp: i64,
    out: *mut WickraAutoFibOutput,
) -> bool {
    let Some(ind) = handle.as_mut() else {
        return false;
    };
    if out.is_null() {
        return false;
    }
    let Ok(input) = Candle::new(open, high, low, close, volume, timestamp) else {
        return false;
    };
    match ind.update(input) {
        Some(out_val) => {
            *out = WickraAutoFibOutput {
                level_0: out_val.level_0,
                level_236: out_val.level_236,
                level_382: out_val.level_382,
                level_500: out_val.level_500,
                level_618: out_val.level_618,
                level_786: out_val.level_786,
                level_1000: out_val.level_1000,
            };
            true
        }
        None => false,
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_auto_fib_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_auto_fib_warmup_period(handle: *mut AutoFib) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_auto_fib_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_auto_fib_is_ready(handle: *mut AutoFib) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_auto_fib_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_auto_fib_name(handle: *mut AutoFib) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_auto_fib_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_auto_fib_reset(handle: *mut AutoFib) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_auto_fib_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_auto_fib_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_auto_fib_free(handle: *mut AutoFib) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `BollingerBands` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_bollinger_bands_free`.
#[no_mangle]
pub extern "C" fn wickra_bollinger_bands_new(
    period: usize,
    multiplier: f64,
) -> *mut BollingerBands {
    match BollingerBands::new(period, multiplier) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one input; on `true` the freshly computed multi-field output is written to
/// `*out`. Returns `false` during warmup, on a `NULL` handle / `out`, or on invalid input.
///
/// # Safety
/// `handle` (from `wickra_bollinger_bands_new`, not freed) and `out` must be valid or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_bollinger_bands_update(
    handle: *mut BollingerBands,
    value: f64,
    out: *mut WickraBollingerOutput,
) -> bool {
    let Some(ind) = handle.as_mut() else {
        return false;
    };
    if out.is_null() {
        return false;
    }
    match ind.update(value) {
        Some(out_val) => {
            *out = WickraBollingerOutput {
                upper: out_val.upper,
                middle: out_val.middle,
                lower: out_val.lower,
                stddev: out_val.stddev,
            };
            true
        }
        None => false,
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_bollinger_bands_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_bollinger_bands_warmup_period(
    handle: *mut BollingerBands,
) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_bollinger_bands_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_bollinger_bands_is_ready(handle: *mut BollingerBands) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_bollinger_bands_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_bollinger_bands_name(handle: *mut BollingerBands) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_bollinger_bands_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_bollinger_bands_reset(handle: *mut BollingerBands) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_bollinger_bands_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_bollinger_bands_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_bollinger_bands_free(handle: *mut BollingerBands) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `BomarBands` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_bomar_bands_free`.
#[no_mangle]
pub extern "C" fn wickra_bomar_bands_new(period: usize, coverage: f64) -> *mut BomarBands {
    match BomarBands::new(period, coverage) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one input; on `true` the freshly computed multi-field output is written to
/// `*out`. Returns `false` during warmup, on a `NULL` handle / `out`, or on invalid input.
///
/// # Safety
/// `handle` (from `wickra_bomar_bands_new`, not freed) and `out` must be valid or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_bomar_bands_update(
    handle: *mut BomarBands,
    value: f64,
    out: *mut WickraBomarBandsOutput,
) -> bool {
    let Some(ind) = handle.as_mut() else {
        return false;
    };
    if out.is_null() {
        return false;
    }
    match ind.update(value) {
        Some(out_val) => {
            *out = WickraBomarBandsOutput {
                upper: out_val.upper,
                middle: out_val.middle,
                lower: out_val.lower,
            };
            true
        }
        None => false,
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_bomar_bands_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_bomar_bands_warmup_period(handle: *mut BomarBands) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_bomar_bands_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_bomar_bands_is_ready(handle: *mut BomarBands) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_bomar_bands_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_bomar_bands_name(handle: *mut BomarBands) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_bomar_bands_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_bomar_bands_reset(handle: *mut BomarBands) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_bomar_bands_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_bomar_bands_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_bomar_bands_free(handle: *mut BomarBands) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `Camarilla` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_camarilla_free`.
#[no_mangle]
pub extern "C" fn wickra_camarilla_new() -> *mut Camarilla {
    Box::into_raw(Box::new(Camarilla::new()))
}

/// Feed one input; on `true` the freshly computed multi-field output is written to
/// `*out`. Returns `false` during warmup, on a `NULL` handle / `out`, or on invalid input.
///
/// # Safety
/// `handle` (from `wickra_camarilla_new`, not freed) and `out` must be valid or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_camarilla_update(
    handle: *mut Camarilla,
    open: f64,
    high: f64,
    low: f64,
    close: f64,
    volume: f64,
    timestamp: i64,
    out: *mut WickraCamarillaPivotsOutput,
) -> bool {
    let Some(ind) = handle.as_mut() else {
        return false;
    };
    if out.is_null() {
        return false;
    }
    let Ok(input) = Candle::new(open, high, low, close, volume, timestamp) else {
        return false;
    };
    match ind.update(input) {
        Some(out_val) => {
            *out = WickraCamarillaPivotsOutput {
                pp: out_val.pp,
                r1: out_val.r1,
                r2: out_val.r2,
                r3: out_val.r3,
                r4: out_val.r4,
                s1: out_val.s1,
                s2: out_val.s2,
                s3: out_val.s3,
                s4: out_val.s4,
            };
            true
        }
        None => false,
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_camarilla_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_camarilla_warmup_period(handle: *mut Camarilla) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_camarilla_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_camarilla_is_ready(handle: *mut Camarilla) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_camarilla_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_camarilla_name(handle: *mut Camarilla) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_camarilla_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_camarilla_reset(handle: *mut Camarilla) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_camarilla_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_camarilla_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_camarilla_free(handle: *mut Camarilla) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `CandleVolume` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_candle_volume_free`.
#[no_mangle]
pub extern "C" fn wickra_candle_volume_new(period: usize) -> *mut CandleVolume {
    match CandleVolume::new(period) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one input; on `true` the freshly computed multi-field output is written to
/// `*out`. Returns `false` during warmup, on a `NULL` handle / `out`, or on invalid input.
///
/// # Safety
/// `handle` (from `wickra_candle_volume_new`, not freed) and `out` must be valid or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_candle_volume_update(
    handle: *mut CandleVolume,
    open: f64,
    high: f64,
    low: f64,
    close: f64,
    volume: f64,
    timestamp: i64,
    out: *mut WickraCandleVolumeOutput,
) -> bool {
    let Some(ind) = handle.as_mut() else {
        return false;
    };
    if out.is_null() {
        return false;
    }
    let Ok(input) = Candle::new(open, high, low, close, volume, timestamp) else {
        return false;
    };
    match ind.update(input) {
        Some(out_val) => {
            *out = WickraCandleVolumeOutput {
                body: out_val.body,
                width: out_val.width,
            };
            true
        }
        None => false,
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_candle_volume_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_candle_volume_warmup_period(handle: *mut CandleVolume) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_candle_volume_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_candle_volume_is_ready(handle: *mut CandleVolume) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_candle_volume_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_candle_volume_name(handle: *mut CandleVolume) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_candle_volume_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_candle_volume_reset(handle: *mut CandleVolume) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_candle_volume_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_candle_volume_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_candle_volume_free(handle: *mut CandleVolume) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `CentralPivotRange` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_central_pivot_range_free`.
#[no_mangle]
pub extern "C" fn wickra_central_pivot_range_new() -> *mut CentralPivotRange {
    Box::into_raw(Box::new(CentralPivotRange::new()))
}

/// Feed one input; on `true` the freshly computed multi-field output is written to
/// `*out`. Returns `false` during warmup, on a `NULL` handle / `out`, or on invalid input.
///
/// # Safety
/// `handle` (from `wickra_central_pivot_range_new`, not freed) and `out` must be valid or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_central_pivot_range_update(
    handle: *mut CentralPivotRange,
    open: f64,
    high: f64,
    low: f64,
    close: f64,
    volume: f64,
    timestamp: i64,
    out: *mut WickraCentralPivotRangeOutput,
) -> bool {
    let Some(ind) = handle.as_mut() else {
        return false;
    };
    if out.is_null() {
        return false;
    }
    let Ok(input) = Candle::new(open, high, low, close, volume, timestamp) else {
        return false;
    };
    match ind.update(input) {
        Some(out_val) => {
            *out = WickraCentralPivotRangeOutput {
                pivot: out_val.pivot,
                tc: out_val.tc,
                bc: out_val.bc,
            };
            true
        }
        None => false,
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_central_pivot_range_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_central_pivot_range_warmup_period(
    handle: *mut CentralPivotRange,
) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_central_pivot_range_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_central_pivot_range_is_ready(
    handle: *mut CentralPivotRange,
) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_central_pivot_range_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_central_pivot_range_name(
    handle: *mut CentralPivotRange,
) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_central_pivot_range_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_central_pivot_range_reset(handle: *mut CentralPivotRange) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_central_pivot_range_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_central_pivot_range_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_central_pivot_range_free(handle: *mut CentralPivotRange) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `ChandeKrollStop` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_chande_kroll_stop_free`.
#[no_mangle]
pub extern "C" fn wickra_chande_kroll_stop_new(
    atr_period: usize,
    atr_multiplier: f64,
    stop_period: usize,
) -> *mut ChandeKrollStop {
    match ChandeKrollStop::new(atr_period, atr_multiplier, stop_period) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one input; on `true` the freshly computed multi-field output is written to
/// `*out`. Returns `false` during warmup, on a `NULL` handle / `out`, or on invalid input.
///
/// # Safety
/// `handle` (from `wickra_chande_kroll_stop_new`, not freed) and `out` must be valid or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_chande_kroll_stop_update(
    handle: *mut ChandeKrollStop,
    open: f64,
    high: f64,
    low: f64,
    close: f64,
    volume: f64,
    timestamp: i64,
    out: *mut WickraChandeKrollStopOutput,
) -> bool {
    let Some(ind) = handle.as_mut() else {
        return false;
    };
    if out.is_null() {
        return false;
    }
    let Ok(input) = Candle::new(open, high, low, close, volume, timestamp) else {
        return false;
    };
    match ind.update(input) {
        Some(out_val) => {
            *out = WickraChandeKrollStopOutput {
                stop_long: out_val.stop_long,
                stop_short: out_val.stop_short,
            };
            true
        }
        None => false,
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_chande_kroll_stop_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_chande_kroll_stop_warmup_period(
    handle: *mut ChandeKrollStop,
) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_chande_kroll_stop_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_chande_kroll_stop_is_ready(handle: *mut ChandeKrollStop) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_chande_kroll_stop_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_chande_kroll_stop_name(
    handle: *mut ChandeKrollStop,
) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_chande_kroll_stop_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_chande_kroll_stop_reset(handle: *mut ChandeKrollStop) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_chande_kroll_stop_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_chande_kroll_stop_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_chande_kroll_stop_free(handle: *mut ChandeKrollStop) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `ChandelierExit` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_chandelier_exit_free`.
#[no_mangle]
pub extern "C" fn wickra_chandelier_exit_new(
    period: usize,
    multiplier: f64,
) -> *mut ChandelierExit {
    match ChandelierExit::new(period, multiplier) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one input; on `true` the freshly computed multi-field output is written to
/// `*out`. Returns `false` during warmup, on a `NULL` handle / `out`, or on invalid input.
///
/// # Safety
/// `handle` (from `wickra_chandelier_exit_new`, not freed) and `out` must be valid or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_chandelier_exit_update(
    handle: *mut ChandelierExit,
    open: f64,
    high: f64,
    low: f64,
    close: f64,
    volume: f64,
    timestamp: i64,
    out: *mut WickraChandelierExitOutput,
) -> bool {
    let Some(ind) = handle.as_mut() else {
        return false;
    };
    if out.is_null() {
        return false;
    }
    let Ok(input) = Candle::new(open, high, low, close, volume, timestamp) else {
        return false;
    };
    match ind.update(input) {
        Some(out_val) => {
            *out = WickraChandelierExitOutput {
                long_stop: out_val.long_stop,
                short_stop: out_val.short_stop,
            };
            true
        }
        None => false,
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_chandelier_exit_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_chandelier_exit_warmup_period(
    handle: *mut ChandelierExit,
) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_chandelier_exit_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_chandelier_exit_is_ready(handle: *mut ChandelierExit) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_chandelier_exit_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_chandelier_exit_name(handle: *mut ChandelierExit) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_chandelier_exit_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_chandelier_exit_reset(handle: *mut ChandelierExit) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_chandelier_exit_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_chandelier_exit_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_chandelier_exit_free(handle: *mut ChandelierExit) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `ClassicPivots` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_classic_pivots_free`.
#[no_mangle]
pub extern "C" fn wickra_classic_pivots_new() -> *mut ClassicPivots {
    Box::into_raw(Box::new(ClassicPivots::new()))
}

/// Feed one input; on `true` the freshly computed multi-field output is written to
/// `*out`. Returns `false` during warmup, on a `NULL` handle / `out`, or on invalid input.
///
/// # Safety
/// `handle` (from `wickra_classic_pivots_new`, not freed) and `out` must be valid or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_classic_pivots_update(
    handle: *mut ClassicPivots,
    open: f64,
    high: f64,
    low: f64,
    close: f64,
    volume: f64,
    timestamp: i64,
    out: *mut WickraClassicPivotsOutput,
) -> bool {
    let Some(ind) = handle.as_mut() else {
        return false;
    };
    if out.is_null() {
        return false;
    }
    let Ok(input) = Candle::new(open, high, low, close, volume, timestamp) else {
        return false;
    };
    match ind.update(input) {
        Some(out_val) => {
            *out = WickraClassicPivotsOutput {
                pp: out_val.pp,
                r1: out_val.r1,
                r2: out_val.r2,
                r3: out_val.r3,
                s1: out_val.s1,
                s2: out_val.s2,
                s3: out_val.s3,
            };
            true
        }
        None => false,
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_classic_pivots_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_classic_pivots_warmup_period(handle: *mut ClassicPivots) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_classic_pivots_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_classic_pivots_is_ready(handle: *mut ClassicPivots) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_classic_pivots_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_classic_pivots_name(handle: *mut ClassicPivots) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_classic_pivots_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_classic_pivots_reset(handle: *mut ClassicPivots) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_classic_pivots_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_classic_pivots_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_classic_pivots_free(handle: *mut ClassicPivots) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `Cointegration` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_cointegration_free`.
#[no_mangle]
pub extern "C" fn wickra_cointegration_new(period: usize, adf_lags: usize) -> *mut Cointegration {
    match Cointegration::new(period, adf_lags) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one input; on `true` the freshly computed multi-field output is written to
/// `*out`. Returns `false` during warmup, on a `NULL` handle / `out`, or on invalid input.
///
/// # Safety
/// `handle` (from `wickra_cointegration_new`, not freed) and `out` must be valid or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_cointegration_update(
    handle: *mut Cointegration,
    x: f64,
    y: f64,
    out: *mut WickraCointegrationOutput,
) -> bool {
    let Some(ind) = handle.as_mut() else {
        return false;
    };
    if out.is_null() {
        return false;
    }
    match ind.update((x, y)) {
        Some(out_val) => {
            *out = WickraCointegrationOutput {
                hedge_ratio: out_val.hedge_ratio,
                spread: out_val.spread,
                adf_stat: out_val.adf_stat,
            };
            true
        }
        None => false,
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_cointegration_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_cointegration_warmup_period(handle: *mut Cointegration) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_cointegration_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_cointegration_is_ready(handle: *mut Cointegration) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_cointegration_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_cointegration_name(handle: *mut Cointegration) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_cointegration_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_cointegration_reset(handle: *mut Cointegration) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_cointegration_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_cointegration_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_cointegration_free(handle: *mut Cointegration) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `CompositeProfile` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_composite_profile_free`.
#[no_mangle]
pub extern "C" fn wickra_composite_profile_new(
    period: usize,
    bins: usize,
    value_area_pct: f64,
) -> *mut CompositeProfile {
    match CompositeProfile::new(period, bins, value_area_pct) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one input; on `true` the freshly computed multi-field output is written to
/// `*out`. Returns `false` during warmup, on a `NULL` handle / `out`, or on invalid input.
///
/// # Safety
/// `handle` (from `wickra_composite_profile_new`, not freed) and `out` must be valid or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_composite_profile_update(
    handle: *mut CompositeProfile,
    open: f64,
    high: f64,
    low: f64,
    close: f64,
    volume: f64,
    timestamp: i64,
    out: *mut WickraCompositeProfileOutput,
) -> bool {
    let Some(ind) = handle.as_mut() else {
        return false;
    };
    if out.is_null() {
        return false;
    }
    let Ok(input) = Candle::new(open, high, low, close, volume, timestamp) else {
        return false;
    };
    match ind.update(input) {
        Some(out_val) => {
            *out = WickraCompositeProfileOutput {
                poc: out_val.poc,
                vah: out_val.vah,
                val: out_val.val,
            };
            true
        }
        None => false,
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_composite_profile_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_composite_profile_warmup_period(
    handle: *mut CompositeProfile,
) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_composite_profile_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_composite_profile_is_ready(handle: *mut CompositeProfile) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_composite_profile_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_composite_profile_name(
    handle: *mut CompositeProfile,
) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_composite_profile_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_composite_profile_reset(handle: *mut CompositeProfile) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_composite_profile_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_composite_profile_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_composite_profile_free(handle: *mut CompositeProfile) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `DemarkPivots` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_demark_pivots_free`.
#[no_mangle]
pub extern "C" fn wickra_demark_pivots_new() -> *mut DemarkPivots {
    Box::into_raw(Box::new(DemarkPivots::new()))
}

/// Feed one input; on `true` the freshly computed multi-field output is written to
/// `*out`. Returns `false` during warmup, on a `NULL` handle / `out`, or on invalid input.
///
/// # Safety
/// `handle` (from `wickra_demark_pivots_new`, not freed) and `out` must be valid or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_demark_pivots_update(
    handle: *mut DemarkPivots,
    open: f64,
    high: f64,
    low: f64,
    close: f64,
    volume: f64,
    timestamp: i64,
    out: *mut WickraDemarkPivotsOutput,
) -> bool {
    let Some(ind) = handle.as_mut() else {
        return false;
    };
    if out.is_null() {
        return false;
    }
    let Ok(input) = Candle::new(open, high, low, close, volume, timestamp) else {
        return false;
    };
    match ind.update(input) {
        Some(out_val) => {
            *out = WickraDemarkPivotsOutput {
                pp: out_val.pp,
                r1: out_val.r1,
                s1: out_val.s1,
            };
            true
        }
        None => false,
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_demark_pivots_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_demark_pivots_warmup_period(handle: *mut DemarkPivots) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_demark_pivots_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_demark_pivots_is_ready(handle: *mut DemarkPivots) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_demark_pivots_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_demark_pivots_name(handle: *mut DemarkPivots) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_demark_pivots_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_demark_pivots_reset(handle: *mut DemarkPivots) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_demark_pivots_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_demark_pivots_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_demark_pivots_free(handle: *mut DemarkPivots) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `Donchian` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_donchian_free`.
#[no_mangle]
pub extern "C" fn wickra_donchian_new(period: usize) -> *mut Donchian {
    match Donchian::new(period) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one input; on `true` the freshly computed multi-field output is written to
/// `*out`. Returns `false` during warmup, on a `NULL` handle / `out`, or on invalid input.
///
/// # Safety
/// `handle` (from `wickra_donchian_new`, not freed) and `out` must be valid or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_donchian_update(
    handle: *mut Donchian,
    open: f64,
    high: f64,
    low: f64,
    close: f64,
    volume: f64,
    timestamp: i64,
    out: *mut WickraDonchianOutput,
) -> bool {
    let Some(ind) = handle.as_mut() else {
        return false;
    };
    if out.is_null() {
        return false;
    }
    let Ok(input) = Candle::new(open, high, low, close, volume, timestamp) else {
        return false;
    };
    match ind.update(input) {
        Some(out_val) => {
            *out = WickraDonchianOutput {
                upper: out_val.upper,
                middle: out_val.middle,
                lower: out_val.lower,
            };
            true
        }
        None => false,
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_donchian_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_donchian_warmup_period(handle: *mut Donchian) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_donchian_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_donchian_is_ready(handle: *mut Donchian) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_donchian_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_donchian_name(handle: *mut Donchian) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_donchian_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_donchian_reset(handle: *mut Donchian) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_donchian_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_donchian_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_donchian_free(handle: *mut Donchian) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `DonchianStop` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_donchian_stop_free`.
#[no_mangle]
pub extern "C" fn wickra_donchian_stop_new(period: usize) -> *mut DonchianStop {
    match DonchianStop::new(period) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one input; on `true` the freshly computed multi-field output is written to
/// `*out`. Returns `false` during warmup, on a `NULL` handle / `out`, or on invalid input.
///
/// # Safety
/// `handle` (from `wickra_donchian_stop_new`, not freed) and `out` must be valid or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_donchian_stop_update(
    handle: *mut DonchianStop,
    open: f64,
    high: f64,
    low: f64,
    close: f64,
    volume: f64,
    timestamp: i64,
    out: *mut WickraDonchianStopOutput,
) -> bool {
    let Some(ind) = handle.as_mut() else {
        return false;
    };
    if out.is_null() {
        return false;
    }
    let Ok(input) = Candle::new(open, high, low, close, volume, timestamp) else {
        return false;
    };
    match ind.update(input) {
        Some(out_val) => {
            *out = WickraDonchianStopOutput {
                stop_long: out_val.stop_long,
                stop_short: out_val.stop_short,
            };
            true
        }
        None => false,
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_donchian_stop_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_donchian_stop_warmup_period(handle: *mut DonchianStop) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_donchian_stop_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_donchian_stop_is_ready(handle: *mut DonchianStop) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_donchian_stop_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_donchian_stop_name(handle: *mut DonchianStop) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_donchian_stop_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_donchian_stop_reset(handle: *mut DonchianStop) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_donchian_stop_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_donchian_stop_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_donchian_stop_free(handle: *mut DonchianStop) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `DoubleBollinger` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_double_bollinger_free`.
#[no_mangle]
pub extern "C" fn wickra_double_bollinger_new(
    period: usize,
    k_inner: f64,
    k_outer: f64,
) -> *mut DoubleBollinger {
    match DoubleBollinger::new(period, k_inner, k_outer) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one input; on `true` the freshly computed multi-field output is written to
/// `*out`. Returns `false` during warmup, on a `NULL` handle / `out`, or on invalid input.
///
/// # Safety
/// `handle` (from `wickra_double_bollinger_new`, not freed) and `out` must be valid or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_double_bollinger_update(
    handle: *mut DoubleBollinger,
    value: f64,
    out: *mut WickraDoubleBollingerOutput,
) -> bool {
    let Some(ind) = handle.as_mut() else {
        return false;
    };
    if out.is_null() {
        return false;
    }
    match ind.update(value) {
        Some(out_val) => {
            *out = WickraDoubleBollingerOutput {
                upper_outer: out_val.upper_outer,
                upper_inner: out_val.upper_inner,
                middle: out_val.middle,
                lower_inner: out_val.lower_inner,
                lower_outer: out_val.lower_outer,
            };
            true
        }
        None => false,
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_double_bollinger_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_double_bollinger_warmup_period(
    handle: *mut DoubleBollinger,
) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_double_bollinger_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_double_bollinger_is_ready(handle: *mut DoubleBollinger) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_double_bollinger_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_double_bollinger_name(
    handle: *mut DoubleBollinger,
) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_double_bollinger_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_double_bollinger_reset(handle: *mut DoubleBollinger) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_double_bollinger_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_double_bollinger_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_double_bollinger_free(handle: *mut DoubleBollinger) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `ElderRay` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_elder_ray_free`.
#[no_mangle]
pub extern "C" fn wickra_elder_ray_new(period: usize) -> *mut ElderRay {
    match ElderRay::new(period) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one input; on `true` the freshly computed multi-field output is written to
/// `*out`. Returns `false` during warmup, on a `NULL` handle / `out`, or on invalid input.
///
/// # Safety
/// `handle` (from `wickra_elder_ray_new`, not freed) and `out` must be valid or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_elder_ray_update(
    handle: *mut ElderRay,
    open: f64,
    high: f64,
    low: f64,
    close: f64,
    volume: f64,
    timestamp: i64,
    out: *mut WickraElderRayOutput,
) -> bool {
    let Some(ind) = handle.as_mut() else {
        return false;
    };
    if out.is_null() {
        return false;
    }
    let Ok(input) = Candle::new(open, high, low, close, volume, timestamp) else {
        return false;
    };
    match ind.update(input) {
        Some(out_val) => {
            *out = WickraElderRayOutput {
                bull_power: out_val.bull_power,
                bear_power: out_val.bear_power,
            };
            true
        }
        None => false,
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_elder_ray_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_elder_ray_warmup_period(handle: *mut ElderRay) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_elder_ray_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_elder_ray_is_ready(handle: *mut ElderRay) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_elder_ray_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_elder_ray_name(handle: *mut ElderRay) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_elder_ray_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_elder_ray_reset(handle: *mut ElderRay) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_elder_ray_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_elder_ray_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_elder_ray_free(handle: *mut ElderRay) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `ElderSafeZone` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_elder_safe_zone_free`.
#[no_mangle]
pub extern "C" fn wickra_elder_safe_zone_new(period: usize, coeff: f64) -> *mut ElderSafeZone {
    match ElderSafeZone::new(period, coeff) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one input; on `true` the freshly computed multi-field output is written to
/// `*out`. Returns `false` during warmup, on a `NULL` handle / `out`, or on invalid input.
///
/// # Safety
/// `handle` (from `wickra_elder_safe_zone_new`, not freed) and `out` must be valid or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_elder_safe_zone_update(
    handle: *mut ElderSafeZone,
    open: f64,
    high: f64,
    low: f64,
    close: f64,
    volume: f64,
    timestamp: i64,
    out: *mut WickraElderSafeZoneOutput,
) -> bool {
    let Some(ind) = handle.as_mut() else {
        return false;
    };
    if out.is_null() {
        return false;
    }
    let Ok(input) = Candle::new(open, high, low, close, volume, timestamp) else {
        return false;
    };
    match ind.update(input) {
        Some(out_val) => {
            *out = WickraElderSafeZoneOutput {
                value: out_val.value,
                direction: out_val.direction,
            };
            true
        }
        None => false,
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_elder_safe_zone_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_elder_safe_zone_warmup_period(handle: *mut ElderSafeZone) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_elder_safe_zone_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_elder_safe_zone_is_ready(handle: *mut ElderSafeZone) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_elder_safe_zone_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_elder_safe_zone_name(handle: *mut ElderSafeZone) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_elder_safe_zone_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_elder_safe_zone_reset(handle: *mut ElderSafeZone) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_elder_safe_zone_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_elder_safe_zone_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_elder_safe_zone_free(handle: *mut ElderSafeZone) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `Equivolume` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_equivolume_free`.
#[no_mangle]
pub extern "C" fn wickra_equivolume_new(period: usize) -> *mut Equivolume {
    match Equivolume::new(period) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one input; on `true` the freshly computed multi-field output is written to
/// `*out`. Returns `false` during warmup, on a `NULL` handle / `out`, or on invalid input.
///
/// # Safety
/// `handle` (from `wickra_equivolume_new`, not freed) and `out` must be valid or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_equivolume_update(
    handle: *mut Equivolume,
    open: f64,
    high: f64,
    low: f64,
    close: f64,
    volume: f64,
    timestamp: i64,
    out: *mut WickraEquivolumeOutput,
) -> bool {
    let Some(ind) = handle.as_mut() else {
        return false;
    };
    if out.is_null() {
        return false;
    }
    let Ok(input) = Candle::new(open, high, low, close, volume, timestamp) else {
        return false;
    };
    match ind.update(input) {
        Some(out_val) => {
            *out = WickraEquivolumeOutput {
                height: out_val.height,
                width: out_val.width,
            };
            true
        }
        None => false,
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_equivolume_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_equivolume_warmup_period(handle: *mut Equivolume) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_equivolume_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_equivolume_is_ready(handle: *mut Equivolume) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_equivolume_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_equivolume_name(handle: *mut Equivolume) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_equivolume_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_equivolume_reset(handle: *mut Equivolume) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_equivolume_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_equivolume_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_equivolume_free(handle: *mut Equivolume) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `FibArcs` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_fib_arcs_free`.
#[no_mangle]
pub extern "C" fn wickra_fib_arcs_new() -> *mut FibArcs {
    Box::into_raw(Box::new(FibArcs::new()))
}

/// Feed one input; on `true` the freshly computed multi-field output is written to
/// `*out`. Returns `false` during warmup, on a `NULL` handle / `out`, or on invalid input.
///
/// # Safety
/// `handle` (from `wickra_fib_arcs_new`, not freed) and `out` must be valid or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_fib_arcs_update(
    handle: *mut FibArcs,
    open: f64,
    high: f64,
    low: f64,
    close: f64,
    volume: f64,
    timestamp: i64,
    out: *mut WickraFibArcsOutput,
) -> bool {
    let Some(ind) = handle.as_mut() else {
        return false;
    };
    if out.is_null() {
        return false;
    }
    let Ok(input) = Candle::new(open, high, low, close, volume, timestamp) else {
        return false;
    };
    match ind.update(input) {
        Some(out_val) => {
            *out = WickraFibArcsOutput {
                arc_382: out_val.arc_382,
                arc_500: out_val.arc_500,
                arc_618: out_val.arc_618,
            };
            true
        }
        None => false,
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_fib_arcs_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_fib_arcs_warmup_period(handle: *mut FibArcs) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_fib_arcs_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_fib_arcs_is_ready(handle: *mut FibArcs) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_fib_arcs_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_fib_arcs_name(handle: *mut FibArcs) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_fib_arcs_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_fib_arcs_reset(handle: *mut FibArcs) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_fib_arcs_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_fib_arcs_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_fib_arcs_free(handle: *mut FibArcs) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `FibChannel` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_fib_channel_free`.
#[no_mangle]
pub extern "C" fn wickra_fib_channel_new() -> *mut FibChannel {
    Box::into_raw(Box::new(FibChannel::new()))
}

/// Feed one input; on `true` the freshly computed multi-field output is written to
/// `*out`. Returns `false` during warmup, on a `NULL` handle / `out`, or on invalid input.
///
/// # Safety
/// `handle` (from `wickra_fib_channel_new`, not freed) and `out` must be valid or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_fib_channel_update(
    handle: *mut FibChannel,
    open: f64,
    high: f64,
    low: f64,
    close: f64,
    volume: f64,
    timestamp: i64,
    out: *mut WickraFibChannelOutput,
) -> bool {
    let Some(ind) = handle.as_mut() else {
        return false;
    };
    if out.is_null() {
        return false;
    }
    let Ok(input) = Candle::new(open, high, low, close, volume, timestamp) else {
        return false;
    };
    match ind.update(input) {
        Some(out_val) => {
            *out = WickraFibChannelOutput {
                base: out_val.base,
                level_618: out_val.level_618,
                level_1000: out_val.level_1000,
                level_1618: out_val.level_1618,
            };
            true
        }
        None => false,
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_fib_channel_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_fib_channel_warmup_period(handle: *mut FibChannel) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_fib_channel_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_fib_channel_is_ready(handle: *mut FibChannel) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_fib_channel_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_fib_channel_name(handle: *mut FibChannel) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_fib_channel_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_fib_channel_reset(handle: *mut FibChannel) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_fib_channel_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_fib_channel_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_fib_channel_free(handle: *mut FibChannel) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `FibConfluence` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_fib_confluence_free`.
#[no_mangle]
pub extern "C" fn wickra_fib_confluence_new() -> *mut FibConfluence {
    Box::into_raw(Box::new(FibConfluence::new()))
}

/// Feed one input; on `true` the freshly computed multi-field output is written to
/// `*out`. Returns `false` during warmup, on a `NULL` handle / `out`, or on invalid input.
///
/// # Safety
/// `handle` (from `wickra_fib_confluence_new`, not freed) and `out` must be valid or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_fib_confluence_update(
    handle: *mut FibConfluence,
    open: f64,
    high: f64,
    low: f64,
    close: f64,
    volume: f64,
    timestamp: i64,
    out: *mut WickraFibConfluenceOutput,
) -> bool {
    let Some(ind) = handle.as_mut() else {
        return false;
    };
    if out.is_null() {
        return false;
    }
    let Ok(input) = Candle::new(open, high, low, close, volume, timestamp) else {
        return false;
    };
    match ind.update(input) {
        Some(out_val) => {
            *out = WickraFibConfluenceOutput {
                price: out_val.price,
                strength: out_val.strength,
            };
            true
        }
        None => false,
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_fib_confluence_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_fib_confluence_warmup_period(handle: *mut FibConfluence) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_fib_confluence_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_fib_confluence_is_ready(handle: *mut FibConfluence) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_fib_confluence_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_fib_confluence_name(handle: *mut FibConfluence) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_fib_confluence_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_fib_confluence_reset(handle: *mut FibConfluence) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_fib_confluence_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_fib_confluence_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_fib_confluence_free(handle: *mut FibConfluence) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `FibExtension` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_fib_extension_free`.
#[no_mangle]
pub extern "C" fn wickra_fib_extension_new() -> *mut FibExtension {
    Box::into_raw(Box::new(FibExtension::new()))
}

/// Feed one input; on `true` the freshly computed multi-field output is written to
/// `*out`. Returns `false` during warmup, on a `NULL` handle / `out`, or on invalid input.
///
/// # Safety
/// `handle` (from `wickra_fib_extension_new`, not freed) and `out` must be valid or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_fib_extension_update(
    handle: *mut FibExtension,
    open: f64,
    high: f64,
    low: f64,
    close: f64,
    volume: f64,
    timestamp: i64,
    out: *mut WickraFibExtensionOutput,
) -> bool {
    let Some(ind) = handle.as_mut() else {
        return false;
    };
    if out.is_null() {
        return false;
    }
    let Ok(input) = Candle::new(open, high, low, close, volume, timestamp) else {
        return false;
    };
    match ind.update(input) {
        Some(out_val) => {
            *out = WickraFibExtensionOutput {
                level_1272: out_val.level_1272,
                level_1414: out_val.level_1414,
                level_1618: out_val.level_1618,
                level_2000: out_val.level_2000,
                level_2618: out_val.level_2618,
            };
            true
        }
        None => false,
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_fib_extension_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_fib_extension_warmup_period(handle: *mut FibExtension) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_fib_extension_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_fib_extension_is_ready(handle: *mut FibExtension) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_fib_extension_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_fib_extension_name(handle: *mut FibExtension) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_fib_extension_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_fib_extension_reset(handle: *mut FibExtension) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_fib_extension_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_fib_extension_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_fib_extension_free(handle: *mut FibExtension) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `FibFan` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_fib_fan_free`.
#[no_mangle]
pub extern "C" fn wickra_fib_fan_new() -> *mut FibFan {
    Box::into_raw(Box::new(FibFan::new()))
}

/// Feed one input; on `true` the freshly computed multi-field output is written to
/// `*out`. Returns `false` during warmup, on a `NULL` handle / `out`, or on invalid input.
///
/// # Safety
/// `handle` (from `wickra_fib_fan_new`, not freed) and `out` must be valid or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_fib_fan_update(
    handle: *mut FibFan,
    open: f64,
    high: f64,
    low: f64,
    close: f64,
    volume: f64,
    timestamp: i64,
    out: *mut WickraFibFanOutput,
) -> bool {
    let Some(ind) = handle.as_mut() else {
        return false;
    };
    if out.is_null() {
        return false;
    }
    let Ok(input) = Candle::new(open, high, low, close, volume, timestamp) else {
        return false;
    };
    match ind.update(input) {
        Some(out_val) => {
            *out = WickraFibFanOutput {
                fan_382: out_val.fan_382,
                fan_500: out_val.fan_500,
                fan_618: out_val.fan_618,
            };
            true
        }
        None => false,
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_fib_fan_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_fib_fan_warmup_period(handle: *mut FibFan) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_fib_fan_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_fib_fan_is_ready(handle: *mut FibFan) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_fib_fan_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_fib_fan_name(handle: *mut FibFan) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_fib_fan_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_fib_fan_reset(handle: *mut FibFan) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_fib_fan_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_fib_fan_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_fib_fan_free(handle: *mut FibFan) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `FibProjection` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_fib_projection_free`.
#[no_mangle]
pub extern "C" fn wickra_fib_projection_new() -> *mut FibProjection {
    Box::into_raw(Box::new(FibProjection::new()))
}

/// Feed one input; on `true` the freshly computed multi-field output is written to
/// `*out`. Returns `false` during warmup, on a `NULL` handle / `out`, or on invalid input.
///
/// # Safety
/// `handle` (from `wickra_fib_projection_new`, not freed) and `out` must be valid or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_fib_projection_update(
    handle: *mut FibProjection,
    open: f64,
    high: f64,
    low: f64,
    close: f64,
    volume: f64,
    timestamp: i64,
    out: *mut WickraFibProjectionOutput,
) -> bool {
    let Some(ind) = handle.as_mut() else {
        return false;
    };
    if out.is_null() {
        return false;
    }
    let Ok(input) = Candle::new(open, high, low, close, volume, timestamp) else {
        return false;
    };
    match ind.update(input) {
        Some(out_val) => {
            *out = WickraFibProjectionOutput {
                level_618: out_val.level_618,
                level_1000: out_val.level_1000,
                level_1618: out_val.level_1618,
                level_2618: out_val.level_2618,
            };
            true
        }
        None => false,
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_fib_projection_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_fib_projection_warmup_period(handle: *mut FibProjection) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_fib_projection_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_fib_projection_is_ready(handle: *mut FibProjection) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_fib_projection_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_fib_projection_name(handle: *mut FibProjection) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_fib_projection_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_fib_projection_reset(handle: *mut FibProjection) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_fib_projection_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_fib_projection_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_fib_projection_free(handle: *mut FibProjection) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `FibRetracement` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_fib_retracement_free`.
#[no_mangle]
pub extern "C" fn wickra_fib_retracement_new() -> *mut FibRetracement {
    Box::into_raw(Box::new(FibRetracement::new()))
}

/// Feed one input; on `true` the freshly computed multi-field output is written to
/// `*out`. Returns `false` during warmup, on a `NULL` handle / `out`, or on invalid input.
///
/// # Safety
/// `handle` (from `wickra_fib_retracement_new`, not freed) and `out` must be valid or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_fib_retracement_update(
    handle: *mut FibRetracement,
    open: f64,
    high: f64,
    low: f64,
    close: f64,
    volume: f64,
    timestamp: i64,
    out: *mut WickraFibRetracementOutput,
) -> bool {
    let Some(ind) = handle.as_mut() else {
        return false;
    };
    if out.is_null() {
        return false;
    }
    let Ok(input) = Candle::new(open, high, low, close, volume, timestamp) else {
        return false;
    };
    match ind.update(input) {
        Some(out_val) => {
            *out = WickraFibRetracementOutput {
                level_0: out_val.level_0,
                level_236: out_val.level_236,
                level_382: out_val.level_382,
                level_500: out_val.level_500,
                level_618: out_val.level_618,
                level_786: out_val.level_786,
                level_1000: out_val.level_1000,
            };
            true
        }
        None => false,
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_fib_retracement_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_fib_retracement_warmup_period(
    handle: *mut FibRetracement,
) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_fib_retracement_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_fib_retracement_is_ready(handle: *mut FibRetracement) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_fib_retracement_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_fib_retracement_name(handle: *mut FibRetracement) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_fib_retracement_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_fib_retracement_reset(handle: *mut FibRetracement) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_fib_retracement_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_fib_retracement_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_fib_retracement_free(handle: *mut FibRetracement) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `FibTimeZones` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_fib_time_zones_free`.
#[no_mangle]
pub extern "C" fn wickra_fib_time_zones_new() -> *mut FibTimeZones {
    Box::into_raw(Box::new(FibTimeZones::new()))
}

/// Feed one input; on `true` the freshly computed multi-field output is written to
/// `*out`. Returns `false` during warmup, on a `NULL` handle / `out`, or on invalid input.
///
/// # Safety
/// `handle` (from `wickra_fib_time_zones_new`, not freed) and `out` must be valid or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_fib_time_zones_update(
    handle: *mut FibTimeZones,
    open: f64,
    high: f64,
    low: f64,
    close: f64,
    volume: f64,
    timestamp: i64,
    out: *mut WickraFibTimeZonesOutput,
) -> bool {
    let Some(ind) = handle.as_mut() else {
        return false;
    };
    if out.is_null() {
        return false;
    }
    let Ok(input) = Candle::new(open, high, low, close, volume, timestamp) else {
        return false;
    };
    match ind.update(input) {
        Some(out_val) => {
            *out = WickraFibTimeZonesOutput {
                on_zone: out_val.on_zone,
                bars_to_next: out_val.bars_to_next,
            };
            true
        }
        None => false,
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_fib_time_zones_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_fib_time_zones_warmup_period(handle: *mut FibTimeZones) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_fib_time_zones_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_fib_time_zones_is_ready(handle: *mut FibTimeZones) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_fib_time_zones_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_fib_time_zones_name(handle: *mut FibTimeZones) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_fib_time_zones_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_fib_time_zones_reset(handle: *mut FibTimeZones) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_fib_time_zones_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_fib_time_zones_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_fib_time_zones_free(handle: *mut FibTimeZones) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `FibonacciPivots` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_fibonacci_pivots_free`.
#[no_mangle]
pub extern "C" fn wickra_fibonacci_pivots_new() -> *mut FibonacciPivots {
    Box::into_raw(Box::new(FibonacciPivots::new()))
}

/// Feed one input; on `true` the freshly computed multi-field output is written to
/// `*out`. Returns `false` during warmup, on a `NULL` handle / `out`, or on invalid input.
///
/// # Safety
/// `handle` (from `wickra_fibonacci_pivots_new`, not freed) and `out` must be valid or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_fibonacci_pivots_update(
    handle: *mut FibonacciPivots,
    open: f64,
    high: f64,
    low: f64,
    close: f64,
    volume: f64,
    timestamp: i64,
    out: *mut WickraFibonacciPivotsOutput,
) -> bool {
    let Some(ind) = handle.as_mut() else {
        return false;
    };
    if out.is_null() {
        return false;
    }
    let Ok(input) = Candle::new(open, high, low, close, volume, timestamp) else {
        return false;
    };
    match ind.update(input) {
        Some(out_val) => {
            *out = WickraFibonacciPivotsOutput {
                pp: out_val.pp,
                r1: out_val.r1,
                r2: out_val.r2,
                r3: out_val.r3,
                s1: out_val.s1,
                s2: out_val.s2,
                s3: out_val.s3,
            };
            true
        }
        None => false,
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_fibonacci_pivots_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_fibonacci_pivots_warmup_period(
    handle: *mut FibonacciPivots,
) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_fibonacci_pivots_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_fibonacci_pivots_is_ready(handle: *mut FibonacciPivots) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_fibonacci_pivots_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_fibonacci_pivots_name(
    handle: *mut FibonacciPivots,
) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_fibonacci_pivots_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_fibonacci_pivots_reset(handle: *mut FibonacciPivots) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_fibonacci_pivots_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_fibonacci_pivots_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_fibonacci_pivots_free(handle: *mut FibonacciPivots) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `FractalChaosBands` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_fractal_chaos_bands_free`.
#[no_mangle]
pub extern "C" fn wickra_fractal_chaos_bands_new(k: usize) -> *mut FractalChaosBands {
    match FractalChaosBands::new(k) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one input; on `true` the freshly computed multi-field output is written to
/// `*out`. Returns `false` during warmup, on a `NULL` handle / `out`, or on invalid input.
///
/// # Safety
/// `handle` (from `wickra_fractal_chaos_bands_new`, not freed) and `out` must be valid or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_fractal_chaos_bands_update(
    handle: *mut FractalChaosBands,
    open: f64,
    high: f64,
    low: f64,
    close: f64,
    volume: f64,
    timestamp: i64,
    out: *mut WickraFractalChaosBandsOutput,
) -> bool {
    let Some(ind) = handle.as_mut() else {
        return false;
    };
    if out.is_null() {
        return false;
    }
    let Ok(input) = Candle::new(open, high, low, close, volume, timestamp) else {
        return false;
    };
    match ind.update(input) {
        Some(out_val) => {
            *out = WickraFractalChaosBandsOutput {
                upper: out_val.upper,
                lower: out_val.lower,
            };
            true
        }
        None => false,
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_fractal_chaos_bands_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_fractal_chaos_bands_warmup_period(
    handle: *mut FractalChaosBands,
) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_fractal_chaos_bands_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_fractal_chaos_bands_is_ready(
    handle: *mut FractalChaosBands,
) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_fractal_chaos_bands_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_fractal_chaos_bands_name(
    handle: *mut FractalChaosBands,
) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_fractal_chaos_bands_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_fractal_chaos_bands_reset(handle: *mut FractalChaosBands) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_fractal_chaos_bands_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_fractal_chaos_bands_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_fractal_chaos_bands_free(handle: *mut FractalChaosBands) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `GatorOscillator` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_gator_oscillator_free`.
#[no_mangle]
pub extern "C" fn wickra_gator_oscillator_new(
    jaw_period: usize,
    teeth_period: usize,
    lips_period: usize,
) -> *mut GatorOscillator {
    match GatorOscillator::new(jaw_period, teeth_period, lips_period) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one input; on `true` the freshly computed multi-field output is written to
/// `*out`. Returns `false` during warmup, on a `NULL` handle / `out`, or on invalid input.
///
/// # Safety
/// `handle` (from `wickra_gator_oscillator_new`, not freed) and `out` must be valid or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_gator_oscillator_update(
    handle: *mut GatorOscillator,
    open: f64,
    high: f64,
    low: f64,
    close: f64,
    volume: f64,
    timestamp: i64,
    out: *mut WickraGatorOscillatorOutput,
) -> bool {
    let Some(ind) = handle.as_mut() else {
        return false;
    };
    if out.is_null() {
        return false;
    }
    let Ok(input) = Candle::new(open, high, low, close, volume, timestamp) else {
        return false;
    };
    match ind.update(input) {
        Some(out_val) => {
            *out = WickraGatorOscillatorOutput {
                upper: out_val.upper,
                lower: out_val.lower,
            };
            true
        }
        None => false,
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_gator_oscillator_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_gator_oscillator_warmup_period(
    handle: *mut GatorOscillator,
) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_gator_oscillator_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_gator_oscillator_is_ready(handle: *mut GatorOscillator) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_gator_oscillator_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_gator_oscillator_name(
    handle: *mut GatorOscillator,
) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_gator_oscillator_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_gator_oscillator_reset(handle: *mut GatorOscillator) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_gator_oscillator_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_gator_oscillator_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_gator_oscillator_free(handle: *mut GatorOscillator) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `GoldenPocket` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_golden_pocket_free`.
#[no_mangle]
pub extern "C" fn wickra_golden_pocket_new() -> *mut GoldenPocket {
    Box::into_raw(Box::new(GoldenPocket::new()))
}

/// Feed one input; on `true` the freshly computed multi-field output is written to
/// `*out`. Returns `false` during warmup, on a `NULL` handle / `out`, or on invalid input.
///
/// # Safety
/// `handle` (from `wickra_golden_pocket_new`, not freed) and `out` must be valid or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_golden_pocket_update(
    handle: *mut GoldenPocket,
    open: f64,
    high: f64,
    low: f64,
    close: f64,
    volume: f64,
    timestamp: i64,
    out: *mut WickraGoldenPocketOutput,
) -> bool {
    let Some(ind) = handle.as_mut() else {
        return false;
    };
    if out.is_null() {
        return false;
    }
    let Ok(input) = Candle::new(open, high, low, close, volume, timestamp) else {
        return false;
    };
    match ind.update(input) {
        Some(out_val) => {
            *out = WickraGoldenPocketOutput {
                low: out_val.low,
                mid: out_val.mid,
                high: out_val.high,
            };
            true
        }
        None => false,
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_golden_pocket_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_golden_pocket_warmup_period(handle: *mut GoldenPocket) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_golden_pocket_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_golden_pocket_is_ready(handle: *mut GoldenPocket) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_golden_pocket_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_golden_pocket_name(handle: *mut GoldenPocket) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_golden_pocket_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_golden_pocket_reset(handle: *mut GoldenPocket) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_golden_pocket_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_golden_pocket_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_golden_pocket_free(handle: *mut GoldenPocket) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `HeikinAshi` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_heikin_ashi_free`.
#[no_mangle]
pub extern "C" fn wickra_heikin_ashi_new() -> *mut HeikinAshi {
    Box::into_raw(Box::new(HeikinAshi::new()))
}

/// Feed one input; on `true` the freshly computed multi-field output is written to
/// `*out`. Returns `false` during warmup, on a `NULL` handle / `out`, or on invalid input.
///
/// # Safety
/// `handle` (from `wickra_heikin_ashi_new`, not freed) and `out` must be valid or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_heikin_ashi_update(
    handle: *mut HeikinAshi,
    open: f64,
    high: f64,
    low: f64,
    close: f64,
    volume: f64,
    timestamp: i64,
    out: *mut WickraHeikinAshiOutput,
) -> bool {
    let Some(ind) = handle.as_mut() else {
        return false;
    };
    if out.is_null() {
        return false;
    }
    let Ok(input) = Candle::new(open, high, low, close, volume, timestamp) else {
        return false;
    };
    match ind.update(input) {
        Some(out_val) => {
            *out = WickraHeikinAshiOutput {
                open: out_val.open,
                high: out_val.high,
                low: out_val.low,
                close: out_val.close,
            };
            true
        }
        None => false,
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_heikin_ashi_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_heikin_ashi_warmup_period(handle: *mut HeikinAshi) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_heikin_ashi_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_heikin_ashi_is_ready(handle: *mut HeikinAshi) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_heikin_ashi_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_heikin_ashi_name(handle: *mut HeikinAshi) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_heikin_ashi_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_heikin_ashi_reset(handle: *mut HeikinAshi) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_heikin_ashi_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_heikin_ashi_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_heikin_ashi_free(handle: *mut HeikinAshi) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `HighLowVolumeNodes` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_high_low_volume_nodes_free`.
#[no_mangle]
pub extern "C" fn wickra_high_low_volume_nodes_new(
    period: usize,
    bins: usize,
) -> *mut HighLowVolumeNodes {
    match HighLowVolumeNodes::new(period, bins) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one input; on `true` the freshly computed multi-field output is written to
/// `*out`. Returns `false` during warmup, on a `NULL` handle / `out`, or on invalid input.
///
/// # Safety
/// `handle` (from `wickra_high_low_volume_nodes_new`, not freed) and `out` must be valid or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_high_low_volume_nodes_update(
    handle: *mut HighLowVolumeNodes,
    open: f64,
    high: f64,
    low: f64,
    close: f64,
    volume: f64,
    timestamp: i64,
    out: *mut WickraHighLowVolumeNodesOutput,
) -> bool {
    let Some(ind) = handle.as_mut() else {
        return false;
    };
    if out.is_null() {
        return false;
    }
    let Ok(input) = Candle::new(open, high, low, close, volume, timestamp) else {
        return false;
    };
    match ind.update(input) {
        Some(out_val) => {
            *out = WickraHighLowVolumeNodesOutput {
                hvn: out_val.hvn,
                lvn: out_val.lvn,
            };
            true
        }
        None => false,
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_high_low_volume_nodes_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_high_low_volume_nodes_warmup_period(
    handle: *mut HighLowVolumeNodes,
) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_high_low_volume_nodes_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_high_low_volume_nodes_is_ready(
    handle: *mut HighLowVolumeNodes,
) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_high_low_volume_nodes_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_high_low_volume_nodes_name(
    handle: *mut HighLowVolumeNodes,
) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_high_low_volume_nodes_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_high_low_volume_nodes_reset(handle: *mut HighLowVolumeNodes) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_high_low_volume_nodes_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_high_low_volume_nodes_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_high_low_volume_nodes_free(handle: *mut HighLowVolumeNodes) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `HtPhasor` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_ht_phasor_free`.
#[no_mangle]
pub extern "C" fn wickra_ht_phasor_new() -> *mut HtPhasor {
    Box::into_raw(Box::new(HtPhasor::new()))
}

/// Feed one input; on `true` the freshly computed multi-field output is written to
/// `*out`. Returns `false` during warmup, on a `NULL` handle / `out`, or on invalid input.
///
/// # Safety
/// `handle` (from `wickra_ht_phasor_new`, not freed) and `out` must be valid or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_ht_phasor_update(
    handle: *mut HtPhasor,
    value: f64,
    out: *mut WickraHtPhasorOutput,
) -> bool {
    let Some(ind) = handle.as_mut() else {
        return false;
    };
    if out.is_null() {
        return false;
    }
    match ind.update(value) {
        Some(out_val) => {
            *out = WickraHtPhasorOutput {
                inphase: out_val.inphase,
                quadrature: out_val.quadrature,
            };
            true
        }
        None => false,
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_ht_phasor_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_ht_phasor_warmup_period(handle: *mut HtPhasor) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_ht_phasor_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_ht_phasor_is_ready(handle: *mut HtPhasor) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_ht_phasor_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_ht_phasor_name(handle: *mut HtPhasor) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_ht_phasor_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_ht_phasor_reset(handle: *mut HtPhasor) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_ht_phasor_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_ht_phasor_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_ht_phasor_free(handle: *mut HtPhasor) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `HurstChannel` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_hurst_channel_free`.
#[no_mangle]
pub extern "C" fn wickra_hurst_channel_new(period: usize, multiplier: f64) -> *mut HurstChannel {
    match HurstChannel::new(period, multiplier) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one input; on `true` the freshly computed multi-field output is written to
/// `*out`. Returns `false` during warmup, on a `NULL` handle / `out`, or on invalid input.
///
/// # Safety
/// `handle` (from `wickra_hurst_channel_new`, not freed) and `out` must be valid or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_hurst_channel_update(
    handle: *mut HurstChannel,
    open: f64,
    high: f64,
    low: f64,
    close: f64,
    volume: f64,
    timestamp: i64,
    out: *mut WickraHurstChannelOutput,
) -> bool {
    let Some(ind) = handle.as_mut() else {
        return false;
    };
    if out.is_null() {
        return false;
    }
    let Ok(input) = Candle::new(open, high, low, close, volume, timestamp) else {
        return false;
    };
    match ind.update(input) {
        Some(out_val) => {
            *out = WickraHurstChannelOutput {
                upper: out_val.upper,
                middle: out_val.middle,
                lower: out_val.lower,
            };
            true
        }
        None => false,
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_hurst_channel_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_hurst_channel_warmup_period(handle: *mut HurstChannel) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_hurst_channel_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_hurst_channel_is_ready(handle: *mut HurstChannel) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_hurst_channel_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_hurst_channel_name(handle: *mut HurstChannel) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_hurst_channel_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_hurst_channel_reset(handle: *mut HurstChannel) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_hurst_channel_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_hurst_channel_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_hurst_channel_free(handle: *mut HurstChannel) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `Ichimoku` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_ichimoku_free`.
#[no_mangle]
pub extern "C" fn wickra_ichimoku_new(
    tenkan_period: usize,
    kijun_period: usize,
    senkou_b_period: usize,
    displacement: usize,
) -> *mut Ichimoku {
    match Ichimoku::new(tenkan_period, kijun_period, senkou_b_period, displacement) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one input; on `true` the freshly computed multi-field output is written to
/// `*out`. Returns `false` during warmup, on a `NULL` handle / `out`, or on invalid input.
///
/// # Safety
/// `handle` (from `wickra_ichimoku_new`, not freed) and `out` must be valid or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_ichimoku_update(
    handle: *mut Ichimoku,
    open: f64,
    high: f64,
    low: f64,
    close: f64,
    volume: f64,
    timestamp: i64,
    out: *mut WickraIchimokuOutput,
) -> bool {
    let Some(ind) = handle.as_mut() else {
        return false;
    };
    if out.is_null() {
        return false;
    }
    let Ok(input) = Candle::new(open, high, low, close, volume, timestamp) else {
        return false;
    };
    match ind.update(input) {
        Some(out_val) => {
            *out = WickraIchimokuOutput {
                tenkan: out_val.tenkan.unwrap_or(f64::NAN),
                kijun: out_val.kijun.unwrap_or(f64::NAN),
                senkou_a: out_val.senkou_a.unwrap_or(f64::NAN),
                senkou_b: out_val.senkou_b.unwrap_or(f64::NAN),
                chikou: out_val.chikou.unwrap_or(f64::NAN),
            };
            true
        }
        None => false,
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_ichimoku_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_ichimoku_warmup_period(handle: *mut Ichimoku) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_ichimoku_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_ichimoku_is_ready(handle: *mut Ichimoku) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_ichimoku_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_ichimoku_name(handle: *mut Ichimoku) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_ichimoku_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_ichimoku_reset(handle: *mut Ichimoku) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_ichimoku_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_ichimoku_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_ichimoku_free(handle: *mut Ichimoku) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `InitialBalance` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_initial_balance_free`.
#[no_mangle]
pub extern "C" fn wickra_initial_balance_new(period: usize) -> *mut InitialBalance {
    match InitialBalance::new(period) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one input; on `true` the freshly computed multi-field output is written to
/// `*out`. Returns `false` during warmup, on a `NULL` handle / `out`, or on invalid input.
///
/// # Safety
/// `handle` (from `wickra_initial_balance_new`, not freed) and `out` must be valid or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_initial_balance_update(
    handle: *mut InitialBalance,
    open: f64,
    high: f64,
    low: f64,
    close: f64,
    volume: f64,
    timestamp: i64,
    out: *mut WickraInitialBalanceOutput,
) -> bool {
    let Some(ind) = handle.as_mut() else {
        return false;
    };
    if out.is_null() {
        return false;
    }
    let Ok(input) = Candle::new(open, high, low, close, volume, timestamp) else {
        return false;
    };
    match ind.update(input) {
        Some(out_val) => {
            *out = WickraInitialBalanceOutput {
                high: out_val.high,
                low: out_val.low,
            };
            true
        }
        None => false,
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_initial_balance_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_initial_balance_warmup_period(
    handle: *mut InitialBalance,
) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_initial_balance_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_initial_balance_is_ready(handle: *mut InitialBalance) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_initial_balance_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_initial_balance_name(handle: *mut InitialBalance) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_initial_balance_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_initial_balance_reset(handle: *mut InitialBalance) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_initial_balance_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_initial_balance_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_initial_balance_free(handle: *mut InitialBalance) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `KalmanHedgeRatio` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_kalman_hedge_ratio_free`.
#[no_mangle]
pub extern "C" fn wickra_kalman_hedge_ratio_new(
    delta: f64,
    observation_var: f64,
) -> *mut KalmanHedgeRatio {
    match KalmanHedgeRatio::new(delta, observation_var) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one input; on `true` the freshly computed multi-field output is written to
/// `*out`. Returns `false` during warmup, on a `NULL` handle / `out`, or on invalid input.
///
/// # Safety
/// `handle` (from `wickra_kalman_hedge_ratio_new`, not freed) and `out` must be valid or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_kalman_hedge_ratio_update(
    handle: *mut KalmanHedgeRatio,
    x: f64,
    y: f64,
    out: *mut WickraKalmanHedgeRatioOutput,
) -> bool {
    let Some(ind) = handle.as_mut() else {
        return false;
    };
    if out.is_null() {
        return false;
    }
    match ind.update((x, y)) {
        Some(out_val) => {
            *out = WickraKalmanHedgeRatioOutput {
                hedge_ratio: out_val.hedge_ratio,
                intercept: out_val.intercept,
                spread: out_val.spread,
            };
            true
        }
        None => false,
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_kalman_hedge_ratio_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_kalman_hedge_ratio_warmup_period(
    handle: *mut KalmanHedgeRatio,
) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_kalman_hedge_ratio_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_kalman_hedge_ratio_is_ready(handle: *mut KalmanHedgeRatio) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_kalman_hedge_ratio_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_kalman_hedge_ratio_name(
    handle: *mut KalmanHedgeRatio,
) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_kalman_hedge_ratio_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_kalman_hedge_ratio_reset(handle: *mut KalmanHedgeRatio) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_kalman_hedge_ratio_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_kalman_hedge_ratio_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_kalman_hedge_ratio_free(handle: *mut KalmanHedgeRatio) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `KaseDevStop` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_kase_dev_stop_free`.
#[no_mangle]
pub extern "C" fn wickra_kase_dev_stop_new(period: usize, dev: f64) -> *mut KaseDevStop {
    match KaseDevStop::new(period, dev) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one input; on `true` the freshly computed multi-field output is written to
/// `*out`. Returns `false` during warmup, on a `NULL` handle / `out`, or on invalid input.
///
/// # Safety
/// `handle` (from `wickra_kase_dev_stop_new`, not freed) and `out` must be valid or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_kase_dev_stop_update(
    handle: *mut KaseDevStop,
    open: f64,
    high: f64,
    low: f64,
    close: f64,
    volume: f64,
    timestamp: i64,
    out: *mut WickraKaseDevStopOutput,
) -> bool {
    let Some(ind) = handle.as_mut() else {
        return false;
    };
    if out.is_null() {
        return false;
    }
    let Ok(input) = Candle::new(open, high, low, close, volume, timestamp) else {
        return false;
    };
    match ind.update(input) {
        Some(out_val) => {
            *out = WickraKaseDevStopOutput {
                value: out_val.value,
                direction: out_val.direction,
            };
            true
        }
        None => false,
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_kase_dev_stop_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_kase_dev_stop_warmup_period(handle: *mut KaseDevStop) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_kase_dev_stop_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_kase_dev_stop_is_ready(handle: *mut KaseDevStop) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_kase_dev_stop_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_kase_dev_stop_name(handle: *mut KaseDevStop) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_kase_dev_stop_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_kase_dev_stop_reset(handle: *mut KaseDevStop) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_kase_dev_stop_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_kase_dev_stop_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_kase_dev_stop_free(handle: *mut KaseDevStop) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `KasePermissionStochastic` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_kase_permission_stochastic_free`.
#[no_mangle]
pub extern "C" fn wickra_kase_permission_stochastic_new(
    length: usize,
    smooth: usize,
) -> *mut KasePermissionStochastic {
    match KasePermissionStochastic::new(length, smooth) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one input; on `true` the freshly computed multi-field output is written to
/// `*out`. Returns `false` during warmup, on a `NULL` handle / `out`, or on invalid input.
///
/// # Safety
/// `handle` (from `wickra_kase_permission_stochastic_new`, not freed) and `out` must be valid or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_kase_permission_stochastic_update(
    handle: *mut KasePermissionStochastic,
    open: f64,
    high: f64,
    low: f64,
    close: f64,
    volume: f64,
    timestamp: i64,
    out: *mut WickraKasePermissionStochasticOutput,
) -> bool {
    let Some(ind) = handle.as_mut() else {
        return false;
    };
    if out.is_null() {
        return false;
    }
    let Ok(input) = Candle::new(open, high, low, close, volume, timestamp) else {
        return false;
    };
    match ind.update(input) {
        Some(out_val) => {
            *out = WickraKasePermissionStochasticOutput {
                fast: out_val.fast,
                slow: out_val.slow,
            };
            true
        }
        None => false,
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_kase_permission_stochastic_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_kase_permission_stochastic_warmup_period(
    handle: *mut KasePermissionStochastic,
) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_kase_permission_stochastic_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_kase_permission_stochastic_is_ready(
    handle: *mut KasePermissionStochastic,
) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_kase_permission_stochastic_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_kase_permission_stochastic_name(
    handle: *mut KasePermissionStochastic,
) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_kase_permission_stochastic_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_kase_permission_stochastic_reset(
    handle: *mut KasePermissionStochastic,
) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_kase_permission_stochastic_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_kase_permission_stochastic_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_kase_permission_stochastic_free(
    handle: *mut KasePermissionStochastic,
) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `Keltner` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_keltner_free`.
#[no_mangle]
pub extern "C" fn wickra_keltner_new(
    ema_period: usize,
    atr_period: usize,
    multiplier: f64,
) -> *mut Keltner {
    match Keltner::new(ema_period, atr_period, multiplier) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one input; on `true` the freshly computed multi-field output is written to
/// `*out`. Returns `false` during warmup, on a `NULL` handle / `out`, or on invalid input.
///
/// # Safety
/// `handle` (from `wickra_keltner_new`, not freed) and `out` must be valid or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_keltner_update(
    handle: *mut Keltner,
    open: f64,
    high: f64,
    low: f64,
    close: f64,
    volume: f64,
    timestamp: i64,
    out: *mut WickraKeltnerOutput,
) -> bool {
    let Some(ind) = handle.as_mut() else {
        return false;
    };
    if out.is_null() {
        return false;
    }
    let Ok(input) = Candle::new(open, high, low, close, volume, timestamp) else {
        return false;
    };
    match ind.update(input) {
        Some(out_val) => {
            *out = WickraKeltnerOutput {
                upper: out_val.upper,
                middle: out_val.middle,
                lower: out_val.lower,
            };
            true
        }
        None => false,
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_keltner_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_keltner_warmup_period(handle: *mut Keltner) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_keltner_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_keltner_is_ready(handle: *mut Keltner) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_keltner_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_keltner_name(handle: *mut Keltner) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_keltner_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_keltner_reset(handle: *mut Keltner) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_keltner_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_keltner_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_keltner_free(handle: *mut Keltner) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `Kst` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_kst_free`.
#[no_mangle]
pub extern "C" fn wickra_kst_new(
    roc1: usize,
    roc2: usize,
    roc3: usize,
    roc4: usize,
    sma1: usize,
    sma2: usize,
    sma3: usize,
    sma4: usize,
    signal: usize,
) -> *mut Kst {
    match Kst::new(roc1, roc2, roc3, roc4, sma1, sma2, sma3, sma4, signal) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one input; on `true` the freshly computed multi-field output is written to
/// `*out`. Returns `false` during warmup, on a `NULL` handle / `out`, or on invalid input.
///
/// # Safety
/// `handle` (from `wickra_kst_new`, not freed) and `out` must be valid or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_kst_update(
    handle: *mut Kst,
    value: f64,
    out: *mut WickraKstOutput,
) -> bool {
    let Some(ind) = handle.as_mut() else {
        return false;
    };
    if out.is_null() {
        return false;
    }
    match ind.update(value) {
        Some(out_val) => {
            *out = WickraKstOutput {
                kst: out_val.kst,
                signal: out_val.signal,
            };
            true
        }
        None => false,
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_kst_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_kst_warmup_period(handle: *mut Kst) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_kst_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_kst_is_ready(handle: *mut Kst) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_kst_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_kst_name(handle: *mut Kst) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_kst_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_kst_reset(handle: *mut Kst) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_kst_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_kst_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_kst_free(handle: *mut Kst) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `LeadLagCrossCorrelation` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_lead_lag_cross_correlation_free`.
#[no_mangle]
pub extern "C" fn wickra_lead_lag_cross_correlation_new(
    window: usize,
    max_lag: usize,
) -> *mut LeadLagCrossCorrelation {
    match LeadLagCrossCorrelation::new(window, max_lag) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one input; on `true` the freshly computed multi-field output is written to
/// `*out`. Returns `false` during warmup, on a `NULL` handle / `out`, or on invalid input.
///
/// # Safety
/// `handle` (from `wickra_lead_lag_cross_correlation_new`, not freed) and `out` must be valid or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_lead_lag_cross_correlation_update(
    handle: *mut LeadLagCrossCorrelation,
    x: f64,
    y: f64,
    out: *mut WickraLeadLagCrossCorrelationOutput,
) -> bool {
    let Some(ind) = handle.as_mut() else {
        return false;
    };
    if out.is_null() {
        return false;
    }
    match ind.update((x, y)) {
        Some(out_val) => {
            *out = WickraLeadLagCrossCorrelationOutput {
                lag: out_val.lag,
                correlation: out_val.correlation,
            };
            true
        }
        None => false,
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_lead_lag_cross_correlation_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_lead_lag_cross_correlation_warmup_period(
    handle: *mut LeadLagCrossCorrelation,
) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_lead_lag_cross_correlation_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_lead_lag_cross_correlation_is_ready(
    handle: *mut LeadLagCrossCorrelation,
) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_lead_lag_cross_correlation_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_lead_lag_cross_correlation_name(
    handle: *mut LeadLagCrossCorrelation,
) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_lead_lag_cross_correlation_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_lead_lag_cross_correlation_reset(
    handle: *mut LeadLagCrossCorrelation,
) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_lead_lag_cross_correlation_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_lead_lag_cross_correlation_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_lead_lag_cross_correlation_free(
    handle: *mut LeadLagCrossCorrelation,
) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `LinRegChannel` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_lin_reg_channel_free`.
#[no_mangle]
pub extern "C" fn wickra_lin_reg_channel_new(period: usize, multiplier: f64) -> *mut LinRegChannel {
    match LinRegChannel::new(period, multiplier) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one input; on `true` the freshly computed multi-field output is written to
/// `*out`. Returns `false` during warmup, on a `NULL` handle / `out`, or on invalid input.
///
/// # Safety
/// `handle` (from `wickra_lin_reg_channel_new`, not freed) and `out` must be valid or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_lin_reg_channel_update(
    handle: *mut LinRegChannel,
    value: f64,
    out: *mut WickraLinRegChannelOutput,
) -> bool {
    let Some(ind) = handle.as_mut() else {
        return false;
    };
    if out.is_null() {
        return false;
    }
    match ind.update(value) {
        Some(out_val) => {
            *out = WickraLinRegChannelOutput {
                upper: out_val.upper,
                middle: out_val.middle,
                lower: out_val.lower,
            };
            true
        }
        None => false,
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_lin_reg_channel_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_lin_reg_channel_warmup_period(handle: *mut LinRegChannel) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_lin_reg_channel_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_lin_reg_channel_is_ready(handle: *mut LinRegChannel) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_lin_reg_channel_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_lin_reg_channel_name(handle: *mut LinRegChannel) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_lin_reg_channel_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_lin_reg_channel_reset(handle: *mut LinRegChannel) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_lin_reg_channel_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_lin_reg_channel_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_lin_reg_channel_free(handle: *mut LinRegChannel) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `LiquidationFeatures` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_liquidation_features_free`.
#[no_mangle]
pub extern "C" fn wickra_liquidation_features_new() -> *mut LiquidationFeatures {
    Box::into_raw(Box::new(LiquidationFeatures::new()))
}

/// Feed one input; on `true` the freshly computed multi-field output is written to
/// `*out`. Returns `false` during warmup, on a `NULL` handle / `out`, or on invalid input.
///
/// # Safety
/// `handle` (from `wickra_liquidation_features_new`, not freed) and `out` must be valid or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_liquidation_features_update(
    handle: *mut LiquidationFeatures,
    funding_rate: f64,
    mark_price: f64,
    index_price: f64,
    futures_price: f64,
    open_interest: f64,
    long_size: f64,
    short_size: f64,
    taker_buy_volume: f64,
    taker_sell_volume: f64,
    long_liquidation: f64,
    short_liquidation: f64,
    timestamp: i64,
    out: *mut WickraLiquidationFeaturesOutput,
) -> bool {
    let Some(ind) = handle.as_mut() else {
        return false;
    };
    if out.is_null() {
        return false;
    }
    let Ok(input) = DerivativesTick::new(
        funding_rate,
        mark_price,
        index_price,
        futures_price,
        open_interest,
        long_size,
        short_size,
        taker_buy_volume,
        taker_sell_volume,
        long_liquidation,
        short_liquidation,
        timestamp,
    ) else {
        return false;
    };
    match ind.update(input) {
        Some(out_val) => {
            *out = WickraLiquidationFeaturesOutput {
                long: out_val.long,
                short: out_val.short,
                net: out_val.net,
                total: out_val.total,
                imbalance: out_val.imbalance,
            };
            true
        }
        None => false,
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_liquidation_features_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_liquidation_features_warmup_period(
    handle: *mut LiquidationFeatures,
) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_liquidation_features_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_liquidation_features_is_ready(
    handle: *mut LiquidationFeatures,
) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_liquidation_features_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_liquidation_features_name(
    handle: *mut LiquidationFeatures,
) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_liquidation_features_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_liquidation_features_reset(handle: *mut LiquidationFeatures) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_liquidation_features_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_liquidation_features_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_liquidation_features_free(handle: *mut LiquidationFeatures) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `MaEnvelope` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_ma_envelope_free`.
#[no_mangle]
pub extern "C" fn wickra_ma_envelope_new(period: usize, percent: f64) -> *mut MaEnvelope {
    match MaEnvelope::new(period, percent) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one input; on `true` the freshly computed multi-field output is written to
/// `*out`. Returns `false` during warmup, on a `NULL` handle / `out`, or on invalid input.
///
/// # Safety
/// `handle` (from `wickra_ma_envelope_new`, not freed) and `out` must be valid or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_ma_envelope_update(
    handle: *mut MaEnvelope,
    value: f64,
    out: *mut WickraMaEnvelopeOutput,
) -> bool {
    let Some(ind) = handle.as_mut() else {
        return false;
    };
    if out.is_null() {
        return false;
    }
    match ind.update(value) {
        Some(out_val) => {
            *out = WickraMaEnvelopeOutput {
                upper: out_val.upper,
                middle: out_val.middle,
                lower: out_val.lower,
            };
            true
        }
        None => false,
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_ma_envelope_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_ma_envelope_warmup_period(handle: *mut MaEnvelope) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_ma_envelope_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_ma_envelope_is_ready(handle: *mut MaEnvelope) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_ma_envelope_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_ma_envelope_name(handle: *mut MaEnvelope) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_ma_envelope_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_ma_envelope_reset(handle: *mut MaEnvelope) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_ma_envelope_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_ma_envelope_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_ma_envelope_free(handle: *mut MaEnvelope) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `MacdIndicator` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_macd_indicator_free`.
#[no_mangle]
pub extern "C" fn wickra_macd_indicator_new(
    fast: usize,
    slow: usize,
    signal: usize,
) -> *mut MacdIndicator {
    match MacdIndicator::new(fast, slow, signal) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one input; on `true` the freshly computed multi-field output is written to
/// `*out`. Returns `false` during warmup, on a `NULL` handle / `out`, or on invalid input.
///
/// # Safety
/// `handle` (from `wickra_macd_indicator_new`, not freed) and `out` must be valid or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_macd_indicator_update(
    handle: *mut MacdIndicator,
    value: f64,
    out: *mut WickraMacdOutput,
) -> bool {
    let Some(ind) = handle.as_mut() else {
        return false;
    };
    if out.is_null() {
        return false;
    }
    match ind.update(value) {
        Some(out_val) => {
            *out = WickraMacdOutput {
                macd: out_val.macd,
                signal: out_val.signal,
                histogram: out_val.histogram,
            };
            true
        }
        None => false,
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_macd_indicator_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_macd_indicator_warmup_period(handle: *mut MacdIndicator) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_macd_indicator_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_macd_indicator_is_ready(handle: *mut MacdIndicator) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_macd_indicator_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_macd_indicator_name(handle: *mut MacdIndicator) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_macd_indicator_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_macd_indicator_reset(handle: *mut MacdIndicator) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_macd_indicator_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_macd_indicator_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_macd_indicator_free(handle: *mut MacdIndicator) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `MacdFix` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_macd_fix_free`.
#[no_mangle]
pub extern "C" fn wickra_macd_fix_new(signal: usize) -> *mut MacdFix {
    match MacdFix::new(signal) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one input; on `true` the freshly computed multi-field output is written to
/// `*out`. Returns `false` during warmup, on a `NULL` handle / `out`, or on invalid input.
///
/// # Safety
/// `handle` (from `wickra_macd_fix_new`, not freed) and `out` must be valid or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_macd_fix_update(
    handle: *mut MacdFix,
    value: f64,
    out: *mut WickraMacdOutput,
) -> bool {
    let Some(ind) = handle.as_mut() else {
        return false;
    };
    if out.is_null() {
        return false;
    }
    match ind.update(value) {
        Some(out_val) => {
            *out = WickraMacdOutput {
                macd: out_val.macd,
                signal: out_val.signal,
                histogram: out_val.histogram,
            };
            true
        }
        None => false,
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_macd_fix_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_macd_fix_warmup_period(handle: *mut MacdFix) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_macd_fix_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_macd_fix_is_ready(handle: *mut MacdFix) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_macd_fix_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_macd_fix_name(handle: *mut MacdFix) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_macd_fix_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_macd_fix_reset(handle: *mut MacdFix) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_macd_fix_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_macd_fix_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_macd_fix_free(handle: *mut MacdFix) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `Mama` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_mama_free`.
#[no_mangle]
pub extern "C" fn wickra_mama_new(fast_limit: f64, slow_limit: f64) -> *mut Mama {
    match Mama::new(fast_limit, slow_limit) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one input; on `true` the freshly computed multi-field output is written to
/// `*out`. Returns `false` during warmup, on a `NULL` handle / `out`, or on invalid input.
///
/// # Safety
/// `handle` (from `wickra_mama_new`, not freed) and `out` must be valid or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_mama_update(
    handle: *mut Mama,
    value: f64,
    out: *mut WickraMamaOutput,
) -> bool {
    let Some(ind) = handle.as_mut() else {
        return false;
    };
    if out.is_null() {
        return false;
    }
    match ind.update(value) {
        Some(out_val) => {
            *out = WickraMamaOutput {
                mama: out_val.mama,
                fama: out_val.fama,
            };
            true
        }
        None => false,
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_mama_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_mama_warmup_period(handle: *mut Mama) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_mama_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_mama_is_ready(handle: *mut Mama) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_mama_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_mama_name(handle: *mut Mama) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_mama_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_mama_reset(handle: *mut Mama) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_mama_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_mama_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_mama_free(handle: *mut Mama) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `MedianChannel` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_median_channel_free`.
#[no_mangle]
pub extern "C" fn wickra_median_channel_new(period: usize, multiplier: f64) -> *mut MedianChannel {
    match MedianChannel::new(period, multiplier) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one input; on `true` the freshly computed multi-field output is written to
/// `*out`. Returns `false` during warmup, on a `NULL` handle / `out`, or on invalid input.
///
/// # Safety
/// `handle` (from `wickra_median_channel_new`, not freed) and `out` must be valid or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_median_channel_update(
    handle: *mut MedianChannel,
    value: f64,
    out: *mut WickraMedianChannelOutput,
) -> bool {
    let Some(ind) = handle.as_mut() else {
        return false;
    };
    if out.is_null() {
        return false;
    }
    match ind.update(value) {
        Some(out_val) => {
            *out = WickraMedianChannelOutput {
                upper: out_val.upper,
                middle: out_val.middle,
                lower: out_val.lower,
            };
            true
        }
        None => false,
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_median_channel_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_median_channel_warmup_period(handle: *mut MedianChannel) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_median_channel_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_median_channel_is_ready(handle: *mut MedianChannel) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_median_channel_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_median_channel_name(handle: *mut MedianChannel) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_median_channel_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_median_channel_reset(handle: *mut MedianChannel) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_median_channel_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_median_channel_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_median_channel_free(handle: *mut MedianChannel) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `ModifiedMaStop` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_modified_ma_stop_free`.
#[no_mangle]
pub extern "C" fn wickra_modified_ma_stop_new(period: usize) -> *mut ModifiedMaStop {
    match ModifiedMaStop::new(period) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one input; on `true` the freshly computed multi-field output is written to
/// `*out`. Returns `false` during warmup, on a `NULL` handle / `out`, or on invalid input.
///
/// # Safety
/// `handle` (from `wickra_modified_ma_stop_new`, not freed) and `out` must be valid or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_modified_ma_stop_update(
    handle: *mut ModifiedMaStop,
    open: f64,
    high: f64,
    low: f64,
    close: f64,
    volume: f64,
    timestamp: i64,
    out: *mut WickraModifiedMaStopOutput,
) -> bool {
    let Some(ind) = handle.as_mut() else {
        return false;
    };
    if out.is_null() {
        return false;
    }
    let Ok(input) = Candle::new(open, high, low, close, volume, timestamp) else {
        return false;
    };
    match ind.update(input) {
        Some(out_val) => {
            *out = WickraModifiedMaStopOutput {
                value: out_val.value,
                direction: out_val.direction,
            };
            true
        }
        None => false,
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_modified_ma_stop_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_modified_ma_stop_warmup_period(
    handle: *mut ModifiedMaStop,
) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_modified_ma_stop_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_modified_ma_stop_is_ready(handle: *mut ModifiedMaStop) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_modified_ma_stop_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_modified_ma_stop_name(
    handle: *mut ModifiedMaStop,
) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_modified_ma_stop_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_modified_ma_stop_reset(handle: *mut ModifiedMaStop) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_modified_ma_stop_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_modified_ma_stop_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_modified_ma_stop_free(handle: *mut ModifiedMaStop) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `MurreyMathLines` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_murrey_math_lines_free`.
#[no_mangle]
pub extern "C" fn wickra_murrey_math_lines_new(period: usize) -> *mut MurreyMathLines {
    match MurreyMathLines::new(period) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one input; on `true` the freshly computed multi-field output is written to
/// `*out`. Returns `false` during warmup, on a `NULL` handle / `out`, or on invalid input.
///
/// # Safety
/// `handle` (from `wickra_murrey_math_lines_new`, not freed) and `out` must be valid or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_murrey_math_lines_update(
    handle: *mut MurreyMathLines,
    open: f64,
    high: f64,
    low: f64,
    close: f64,
    volume: f64,
    timestamp: i64,
    out: *mut WickraMurreyMathLinesOutput,
) -> bool {
    let Some(ind) = handle.as_mut() else {
        return false;
    };
    if out.is_null() {
        return false;
    }
    let Ok(input) = Candle::new(open, high, low, close, volume, timestamp) else {
        return false;
    };
    match ind.update(input) {
        Some(out_val) => {
            *out = WickraMurreyMathLinesOutput {
                mm8_8: out_val.mm8_8,
                mm7_8: out_val.mm7_8,
                mm6_8: out_val.mm6_8,
                mm5_8: out_val.mm5_8,
                mm4_8: out_val.mm4_8,
                mm3_8: out_val.mm3_8,
                mm2_8: out_val.mm2_8,
                mm1_8: out_val.mm1_8,
                mm0_8: out_val.mm0_8,
            };
            true
        }
        None => false,
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_murrey_math_lines_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_murrey_math_lines_warmup_period(
    handle: *mut MurreyMathLines,
) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_murrey_math_lines_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_murrey_math_lines_is_ready(handle: *mut MurreyMathLines) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_murrey_math_lines_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_murrey_math_lines_name(
    handle: *mut MurreyMathLines,
) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_murrey_math_lines_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_murrey_math_lines_reset(handle: *mut MurreyMathLines) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_murrey_math_lines_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_murrey_math_lines_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_murrey_math_lines_free(handle: *mut MurreyMathLines) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `Nrtr` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_nrtr_free`.
#[no_mangle]
pub extern "C" fn wickra_nrtr_new(pct: f64) -> *mut Nrtr {
    match Nrtr::new(pct) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one input; on `true` the freshly computed multi-field output is written to
/// `*out`. Returns `false` during warmup, on a `NULL` handle / `out`, or on invalid input.
///
/// # Safety
/// `handle` (from `wickra_nrtr_new`, not freed) and `out` must be valid or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_nrtr_update(
    handle: *mut Nrtr,
    open: f64,
    high: f64,
    low: f64,
    close: f64,
    volume: f64,
    timestamp: i64,
    out: *mut WickraNrtrOutput,
) -> bool {
    let Some(ind) = handle.as_mut() else {
        return false;
    };
    if out.is_null() {
        return false;
    }
    let Ok(input) = Candle::new(open, high, low, close, volume, timestamp) else {
        return false;
    };
    match ind.update(input) {
        Some(out_val) => {
            *out = WickraNrtrOutput {
                value: out_val.value,
                direction: out_val.direction,
            };
            true
        }
        None => false,
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_nrtr_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_nrtr_warmup_period(handle: *mut Nrtr) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_nrtr_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_nrtr_is_ready(handle: *mut Nrtr) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_nrtr_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_nrtr_name(handle: *mut Nrtr) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_nrtr_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_nrtr_reset(handle: *mut Nrtr) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_nrtr_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_nrtr_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_nrtr_free(handle: *mut Nrtr) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `OpeningRange` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_opening_range_free`.
#[no_mangle]
pub extern "C" fn wickra_opening_range_new(period: usize) -> *mut OpeningRange {
    match OpeningRange::new(period) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one input; on `true` the freshly computed multi-field output is written to
/// `*out`. Returns `false` during warmup, on a `NULL` handle / `out`, or on invalid input.
///
/// # Safety
/// `handle` (from `wickra_opening_range_new`, not freed) and `out` must be valid or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_opening_range_update(
    handle: *mut OpeningRange,
    open: f64,
    high: f64,
    low: f64,
    close: f64,
    volume: f64,
    timestamp: i64,
    out: *mut WickraOpeningRangeOutput,
) -> bool {
    let Some(ind) = handle.as_mut() else {
        return false;
    };
    if out.is_null() {
        return false;
    }
    let Ok(input) = Candle::new(open, high, low, close, volume, timestamp) else {
        return false;
    };
    match ind.update(input) {
        Some(out_val) => {
            *out = WickraOpeningRangeOutput {
                high: out_val.high,
                low: out_val.low,
                breakout_distance: out_val.breakout_distance,
            };
            true
        }
        None => false,
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_opening_range_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_opening_range_warmup_period(handle: *mut OpeningRange) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_opening_range_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_opening_range_is_ready(handle: *mut OpeningRange) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_opening_range_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_opening_range_name(handle: *mut OpeningRange) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_opening_range_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_opening_range_reset(handle: *mut OpeningRange) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_opening_range_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_opening_range_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_opening_range_free(handle: *mut OpeningRange) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `OvernightIntradayReturn` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_overnight_intraday_return_free`.
#[no_mangle]
pub extern "C" fn wickra_overnight_intraday_return_new(
    utc_offset_minutes: i32,
) -> *mut OvernightIntradayReturn {
    Box::into_raw(Box::new(OvernightIntradayReturn::new(utc_offset_minutes)))
}

/// Feed one input; on `true` the freshly computed multi-field output is written to
/// `*out`. Returns `false` during warmup, on a `NULL` handle / `out`, or on invalid input.
///
/// # Safety
/// `handle` (from `wickra_overnight_intraday_return_new`, not freed) and `out` must be valid or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_overnight_intraday_return_update(
    handle: *mut OvernightIntradayReturn,
    open: f64,
    high: f64,
    low: f64,
    close: f64,
    volume: f64,
    timestamp: i64,
    out: *mut WickraOvernightIntradayReturnOutput,
) -> bool {
    let Some(ind) = handle.as_mut() else {
        return false;
    };
    if out.is_null() {
        return false;
    }
    let Ok(input) = Candle::new(open, high, low, close, volume, timestamp) else {
        return false;
    };
    match ind.update(input) {
        Some(out_val) => {
            *out = WickraOvernightIntradayReturnOutput {
                overnight: out_val.overnight,
                intraday: out_val.intraday,
            };
            true
        }
        None => false,
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_overnight_intraday_return_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_overnight_intraday_return_warmup_period(
    handle: *mut OvernightIntradayReturn,
) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_overnight_intraday_return_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_overnight_intraday_return_is_ready(
    handle: *mut OvernightIntradayReturn,
) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_overnight_intraday_return_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_overnight_intraday_return_name(
    handle: *mut OvernightIntradayReturn,
) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_overnight_intraday_return_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_overnight_intraday_return_reset(
    handle: *mut OvernightIntradayReturn,
) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_overnight_intraday_return_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_overnight_intraday_return_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_overnight_intraday_return_free(
    handle: *mut OvernightIntradayReturn,
) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `ProjectionBands` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_projection_bands_free`.
#[no_mangle]
pub extern "C" fn wickra_projection_bands_new(period: usize) -> *mut ProjectionBands {
    match ProjectionBands::new(period) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one input; on `true` the freshly computed multi-field output is written to
/// `*out`. Returns `false` during warmup, on a `NULL` handle / `out`, or on invalid input.
///
/// # Safety
/// `handle` (from `wickra_projection_bands_new`, not freed) and `out` must be valid or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_projection_bands_update(
    handle: *mut ProjectionBands,
    open: f64,
    high: f64,
    low: f64,
    close: f64,
    volume: f64,
    timestamp: i64,
    out: *mut WickraProjectionBandsOutput,
) -> bool {
    let Some(ind) = handle.as_mut() else {
        return false;
    };
    if out.is_null() {
        return false;
    }
    let Ok(input) = Candle::new(open, high, low, close, volume, timestamp) else {
        return false;
    };
    match ind.update(input) {
        Some(out_val) => {
            *out = WickraProjectionBandsOutput {
                upper: out_val.upper,
                middle: out_val.middle,
                lower: out_val.lower,
            };
            true
        }
        None => false,
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_projection_bands_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_projection_bands_warmup_period(
    handle: *mut ProjectionBands,
) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_projection_bands_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_projection_bands_is_ready(handle: *mut ProjectionBands) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_projection_bands_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_projection_bands_name(
    handle: *mut ProjectionBands,
) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_projection_bands_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_projection_bands_reset(handle: *mut ProjectionBands) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_projection_bands_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_projection_bands_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_projection_bands_free(handle: *mut ProjectionBands) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `Qqe` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_qqe_free`.
#[no_mangle]
pub extern "C" fn wickra_qqe_new(rsi_period: usize, smoothing: usize, factor: f64) -> *mut Qqe {
    match Qqe::new(rsi_period, smoothing, factor) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one input; on `true` the freshly computed multi-field output is written to
/// `*out`. Returns `false` during warmup, on a `NULL` handle / `out`, or on invalid input.
///
/// # Safety
/// `handle` (from `wickra_qqe_new`, not freed) and `out` must be valid or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_qqe_update(
    handle: *mut Qqe,
    value: f64,
    out: *mut WickraQqeOutput,
) -> bool {
    let Some(ind) = handle.as_mut() else {
        return false;
    };
    if out.is_null() {
        return false;
    }
    match ind.update(value) {
        Some(out_val) => {
            *out = WickraQqeOutput {
                rsi_ma: out_val.rsi_ma,
                trailing_line: out_val.trailing_line,
            };
            true
        }
        None => false,
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_qqe_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_qqe_warmup_period(handle: *mut Qqe) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_qqe_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_qqe_is_ready(handle: *mut Qqe) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_qqe_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_qqe_name(handle: *mut Qqe) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_qqe_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_qqe_reset(handle: *mut Qqe) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_qqe_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_qqe_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_qqe_free(handle: *mut Qqe) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `QuartileBands` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_quartile_bands_free`.
#[no_mangle]
pub extern "C" fn wickra_quartile_bands_new(period: usize) -> *mut QuartileBands {
    match QuartileBands::new(period) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one input; on `true` the freshly computed multi-field output is written to
/// `*out`. Returns `false` during warmup, on a `NULL` handle / `out`, or on invalid input.
///
/// # Safety
/// `handle` (from `wickra_quartile_bands_new`, not freed) and `out` must be valid or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_quartile_bands_update(
    handle: *mut QuartileBands,
    value: f64,
    out: *mut WickraQuartileBandsOutput,
) -> bool {
    let Some(ind) = handle.as_mut() else {
        return false;
    };
    if out.is_null() {
        return false;
    }
    match ind.update(value) {
        Some(out_val) => {
            *out = WickraQuartileBandsOutput {
                upper: out_val.upper,
                middle: out_val.middle,
                lower: out_val.lower,
            };
            true
        }
        None => false,
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_quartile_bands_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_quartile_bands_warmup_period(handle: *mut QuartileBands) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_quartile_bands_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_quartile_bands_is_ready(handle: *mut QuartileBands) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_quartile_bands_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_quartile_bands_name(handle: *mut QuartileBands) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_quartile_bands_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_quartile_bands_reset(handle: *mut QuartileBands) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_quartile_bands_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_quartile_bands_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_quartile_bands_free(handle: *mut QuartileBands) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `RelativeStrengthAB` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_relative_strength_ab_free`.
#[no_mangle]
pub extern "C" fn wickra_relative_strength_ab_new(
    ma_period: usize,
    rsi_period: usize,
) -> *mut RelativeStrengthAB {
    match RelativeStrengthAB::new(ma_period, rsi_period) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one input; on `true` the freshly computed multi-field output is written to
/// `*out`. Returns `false` during warmup, on a `NULL` handle / `out`, or on invalid input.
///
/// # Safety
/// `handle` (from `wickra_relative_strength_ab_new`, not freed) and `out` must be valid or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_relative_strength_ab_update(
    handle: *mut RelativeStrengthAB,
    x: f64,
    y: f64,
    out: *mut WickraRelativeStrengthOutput,
) -> bool {
    let Some(ind) = handle.as_mut() else {
        return false;
    };
    if out.is_null() {
        return false;
    }
    match ind.update((x, y)) {
        Some(out_val) => {
            *out = WickraRelativeStrengthOutput {
                ratio: out_val.ratio,
                ratio_ma: out_val.ratio_ma,
                ratio_rsi: out_val.ratio_rsi,
            };
            true
        }
        None => false,
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_relative_strength_ab_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_relative_strength_ab_warmup_period(
    handle: *mut RelativeStrengthAB,
) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_relative_strength_ab_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_relative_strength_ab_is_ready(
    handle: *mut RelativeStrengthAB,
) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_relative_strength_ab_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_relative_strength_ab_name(
    handle: *mut RelativeStrengthAB,
) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_relative_strength_ab_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_relative_strength_ab_reset(handle: *mut RelativeStrengthAB) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_relative_strength_ab_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_relative_strength_ab_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_relative_strength_ab_free(handle: *mut RelativeStrengthAB) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `Rwi` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_rwi_free`.
#[no_mangle]
pub extern "C" fn wickra_rwi_new(period: usize) -> *mut Rwi {
    match Rwi::new(period) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one input; on `true` the freshly computed multi-field output is written to
/// `*out`. Returns `false` during warmup, on a `NULL` handle / `out`, or on invalid input.
///
/// # Safety
/// `handle` (from `wickra_rwi_new`, not freed) and `out` must be valid or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_rwi_update(
    handle: *mut Rwi,
    open: f64,
    high: f64,
    low: f64,
    close: f64,
    volume: f64,
    timestamp: i64,
    out: *mut WickraRwiOutput,
) -> bool {
    let Some(ind) = handle.as_mut() else {
        return false;
    };
    if out.is_null() {
        return false;
    }
    let Ok(input) = Candle::new(open, high, low, close, volume, timestamp) else {
        return false;
    };
    match ind.update(input) {
        Some(out_val) => {
            *out = WickraRwiOutput {
                high: out_val.high,
                low: out_val.low,
            };
            true
        }
        None => false,
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_rwi_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_rwi_warmup_period(handle: *mut Rwi) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_rwi_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_rwi_is_ready(handle: *mut Rwi) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_rwi_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_rwi_name(handle: *mut Rwi) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_rwi_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_rwi_reset(handle: *mut Rwi) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_rwi_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_rwi_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_rwi_free(handle: *mut Rwi) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `SessionHighLow` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_session_high_low_free`.
#[no_mangle]
pub extern "C" fn wickra_session_high_low_new(utc_offset_minutes: i32) -> *mut SessionHighLow {
    Box::into_raw(Box::new(SessionHighLow::new(utc_offset_minutes)))
}

/// Feed one input; on `true` the freshly computed multi-field output is written to
/// `*out`. Returns `false` during warmup, on a `NULL` handle / `out`, or on invalid input.
///
/// # Safety
/// `handle` (from `wickra_session_high_low_new`, not freed) and `out` must be valid or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_session_high_low_update(
    handle: *mut SessionHighLow,
    open: f64,
    high: f64,
    low: f64,
    close: f64,
    volume: f64,
    timestamp: i64,
    out: *mut WickraSessionHighLowOutput,
) -> bool {
    let Some(ind) = handle.as_mut() else {
        return false;
    };
    if out.is_null() {
        return false;
    }
    let Ok(input) = Candle::new(open, high, low, close, volume, timestamp) else {
        return false;
    };
    match ind.update(input) {
        Some(out_val) => {
            *out = WickraSessionHighLowOutput {
                high: out_val.high,
                low: out_val.low,
            };
            true
        }
        None => false,
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_session_high_low_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_session_high_low_warmup_period(
    handle: *mut SessionHighLow,
) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_session_high_low_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_session_high_low_is_ready(handle: *mut SessionHighLow) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_session_high_low_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_session_high_low_name(
    handle: *mut SessionHighLow,
) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_session_high_low_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_session_high_low_reset(handle: *mut SessionHighLow) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_session_high_low_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_session_high_low_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_session_high_low_free(handle: *mut SessionHighLow) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `SessionRange` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_session_range_free`.
#[no_mangle]
pub extern "C" fn wickra_session_range_new(utc_offset_minutes: i32) -> *mut SessionRange {
    Box::into_raw(Box::new(SessionRange::new(utc_offset_minutes)))
}

/// Feed one input; on `true` the freshly computed multi-field output is written to
/// `*out`. Returns `false` during warmup, on a `NULL` handle / `out`, or on invalid input.
///
/// # Safety
/// `handle` (from `wickra_session_range_new`, not freed) and `out` must be valid or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_session_range_update(
    handle: *mut SessionRange,
    open: f64,
    high: f64,
    low: f64,
    close: f64,
    volume: f64,
    timestamp: i64,
    out: *mut WickraSessionRangeOutput,
) -> bool {
    let Some(ind) = handle.as_mut() else {
        return false;
    };
    if out.is_null() {
        return false;
    }
    let Ok(input) = Candle::new(open, high, low, close, volume, timestamp) else {
        return false;
    };
    match ind.update(input) {
        Some(out_val) => {
            *out = WickraSessionRangeOutput {
                asia: out_val.asia,
                eu: out_val.eu,
                us: out_val.us,
            };
            true
        }
        None => false,
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_session_range_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_session_range_warmup_period(handle: *mut SessionRange) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_session_range_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_session_range_is_ready(handle: *mut SessionRange) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_session_range_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_session_range_name(handle: *mut SessionRange) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_session_range_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_session_range_reset(handle: *mut SessionRange) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_session_range_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_session_range_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_session_range_free(handle: *mut SessionRange) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `SmoothedHeikinAshi` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_smoothed_heikin_ashi_free`.
#[no_mangle]
pub extern "C" fn wickra_smoothed_heikin_ashi_new(period: usize) -> *mut SmoothedHeikinAshi {
    match SmoothedHeikinAshi::new(period) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one input; on `true` the freshly computed multi-field output is written to
/// `*out`. Returns `false` during warmup, on a `NULL` handle / `out`, or on invalid input.
///
/// # Safety
/// `handle` (from `wickra_smoothed_heikin_ashi_new`, not freed) and `out` must be valid or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_smoothed_heikin_ashi_update(
    handle: *mut SmoothedHeikinAshi,
    open: f64,
    high: f64,
    low: f64,
    close: f64,
    volume: f64,
    timestamp: i64,
    out: *mut WickraSmoothedHeikinAshiOutput,
) -> bool {
    let Some(ind) = handle.as_mut() else {
        return false;
    };
    if out.is_null() {
        return false;
    }
    let Ok(input) = Candle::new(open, high, low, close, volume, timestamp) else {
        return false;
    };
    match ind.update(input) {
        Some(out_val) => {
            *out = WickraSmoothedHeikinAshiOutput {
                open: out_val.open,
                high: out_val.high,
                low: out_val.low,
                close: out_val.close,
            };
            true
        }
        None => false,
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_smoothed_heikin_ashi_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_smoothed_heikin_ashi_warmup_period(
    handle: *mut SmoothedHeikinAshi,
) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_smoothed_heikin_ashi_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_smoothed_heikin_ashi_is_ready(
    handle: *mut SmoothedHeikinAshi,
) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_smoothed_heikin_ashi_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_smoothed_heikin_ashi_name(
    handle: *mut SmoothedHeikinAshi,
) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_smoothed_heikin_ashi_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_smoothed_heikin_ashi_reset(handle: *mut SmoothedHeikinAshi) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_smoothed_heikin_ashi_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_smoothed_heikin_ashi_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_smoothed_heikin_ashi_free(handle: *mut SmoothedHeikinAshi) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `SpreadBollingerBands` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_spread_bollinger_bands_free`.
#[no_mangle]
pub extern "C" fn wickra_spread_bollinger_bands_new(
    period: usize,
    num_std: f64,
) -> *mut SpreadBollingerBands {
    match SpreadBollingerBands::new(period, num_std) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one input; on `true` the freshly computed multi-field output is written to
/// `*out`. Returns `false` during warmup, on a `NULL` handle / `out`, or on invalid input.
///
/// # Safety
/// `handle` (from `wickra_spread_bollinger_bands_new`, not freed) and `out` must be valid or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_spread_bollinger_bands_update(
    handle: *mut SpreadBollingerBands,
    x: f64,
    y: f64,
    out: *mut WickraSpreadBollingerBandsOutput,
) -> bool {
    let Some(ind) = handle.as_mut() else {
        return false;
    };
    if out.is_null() {
        return false;
    }
    match ind.update((x, y)) {
        Some(out_val) => {
            *out = WickraSpreadBollingerBandsOutput {
                middle: out_val.middle,
                upper: out_val.upper,
                lower: out_val.lower,
                percent_b: out_val.percent_b,
            };
            true
        }
        None => false,
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_spread_bollinger_bands_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_spread_bollinger_bands_warmup_period(
    handle: *mut SpreadBollingerBands,
) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_spread_bollinger_bands_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_spread_bollinger_bands_is_ready(
    handle: *mut SpreadBollingerBands,
) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_spread_bollinger_bands_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_spread_bollinger_bands_name(
    handle: *mut SpreadBollingerBands,
) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_spread_bollinger_bands_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_spread_bollinger_bands_reset(handle: *mut SpreadBollingerBands) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_spread_bollinger_bands_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_spread_bollinger_bands_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_spread_bollinger_bands_free(handle: *mut SpreadBollingerBands) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `StandardErrorBands` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_standard_error_bands_free`.
#[no_mangle]
pub extern "C" fn wickra_standard_error_bands_new(
    period: usize,
    multiplier: f64,
) -> *mut StandardErrorBands {
    match StandardErrorBands::new(period, multiplier) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one input; on `true` the freshly computed multi-field output is written to
/// `*out`. Returns `false` during warmup, on a `NULL` handle / `out`, or on invalid input.
///
/// # Safety
/// `handle` (from `wickra_standard_error_bands_new`, not freed) and `out` must be valid or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_standard_error_bands_update(
    handle: *mut StandardErrorBands,
    value: f64,
    out: *mut WickraStandardErrorBandsOutput,
) -> bool {
    let Some(ind) = handle.as_mut() else {
        return false;
    };
    if out.is_null() {
        return false;
    }
    match ind.update(value) {
        Some(out_val) => {
            *out = WickraStandardErrorBandsOutput {
                upper: out_val.upper,
                middle: out_val.middle,
                lower: out_val.lower,
            };
            true
        }
        None => false,
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_standard_error_bands_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_standard_error_bands_warmup_period(
    handle: *mut StandardErrorBands,
) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_standard_error_bands_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_standard_error_bands_is_ready(
    handle: *mut StandardErrorBands,
) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_standard_error_bands_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_standard_error_bands_name(
    handle: *mut StandardErrorBands,
) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_standard_error_bands_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_standard_error_bands_reset(handle: *mut StandardErrorBands) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_standard_error_bands_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_standard_error_bands_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_standard_error_bands_free(handle: *mut StandardErrorBands) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `StarcBands` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_starc_bands_free`.
#[no_mangle]
pub extern "C" fn wickra_starc_bands_new(
    sma_period: usize,
    atr_period: usize,
    multiplier: f64,
) -> *mut StarcBands {
    match StarcBands::new(sma_period, atr_period, multiplier) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one input; on `true` the freshly computed multi-field output is written to
/// `*out`. Returns `false` during warmup, on a `NULL` handle / `out`, or on invalid input.
///
/// # Safety
/// `handle` (from `wickra_starc_bands_new`, not freed) and `out` must be valid or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_starc_bands_update(
    handle: *mut StarcBands,
    open: f64,
    high: f64,
    low: f64,
    close: f64,
    volume: f64,
    timestamp: i64,
    out: *mut WickraStarcBandsOutput,
) -> bool {
    let Some(ind) = handle.as_mut() else {
        return false;
    };
    if out.is_null() {
        return false;
    }
    let Ok(input) = Candle::new(open, high, low, close, volume, timestamp) else {
        return false;
    };
    match ind.update(input) {
        Some(out_val) => {
            *out = WickraStarcBandsOutput {
                upper: out_val.upper,
                middle: out_val.middle,
                lower: out_val.lower,
            };
            true
        }
        None => false,
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_starc_bands_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_starc_bands_warmup_period(handle: *mut StarcBands) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_starc_bands_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_starc_bands_is_ready(handle: *mut StarcBands) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_starc_bands_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_starc_bands_name(handle: *mut StarcBands) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_starc_bands_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_starc_bands_reset(handle: *mut StarcBands) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_starc_bands_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_starc_bands_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_starc_bands_free(handle: *mut StarcBands) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `Stochastic` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_stochastic_free`.
#[no_mangle]
pub extern "C" fn wickra_stochastic_new(k_period: usize, d_period: usize) -> *mut Stochastic {
    match Stochastic::new(k_period, d_period) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one input; on `true` the freshly computed multi-field output is written to
/// `*out`. Returns `false` during warmup, on a `NULL` handle / `out`, or on invalid input.
///
/// # Safety
/// `handle` (from `wickra_stochastic_new`, not freed) and `out` must be valid or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_stochastic_update(
    handle: *mut Stochastic,
    open: f64,
    high: f64,
    low: f64,
    close: f64,
    volume: f64,
    timestamp: i64,
    out: *mut WickraStochasticOutput,
) -> bool {
    let Some(ind) = handle.as_mut() else {
        return false;
    };
    if out.is_null() {
        return false;
    }
    let Ok(input) = Candle::new(open, high, low, close, volume, timestamp) else {
        return false;
    };
    match ind.update(input) {
        Some(out_val) => {
            *out = WickraStochasticOutput {
                k: out_val.k,
                d: out_val.d,
            };
            true
        }
        None => false,
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_stochastic_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_stochastic_warmup_period(handle: *mut Stochastic) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_stochastic_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_stochastic_is_ready(handle: *mut Stochastic) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_stochastic_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_stochastic_name(handle: *mut Stochastic) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_stochastic_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_stochastic_reset(handle: *mut Stochastic) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_stochastic_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_stochastic_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_stochastic_free(handle: *mut Stochastic) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `SuperTrend` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_super_trend_free`.
#[no_mangle]
pub extern "C" fn wickra_super_trend_new(atr_period: usize, multiplier: f64) -> *mut SuperTrend {
    match SuperTrend::new(atr_period, multiplier) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one input; on `true` the freshly computed multi-field output is written to
/// `*out`. Returns `false` during warmup, on a `NULL` handle / `out`, or on invalid input.
///
/// # Safety
/// `handle` (from `wickra_super_trend_new`, not freed) and `out` must be valid or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_super_trend_update(
    handle: *mut SuperTrend,
    open: f64,
    high: f64,
    low: f64,
    close: f64,
    volume: f64,
    timestamp: i64,
    out: *mut WickraSuperTrendOutput,
) -> bool {
    let Some(ind) = handle.as_mut() else {
        return false;
    };
    if out.is_null() {
        return false;
    }
    let Ok(input) = Candle::new(open, high, low, close, volume, timestamp) else {
        return false;
    };
    match ind.update(input) {
        Some(out_val) => {
            *out = WickraSuperTrendOutput {
                value: out_val.value,
                direction: out_val.direction,
            };
            true
        }
        None => false,
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_super_trend_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_super_trend_warmup_period(handle: *mut SuperTrend) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_super_trend_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_super_trend_is_ready(handle: *mut SuperTrend) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_super_trend_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_super_trend_name(handle: *mut SuperTrend) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_super_trend_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_super_trend_reset(handle: *mut SuperTrend) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_super_trend_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_super_trend_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_super_trend_free(handle: *mut SuperTrend) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `TdLines` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_td_lines_free`.
#[no_mangle]
pub extern "C" fn wickra_td_lines_new(lookback: usize, target: usize) -> *mut TdLines {
    match TdLines::new(lookback, target) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one input; on `true` the freshly computed multi-field output is written to
/// `*out`. Returns `false` during warmup, on a `NULL` handle / `out`, or on invalid input.
///
/// # Safety
/// `handle` (from `wickra_td_lines_new`, not freed) and `out` must be valid or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_td_lines_update(
    handle: *mut TdLines,
    open: f64,
    high: f64,
    low: f64,
    close: f64,
    volume: f64,
    timestamp: i64,
    out: *mut WickraTdLinesOutput,
) -> bool {
    let Some(ind) = handle.as_mut() else {
        return false;
    };
    if out.is_null() {
        return false;
    }
    let Ok(input) = Candle::new(open, high, low, close, volume, timestamp) else {
        return false;
    };
    match ind.update(input) {
        Some(out_val) => {
            *out = WickraTdLinesOutput {
                resistance: out_val.resistance,
                support: out_val.support,
            };
            true
        }
        None => false,
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_td_lines_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_td_lines_warmup_period(handle: *mut TdLines) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_td_lines_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_td_lines_is_ready(handle: *mut TdLines) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_td_lines_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_td_lines_name(handle: *mut TdLines) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_td_lines_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_td_lines_reset(handle: *mut TdLines) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_td_lines_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_td_lines_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_td_lines_free(handle: *mut TdLines) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `TdMovingAverage` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_td_moving_average_free`.
#[no_mangle]
pub extern "C" fn wickra_td_moving_average_new(
    period_st1: usize,
    period_st2: usize,
) -> *mut TdMovingAverage {
    match TdMovingAverage::new(period_st1, period_st2) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one input; on `true` the freshly computed multi-field output is written to
/// `*out`. Returns `false` during warmup, on a `NULL` handle / `out`, or on invalid input.
///
/// # Safety
/// `handle` (from `wickra_td_moving_average_new`, not freed) and `out` must be valid or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_td_moving_average_update(
    handle: *mut TdMovingAverage,
    open: f64,
    high: f64,
    low: f64,
    close: f64,
    volume: f64,
    timestamp: i64,
    out: *mut WickraTdMovingAverageOutput,
) -> bool {
    let Some(ind) = handle.as_mut() else {
        return false;
    };
    if out.is_null() {
        return false;
    }
    let Ok(input) = Candle::new(open, high, low, close, volume, timestamp) else {
        return false;
    };
    match ind.update(input) {
        Some(out_val) => {
            *out = WickraTdMovingAverageOutput {
                st1: out_val.st1,
                st2: out_val.st2,
            };
            true
        }
        None => false,
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_td_moving_average_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_td_moving_average_warmup_period(
    handle: *mut TdMovingAverage,
) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_td_moving_average_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_td_moving_average_is_ready(handle: *mut TdMovingAverage) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_td_moving_average_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_td_moving_average_name(
    handle: *mut TdMovingAverage,
) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_td_moving_average_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_td_moving_average_reset(handle: *mut TdMovingAverage) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_td_moving_average_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_td_moving_average_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_td_moving_average_free(handle: *mut TdMovingAverage) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `TdRangeProjection` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_td_range_projection_free`.
#[no_mangle]
pub extern "C" fn wickra_td_range_projection_new() -> *mut TdRangeProjection {
    Box::into_raw(Box::new(TdRangeProjection::new()))
}

/// Feed one input; on `true` the freshly computed multi-field output is written to
/// `*out`. Returns `false` during warmup, on a `NULL` handle / `out`, or on invalid input.
///
/// # Safety
/// `handle` (from `wickra_td_range_projection_new`, not freed) and `out` must be valid or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_td_range_projection_update(
    handle: *mut TdRangeProjection,
    open: f64,
    high: f64,
    low: f64,
    close: f64,
    volume: f64,
    timestamp: i64,
    out: *mut WickraTdRangeProjectionOutput,
) -> bool {
    let Some(ind) = handle.as_mut() else {
        return false;
    };
    if out.is_null() {
        return false;
    }
    let Ok(input) = Candle::new(open, high, low, close, volume, timestamp) else {
        return false;
    };
    match ind.update(input) {
        Some(out_val) => {
            *out = WickraTdRangeProjectionOutput {
                high: out_val.high,
                low: out_val.low,
            };
            true
        }
        None => false,
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_td_range_projection_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_td_range_projection_warmup_period(
    handle: *mut TdRangeProjection,
) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_td_range_projection_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_td_range_projection_is_ready(
    handle: *mut TdRangeProjection,
) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_td_range_projection_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_td_range_projection_name(
    handle: *mut TdRangeProjection,
) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_td_range_projection_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_td_range_projection_reset(handle: *mut TdRangeProjection) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_td_range_projection_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_td_range_projection_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_td_range_projection_free(handle: *mut TdRangeProjection) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `TdRiskLevel` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_td_risk_level_free`.
#[no_mangle]
pub extern "C" fn wickra_td_risk_level_new(lookback: usize, target: usize) -> *mut TdRiskLevel {
    match TdRiskLevel::new(lookback, target) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one input; on `true` the freshly computed multi-field output is written to
/// `*out`. Returns `false` during warmup, on a `NULL` handle / `out`, or on invalid input.
///
/// # Safety
/// `handle` (from `wickra_td_risk_level_new`, not freed) and `out` must be valid or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_td_risk_level_update(
    handle: *mut TdRiskLevel,
    open: f64,
    high: f64,
    low: f64,
    close: f64,
    volume: f64,
    timestamp: i64,
    out: *mut WickraTdRiskLevelOutput,
) -> bool {
    let Some(ind) = handle.as_mut() else {
        return false;
    };
    if out.is_null() {
        return false;
    }
    let Ok(input) = Candle::new(open, high, low, close, volume, timestamp) else {
        return false;
    };
    match ind.update(input) {
        Some(out_val) => {
            *out = WickraTdRiskLevelOutput {
                buy_risk: out_val.buy_risk,
                sell_risk: out_val.sell_risk,
            };
            true
        }
        None => false,
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_td_risk_level_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_td_risk_level_warmup_period(handle: *mut TdRiskLevel) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_td_risk_level_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_td_risk_level_is_ready(handle: *mut TdRiskLevel) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_td_risk_level_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_td_risk_level_name(handle: *mut TdRiskLevel) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_td_risk_level_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_td_risk_level_reset(handle: *mut TdRiskLevel) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_td_risk_level_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_td_risk_level_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_td_risk_level_free(handle: *mut TdRiskLevel) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `TdSequential` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_td_sequential_free`.
#[no_mangle]
pub extern "C" fn wickra_td_sequential_new(
    setup_lookback: usize,
    setup_target: usize,
    countdown_lookback: usize,
    countdown_target: usize,
) -> *mut TdSequential {
    match TdSequential::new(
        setup_lookback,
        setup_target,
        countdown_lookback,
        countdown_target,
    ) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one input; on `true` the freshly computed multi-field output is written to
/// `*out`. Returns `false` during warmup, on a `NULL` handle / `out`, or on invalid input.
///
/// # Safety
/// `handle` (from `wickra_td_sequential_new`, not freed) and `out` must be valid or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_td_sequential_update(
    handle: *mut TdSequential,
    open: f64,
    high: f64,
    low: f64,
    close: f64,
    volume: f64,
    timestamp: i64,
    out: *mut WickraTdSequentialOutput,
) -> bool {
    let Some(ind) = handle.as_mut() else {
        return false;
    };
    if out.is_null() {
        return false;
    }
    let Ok(input) = Candle::new(open, high, low, close, volume, timestamp) else {
        return false;
    };
    match ind.update(input) {
        Some(out_val) => {
            *out = WickraTdSequentialOutput {
                setup: out_val.setup,
                countdown: out_val.countdown,
                direction: out_val.direction,
            };
            true
        }
        None => false,
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_td_sequential_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_td_sequential_warmup_period(handle: *mut TdSequential) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_td_sequential_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_td_sequential_is_ready(handle: *mut TdSequential) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_td_sequential_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_td_sequential_name(handle: *mut TdSequential) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_td_sequential_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_td_sequential_reset(handle: *mut TdSequential) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_td_sequential_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_td_sequential_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_td_sequential_free(handle: *mut TdSequential) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `TtmSqueeze` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_ttm_squeeze_free`.
#[no_mangle]
pub extern "C" fn wickra_ttm_squeeze_new(
    period: usize,
    bb_mult: f64,
    kc_mult: f64,
) -> *mut TtmSqueeze {
    match TtmSqueeze::new(period, bb_mult, kc_mult) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one input; on `true` the freshly computed multi-field output is written to
/// `*out`. Returns `false` during warmup, on a `NULL` handle / `out`, or on invalid input.
///
/// # Safety
/// `handle` (from `wickra_ttm_squeeze_new`, not freed) and `out` must be valid or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_ttm_squeeze_update(
    handle: *mut TtmSqueeze,
    open: f64,
    high: f64,
    low: f64,
    close: f64,
    volume: f64,
    timestamp: i64,
    out: *mut WickraTtmSqueezeOutput,
) -> bool {
    let Some(ind) = handle.as_mut() else {
        return false;
    };
    if out.is_null() {
        return false;
    }
    let Ok(input) = Candle::new(open, high, low, close, volume, timestamp) else {
        return false;
    };
    match ind.update(input) {
        Some(out_val) => {
            *out = WickraTtmSqueezeOutput {
                squeeze: out_val.squeeze,
                momentum: out_val.momentum,
            };
            true
        }
        None => false,
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_ttm_squeeze_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_ttm_squeeze_warmup_period(handle: *mut TtmSqueeze) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_ttm_squeeze_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_ttm_squeeze_is_ready(handle: *mut TtmSqueeze) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_ttm_squeeze_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_ttm_squeeze_name(handle: *mut TtmSqueeze) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_ttm_squeeze_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_ttm_squeeze_reset(handle: *mut TtmSqueeze) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_ttm_squeeze_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_ttm_squeeze_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_ttm_squeeze_free(handle: *mut TtmSqueeze) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `ValueArea` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_value_area_free`.
#[no_mangle]
pub extern "C" fn wickra_value_area_new(
    period: usize,
    bin_count: usize,
    value_area_pct: f64,
) -> *mut ValueArea {
    match ValueArea::new(period, bin_count, value_area_pct) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one input; on `true` the freshly computed multi-field output is written to
/// `*out`. Returns `false` during warmup, on a `NULL` handle / `out`, or on invalid input.
///
/// # Safety
/// `handle` (from `wickra_value_area_new`, not freed) and `out` must be valid or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_value_area_update(
    handle: *mut ValueArea,
    open: f64,
    high: f64,
    low: f64,
    close: f64,
    volume: f64,
    timestamp: i64,
    out: *mut WickraValueAreaOutput,
) -> bool {
    let Some(ind) = handle.as_mut() else {
        return false;
    };
    if out.is_null() {
        return false;
    }
    let Ok(input) = Candle::new(open, high, low, close, volume, timestamp) else {
        return false;
    };
    match ind.update(input) {
        Some(out_val) => {
            *out = WickraValueAreaOutput {
                poc: out_val.poc,
                vah: out_val.vah,
                val: out_val.val,
            };
            true
        }
        None => false,
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_value_area_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_value_area_warmup_period(handle: *mut ValueArea) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_value_area_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_value_area_is_ready(handle: *mut ValueArea) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_value_area_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_value_area_name(handle: *mut ValueArea) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_value_area_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_value_area_reset(handle: *mut ValueArea) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_value_area_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_value_area_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_value_area_free(handle: *mut ValueArea) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `VolatilityCone` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_volatility_cone_free`.
#[no_mangle]
pub extern "C" fn wickra_volatility_cone_new(
    window: usize,
    lookback: usize,
) -> *mut VolatilityCone {
    match VolatilityCone::new(window, lookback) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one input; on `true` the freshly computed multi-field output is written to
/// `*out`. Returns `false` during warmup, on a `NULL` handle / `out`, or on invalid input.
///
/// # Safety
/// `handle` (from `wickra_volatility_cone_new`, not freed) and `out` must be valid or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_volatility_cone_update(
    handle: *mut VolatilityCone,
    open: f64,
    high: f64,
    low: f64,
    close: f64,
    volume: f64,
    timestamp: i64,
    out: *mut WickraVolatilityConeOutput,
) -> bool {
    let Some(ind) = handle.as_mut() else {
        return false;
    };
    if out.is_null() {
        return false;
    }
    let Ok(input) = Candle::new(open, high, low, close, volume, timestamp) else {
        return false;
    };
    match ind.update(input) {
        Some(out_val) => {
            *out = WickraVolatilityConeOutput {
                current: out_val.current,
                min: out_val.min,
                median: out_val.median,
                max: out_val.max,
                percentile: out_val.percentile,
            };
            true
        }
        None => false,
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_volatility_cone_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_volatility_cone_warmup_period(
    handle: *mut VolatilityCone,
) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_volatility_cone_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_volatility_cone_is_ready(handle: *mut VolatilityCone) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_volatility_cone_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_volatility_cone_name(handle: *mut VolatilityCone) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_volatility_cone_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_volatility_cone_reset(handle: *mut VolatilityCone) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_volatility_cone_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_volatility_cone_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_volatility_cone_free(handle: *mut VolatilityCone) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `VolumeWeightedMacd` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_volume_weighted_macd_free`.
#[no_mangle]
pub extern "C" fn wickra_volume_weighted_macd_new(
    fast: usize,
    slow: usize,
    signal: usize,
) -> *mut VolumeWeightedMacd {
    match VolumeWeightedMacd::new(fast, slow, signal) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one input; on `true` the freshly computed multi-field output is written to
/// `*out`. Returns `false` during warmup, on a `NULL` handle / `out`, or on invalid input.
///
/// # Safety
/// `handle` (from `wickra_volume_weighted_macd_new`, not freed) and `out` must be valid or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_volume_weighted_macd_update(
    handle: *mut VolumeWeightedMacd,
    open: f64,
    high: f64,
    low: f64,
    close: f64,
    volume: f64,
    timestamp: i64,
    out: *mut WickraVolumeWeightedMacdOutput,
) -> bool {
    let Some(ind) = handle.as_mut() else {
        return false;
    };
    if out.is_null() {
        return false;
    }
    let Ok(input) = Candle::new(open, high, low, close, volume, timestamp) else {
        return false;
    };
    match ind.update(input) {
        Some(out_val) => {
            *out = WickraVolumeWeightedMacdOutput {
                macd: out_val.macd,
                signal: out_val.signal,
                histogram: out_val.histogram,
            };
            true
        }
        None => false,
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_volume_weighted_macd_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_volume_weighted_macd_warmup_period(
    handle: *mut VolumeWeightedMacd,
) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_volume_weighted_macd_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_volume_weighted_macd_is_ready(
    handle: *mut VolumeWeightedMacd,
) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_volume_weighted_macd_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_volume_weighted_macd_name(
    handle: *mut VolumeWeightedMacd,
) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_volume_weighted_macd_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_volume_weighted_macd_reset(handle: *mut VolumeWeightedMacd) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_volume_weighted_macd_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_volume_weighted_macd_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_volume_weighted_macd_free(handle: *mut VolumeWeightedMacd) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `VolumeWeightedSr` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_volume_weighted_sr_free`.
#[no_mangle]
pub extern "C" fn wickra_volume_weighted_sr_new(period: usize) -> *mut VolumeWeightedSr {
    match VolumeWeightedSr::new(period) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one input; on `true` the freshly computed multi-field output is written to
/// `*out`. Returns `false` during warmup, on a `NULL` handle / `out`, or on invalid input.
///
/// # Safety
/// `handle` (from `wickra_volume_weighted_sr_new`, not freed) and `out` must be valid or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_volume_weighted_sr_update(
    handle: *mut VolumeWeightedSr,
    open: f64,
    high: f64,
    low: f64,
    close: f64,
    volume: f64,
    timestamp: i64,
    out: *mut WickraVolumeWeightedSrOutput,
) -> bool {
    let Some(ind) = handle.as_mut() else {
        return false;
    };
    if out.is_null() {
        return false;
    }
    let Ok(input) = Candle::new(open, high, low, close, volume, timestamp) else {
        return false;
    };
    match ind.update(input) {
        Some(out_val) => {
            *out = WickraVolumeWeightedSrOutput {
                support: out_val.support,
                resistance: out_val.resistance,
            };
            true
        }
        None => false,
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_volume_weighted_sr_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_volume_weighted_sr_warmup_period(
    handle: *mut VolumeWeightedSr,
) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_volume_weighted_sr_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_volume_weighted_sr_is_ready(handle: *mut VolumeWeightedSr) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_volume_weighted_sr_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_volume_weighted_sr_name(
    handle: *mut VolumeWeightedSr,
) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_volume_weighted_sr_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_volume_weighted_sr_reset(handle: *mut VolumeWeightedSr) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_volume_weighted_sr_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_volume_weighted_sr_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_volume_weighted_sr_free(handle: *mut VolumeWeightedSr) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `Vortex` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_vortex_free`.
#[no_mangle]
pub extern "C" fn wickra_vortex_new(period: usize) -> *mut Vortex {
    match Vortex::new(period) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one input; on `true` the freshly computed multi-field output is written to
/// `*out`. Returns `false` during warmup, on a `NULL` handle / `out`, or on invalid input.
///
/// # Safety
/// `handle` (from `wickra_vortex_new`, not freed) and `out` must be valid or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_vortex_update(
    handle: *mut Vortex,
    open: f64,
    high: f64,
    low: f64,
    close: f64,
    volume: f64,
    timestamp: i64,
    out: *mut WickraVortexOutput,
) -> bool {
    let Some(ind) = handle.as_mut() else {
        return false;
    };
    if out.is_null() {
        return false;
    }
    let Ok(input) = Candle::new(open, high, low, close, volume, timestamp) else {
        return false;
    };
    match ind.update(input) {
        Some(out_val) => {
            *out = WickraVortexOutput {
                plus: out_val.plus,
                minus: out_val.minus,
            };
            true
        }
        None => false,
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_vortex_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_vortex_warmup_period(handle: *mut Vortex) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_vortex_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_vortex_is_ready(handle: *mut Vortex) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_vortex_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_vortex_name(handle: *mut Vortex) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_vortex_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_vortex_reset(handle: *mut Vortex) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_vortex_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_vortex_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_vortex_free(handle: *mut Vortex) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `VwapStdDevBands` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_vwap_std_dev_bands_free`.
#[no_mangle]
pub extern "C" fn wickra_vwap_std_dev_bands_new(multiplier: f64) -> *mut VwapStdDevBands {
    match VwapStdDevBands::new(multiplier) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one input; on `true` the freshly computed multi-field output is written to
/// `*out`. Returns `false` during warmup, on a `NULL` handle / `out`, or on invalid input.
///
/// # Safety
/// `handle` (from `wickra_vwap_std_dev_bands_new`, not freed) and `out` must be valid or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_vwap_std_dev_bands_update(
    handle: *mut VwapStdDevBands,
    open: f64,
    high: f64,
    low: f64,
    close: f64,
    volume: f64,
    timestamp: i64,
    out: *mut WickraVwapStdDevBandsOutput,
) -> bool {
    let Some(ind) = handle.as_mut() else {
        return false;
    };
    if out.is_null() {
        return false;
    }
    let Ok(input) = Candle::new(open, high, low, close, volume, timestamp) else {
        return false;
    };
    match ind.update(input) {
        Some(out_val) => {
            *out = WickraVwapStdDevBandsOutput {
                upper: out_val.upper,
                middle: out_val.middle,
                lower: out_val.lower,
                stddev: out_val.stddev,
            };
            true
        }
        None => false,
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_vwap_std_dev_bands_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_vwap_std_dev_bands_warmup_period(
    handle: *mut VwapStdDevBands,
) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_vwap_std_dev_bands_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_vwap_std_dev_bands_is_ready(handle: *mut VwapStdDevBands) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_vwap_std_dev_bands_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_vwap_std_dev_bands_name(
    handle: *mut VwapStdDevBands,
) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_vwap_std_dev_bands_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_vwap_std_dev_bands_reset(handle: *mut VwapStdDevBands) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_vwap_std_dev_bands_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_vwap_std_dev_bands_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_vwap_std_dev_bands_free(handle: *mut VwapStdDevBands) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `WaveTrend` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_wave_trend_free`.
#[no_mangle]
pub extern "C" fn wickra_wave_trend_new(
    channel_period: usize,
    average_period: usize,
    signal_period: usize,
) -> *mut WaveTrend {
    match WaveTrend::new(channel_period, average_period, signal_period) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one input; on `true` the freshly computed multi-field output is written to
/// `*out`. Returns `false` during warmup, on a `NULL` handle / `out`, or on invalid input.
///
/// # Safety
/// `handle` (from `wickra_wave_trend_new`, not freed) and `out` must be valid or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_wave_trend_update(
    handle: *mut WaveTrend,
    open: f64,
    high: f64,
    low: f64,
    close: f64,
    volume: f64,
    timestamp: i64,
    out: *mut WickraWaveTrendOutput,
) -> bool {
    let Some(ind) = handle.as_mut() else {
        return false;
    };
    if out.is_null() {
        return false;
    }
    let Ok(input) = Candle::new(open, high, low, close, volume, timestamp) else {
        return false;
    };
    match ind.update(input) {
        Some(out_val) => {
            *out = WickraWaveTrendOutput {
                wt1: out_val.wt1,
                wt2: out_val.wt2,
            };
            true
        }
        None => false,
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_wave_trend_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_wave_trend_warmup_period(handle: *mut WaveTrend) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_wave_trend_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_wave_trend_is_ready(handle: *mut WaveTrend) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_wave_trend_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_wave_trend_name(handle: *mut WaveTrend) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_wave_trend_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_wave_trend_reset(handle: *mut WaveTrend) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_wave_trend_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_wave_trend_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_wave_trend_free(handle: *mut WaveTrend) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `WilliamsFractals` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_williams_fractals_free`.
#[no_mangle]
pub extern "C" fn wickra_williams_fractals_new() -> *mut WilliamsFractals {
    Box::into_raw(Box::new(WilliamsFractals::new()))
}

/// Feed one input; on `true` the freshly computed multi-field output is written to
/// `*out`. Returns `false` during warmup, on a `NULL` handle / `out`, or on invalid input.
///
/// # Safety
/// `handle` (from `wickra_williams_fractals_new`, not freed) and `out` must be valid or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_williams_fractals_update(
    handle: *mut WilliamsFractals,
    open: f64,
    high: f64,
    low: f64,
    close: f64,
    volume: f64,
    timestamp: i64,
    out: *mut WickraWilliamsFractalsOutput,
) -> bool {
    let Some(ind) = handle.as_mut() else {
        return false;
    };
    if out.is_null() {
        return false;
    }
    let Ok(input) = Candle::new(open, high, low, close, volume, timestamp) else {
        return false;
    };
    match ind.update(input) {
        Some(out_val) => {
            *out = WickraWilliamsFractalsOutput {
                up: out_val.up.unwrap_or(f64::NAN),
                down: out_val.down.unwrap_or(f64::NAN),
            };
            true
        }
        None => false,
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_williams_fractals_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_williams_fractals_warmup_period(
    handle: *mut WilliamsFractals,
) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_williams_fractals_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_williams_fractals_is_ready(handle: *mut WilliamsFractals) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_williams_fractals_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_williams_fractals_name(
    handle: *mut WilliamsFractals,
) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_williams_fractals_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_williams_fractals_reset(handle: *mut WilliamsFractals) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_williams_fractals_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_williams_fractals_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_williams_fractals_free(handle: *mut WilliamsFractals) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `WoodiePivots` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_woodie_pivots_free`.
#[no_mangle]
pub extern "C" fn wickra_woodie_pivots_new() -> *mut WoodiePivots {
    Box::into_raw(Box::new(WoodiePivots::new()))
}

/// Feed one input; on `true` the freshly computed multi-field output is written to
/// `*out`. Returns `false` during warmup, on a `NULL` handle / `out`, or on invalid input.
///
/// # Safety
/// `handle` (from `wickra_woodie_pivots_new`, not freed) and `out` must be valid or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_woodie_pivots_update(
    handle: *mut WoodiePivots,
    open: f64,
    high: f64,
    low: f64,
    close: f64,
    volume: f64,
    timestamp: i64,
    out: *mut WickraWoodiePivotsOutput,
) -> bool {
    let Some(ind) = handle.as_mut() else {
        return false;
    };
    if out.is_null() {
        return false;
    }
    let Ok(input) = Candle::new(open, high, low, close, volume, timestamp) else {
        return false;
    };
    match ind.update(input) {
        Some(out_val) => {
            *out = WickraWoodiePivotsOutput {
                pp: out_val.pp,
                r1: out_val.r1,
                r2: out_val.r2,
                s1: out_val.s1,
                s2: out_val.s2,
            };
            true
        }
        None => false,
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_woodie_pivots_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_woodie_pivots_warmup_period(handle: *mut WoodiePivots) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_woodie_pivots_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_woodie_pivots_is_ready(handle: *mut WoodiePivots) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_woodie_pivots_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_woodie_pivots_name(handle: *mut WoodiePivots) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_woodie_pivots_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_woodie_pivots_reset(handle: *mut WoodiePivots) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_woodie_pivots_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_woodie_pivots_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_woodie_pivots_free(handle: *mut WoodiePivots) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `ZeroLagMacd` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_zero_lag_macd_free`.
#[no_mangle]
pub extern "C" fn wickra_zero_lag_macd_new(
    fast: usize,
    slow: usize,
    signal: usize,
) -> *mut ZeroLagMacd {
    match ZeroLagMacd::new(fast, slow, signal) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one input; on `true` the freshly computed multi-field output is written to
/// `*out`. Returns `false` during warmup, on a `NULL` handle / `out`, or on invalid input.
///
/// # Safety
/// `handle` (from `wickra_zero_lag_macd_new`, not freed) and `out` must be valid or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_zero_lag_macd_update(
    handle: *mut ZeroLagMacd,
    value: f64,
    out: *mut WickraZeroLagMacdOutput,
) -> bool {
    let Some(ind) = handle.as_mut() else {
        return false;
    };
    if out.is_null() {
        return false;
    }
    match ind.update(value) {
        Some(out_val) => {
            *out = WickraZeroLagMacdOutput {
                macd: out_val.macd,
                signal: out_val.signal,
                histogram: out_val.histogram,
            };
            true
        }
        None => false,
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_zero_lag_macd_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_zero_lag_macd_warmup_period(handle: *mut ZeroLagMacd) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_zero_lag_macd_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_zero_lag_macd_is_ready(handle: *mut ZeroLagMacd) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_zero_lag_macd_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_zero_lag_macd_name(handle: *mut ZeroLagMacd) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_zero_lag_macd_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_zero_lag_macd_reset(handle: *mut ZeroLagMacd) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_zero_lag_macd_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_zero_lag_macd_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_zero_lag_macd_free(handle: *mut ZeroLagMacd) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `ZigZag` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_zig_zag_free`.
#[no_mangle]
pub extern "C" fn wickra_zig_zag_new(threshold: f64) -> *mut ZigZag {
    match ZigZag::new(threshold) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one input; on `true` the freshly computed multi-field output is written to
/// `*out`. Returns `false` during warmup, on a `NULL` handle / `out`, or on invalid input.
///
/// # Safety
/// `handle` (from `wickra_zig_zag_new`, not freed) and `out` must be valid or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_zig_zag_update(
    handle: *mut ZigZag,
    open: f64,
    high: f64,
    low: f64,
    close: f64,
    volume: f64,
    timestamp: i64,
    out: *mut WickraZigZagOutput,
) -> bool {
    let Some(ind) = handle.as_mut() else {
        return false;
    };
    if out.is_null() {
        return false;
    }
    let Ok(input) = Candle::new(open, high, low, close, volume, timestamp) else {
        return false;
    };
    match ind.update(input) {
        Some(out_val) => {
            *out = WickraZigZagOutput {
                swing: out_val.swing,
                direction: out_val.direction,
            };
            true
        }
        None => false,
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_zig_zag_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_zig_zag_warmup_period(handle: *mut ZigZag) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_zig_zag_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_zig_zag_is_ready(handle: *mut ZigZag) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_zig_zag_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_zig_zag_name(handle: *mut ZigZag) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_zig_zag_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_zig_zag_reset(handle: *mut ZigZag) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_zig_zag_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_zig_zag_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_zig_zag_free(handle: *mut ZigZag) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

// ===== Vec<f64>-output indicators (profiles; caller buffer + length return) =====

/// Scalar fields of `TpoProfileOutput` (its `Vec` payload is delivered separately).
#[repr(C)]
#[derive(Clone, Copy, Debug)]
pub struct WickraTpoProfileOutputScalars {
    pub price_low: f64,
    pub price_high: f64,
}

/// Scalar fields of `VolumeProfileOutput` (its `Vec` payload is delivered separately).
#[repr(C)]
#[derive(Clone, Copy, Debug)]
pub struct WickraVolumeProfileOutputScalars {
    pub price_low: f64,
    pub price_high: f64,
}

/// Create a `DayOfWeekProfile` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_day_of_week_profile_free`.
#[no_mangle]
pub extern "C" fn wickra_day_of_week_profile_new(utc_offset_minutes: i32) -> *mut DayOfWeekProfile {
    Box::into_raw(Box::new(DayOfWeekProfile::new(utc_offset_minutes)))
}

/// Feed one input. On warmup / `NULL` handle / invalid input returns `-1`. Otherwise
/// returns the length of the `bins` payload and writes up to `cap` of its values
/// into `values` (enlarge `cap` if the return exceeds it); the scalar fields are written
/// to `*scalars` when non-`NULL`.
///
/// # Safety
/// `handle` (from `wickra_day_of_week_profile_new`, not freed), `scalars` and `values` must be valid
/// or `NULL`; when non-`NULL`, `values` must cover `cap` `double`s.
#[no_mangle]
pub unsafe extern "C" fn wickra_day_of_week_profile_update(
    handle: *mut DayOfWeekProfile,
    open: f64,
    high: f64,
    low: f64,
    close: f64,
    volume: f64,
    timestamp: i64,
    values: *mut f64,
    cap: usize,
) -> isize {
    let Some(ind) = handle.as_mut() else {
        return -1;
    };
    let Ok(input) = Candle::new(open, high, low, close, volume, timestamp) else {
        return -1;
    };
    match ind.update(input) {
        Some(out_val) => {
            if !values.is_null() {
                let slots = slice::from_raw_parts_mut(values, cap);
                for (slot, &v) in slots.iter_mut().zip(&out_val.bins) {
                    *slot = v;
                }
            }
            isize::try_from(out_val.bins.len()).unwrap_or(isize::MAX)
        }
        None => -1,
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_day_of_week_profile_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_day_of_week_profile_warmup_period(
    handle: *mut DayOfWeekProfile,
) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_day_of_week_profile_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_day_of_week_profile_is_ready(
    handle: *mut DayOfWeekProfile,
) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_day_of_week_profile_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_day_of_week_profile_name(
    handle: *mut DayOfWeekProfile,
) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_day_of_week_profile_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_day_of_week_profile_reset(handle: *mut DayOfWeekProfile) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_day_of_week_profile_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_day_of_week_profile_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_day_of_week_profile_free(handle: *mut DayOfWeekProfile) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `IntradayVolatilityProfile` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_intraday_volatility_profile_free`.
#[no_mangle]
pub extern "C" fn wickra_intraday_volatility_profile_new(
    buckets: usize,
    utc_offset_minutes: i32,
) -> *mut IntradayVolatilityProfile {
    match IntradayVolatilityProfile::new(buckets, utc_offset_minutes) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one input. On warmup / `NULL` handle / invalid input returns `-1`. Otherwise
/// returns the length of the `bins` payload and writes up to `cap` of its values
/// into `values` (enlarge `cap` if the return exceeds it); the scalar fields are written
/// to `*scalars` when non-`NULL`.
///
/// # Safety
/// `handle` (from `wickra_intraday_volatility_profile_new`, not freed), `scalars` and `values` must be valid
/// or `NULL`; when non-`NULL`, `values` must cover `cap` `double`s.
#[no_mangle]
pub unsafe extern "C" fn wickra_intraday_volatility_profile_update(
    handle: *mut IntradayVolatilityProfile,
    open: f64,
    high: f64,
    low: f64,
    close: f64,
    volume: f64,
    timestamp: i64,
    values: *mut f64,
    cap: usize,
) -> isize {
    let Some(ind) = handle.as_mut() else {
        return -1;
    };
    let Ok(input) = Candle::new(open, high, low, close, volume, timestamp) else {
        return -1;
    };
    match ind.update(input) {
        Some(out_val) => {
            if !values.is_null() {
                let slots = slice::from_raw_parts_mut(values, cap);
                for (slot, &v) in slots.iter_mut().zip(&out_val.bins) {
                    *slot = v;
                }
            }
            isize::try_from(out_val.bins.len()).unwrap_or(isize::MAX)
        }
        None => -1,
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_intraday_volatility_profile_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_intraday_volatility_profile_warmup_period(
    handle: *mut IntradayVolatilityProfile,
) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_intraday_volatility_profile_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_intraday_volatility_profile_is_ready(
    handle: *mut IntradayVolatilityProfile,
) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_intraday_volatility_profile_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_intraday_volatility_profile_name(
    handle: *mut IntradayVolatilityProfile,
) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_intraday_volatility_profile_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_intraday_volatility_profile_reset(
    handle: *mut IntradayVolatilityProfile,
) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_intraday_volatility_profile_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_intraday_volatility_profile_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_intraday_volatility_profile_free(
    handle: *mut IntradayVolatilityProfile,
) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `TimeOfDayReturnProfile` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_time_of_day_return_profile_free`.
#[no_mangle]
pub extern "C" fn wickra_time_of_day_return_profile_new(
    buckets: usize,
    utc_offset_minutes: i32,
) -> *mut TimeOfDayReturnProfile {
    match TimeOfDayReturnProfile::new(buckets, utc_offset_minutes) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one input. On warmup / `NULL` handle / invalid input returns `-1`. Otherwise
/// returns the length of the `bins` payload and writes up to `cap` of its values
/// into `values` (enlarge `cap` if the return exceeds it); the scalar fields are written
/// to `*scalars` when non-`NULL`.
///
/// # Safety
/// `handle` (from `wickra_time_of_day_return_profile_new`, not freed), `scalars` and `values` must be valid
/// or `NULL`; when non-`NULL`, `values` must cover `cap` `double`s.
#[no_mangle]
pub unsafe extern "C" fn wickra_time_of_day_return_profile_update(
    handle: *mut TimeOfDayReturnProfile,
    open: f64,
    high: f64,
    low: f64,
    close: f64,
    volume: f64,
    timestamp: i64,
    values: *mut f64,
    cap: usize,
) -> isize {
    let Some(ind) = handle.as_mut() else {
        return -1;
    };
    let Ok(input) = Candle::new(open, high, low, close, volume, timestamp) else {
        return -1;
    };
    match ind.update(input) {
        Some(out_val) => {
            if !values.is_null() {
                let slots = slice::from_raw_parts_mut(values, cap);
                for (slot, &v) in slots.iter_mut().zip(&out_val.bins) {
                    *slot = v;
                }
            }
            isize::try_from(out_val.bins.len()).unwrap_or(isize::MAX)
        }
        None => -1,
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_time_of_day_return_profile_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_time_of_day_return_profile_warmup_period(
    handle: *mut TimeOfDayReturnProfile,
) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_time_of_day_return_profile_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_time_of_day_return_profile_is_ready(
    handle: *mut TimeOfDayReturnProfile,
) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_time_of_day_return_profile_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_time_of_day_return_profile_name(
    handle: *mut TimeOfDayReturnProfile,
) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_time_of_day_return_profile_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_time_of_day_return_profile_reset(
    handle: *mut TimeOfDayReturnProfile,
) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_time_of_day_return_profile_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_time_of_day_return_profile_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_time_of_day_return_profile_free(
    handle: *mut TimeOfDayReturnProfile,
) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `TpoProfile` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_tpo_profile_free`.
#[no_mangle]
pub extern "C" fn wickra_tpo_profile_new(period: usize, bin_count: usize) -> *mut TpoProfile {
    match TpoProfile::new(period, bin_count) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one input. On warmup / `NULL` handle / invalid input returns `-1`. Otherwise
/// returns the length of the `counts` payload and writes up to `cap` of its values
/// into `values` (enlarge `cap` if the return exceeds it); the scalar fields are written
/// to `*scalars` when non-`NULL`.
///
/// # Safety
/// `handle` (from `wickra_tpo_profile_new`, not freed), `scalars` and `values` must be valid
/// or `NULL`; when non-`NULL`, `values` must cover `cap` `double`s.
#[no_mangle]
pub unsafe extern "C" fn wickra_tpo_profile_update(
    handle: *mut TpoProfile,
    open: f64,
    high: f64,
    low: f64,
    close: f64,
    volume: f64,
    timestamp: i64,
    scalars: *mut WickraTpoProfileOutputScalars,
    values: *mut f64,
    cap: usize,
) -> isize {
    let Some(ind) = handle.as_mut() else {
        return -1;
    };
    let Ok(input) = Candle::new(open, high, low, close, volume, timestamp) else {
        return -1;
    };
    match ind.update(input) {
        Some(out_val) => {
            if !scalars.is_null() {
                *scalars = WickraTpoProfileOutputScalars {
                    price_low: out_val.price_low,
                    price_high: out_val.price_high,
                };
            }
            if !values.is_null() {
                let slots = slice::from_raw_parts_mut(values, cap);
                for (slot, &v) in slots.iter_mut().zip(&out_val.counts) {
                    *slot = v;
                }
            }
            isize::try_from(out_val.counts.len()).unwrap_or(isize::MAX)
        }
        None => -1,
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_tpo_profile_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_tpo_profile_warmup_period(handle: *mut TpoProfile) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_tpo_profile_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_tpo_profile_is_ready(handle: *mut TpoProfile) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_tpo_profile_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_tpo_profile_name(handle: *mut TpoProfile) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_tpo_profile_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_tpo_profile_reset(handle: *mut TpoProfile) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_tpo_profile_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_tpo_profile_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_tpo_profile_free(handle: *mut TpoProfile) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `VolumeByTimeProfile` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_volume_by_time_profile_free`.
#[no_mangle]
pub extern "C" fn wickra_volume_by_time_profile_new(
    buckets: usize,
    utc_offset_minutes: i32,
) -> *mut VolumeByTimeProfile {
    match VolumeByTimeProfile::new(buckets, utc_offset_minutes) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one input. On warmup / `NULL` handle / invalid input returns `-1`. Otherwise
/// returns the length of the `bins` payload and writes up to `cap` of its values
/// into `values` (enlarge `cap` if the return exceeds it); the scalar fields are written
/// to `*scalars` when non-`NULL`.
///
/// # Safety
/// `handle` (from `wickra_volume_by_time_profile_new`, not freed), `scalars` and `values` must be valid
/// or `NULL`; when non-`NULL`, `values` must cover `cap` `double`s.
#[no_mangle]
pub unsafe extern "C" fn wickra_volume_by_time_profile_update(
    handle: *mut VolumeByTimeProfile,
    open: f64,
    high: f64,
    low: f64,
    close: f64,
    volume: f64,
    timestamp: i64,
    values: *mut f64,
    cap: usize,
) -> isize {
    let Some(ind) = handle.as_mut() else {
        return -1;
    };
    let Ok(input) = Candle::new(open, high, low, close, volume, timestamp) else {
        return -1;
    };
    match ind.update(input) {
        Some(out_val) => {
            if !values.is_null() {
                let slots = slice::from_raw_parts_mut(values, cap);
                for (slot, &v) in slots.iter_mut().zip(&out_val.bins) {
                    *slot = v;
                }
            }
            isize::try_from(out_val.bins.len()).unwrap_or(isize::MAX)
        }
        None => -1,
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_volume_by_time_profile_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_volume_by_time_profile_warmup_period(
    handle: *mut VolumeByTimeProfile,
) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_volume_by_time_profile_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_volume_by_time_profile_is_ready(
    handle: *mut VolumeByTimeProfile,
) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_volume_by_time_profile_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_volume_by_time_profile_name(
    handle: *mut VolumeByTimeProfile,
) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_volume_by_time_profile_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_volume_by_time_profile_reset(handle: *mut VolumeByTimeProfile) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_volume_by_time_profile_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_volume_by_time_profile_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_volume_by_time_profile_free(handle: *mut VolumeByTimeProfile) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `VolumeProfile` indicator.
///
/// Returns `NULL` on invalid parameters; release with `wickra_volume_profile_free`.
#[no_mangle]
pub extern "C" fn wickra_volume_profile_new(period: usize, bin_count: usize) -> *mut VolumeProfile {
    match VolumeProfile::new(period, bin_count) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one input. On warmup / `NULL` handle / invalid input returns `-1`. Otherwise
/// returns the length of the `bins` payload and writes up to `cap` of its values
/// into `values` (enlarge `cap` if the return exceeds it); the scalar fields are written
/// to `*scalars` when non-`NULL`.
///
/// # Safety
/// `handle` (from `wickra_volume_profile_new`, not freed), `scalars` and `values` must be valid
/// or `NULL`; when non-`NULL`, `values` must cover `cap` `double`s.
#[no_mangle]
pub unsafe extern "C" fn wickra_volume_profile_update(
    handle: *mut VolumeProfile,
    open: f64,
    high: f64,
    low: f64,
    close: f64,
    volume: f64,
    timestamp: i64,
    scalars: *mut WickraVolumeProfileOutputScalars,
    values: *mut f64,
    cap: usize,
) -> isize {
    let Some(ind) = handle.as_mut() else {
        return -1;
    };
    let Ok(input) = Candle::new(open, high, low, close, volume, timestamp) else {
        return -1;
    };
    match ind.update(input) {
        Some(out_val) => {
            if !scalars.is_null() {
                *scalars = WickraVolumeProfileOutputScalars {
                    price_low: out_val.price_low,
                    price_high: out_val.price_high,
                };
            }
            if !values.is_null() {
                let slots = slice::from_raw_parts_mut(values, cap);
                for (slot, &v) in slots.iter_mut().zip(&out_val.bins) {
                    *slot = v;
                }
            }
            isize::try_from(out_val.bins.len()).unwrap_or(isize::MAX)
        }
        None => -1,
    }
}

/// Number of updates the indicator needs before it produces a non-`NaN` output.
/// Returns `0` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_volume_profile_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_volume_profile_warmup_period(handle: *mut VolumeProfile) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns
/// `false` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_volume_profile_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_volume_profile_is_ready(handle: *mut VolumeProfile) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_volume_profile_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_volume_profile_name(handle: *mut VolumeProfile) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_volume_profile_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_volume_profile_reset(handle: *mut VolumeProfile) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_volume_profile_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_volume_profile_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_volume_profile_free(handle: *mut VolumeProfile) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

// ===== Alt-chart bar builders (BarBuilder -> bars via caller buffer + count) =====

/// C-ABI view of one `DollarBar` (alt-chart bar element).
#[repr(C)]
#[derive(Clone, Copy, Debug)]
pub struct WickraDollarBar {
    pub open: f64,
    pub high: f64,
    pub low: f64,
    pub close: f64,
    pub volume: f64,
    pub dollar: f64,
}

/// C-ABI view of one `ImbalanceBar` (alt-chart bar element).
#[repr(C)]
#[derive(Clone, Copy, Debug)]
pub struct WickraImbalanceBar {
    pub open: f64,
    pub high: f64,
    pub low: f64,
    pub close: f64,
    pub imbalance: f64,
    pub direction: i8,
}

/// C-ABI view of one `KagiBar` (alt-chart bar element).
#[repr(C)]
#[derive(Clone, Copy, Debug)]
pub struct WickraKagiBar {
    pub start: f64,
    pub end: f64,
    pub direction: i8,
}

/// C-ABI view of one `PnfColumn` (alt-chart bar element).
#[repr(C)]
#[derive(Clone, Copy, Debug)]
pub struct WickraPnfColumn {
    pub direction: i8,
    pub high: f64,
    pub low: f64,
}

/// C-ABI view of one `RangeBar` (alt-chart bar element).
#[repr(C)]
#[derive(Clone, Copy, Debug)]
pub struct WickraRangeBar {
    pub open: f64,
    pub close: f64,
    pub direction: i8,
}

/// C-ABI view of one `RenkoBrick` (alt-chart bar element).
#[repr(C)]
#[derive(Clone, Copy, Debug)]
pub struct WickraRenkoBrick {
    pub open: f64,
    pub close: f64,
    pub direction: i8,
}

/// C-ABI view of one `RunBar` (alt-chart bar element).
#[repr(C)]
#[derive(Clone, Copy, Debug)]
pub struct WickraRunBar {
    pub open: f64,
    pub high: f64,
    pub low: f64,
    pub close: f64,
    pub length: usize,
    pub direction: i8,
}

/// C-ABI view of one `LineBreakBar` (alt-chart bar element).
#[repr(C)]
#[derive(Clone, Copy, Debug)]
pub struct WickraLineBreakBar {
    pub open: f64,
    pub close: f64,
    pub direction: i8,
}

/// C-ABI view of one `TickBar` (alt-chart bar element).
#[repr(C)]
#[derive(Clone, Copy, Debug)]
pub struct WickraTickBar {
    pub open: f64,
    pub high: f64,
    pub low: f64,
    pub close: f64,
    pub volume: f64,
}

/// C-ABI view of one `VolumeBar` (alt-chart bar element).
#[repr(C)]
#[derive(Clone, Copy, Debug)]
pub struct WickraVolumeBar {
    pub open: f64,
    pub high: f64,
    pub low: f64,
    pub close: f64,
    pub volume: f64,
}

/// Create a `DollarBars` bar builder.
///
/// Returns `NULL` on invalid parameters; release with `wickra_dollar_bars_free`.
#[no_mangle]
pub extern "C" fn wickra_dollar_bars_new(dollar_per_bar: f64) -> *mut DollarBars {
    match DollarBars::new(dollar_per_bar) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one OHLCV candle; writes up to `cap` completed bars into `out` and returns the
/// total number completed on this candle (0, 1, or several). If the return exceeds `cap`,
/// the surplus is not written — enlarge `cap` (a single candle rarely completes many bars;
/// `cap >= 64` is safe). Returns 0 on a `NULL` handle or an invalid candle.
///
/// # Safety
/// `handle` (from `wickra_dollar_bars_new`, not freed) and `out` must be valid or `NULL`; when
/// non-`NULL`, `out` must cover `cap` `WickraDollarBar` elements.
#[no_mangle]
pub unsafe extern "C" fn wickra_dollar_bars_update(
    handle: *mut DollarBars,
    open: f64,
    high: f64,
    low: f64,
    close: f64,
    volume: f64,
    timestamp: i64,
    out: *mut WickraDollarBar,
    cap: usize,
) -> usize {
    let Some(builder) = handle.as_mut() else {
        return 0;
    };
    let Ok(candle) = Candle::new(open, high, low, close, volume, timestamp) else {
        return 0;
    };
    let bars = builder.update(candle);
    if !out.is_null() {
        let slots = slice::from_raw_parts_mut(out, cap);
        for (slot, bar) in slots.iter_mut().zip(&bars) {
            *slot = WickraDollarBar {
                open: bar.open,
                high: bar.high,
                low: bar.low,
                close: bar.close,
                volume: bar.volume,
                dollar: bar.dollar,
            };
        }
    }
    bars.len()
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_dollar_bars_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_dollar_bars_name(handle: *mut DollarBars) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_dollar_bars_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_dollar_bars_reset(handle: *mut DollarBars) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_dollar_bars_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_dollar_bars_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_dollar_bars_free(handle: *mut DollarBars) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `ImbalanceBars` bar builder.
///
/// Returns `NULL` on invalid parameters; release with `wickra_imbalance_bars_free`.
#[no_mangle]
pub extern "C" fn wickra_imbalance_bars_new(threshold: f64) -> *mut ImbalanceBars {
    match ImbalanceBars::new(threshold) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one OHLCV candle; writes up to `cap` completed bars into `out` and returns the
/// total number completed on this candle (0, 1, or several). If the return exceeds `cap`,
/// the surplus is not written — enlarge `cap` (a single candle rarely completes many bars;
/// `cap >= 64` is safe). Returns 0 on a `NULL` handle or an invalid candle.
///
/// # Safety
/// `handle` (from `wickra_imbalance_bars_new`, not freed) and `out` must be valid or `NULL`; when
/// non-`NULL`, `out` must cover `cap` `WickraImbalanceBar` elements.
#[no_mangle]
pub unsafe extern "C" fn wickra_imbalance_bars_update(
    handle: *mut ImbalanceBars,
    open: f64,
    high: f64,
    low: f64,
    close: f64,
    volume: f64,
    timestamp: i64,
    out: *mut WickraImbalanceBar,
    cap: usize,
) -> usize {
    let Some(builder) = handle.as_mut() else {
        return 0;
    };
    let Ok(candle) = Candle::new(open, high, low, close, volume, timestamp) else {
        return 0;
    };
    let bars = builder.update(candle);
    if !out.is_null() {
        let slots = slice::from_raw_parts_mut(out, cap);
        for (slot, bar) in slots.iter_mut().zip(&bars) {
            *slot = WickraImbalanceBar {
                open: bar.open,
                high: bar.high,
                low: bar.low,
                close: bar.close,
                imbalance: bar.imbalance,
                direction: bar.direction,
            };
        }
    }
    bars.len()
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_imbalance_bars_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_imbalance_bars_name(handle: *mut ImbalanceBars) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_imbalance_bars_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_imbalance_bars_reset(handle: *mut ImbalanceBars) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_imbalance_bars_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_imbalance_bars_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_imbalance_bars_free(handle: *mut ImbalanceBars) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `KagiBars` bar builder.
///
/// Returns `NULL` on invalid parameters; release with `wickra_kagi_bars_free`.
#[no_mangle]
pub extern "C" fn wickra_kagi_bars_new(reversal: f64) -> *mut KagiBars {
    match KagiBars::new(reversal) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one OHLCV candle; writes up to `cap` completed bars into `out` and returns the
/// total number completed on this candle (0, 1, or several). If the return exceeds `cap`,
/// the surplus is not written — enlarge `cap` (a single candle rarely completes many bars;
/// `cap >= 64` is safe). Returns 0 on a `NULL` handle or an invalid candle.
///
/// # Safety
/// `handle` (from `wickra_kagi_bars_new`, not freed) and `out` must be valid or `NULL`; when
/// non-`NULL`, `out` must cover `cap` `WickraKagiBar` elements.
#[no_mangle]
pub unsafe extern "C" fn wickra_kagi_bars_update(
    handle: *mut KagiBars,
    open: f64,
    high: f64,
    low: f64,
    close: f64,
    volume: f64,
    timestamp: i64,
    out: *mut WickraKagiBar,
    cap: usize,
) -> usize {
    let Some(builder) = handle.as_mut() else {
        return 0;
    };
    let Ok(candle) = Candle::new(open, high, low, close, volume, timestamp) else {
        return 0;
    };
    let bars = builder.update(candle);
    if !out.is_null() {
        let slots = slice::from_raw_parts_mut(out, cap);
        for (slot, bar) in slots.iter_mut().zip(&bars) {
            *slot = WickraKagiBar {
                start: bar.start,
                end: bar.end,
                direction: bar.direction,
            };
        }
    }
    bars.len()
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_kagi_bars_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_kagi_bars_name(handle: *mut KagiBars) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_kagi_bars_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_kagi_bars_reset(handle: *mut KagiBars) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_kagi_bars_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_kagi_bars_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_kagi_bars_free(handle: *mut KagiBars) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `PointAndFigureBars` bar builder.
///
/// Returns `NULL` on invalid parameters; release with `wickra_point_and_figure_bars_free`.
#[no_mangle]
pub extern "C" fn wickra_point_and_figure_bars_new(
    box_size: f64,
    reversal: usize,
) -> *mut PointAndFigureBars {
    match PointAndFigureBars::new(box_size, reversal) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one OHLCV candle; writes up to `cap` completed bars into `out` and returns the
/// total number completed on this candle (0, 1, or several). If the return exceeds `cap`,
/// the surplus is not written — enlarge `cap` (a single candle rarely completes many bars;
/// `cap >= 64` is safe). Returns 0 on a `NULL` handle or an invalid candle.
///
/// # Safety
/// `handle` (from `wickra_point_and_figure_bars_new`, not freed) and `out` must be valid or `NULL`; when
/// non-`NULL`, `out` must cover `cap` `WickraPnfColumn` elements.
#[no_mangle]
pub unsafe extern "C" fn wickra_point_and_figure_bars_update(
    handle: *mut PointAndFigureBars,
    open: f64,
    high: f64,
    low: f64,
    close: f64,
    volume: f64,
    timestamp: i64,
    out: *mut WickraPnfColumn,
    cap: usize,
) -> usize {
    let Some(builder) = handle.as_mut() else {
        return 0;
    };
    let Ok(candle) = Candle::new(open, high, low, close, volume, timestamp) else {
        return 0;
    };
    let bars = builder.update(candle);
    if !out.is_null() {
        let slots = slice::from_raw_parts_mut(out, cap);
        for (slot, bar) in slots.iter_mut().zip(&bars) {
            *slot = WickraPnfColumn {
                direction: bar.direction,
                high: bar.high,
                low: bar.low,
            };
        }
    }
    bars.len()
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_point_and_figure_bars_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_point_and_figure_bars_name(
    handle: *mut PointAndFigureBars,
) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_point_and_figure_bars_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_point_and_figure_bars_reset(handle: *mut PointAndFigureBars) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_point_and_figure_bars_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_point_and_figure_bars_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_point_and_figure_bars_free(handle: *mut PointAndFigureBars) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `RangeBars` bar builder.
///
/// Returns `NULL` on invalid parameters; release with `wickra_range_bars_free`.
#[no_mangle]
pub extern "C" fn wickra_range_bars_new(range: f64) -> *mut RangeBars {
    match RangeBars::new(range) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one OHLCV candle; writes up to `cap` completed bars into `out` and returns the
/// total number completed on this candle (0, 1, or several). If the return exceeds `cap`,
/// the surplus is not written — enlarge `cap` (a single candle rarely completes many bars;
/// `cap >= 64` is safe). Returns 0 on a `NULL` handle or an invalid candle.
///
/// # Safety
/// `handle` (from `wickra_range_bars_new`, not freed) and `out` must be valid or `NULL`; when
/// non-`NULL`, `out` must cover `cap` `WickraRangeBar` elements.
#[no_mangle]
pub unsafe extern "C" fn wickra_range_bars_update(
    handle: *mut RangeBars,
    open: f64,
    high: f64,
    low: f64,
    close: f64,
    volume: f64,
    timestamp: i64,
    out: *mut WickraRangeBar,
    cap: usize,
) -> usize {
    let Some(builder) = handle.as_mut() else {
        return 0;
    };
    let Ok(candle) = Candle::new(open, high, low, close, volume, timestamp) else {
        return 0;
    };
    let bars = builder.update(candle);
    if !out.is_null() {
        let slots = slice::from_raw_parts_mut(out, cap);
        for (slot, bar) in slots.iter_mut().zip(&bars) {
            *slot = WickraRangeBar {
                open: bar.open,
                close: bar.close,
                direction: bar.direction,
            };
        }
    }
    bars.len()
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_range_bars_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_range_bars_name(handle: *mut RangeBars) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_range_bars_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_range_bars_reset(handle: *mut RangeBars) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_range_bars_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_range_bars_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_range_bars_free(handle: *mut RangeBars) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `RenkoBars` bar builder.
///
/// Returns `NULL` on invalid parameters; release with `wickra_renko_bars_free`.
#[no_mangle]
pub extern "C" fn wickra_renko_bars_new(box_size: f64) -> *mut RenkoBars {
    match RenkoBars::new(box_size) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one OHLCV candle; writes up to `cap` completed bars into `out` and returns the
/// total number completed on this candle (0, 1, or several). If the return exceeds `cap`,
/// the surplus is not written — enlarge `cap` (a single candle rarely completes many bars;
/// `cap >= 64` is safe). Returns 0 on a `NULL` handle or an invalid candle.
///
/// # Safety
/// `handle` (from `wickra_renko_bars_new`, not freed) and `out` must be valid or `NULL`; when
/// non-`NULL`, `out` must cover `cap` `WickraRenkoBrick` elements.
#[no_mangle]
pub unsafe extern "C" fn wickra_renko_bars_update(
    handle: *mut RenkoBars,
    open: f64,
    high: f64,
    low: f64,
    close: f64,
    volume: f64,
    timestamp: i64,
    out: *mut WickraRenkoBrick,
    cap: usize,
) -> usize {
    let Some(builder) = handle.as_mut() else {
        return 0;
    };
    let Ok(candle) = Candle::new(open, high, low, close, volume, timestamp) else {
        return 0;
    };
    let bars = builder.update(candle);
    if !out.is_null() {
        let slots = slice::from_raw_parts_mut(out, cap);
        for (slot, bar) in slots.iter_mut().zip(&bars) {
            *slot = WickraRenkoBrick {
                open: bar.open,
                close: bar.close,
                direction: bar.direction,
            };
        }
    }
    bars.len()
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_renko_bars_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_renko_bars_name(handle: *mut RenkoBars) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_renko_bars_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_renko_bars_reset(handle: *mut RenkoBars) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_renko_bars_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_renko_bars_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_renko_bars_free(handle: *mut RenkoBars) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `RunBars` bar builder.
///
/// Returns `NULL` on invalid parameters; release with `wickra_run_bars_free`.
#[no_mangle]
pub extern "C" fn wickra_run_bars_new(run_length: usize) -> *mut RunBars {
    match RunBars::new(run_length) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one OHLCV candle; writes up to `cap` completed bars into `out` and returns the
/// total number completed on this candle (0, 1, or several). If the return exceeds `cap`,
/// the surplus is not written — enlarge `cap` (a single candle rarely completes many bars;
/// `cap >= 64` is safe). Returns 0 on a `NULL` handle or an invalid candle.
///
/// # Safety
/// `handle` (from `wickra_run_bars_new`, not freed) and `out` must be valid or `NULL`; when
/// non-`NULL`, `out` must cover `cap` `WickraRunBar` elements.
#[no_mangle]
pub unsafe extern "C" fn wickra_run_bars_update(
    handle: *mut RunBars,
    open: f64,
    high: f64,
    low: f64,
    close: f64,
    volume: f64,
    timestamp: i64,
    out: *mut WickraRunBar,
    cap: usize,
) -> usize {
    let Some(builder) = handle.as_mut() else {
        return 0;
    };
    let Ok(candle) = Candle::new(open, high, low, close, volume, timestamp) else {
        return 0;
    };
    let bars = builder.update(candle);
    if !out.is_null() {
        let slots = slice::from_raw_parts_mut(out, cap);
        for (slot, bar) in slots.iter_mut().zip(&bars) {
            *slot = WickraRunBar {
                open: bar.open,
                high: bar.high,
                low: bar.low,
                close: bar.close,
                length: bar.length,
                direction: bar.direction,
            };
        }
    }
    bars.len()
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_run_bars_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_run_bars_name(handle: *mut RunBars) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_run_bars_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_run_bars_reset(handle: *mut RunBars) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_run_bars_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_run_bars_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_run_bars_free(handle: *mut RunBars) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `ThreeLineBreakBars` bar builder.
///
/// Returns `NULL` on invalid parameters; release with `wickra_three_line_break_bars_free`.
#[no_mangle]
pub extern "C" fn wickra_three_line_break_bars_new(lines: usize) -> *mut ThreeLineBreakBars {
    match ThreeLineBreakBars::new(lines) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one OHLCV candle; writes up to `cap` completed bars into `out` and returns the
/// total number completed on this candle (0, 1, or several). If the return exceeds `cap`,
/// the surplus is not written — enlarge `cap` (a single candle rarely completes many bars;
/// `cap >= 64` is safe). Returns 0 on a `NULL` handle or an invalid candle.
///
/// # Safety
/// `handle` (from `wickra_three_line_break_bars_new`, not freed) and `out` must be valid or `NULL`; when
/// non-`NULL`, `out` must cover `cap` `WickraLineBreakBar` elements.
#[no_mangle]
pub unsafe extern "C" fn wickra_three_line_break_bars_update(
    handle: *mut ThreeLineBreakBars,
    open: f64,
    high: f64,
    low: f64,
    close: f64,
    volume: f64,
    timestamp: i64,
    out: *mut WickraLineBreakBar,
    cap: usize,
) -> usize {
    let Some(builder) = handle.as_mut() else {
        return 0;
    };
    let Ok(candle) = Candle::new(open, high, low, close, volume, timestamp) else {
        return 0;
    };
    let bars = builder.update(candle);
    if !out.is_null() {
        let slots = slice::from_raw_parts_mut(out, cap);
        for (slot, bar) in slots.iter_mut().zip(&bars) {
            *slot = WickraLineBreakBar {
                open: bar.open,
                close: bar.close,
                direction: bar.direction,
            };
        }
    }
    bars.len()
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_three_line_break_bars_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_three_line_break_bars_name(
    handle: *mut ThreeLineBreakBars,
) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_three_line_break_bars_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_three_line_break_bars_reset(handle: *mut ThreeLineBreakBars) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_three_line_break_bars_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_three_line_break_bars_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_three_line_break_bars_free(handle: *mut ThreeLineBreakBars) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `TickBars` bar builder.
///
/// Returns `NULL` on invalid parameters; release with `wickra_tick_bars_free`.
#[no_mangle]
pub extern "C" fn wickra_tick_bars_new(ticks: usize) -> *mut TickBars {
    match TickBars::new(ticks) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one OHLCV candle; writes up to `cap` completed bars into `out` and returns the
/// total number completed on this candle (0, 1, or several). If the return exceeds `cap`,
/// the surplus is not written — enlarge `cap` (a single candle rarely completes many bars;
/// `cap >= 64` is safe). Returns 0 on a `NULL` handle or an invalid candle.
///
/// # Safety
/// `handle` (from `wickra_tick_bars_new`, not freed) and `out` must be valid or `NULL`; when
/// non-`NULL`, `out` must cover `cap` `WickraTickBar` elements.
#[no_mangle]
pub unsafe extern "C" fn wickra_tick_bars_update(
    handle: *mut TickBars,
    open: f64,
    high: f64,
    low: f64,
    close: f64,
    volume: f64,
    timestamp: i64,
    out: *mut WickraTickBar,
    cap: usize,
) -> usize {
    let Some(builder) = handle.as_mut() else {
        return 0;
    };
    let Ok(candle) = Candle::new(open, high, low, close, volume, timestamp) else {
        return 0;
    };
    let bars = builder.update(candle);
    if !out.is_null() {
        let slots = slice::from_raw_parts_mut(out, cap);
        for (slot, bar) in slots.iter_mut().zip(&bars) {
            *slot = WickraTickBar {
                open: bar.open,
                high: bar.high,
                low: bar.low,
                close: bar.close,
                volume: bar.volume,
            };
        }
    }
    bars.len()
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_tick_bars_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_tick_bars_name(handle: *mut TickBars) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_tick_bars_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_tick_bars_reset(handle: *mut TickBars) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_tick_bars_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_tick_bars_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_tick_bars_free(handle: *mut TickBars) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Create a `VolumeBars` bar builder.
///
/// Returns `NULL` on invalid parameters; release with `wickra_volume_bars_free`.
#[no_mangle]
pub extern "C" fn wickra_volume_bars_new(volume_per_bar: f64) -> *mut VolumeBars {
    match VolumeBars::new(volume_per_bar) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one OHLCV candle; writes up to `cap` completed bars into `out` and returns the
/// total number completed on this candle (0, 1, or several). If the return exceeds `cap`,
/// the surplus is not written — enlarge `cap` (a single candle rarely completes many bars;
/// `cap >= 64` is safe). Returns 0 on a `NULL` handle or an invalid candle.
///
/// # Safety
/// `handle` (from `wickra_volume_bars_new`, not freed) and `out` must be valid or `NULL`; when
/// non-`NULL`, `out` must cover `cap` `WickraVolumeBar` elements.
#[no_mangle]
pub unsafe extern "C" fn wickra_volume_bars_update(
    handle: *mut VolumeBars,
    open: f64,
    high: f64,
    low: f64,
    close: f64,
    volume: f64,
    timestamp: i64,
    out: *mut WickraVolumeBar,
    cap: usize,
) -> usize {
    let Some(builder) = handle.as_mut() else {
        return 0;
    };
    let Ok(candle) = Candle::new(open, high, low, close, volume, timestamp) else {
        return 0;
    };
    let bars = builder.update(candle);
    if !out.is_null() {
        let slots = slice::from_raw_parts_mut(out, cap);
        for (slot, bar) in slots.iter_mut().zip(&bars) {
            *slot = WickraVolumeBar {
                open: bar.open,
                high: bar.high,
                low: bar.low,
                close: bar.close,
                volume: bar.volume,
            };
        }
    }
    bars.len()
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_volume_bars_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_volume_bars_name(handle: *mut VolumeBars) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_volume_bars_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_volume_bars_reset(handle: *mut VolumeBars) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_volume_bars_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_volume_bars_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_volume_bars_free(handle: *mut VolumeBars) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

// ===== Bespoke wirings (MaType-enum ctor, Vec<struct> output) =====

/// Moving-average selector for `wickra_macd_ext_new`: 0=SMA, 1=EMA, 2=WMA, 3=DEMA,
/// 4=TEMA, 5=TRIMA. Any other value is rejected.
fn ma_type_from_u8(value: u8) -> Option<MaType> {
    match value {
        0 => Some(MaType::Sma),
        1 => Some(MaType::Ema),
        2 => Some(MaType::Wma),
        3 => Some(MaType::Dema),
        4 => Some(MaType::Tema),
        5 => Some(MaType::Trima),
        _ => None,
    }
}

/// Create a `MacdExt`. `fast_type` / `slow_type` / `signal_type` pick the moving
/// average per `ma_type_from_u8`. Returns `NULL` on invalid parameters or an unknown
/// MA-type code; release with `wickra_macd_ext_free`.
#[no_mangle]
pub extern "C" fn wickra_macd_ext_new(
    fast: usize,
    fast_type: u8,
    slow: usize,
    slow_type: u8,
    signal: usize,
    signal_type: u8,
) -> *mut MacdExt {
    let (Some(ft), Some(st), Some(gt)) = (
        ma_type_from_u8(fast_type),
        ma_type_from_u8(slow_type),
        ma_type_from_u8(signal_type),
    ) else {
        return ptr::null_mut();
    };
    match MacdExt::new(fast, ft, slow, st, signal, gt) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one value; on `true` the MACD/signal/histogram triple is written to `*out`.
/// Returns `false` during warmup, on a `NULL` handle / `out`.
///
/// # Safety
/// `handle` (from `wickra_macd_ext_new`, not freed) and `out` must be valid or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_macd_ext_update(
    handle: *mut MacdExt,
    value: f64,
    out: *mut WickraMacdOutput,
) -> bool {
    let Some(ind) = handle.as_mut() else {
        return false;
    };
    if out.is_null() {
        return false;
    }
    match ind.update(value) {
        Some(out_val) => {
            *out = WickraMacdOutput {
                macd: out_val.macd,
                signal: out_val.signal,
                histogram: out_val.histogram,
            };
            true
        }
        None => false,
    }
}

/// Number of updates the indicator needs before it produces a value. Returns `0`
/// if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_macd_ext_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_macd_ext_warmup_period(handle: *mut MacdExt) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns `false`
/// if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_macd_ext_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_macd_ext_is_ready(handle: *mut MacdExt) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_macd_ext_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_macd_ext_name(handle: *mut MacdExt) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_macd_ext_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_macd_ext_reset(handle: *mut MacdExt) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_macd_ext_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_macd_ext_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_macd_ext_free(handle: *mut MacdExt) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// C-ABI view of one `FootprintLevel` (price bucket with bid/ask volume).
#[repr(C)]
#[derive(Clone, Copy, Debug)]
pub struct WickraFootprintLevel {
    pub price: f64,
    pub bid_vol: f64,
    pub ask_vol: f64,
}

/// Create a `Footprint` order-flow aggregator. Returns `NULL` on invalid parameters;
/// release with `wickra_footprint_free`.
#[no_mangle]
pub extern "C" fn wickra_footprint_new(tick_size: f64) -> *mut Footprint {
    match Footprint::new(tick_size) {
        Ok(ind) => Box::into_raw(Box::new(ind)),
        Err(_) => ptr::null_mut(),
    }
}

/// Feed one trade. Returns `-1` during warmup / on a `NULL` handle / invalid trade,
/// otherwise the number of price levels in the completed footprint; up to `cap` levels
/// are written to `out` (enlarge `cap` if the return exceeds it).
///
/// # Safety
/// `handle` (from `wickra_footprint_new`, not freed) and `out` must be valid or `NULL`;
/// when non-`NULL`, `out` must cover `cap` `WickraFootprintLevel` elements.
#[no_mangle]
pub unsafe extern "C" fn wickra_footprint_update(
    handle: *mut Footprint,
    price: f64,
    size: f64,
    is_buy: bool,
    timestamp: i64,
    out: *mut WickraFootprintLevel,
    cap: usize,
) -> isize {
    let Some(ind) = handle.as_mut() else {
        return -1;
    };
    let side = if is_buy { Side::Buy } else { Side::Sell };
    let Ok(trade) = Trade::new(price, size, side, timestamp) else {
        return -1;
    };
    match ind.update(trade) {
        Some(out_val) => {
            if !out.is_null() {
                let slots = slice::from_raw_parts_mut(out, cap);
                for (slot, level) in slots.iter_mut().zip(&out_val.levels) {
                    *slot = WickraFootprintLevel {
                        price: level.price,
                        bid_vol: level.bid_vol,
                        ask_vol: level.ask_vol,
                    };
                }
            }
            isize::try_from(out_val.levels.len()).unwrap_or(isize::MAX)
        }
        None => -1,
    }
}

/// Number of updates the indicator needs before it produces a value. Returns `0`
/// if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_footprint_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_footprint_warmup_period(handle: *mut Footprint) -> usize {
    match handle.as_ref() {
        Some(ind) => ind.warmup_period(),
        None => 0,
    }
}

/// Whether the indicator has consumed enough input to emit a value. Returns `false`
/// if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_footprint_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_footprint_is_ready(handle: *mut Footprint) -> bool {
    match handle.as_ref() {
        Some(ind) => ind.is_ready(),
        None => false,
    }
}

/// Canonical indicator name as a NUL-terminated C string with `'static` lifetime
/// (cached on first call). Returns `NULL` if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_footprint_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_footprint_name(handle: *mut Footprint) -> *const c_char {
    match handle.as_ref() {
        Some(ind) => {
            static NAME: OnceLock<CString> = OnceLock::new();
            NAME.get_or_init(|| {
                CString::new(ind.name()).expect("indicator name has no interior NUL")
            })
            .as_ptr()
        }
        None => ptr::null(),
    }
}

/// Reset all internal state. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must be valid (from `wickra_footprint_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_footprint_reset(handle: *mut Footprint) {
    if let Some(ind) = handle.as_mut() {
        ind.reset();
    }
}

/// Destroy a handle created by `wickra_footprint_new`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_footprint_new` and not previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_footprint_free(handle: *mut Footprint) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

// ===== Data layer (tick aggregation; caller buffer + count) =====

/// C-ABI view of an OHLCV candle (the tick aggregator's output).
#[repr(C)]
#[derive(Clone, Copy, Debug)]
pub struct WickraCandle {
    pub open: f64,
    pub high: f64,
    pub low: f64,
    pub close: f64,
    pub volume: f64,
    pub timestamp: i64,
}

/// Opaque tick aggregator: the streaming aggregator plus the buffer of candles
/// the most recent `push` closed, awaiting `drain`. Named `TickAggregator` (the
/// public C-ABI handle); the inner `wickra-data` type is aliased to avoid the
/// name clash.
#[derive(Debug)]
pub struct TickAggregator {
    inner: DataTickAggregator,
    pending: Vec<Candle>,
}

/// Create a tick aggregator with the given bucket size (the same unit as the
/// tick timestamps). `gap_fill` emits a flat placeholder candle for every
/// skipped bucket. Returns `NULL` on a non-positive bucket; release with
/// `wickra_tick_aggregator_free`.
#[no_mangle]
pub extern "C" fn wickra_tick_aggregator_new(bucket: i64, gap_fill: bool) -> *mut TickAggregator {
    match Timeframe::new(bucket) {
        Ok(timeframe) => Box::into_raw(Box::new(TickAggregator {
            inner: DataTickAggregator::new(timeframe).with_gap_fill(gap_fill),
            pending: Vec::new(),
        })),
        Err(_) => ptr::null_mut(),
    }
}

/// Push one trade tick. Buffers the candles it closed inside the handle and
/// returns the count (`0` if the open bar merely grew), or `-1` on a `NULL`
/// handle / invalid tick / malformed timestamp. Read the candles with
/// `wickra_tick_aggregator_drain`; the next `push` replaces the buffer.
///
/// # Safety
/// `handle` must be valid (from `wickra_tick_aggregator_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_tick_aggregator_push(
    handle: *mut TickAggregator,
    price: f64,
    size: f64,
    timestamp: i64,
) -> isize {
    let Some(aggregator) = handle.as_mut() else {
        return -1;
    };
    let Ok(tick) = Tick::new(price, size, timestamp) else {
        return -1;
    };
    let Ok(candles) = aggregator.inner.push(tick) else {
        return -1;
    };
    aggregator.pending = candles;
    isize::try_from(aggregator.pending.len()).unwrap_or(isize::MAX)
}

/// Copy up to `cap` buffered candles (from the last `push`) into `out`, remove
/// them from the buffer, and return the number written. Returns `0` on a `NULL`
/// handle / `out`. Call with `cap` equal to the last `push` return to drain the
/// whole batch losslessly.
///
/// # Safety
/// `handle` (from `wickra_tick_aggregator_new`, not freed) and `out` must be valid
/// or `NULL`; when non-`NULL`, `out` must cover `cap` `WickraCandle` elements.
#[no_mangle]
pub unsafe extern "C" fn wickra_tick_aggregator_drain(
    handle: *mut TickAggregator,
    out: *mut WickraCandle,
    cap: usize,
) -> isize {
    let Some(aggregator) = handle.as_mut() else {
        return 0;
    };
    if out.is_null() {
        return 0;
    }
    let count = aggregator.pending.len().min(cap);
    let slots = slice::from_raw_parts_mut(out, count);
    for (slot, candle) in slots.iter_mut().zip(aggregator.pending.drain(..count)) {
        *slot = WickraCandle {
            open: candle.open,
            high: candle.high,
            low: candle.low,
            close: candle.close,
            volume: candle.volume,
            timestamp: candle.timestamp,
        };
    }
    isize::try_from(count).unwrap_or(isize::MAX)
}

/// Destroy a tick aggregator created by `wickra_tick_aggregator_new`. No-op if
/// `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_tick_aggregator_new` and not
/// previously freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_tick_aggregator_free(handle: *mut TickAggregator) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Convert a core candle into the C-ABI view.
fn candle_to_c(candle: Candle) -> WickraCandle {
    WickraCandle {
        open: candle.open,
        high: candle.high,
        low: candle.low,
        close: candle.close,
        volume: candle.volume,
        timestamp: candle.timestamp,
    }
}

/// Create a resampler aggregating input candles into `timeframe`-sized candles
/// (the same unit as the candle timestamps). Returns `NULL` on a non-positive
/// timeframe; release with `wickra_resampler_free`.
#[no_mangle]
pub extern "C" fn wickra_resampler_new(timeframe: i64) -> *mut Resampler {
    match Timeframe::new(timeframe) {
        Ok(tf) => Box::into_raw(Box::new(Resampler::new(tf))),
        Err(_) => ptr::null_mut(),
    }
}

/// Push one candle. On `true` the completed higher-timeframe candle is written to
/// `*out`; `false` means none closed yet (still aggregating), a `NULL` handle /
/// `out`, or an invalid input candle.
///
/// # Safety
/// `handle` (from `wickra_resampler_new`, not freed) and `out` must be valid or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_resampler_update(
    handle: *mut Resampler,
    open: f64,
    high: f64,
    low: f64,
    close: f64,
    volume: f64,
    timestamp: i64,
    out: *mut WickraCandle,
) -> bool {
    let Some(resampler) = handle.as_mut() else {
        return false;
    };
    if out.is_null() {
        return false;
    }
    let Ok(candle) = Candle::new(open, high, low, close, volume, timestamp) else {
        return false;
    };
    match resampler.push(candle) {
        Ok(Some(emitted)) => {
            *out = candle_to_c(emitted);
            true
        }
        _ => false,
    }
}

/// Flush the final, still-open candle. On `true` it is written to `*out`; `false`
/// means nothing was pending or `handle` / `out` is `NULL`.
///
/// # Safety
/// `handle` (from `wickra_resampler_new`, not freed) and `out` must be valid or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_resampler_flush(
    handle: *mut Resampler,
    out: *mut WickraCandle,
) -> bool {
    let Some(resampler) = handle.as_mut() else {
        return false;
    };
    if out.is_null() {
        return false;
    }
    match resampler.flush() {
        Ok(Some(emitted)) => {
            *out = candle_to_c(emitted);
            true
        }
        _ => false,
    }
}

/// Destroy a resampler created by `wickra_resampler_new`. No-op if `handle` is
/// `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_resampler_new` and not previously
/// freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_resampler_free(handle: *mut Resampler) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Opaque CSV candle reader: parses an entire `timestamp,open,high,low,close,volume`
/// CSV buffer up front and hands the candles out in drain order. Named
/// `CandleReader` (the public C-ABI handle); the inner `wickra-data` reader is
/// reached through its full path to avoid the name clash.
#[derive(Debug)]
pub struct CandleReader {
    candles: Vec<Candle>,
    pos: usize,
}

/// Parse an OHLCV CSV buffer (`len` bytes at `data`) into candles. The first line
/// must be a header naming `timestamp,open,high,low,close,volume` (a leading UTF-8
/// BOM and field whitespace are tolerated). Returns `NULL` on a `NULL` pointer or a
/// malformed CSV (missing column, unparseable row, or an OHLC relation the core
/// rejects). Read the candles with `wickra_candle_reader_read` and release with
/// `wickra_candle_reader_free`.
///
/// # Safety
/// `data` must point to `len` readable bytes, or be `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_candle_reader_new(
    data: *const u8,
    len: usize,
) -> *mut CandleReader {
    if data.is_null() {
        return ptr::null_mut();
    }
    let bytes = slice::from_raw_parts(data, len);
    let Ok(mut reader) = wickra_data::csv::CandleReader::from_reader(bytes) else {
        return ptr::null_mut();
    };
    match reader.read_all() {
        Ok(candles) => Box::into_raw(Box::new(CandleReader { candles, pos: 0 })),
        Err(_) => ptr::null_mut(),
    }
}

/// Number of candles not yet read from the reader. Returns `0` on a `NULL` handle.
///
/// # Safety
/// `handle` must be valid (from `wickra_candle_reader_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_candle_reader_count(handle: *const CandleReader) -> usize {
    match handle.as_ref() {
        Some(reader) => reader.candles.len() - reader.pos,
        None => 0,
    }
}

/// Copy up to `cap` not-yet-read candles into `out`, advance past them, and return
/// the number written. Returns `0` on a `NULL` handle / `out`. Call with `cap` equal
/// to `wickra_candle_reader_count` to drain every candle in one call.
///
/// # Safety
/// `handle` (from `wickra_candle_reader_new`, not freed) and `out` must be valid or
/// `NULL`; when non-`NULL`, `out` must cover `cap` `WickraCandle` elements.
#[no_mangle]
pub unsafe extern "C" fn wickra_candle_reader_read(
    handle: *mut CandleReader,
    out: *mut WickraCandle,
    cap: usize,
) -> usize {
    let Some(reader) = handle.as_mut() else {
        return 0;
    };
    if out.is_null() {
        return 0;
    }
    let count = (reader.candles.len() - reader.pos).min(cap);
    let slots = slice::from_raw_parts_mut(out, count);
    for (slot, candle) in slots
        .iter_mut()
        .zip(&reader.candles[reader.pos..reader.pos + count])
    {
        *slot = candle_to_c(*candle);
    }
    reader.pos += count;
    count
}

/// Destroy a candle reader created by `wickra_candle_reader_new`. No-op if `handle`
/// is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_candle_reader_new` and not previously
/// freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_candle_reader_free(handle: *mut CandleReader) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

// ===== Live Binance kline feed (feature `live-binance`; blocking poll) =====

/// C-ABI view of one kline event from the live Binance feed.
#[cfg(feature = "live-binance")]
#[repr(C)]
#[derive(Clone, Copy, Debug)]
pub struct WickraKlineEvent {
    /// NUL-terminated lowercase symbol (e.g. `b"btcusdt\0"`), truncated to 15 bytes.
    pub symbol: [u8; 16],
    pub open: f64,
    pub high: f64,
    pub low: f64,
    pub close: f64,
    pub volume: f64,
    /// Candle open time as Binance sends it (milliseconds since the Unix epoch).
    pub open_time: i64,
    /// Whether the candle is closed; only closed candles are final.
    pub is_closed: bool,
}

/// Opaque live Binance kline stream: the async stream plus a single-thread tokio
/// runtime that drives it. Each poll runs one async step to completion.
#[cfg(feature = "live-binance")]
#[derive(Debug)]
pub struct BinanceStream {
    runtime: tokio::runtime::Runtime,
    inner: wickra_data::live::binance::BinanceKlineStream,
}

/// Map a `u8` interval code (the `Interval` declaration order, `0..=15`) to the
/// feed's interval. Returns `None` for an unknown code.
#[cfg(feature = "live-binance")]
fn binance_interval(code: u8) -> Option<wickra_data::live::binance::Interval> {
    use wickra_data::live::binance::Interval;
    Some(match code {
        0 => Interval::OneSecond,
        1 => Interval::OneMinute,
        2 => Interval::ThreeMinutes,
        3 => Interval::FiveMinutes,
        4 => Interval::FifteenMinutes,
        5 => Interval::ThirtyMinutes,
        6 => Interval::OneHour,
        7 => Interval::TwoHours,
        8 => Interval::FourHours,
        9 => Interval::SixHours,
        10 => Interval::EightHours,
        11 => Interval::TwelveHours,
        12 => Interval::OneDay,
        13 => Interval::ThreeDays,
        14 => Interval::OneWeek,
        15 => Interval::OneMonth,
        _ => return None,
    })
}

/// Connect to Binance's live kline stream for one or more comma-separated
/// `symbols` (case-insensitive, e.g. `"BTCUSDT,ETHUSDT"`) at the given `interval`
/// code (`0..=15`, the `Interval` declaration order). `base_url` overrides the
/// endpoint (`NULL` = production `wss://stream.binance.com:9443`); pass a `ws://…`
/// URL to target a test server. Returns `NULL` on a null/empty/invalid symbol
/// list, an unknown interval, a bad URL, or a failed initial connect. Release
/// with `wickra_binance_free`.
///
/// # Safety
/// `symbols` must be a valid NUL-terminated UTF-8 C string; `base_url` must be
/// `NULL` or a valid NUL-terminated UTF-8 C string.
#[cfg(feature = "live-binance")]
#[no_mangle]
pub unsafe extern "C" fn wickra_binance_connect(
    symbols: *const c_char,
    interval: u8,
    base_url: *const c_char,
) -> *mut BinanceStream {
    if symbols.is_null() {
        return ptr::null_mut();
    }
    let Ok(symbols_str) = core::ffi::CStr::from_ptr(symbols).to_str() else {
        return ptr::null_mut();
    };
    let symbol_list: Vec<String> = symbols_str
        .split(',')
        .map(str::trim)
        .filter(|s| !s.is_empty())
        .map(str::to_owned)
        .collect();
    if symbol_list.is_empty() {
        return ptr::null_mut();
    }
    let Some(interval) = binance_interval(interval) else {
        return ptr::null_mut();
    };
    let mut config = wickra_data::live::binance::BinanceConfig::default();
    if !base_url.is_null() {
        let Ok(url) = core::ffi::CStr::from_ptr(base_url).to_str() else {
            return ptr::null_mut();
        };
        url.clone_into(&mut config.base_url);
    }
    let Ok(runtime) = tokio::runtime::Builder::new_current_thread()
        .enable_all()
        .build()
    else {
        return ptr::null_mut();
    };
    match runtime.block_on(
        wickra_data::live::binance::BinanceKlineStream::connect_with_config(
            &symbol_list,
            interval,
            config,
        ),
    ) {
        Ok(inner) => Box::into_raw(Box::new(BinanceStream { runtime, inner })),
        Err(_) => ptr::null_mut(),
    }
}

/// Poll for the next kline event, waiting up to `timeout_ms` milliseconds.
/// Returns `1` and writes the event to `*out` when one arrives, `0` on timeout
/// (no event yet — call again), or `-1` if the stream is closed/errored or
/// `handle`/`out` is `NULL`. A dropped connection is reconnected transparently
/// within a single call (which may consume part of the timeout).
///
/// # Safety
/// `handle` (from `wickra_binance_connect`, not freed) and `out` must be valid or `NULL`.
#[cfg(feature = "live-binance")]
#[no_mangle]
pub unsafe extern "C" fn wickra_binance_next(
    handle: *mut BinanceStream,
    out: *mut WickraKlineEvent,
    timeout_ms: i64,
) -> i32 {
    let Some(stream) = handle.as_mut() else {
        return -1;
    };
    if out.is_null() {
        return -1;
    }
    let timeout = std::time::Duration::from_millis(timeout_ms.max(0) as u64);
    let runtime = &stream.runtime;
    let inner = &mut stream.inner;
    let result =
        runtime.block_on(async { tokio::time::timeout(timeout, inner.next_event()).await });
    match result {
        Ok(Ok(Some(event))) => {
            let mut symbol = [0_u8; 16];
            for (slot, byte) in symbol.iter_mut().zip(event.symbol.bytes()).take(15) {
                *slot = byte;
            }
            *out = WickraKlineEvent {
                symbol,
                open: event.candle.open,
                high: event.candle.high,
                low: event.candle.low,
                close: event.candle.close,
                volume: event.candle.volume,
                open_time: event.candle.timestamp,
                is_closed: event.is_closed,
            };
            1
        }
        // Closed stream or any transport/parse error: both terminal for the poll.
        Ok(Ok(None) | Err(_)) => -1,
        Err(_) => 0,
    }
}

/// Close the stream's connection; afterwards every `wickra_binance_next` returns
/// `-1`. No-op on a `NULL` handle.
///
/// # Safety
/// `handle` must be valid (from `wickra_binance_connect`, not freed), or `NULL`.
#[cfg(feature = "live-binance")]
#[no_mangle]
pub unsafe extern "C" fn wickra_binance_close(handle: *mut BinanceStream) {
    if let Some(stream) = handle.as_mut() {
        let runtime = &stream.runtime;
        let inner = &mut stream.inner;
        let _ = runtime.block_on(inner.close());
    }
}

/// Destroy a stream created by `wickra_binance_connect`. No-op if `handle` is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_binance_connect` and not previously freed, or `NULL`.
#[cfg(feature = "live-binance")]
#[no_mangle]
pub unsafe extern "C" fn wickra_binance_free(handle: *mut BinanceStream) {
    if !handle.is_null() {
        drop(Box::from_raw(handle));
    }
}

/// Fetch historical klines from Binance's REST endpoint into `out` (up to `cap`
/// candles). `symbol` is the trading pair (case-insensitive, e.g. `"BTCUSDT"`),
/// `interval` the code `0..=15` (the `Interval` declaration order), and `limit`
/// the number of candles to request (`1..=1000`). `start_ms`/`end_ms` are
/// inclusive Unix-millisecond bounds; pass a negative value for "unset".
/// `base_url` overrides the host (`NULL` = production `https://api.binance.com`;
/// pass an `http://…` URL to target a test server). This blocks until the HTTP
/// response arrives. Returns the number of candles written (`<= cap`), or `-1`
/// on a null/invalid symbol, an unknown interval, an out-of-range limit, a
/// transport/JSON error, or a `NULL` `out`.
///
/// # Safety
/// `symbol` must be a valid NUL-terminated UTF-8 C string; `base_url` must be
/// `NULL` or a valid NUL-terminated UTF-8 C string; when non-`NULL`, `out` must
/// cover `cap` `WickraCandle` elements.
#[cfg(feature = "live-binance")]
#[no_mangle]
pub unsafe extern "C" fn wickra_binance_fetch_klines(
    symbol: *const c_char,
    interval: u8,
    limit: u32,
    start_ms: i64,
    end_ms: i64,
    base_url: *const c_char,
    out: *mut WickraCandle,
    cap: usize,
) -> isize {
    if symbol.is_null() || out.is_null() {
        return -1;
    }
    let Ok(symbol_str) = core::ffi::CStr::from_ptr(symbol).to_str() else {
        return -1;
    };
    let Some(interval) = binance_interval(interval) else {
        return -1;
    };
    let Ok(limit) = u16::try_from(limit) else {
        return -1;
    };
    let start = (start_ms >= 0).then_some(start_ms);
    let end = (end_ms >= 0).then_some(end_ms);
    let fetched = if base_url.is_null() {
        wickra_data::live::binance_rest::fetch_klines(symbol_str, interval, limit, start, end)
    } else {
        let Ok(url) = core::ffi::CStr::from_ptr(base_url).to_str() else {
            return -1;
        };
        let config = wickra_data::live::binance_rest::BinanceRestConfig {
            base_url: url.to_owned(),
        };
        wickra_data::live::binance_rest::fetch_klines_with_config(
            symbol_str, interval, limit, start, end, &config,
        )
    };
    let Ok(candles) = fetched else {
        return -1;
    };
    let count = candles.len().min(cap);
    let slots = slice::from_raw_parts_mut(out, count);
    for (slot, candle) in slots.iter_mut().zip(&candles[..count]) {
        *slot = candle_to_c(*candle);
    }
    isize::try_from(count).unwrap_or(isize::MAX)
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn new_rejects_zero_period() {
        assert!(wickra_sma_new(0).is_null());
    }

    #[test]
    fn scalar_streaming_batch_and_lifecycle() {
        let handle = wickra_sma_new(3);
        assert!(!handle.is_null());
        unsafe {
            assert_eq!(wickra_sma_warmup_period(handle), 3);
            assert!(!wickra_sma_is_ready(handle));
            assert!(wickra_sma_update(handle, 1.0).is_nan());
            assert!(wickra_sma_update(handle, 2.0).is_nan());
            assert!(!wickra_sma_is_ready(handle));
            assert!((wickra_sma_update(handle, 3.0) - 2.0).abs() < 1e-9);
            assert!(wickra_sma_is_ready(handle));

            let name = core::ffi::CStr::from_ptr(wickra_sma_name(handle));
            assert_eq!(name.to_str().unwrap(), "SMA");

            wickra_sma_reset(handle);
            assert!(!wickra_sma_is_ready(handle));
            let input = [1.0_f64, 2.0, 3.0, 4.0, 5.0];
            let mut out = [0.0_f64; 5];
            wickra_sma_batch(handle, input.as_ptr(), out.as_mut_ptr(), 5);
            assert!(out[0].is_nan());
            assert!((out[2] - 2.0).abs() < 1e-9);
            assert!((out[4] - 4.0).abs() < 1e-9);

            wickra_sma_free(handle);
        }
    }

    #[test]
    fn null_handle_is_a_defined_noop() {
        unsafe {
            assert!(wickra_sma_update(ptr::null_mut(), 1.0).is_nan());
            assert_eq!(wickra_sma_warmup_period(ptr::null_mut()), 0);
            assert!(!wickra_sma_is_ready(ptr::null_mut()));
            assert!(wickra_sma_name(ptr::null_mut()).is_null());
            wickra_sma_reset(ptr::null_mut());
            wickra_sma_free(ptr::null_mut());
        }
    }

    #[cfg(feature = "live-binance")]
    #[test]
    fn fetch_klines_rejects_invalid_arguments() {
        // Each of these short-circuits to -1 before any network access, so the
        // test is deterministic and offline.
        unsafe {
            let symbol = c"BTCUSDT".as_ptr();
            let mut out = [WickraCandle {
                open: 0.0,
                high: 0.0,
                low: 0.0,
                close: 0.0,
                volume: 0.0,
                timestamp: 0,
            }; 4];
            // Null symbol.
            assert_eq!(
                wickra_binance_fetch_klines(
                    ptr::null(),
                    6,
                    1,
                    -1,
                    -1,
                    ptr::null(),
                    out.as_mut_ptr(),
                    4
                ),
                -1
            );
            // Null output buffer.
            assert_eq!(
                wickra_binance_fetch_klines(symbol, 6, 1, -1, -1, ptr::null(), ptr::null_mut(), 4),
                -1
            );
            // Unknown interval code.
            assert_eq!(
                wickra_binance_fetch_klines(
                    symbol,
                    99,
                    1,
                    -1,
                    -1,
                    ptr::null(),
                    out.as_mut_ptr(),
                    4
                ),
                -1
            );
        }
    }
}
