diff --git a/.roo/mcp.json b/.roo/mcp.json new file mode 100644 index 00000000..87f93eff --- /dev/null +++ b/.roo/mcp.json @@ -0,0 +1,23 @@ +{ + "mcpServers": { + "dotnet-semantic-mcp": { + "command": "dotnet-semantic-mcp", + "args": [], + "cwd": "${workspaceFolder}", + "alwaysAllow": [ + "ast_map", + "ast_references", + "ast_hierarchy", + "ast_chunk", + "ast_dependencies", + "ast_search", + "ast_details", + "ast_attributes", + "ast_diagnostics", + "diag", + "map" + ], + "disabled": false + } + } +} \ No newline at end of file diff --git a/_sidebar.md b/_sidebar.md index f8d4707b..49889b04 100644 --- a/_sidebar.md +++ b/_sidebar.md @@ -1,316 +1,337 @@ * **Core concepts** - * [Architecture](docs/architecture.md) - * [API](docs/api.md) - * [Benchmarks](docs/benchmarks.md) - * [Indicators](docs/indicators.md) - * [Usage Guides](docs/usage.md) - * [Integration](docs/integration.md) - * [Validation](docs/validation.md) - * [MA Qualities](docs/ma-qualities.md) - * [Error Metrics](docs/errors.md) + * [Architecture](/docs/architecture.md) + * [API](/docs/api.md) + * [Benchmarks](/docs/benchmarks.md) + * [Indicators](/docs/indicators.md) + * [Usage Guides](/docs/usage.md) + * [Integration](/docs/integration.md) + * [Validation](/docs/validation.md) + * [MA Qualities](/docs/ma-qualities.md) + * [Error Metrics](/docs/errors.md) * **Trends (FIR)** - * [Overview](lib/trends_FIR/_index.md) - * [ALMA - Arnaud Legoux MA](lib/trends_FIR/alma/Alma.md) - * [BLMA - Blackman Window MA](lib/trends_FIR/blma/Blma.md) - * [BWMA - Blackman-Harris MA](lib/trends_FIR/bwma/Bwma.md) - * [CONV - Convolution](lib/trends_FIR/conv/Conv.md) - * [DWMA - Double Weighted MA](lib/trends_FIR/dwma/Dwma.md) - * [GWMA - Gaussian Weighted MA](lib/trends_FIR/gwma/Gwma.md) - * [HAMMA - Hamming MA](lib/trends_FIR/hamma/Hamma.md) - * [HANMA - Hanning MA](lib/trends_FIR/hanma/Hanma.md) - * [HMA - Hull MA](lib/trends_FIR/hma/Hma.md) - * [HWMA - Henderson Weighted MA](lib/trends_FIR/hwma/Hwma.md) - * [LSMA - Least Squares MA](lib/trends_FIR/lsma/Lsma.md) - * [PWMA - Pascal Weighted MA](lib/trends_FIR/pwma/Pwma.md) - * [SGMA - Savitzky-Golay MA](lib/trends_FIR/sgma/Sgma.md) - * [SINEMA - Sine Weighted MA](lib/trends_FIR/sinema/Sinema.md) - * [SMA - Simple MA](lib/trends_FIR/sma/Sma.md) - * [TRIMA - Triangular MA](lib/trends_FIR/trima/Trima.md) - * [WMA - Weighted MA](lib/trends_FIR/wma/Wma.md) + * [Overview](/lib/trends_FIR/_index.md) + * [ALMA - Arnaud Legoux MA](/lib/trends_FIR/alma/Alma.md) + * [BLMA - Blackman Window MA](/lib/trends_FIR/blma/Blma.md) + * [BWMA - Blackman-Harris MA](/lib/trends_FIR/bwma/Bwma.md) + * [CONV - Convolution](/lib/trends_FIR/conv/Conv.md) + * [DWMA - Double Weighted MA](/lib/trends_FIR/dwma/Dwma.md) + * [GWMA - Gaussian Weighted MA](/lib/trends_FIR/gwma/Gwma.md) + * [HAMMA - Hamming MA](/lib/trends_FIR/hamma/Hamma.md) + * [HANMA - Hanning MA](/lib/trends_FIR/hanma/Hanma.md) + * [HMA - Hull MA](/lib/trends_FIR/hma/Hma.md) + * [HWMA - Henderson Weighted MA](/lib/trends_FIR/hwma/Hwma.md) + * [LSMA - Least Squares MA](/lib/trends_FIR/lsma/Lsma.md) + * [PWMA - Pascal Weighted MA](/lib/trends_FIR/pwma/Pwma.md) + * [SGMA - Savitzky-Golay MA](/lib/trends_FIR/sgma/Sgma.md) + * [SINEMA - Sine Weighted MA](/lib/trends_FIR/sinema/Sinema.md) + * [SMA - Simple MA](/lib/trends_FIR/sma/Sma.md) + * [TRIMA - Triangular MA](/lib/trends_FIR/trima/Trima.md) + * [WMA - Weighted MA](/lib/trends_FIR/wma/Wma.md) * **Trends (IIR)** - * [Overview](lib/trends_IIR/_index.md) - * [DEMA - Double Exponential MA](lib/trends_IIR/dema/Dema.md) - * [DSMA - Deviation-Scaled MA](lib/trends_IIR/dsma/Dsma.md) - * [EMA - Exponential MA](lib/trends_IIR/ema/Ema.md) - * [FRAMA - Fractal Adaptive MA](lib/trends_IIR/frama/Frama.md) - * [HEMA - Hull Exponential MA](lib/trends_IIR/hema/Hema.md) - * [HTIT - Hilbert Transform Instant Trendline](lib/trends_IIR/htit/Htit.md) - * [JMA - Jurik MA](lib/trends_IIR/jma/Jma.md) - * [KAMA - Kaufman Adaptive MA](lib/trends_IIR/kama/Kama.md) - * [MAMA - MESA Adaptive MA](lib/trends_IIR/mama/Mama.md) - * [MGDI - McGinley Dynamic](lib/trends_IIR/mgdi/Mgdi.md) - * [MMA - Modified MA](lib/trends_IIR/mma/Mma.md) - * [QEMA - Quadruple Exponential MA](lib/trends_IIR/qema/Qema.md) - * [REMA - Regularized Exponential MA](lib/trends_IIR/rema/Rema.md) - * [RGMA - Recursive Gaussian MA](lib/trends_IIR/rgma/Rgma.md) - * [RMA - Rolling MA](lib/trends_IIR/rma/Rma.md) - * [T3 - Tillson T3 MA](lib/trends_IIR/t3/T3.md) - * [TEMA - Triple Exponential MA](lib/trends_IIR/tema/Tema.md) - * [VAMA - Volatility Adjusted MA](lib/trends_IIR/vama/Vama.md) - * [VIDYA - Variable Index Dynamic Average](lib/trends_IIR/vidya/Vidya.md) - * [YZVAMA - Yang-Zhang Volatility Adjusted MA](lib/trends_IIR/yzvama/Yzvama.md) - * [ZLEMA - Zero-Lag Exponential MA](lib/trends_IIR/zlema/Zlema.md) + * [Overview](/lib/trends_IIR/_index.md) + * [DEMA - Double Exponential MA](/lib/trends_IIR/dema/Dema.md) + * [DSMA - Deviation-Scaled MA](/lib/trends_IIR/dsma/Dsma.md) + * [EMA - Exponential MA](/lib/trends_IIR/ema/Ema.md) + * [FRAMA - Fractal Adaptive MA](/lib/trends_IIR/frama/Frama.md) + * [HEMA - Hull Exponential MA](/lib/trends_IIR/hema/Hema.md) + * [HTIT - Hilbert Transform Instant Trendline](/lib/trends_IIR/htit/Htit.md) + * [JMA - Jurik MA](/lib/trends_IIR/jma/Jma.md) + * [KAMA - Kaufman Adaptive MA](/lib/trends_IIR/kama/Kama.md) + * [MAMA - MESA Adaptive MA](/lib/trends_IIR/mama/Mama.md) + * [MGDI - McGinley Dynamic](/lib/trends_IIR/mgdi/Mgdi.md) + * [MMA - Modified MA](/lib/trends_IIR/mma/Mma.md) + * [QEMA - Quadruple Exponential MA](/lib/trends_IIR/qema/Qema.md) + * [REMA - Regularized Exponential MA](/lib/trends_IIR/rema/Rema.md) + * [RGMA - Recursive Gaussian MA](/lib/trends_IIR/rgma/Rgma.md) + * [RMA - Rolling MA](/lib/trends_IIR/rma/Rma.md) + * [T3 - Tillson T3 MA](/lib/trends_IIR/t3/T3.md) + * [TEMA - Triple Exponential MA](/lib/trends_IIR/tema/Tema.md) + * [VAMA - Volatility Adjusted MA](/lib/trends_IIR/vama/Vama.md) + * [VIDYA - Variable Index Dynamic Average](/lib/trends_IIR/vidya/Vidya.md) + * [YZVAMA - Yang-Zhang Volatility Adjusted MA](/lib/trends_IIR/yzvama/Yzvama.md) + * [ZLDEMA - Zero-Lag Double Exponential MA](/lib/trends_IIR/zldema/Zldema.md) + * [ZLEMA - Zero-Lag Exponential MA](/lib/trends_IIR/zlema/Zlema.md) + * [ZLTEMA - Zero-Lag Triple Exponential MA](/lib/trends_IIR/zltema/Zltema.md) * **Filters** - * [Overview](lib/filters/_index.md) - * [BESSEL - Bessel Filter](lib/filters/bessel/Bessel.md) - * [BILATERAL - Bilateral Filter](lib/filters/bilateral/Bilateral.md) - * [BPF - Bandpass Filter](lib/filters/bpf/Bpf.md) - * [BUTTER - Butterworth Filter](lib/filters/butter/Butter.md) - * [CHEBY1 - Chebyshev Type I](lib/filters/cheby1/Cheby1.md) - * [CHEBY2 - Chebyshev Type II](lib/filters/cheby2/Cheby2.md) - * [ELLIPTIC - Elliptic Filter](lib/filters/elliptic/Elliptic.md) - * [GAUSS - Gaussian Filter](lib/filters/gauss/Gauss.md) - * [HANN - Hann Filter](lib/filters/hann/Hann.md) - * [HP - Hodrick-Prescott Filter](lib/filters/hp/Hp.md) - * [HPF - High Pass Filter](lib/filters/hpf/Hpf.md) - * [KALMAN - Kalman Filter](lib/filters/kalman/Kalman.md) - * [LOESS - LOESS Smoothing](lib/filters/loess/Loess.md) - * [NOTCH - Notch Filter](lib/filters/notch/Notch.md) - * [SGF - Savitzky-Golay Filter](lib/filters/sgf/Sgf.md) - * [SSF - Ehlers Super Smooth Filter](lib/filters/ssf/Ssf.md) - * [USF - Ehlers Ultimate Smoother Filter](lib/filters/usf/Usf.md) - * [WIENER - Wiener Filter](lib/filters/wiener/Wiener.md) + * [Overview](/lib/filters/_index.md) + * [BESSEL - Bessel Filter](/lib/filters/bessel/Bessel.md) + * [BILATERAL - Bilateral Filter](/lib/filters/bilateral/Bilateral.md) + * [BPF - Bandpass Filter](/lib/filters/bpf/Bpf.md) + * [BUTTER - Butterworth Filter](/lib/filters/butter/Butter.md) + * [CHEBY1 - Chebyshev Type I](/lib/filters/cheby1/Cheby1.md) + * [CHEBY2 - Chebyshev Type II](/lib/filters/cheby2/Cheby2.md) + * [ELLIPTIC - Elliptic Filter](/lib/filters/elliptic/Elliptic.md) + * [GAUSS - Gaussian Filter](/lib/filters/gauss/Gauss.md) + * [HANN - Hann Filter](/lib/filters/hann/Hann.md) + * [HP - Hodrick-Prescott Filter](/lib/filters/hp/Hp.md) + * [HPF - High Pass Filter](/lib/filters/hpf/Hpf.md) + * [KALMAN - Kalman Filter](/lib/filters/kalman/Kalman.md) + * [LOESS - LOESS Smoothing](/lib/filters/loess/Loess.md) + * [NOTCH - Notch Filter](/lib/filters/notch/Notch.md) + * [SGF - Savitzky-Golay Filter](/lib/filters/sgf/Sgf.md) + * [SSF - Ehlers Super Smooth Filter](/lib/filters/ssf/Ssf.md) + * [USF - Ehlers Ultimate Smoother Filter](/lib/filters/usf/Usf.md) + * [WIENER - Wiener Filter](/lib/filters/wiener/Wiener.md) * **Dynamics** - * [Overview](lib/dynamics/_index.md) - * [ADX - Average Directional Index](lib/dynamics/adx/Adx.md) - * [ADXR - Average Directional Movement Rating](lib/dynamics/adxr/Adxr.md) - * [ALLIGATOR - Williams Alligator](lib/dynamics/alligator/Alligator.md) - * [AMAT - Archer Moving Averages Trends](lib/dynamics/amat/Amat.md) - * [AROON - Aroon](lib/dynamics/aroon/Aroon.md) - * [AROONOSC - Aroon Oscillator](lib/dynamics/aroonosc/AroonOsc.md) - * [CHOP - Choppiness Index](lib/dynamics/chop/Chop.md) - * [DMX - Jurik Directional Movement Index](lib/dynamics/dmx/Dmx.md) - * [DX - Directional Movement Index](lib/dynamics/dx/Dx.md) - * [HT_TRENDMODE - Hilbert Transform Trend Mode](lib/dynamics/ht_trendmode/HtTrendmode.md) - * [ICHIMOKU - Ichimoku Cloud](lib/dynamics/ichimoku/Ichimoku.md) - * [IMI - Intraday Momentum Index](lib/dynamics/imi/Imi.md) - * [QSTICK - Qstick Indicator](lib/dynamics/qstick/Qstick.md) - * [SUPER - SuperTrend](lib/dynamics/super/Super.md) - * [TTM - TTM Trend](lib/dynamics/ttm/Ttm.md) - * [VORTEX - Vortex Indicator](lib/dynamics/vortex/Vortex.md) + * [Overview](/lib/dynamics/_index.md) + * [ADX - Average Directional Index](/lib/dynamics/adx/Adx.md) + * [ADXR - Average Directional Movement Rating](/lib/dynamics/adxr/Adxr.md) + * [ALLIGATOR - Williams Alligator](/lib/dynamics/alligator/Alligator.md) + * [AMAT - Archer Moving Averages Trends](/lib/dynamics/amat/Amat.md) + * [AROON - Aroon](/lib/dynamics/aroon/Aroon.md) + * [AROONOSC - Aroon Oscillator](/lib/dynamics/aroonosc/AroonOsc.md) + * [CHOP - Choppiness Index](/lib/dynamics/chop/Chop.md) + * [DMX - Jurik Directional Movement Index](/lib/dynamics/dmx/Dmx.md) + * [DX - Directional Movement Index](/lib/dynamics/dx/Dx.md) + * [HT_TRENDMODE - Hilbert Transform Trend Mode](/lib/dynamics/ht_trendmode/HtTrendmode.md) + * [ICHIMOKU - Ichimoku Cloud](/lib/dynamics/ichimoku/Ichimoku.cs) + * [IMI - Intraday Momentum Index](/lib/dynamics/imi/Imi.cs) + * [QSTICK - Qstick Indicator](/lib/dynamics/qstick/Qstick.md) + * [SUPER - SuperTrend](/lib/dynamics/super/Super.md) + * [TTM_SQUEEZE - TTM Squeeze](/lib/dynamics/ttm_squeeze/TtmSqueeze.md) + * [TTM_TREND - TTM Trend](/lib/dynamics/ttm_trend/TtmTrend.md) + * [VORTEX - Vortex Indicator](/lib/dynamics/vortex/Vortex.md) * **Oscillators** - * [Overview](lib/oscillators/_index.md) - * [AC - Acceleration Oscillator](lib/oscillators/ac/Ac.md) - * [AO - Awesome Oscillator](lib/oscillators/ao/Ao.md) - * [APO - Absolute Price Oscillator](lib/oscillators/apo/Apo.md) - * [BBB - Bollinger %B](lib/oscillators/bbb/Bbb.md) - * [BBS - Bollinger Band Squeeze](lib/oscillators/bbs/Bbs.md) - * [CFO - Chande Forecast Oscillator](lib/oscillators/cfo/Cfo.md) - * [DPO - Detrended Price Oscillator](lib/oscillators/dpo/Dpo.md) - * [FISHER - Fisher Transform](lib/oscillators/fisher/Fisher.md) - * [INERTIA - Inertia](lib/oscillators/inertia/Inertia.md) - * [KDJ - KDJ Indicator](lib/oscillators/kdj/Kdj.md) - * [PGO - Pretty Good Oscillator](lib/oscillators/pgo/Pgo.md) - * [SMI - Stochastic Momentum Index](lib/oscillators/smi/Smi.md) - * [STOCH - Stochastic Oscillator](lib/oscillators/stoch/Stoch.md) - * [STOCHF - Stochastic Fast](lib/oscillators/stochf/Stochf.md) - * [STOCHRSI - Stochastic RSI](lib/oscillators/stochrsi/Stochrsi.md) - * [TRIX - Triple Exponential Average](lib/oscillators/trix/Trix.md) - * [ULTOSC - Ultimate Oscillator](lib/oscillators/ultosc/Ultosc.md) - * [WILLR - Williams %R](lib/oscillators/willr/Willr.md) + * [Overview](/lib/oscillators/_index.md) + * [AC - Acceleration Oscillator](/lib/oscillators/ac/Ac.md) + * [AO - Awesome Oscillator](/lib/oscillators/ao/Ao.md) + * [APO - Absolute Price Oscillator](/lib/oscillators/apo/Apo.md) + * [BBB - Bollinger %B](/lib/oscillators/bbb/Bbb.md) + * [BBS - Bollinger Band Squeeze](/lib/oscillators/bbs/Bbs.md) + * [CFO - Chande Forecast Oscillator](/lib/oscillators/cfo/Cfo.md) + * [DPO - Detrended Price Oscillator](/lib/oscillators/dpo/Dpo.md) + * [FISHER - Fisher Transform](/lib/oscillators/fisher/Fisher.md) + * [INERTIA - Inertia](/lib/oscillators/inertia/Inertia.md) + * [KDJ - KDJ Indicator](/lib/oscillators/kdj/Kdj.md) + * [PGO - Pretty Good Oscillator](/lib/oscillators/pgo/Pgo.md) + * [SMI - Stochastic Momentum Index](/lib/oscillators/smi/Smi.md) + * [STOCH - Stochastic Oscillator](/lib/oscillators/stoch/Stoch.md) + * [STOCHF - Stochastic Fast](/lib/oscillators/stochf/Stochf.md) + * [STOCHRSI - Stochastic RSI](/lib/oscillators/stochrsi/Stochrsi.md) + * [TRIX - Triple Exponential Average](/lib/oscillators/trix/Trix.md) + * [TTM_WAVE - TTM Wave](/lib/oscillators/ttm_wave/TtmWave.md) + * [ULTOSC - Ultimate Oscillator](/lib/oscillators/ultosc/Ultosc.md) + * [WILLR - Williams %R](/lib/oscillators/willr/Willr.md) * **Momentum** - * [Overview](lib/momentum/_index.md) - * [APO - Absolute Price Oscillator](lib/momentum/apo/Apo.md) - * [BOP - Balance of Power](lib/momentum/bop/Bop.md) - * [CCI - Commodity Channel Index](lib/momentum/cci/Cci.md) - * [CFB - Jurik Composite Fractal Behavior](lib/momentum/cfb/Cfb.md) - * [CMO - Chande Momentum Oscillator](lib/momentum/cmo/Cmo.md) - * [MACD - Moving Average Convergence Divergence](lib/momentum/macd/Macd.md) - * [MOM - Momentum](lib/momentum/mom/Mom.md) - * [PMO - Price Momentum Oscillator](lib/momentum/pmo/Pmo.md) - * [PPO - Percentage Price Oscillator](lib/momentum/ppo/Ppo.md) - * [PRS - Price Relative Strength](lib/momentum/prs/Prs.md) - * [ROC - Rate of Change](lib/momentum/roc/Roc.md) - * [ROCP - Rate of Change Percentage](lib/momentum/rocp/Rocp.md) - * [ROCR - Rate of Change Ratio](lib/momentum/rocr/Rocr.md) - * [RSI - Relative Strength Index](lib/momentum/rsi/Rsi.md) - * [RSX - Jurik Relative Strength X](lib/momentum/rsx/Rsx.md) - * [TSI - True Strength Index](lib/momentum/tsi/Tsi.md) - * [VEL - Jurik Velocity](lib/momentum/vel/Vel.md) + * [Overview](/lib/momentum/_index.md) + * [APO - Absolute Price Oscillator](/lib/momentum/apo/Apo.md) + * [BOP - Balance of Power](/lib/momentum/bop/Bop.md) + * [CCI - Commodity Channel Index](/lib/momentum/cci/Cci.md) + * [CFB - Jurik Composite Fractal Behavior](/lib/momentum/cfb/Cfb.md) + * [CMO - Chande Momentum Oscillator](/lib/momentum/cmo/Cmo.md) + * [MACD - Moving Average Convergence Divergence](/lib/momentum/macd/Macd.md) + * [MOM - Momentum](/lib/momentum/mom/Mom.md) + * [PMO - Price Momentum Oscillator](/lib/momentum/pmo/Pmo.md) + * [PPO - Percentage Price Oscillator](/lib/momentum/ppo/Ppo.md) + * [PRS - Price Relative Strength](/lib/momentum/prs/Prs.md) + * [ROC - Rate of Change](/lib/momentum/roc/Roc.md) + * [ROCP - Rate of Change Percentage](/lib/momentum/rocp/Rocp.md) + * [ROCR - Rate of Change Ratio](/lib/momentum/rocr/Rocr.md) + * [RSI - Relative Strength Index](/lib/momentum/rsi/Rsi.md) + * [RSX - Jurik Relative Strength X](/lib/momentum/rsx/Rsx.md) + * [TSI - True Strength Index](/lib/momentum/tsi/Tsi.md) + * [VEL - Jurik Velocity](/lib/momentum/vel/Vel.md) * **Volatility** - * [Overview](lib/volatility/_index.md) - * [ADR - Average Daily Range](lib/volatility/adr/Adr.md) - * [ATR - Average True Range](lib/volatility/atr/Atr.md) - * [ATRN - ATR Normalized](lib/volatility/atrn/Atrn.md) - * [ATRP - ATR Percent](lib/volatility/atrp/Atrp.md) - * [BBW - Bollinger Band Width](lib/volatility/bbw/Bbw.md) - * [BBWN - Bollinger Band Width Normalized](lib/volatility/bbwn/Bbwn.md) - * [BBWP - Bollinger Band Width Percentile](lib/volatility/bbwp/Bbwp.md) - * [CCV - Close-to-Close Volatility](lib/volatility/ccv/Ccv.md) - * [CV - Conditional Volatility](lib/volatility/cv/Cv.md) - * [CVI - Chaikin's Volatility](lib/volatility/cvi/Cvi.md) - * [EWMA - Exponential Weighted MA Volatility](lib/volatility/ewma/Ewma.md) - * [GKV - Garman-Klass Volatility](lib/volatility/gkv/Gkv.md) - * [HLV - High-Low Volatility](lib/volatility/hlv/Hlv.md) - * [HV - Historical Volatility](lib/volatility/hv/Hv.md) - * [JVOLTY - Jurik Volatility](lib/volatility/jvolty/Jvolty.md) - * [JVOLTYN - Jurik Volatility Normalized](lib/volatility/jvoltyn/Jvoltyn.md) - * [MASSI - Mass Index](lib/volatility/massi/Massi.md) - * [NATR - Normalized ATR](lib/volatility/natr/Natr.md) - * [PV - Parkinson Volatility](lib/volatility/pv/Pv.md) - * [RSV - Rogers-Satchell Volatility](lib/volatility/rsv/Rsv.md) - * [RV - Realized Volatility](lib/volatility/rv/Rv.md) - * [RVI - Relative Volatility Index](lib/volatility/rvi/Rvi.md) - * [TR - True Range](lib/volatility/tr/Tr.md) - * [UI - Ulcer Index](lib/volatility/ui/Ui.md) - * [VOV - Volatility of Volatility](lib/volatility/vov/Vov.md) - * [VR - Volatility Ratio](lib/volatility/vr/Vr.md) - * [YZV - Yang-Zhang Volatility](lib/volatility/yzv/Yzv.md) + * [Overview](/lib/volatility/_index.md) + * [ADR - Average Daily Range](/lib/volatility/adr/Adr.md) + * [ATR - Average True Range](/lib/volatility/atr/Atr.md) + * [ATRN - ATR Normalized](/lib/volatility/atrn/Atrn.md) + * [ATRP - ATR Percent](/lib/volatility/atrp/Atrp.md) + * [BBW - Bollinger Band Width](/lib/volatility/bbw/Bbw.md) + * [BBWN - Bollinger Band Width Normalized](/lib/volatility/bbwn/Bbwn.md) + * [BBWP - Bollinger Band Width Percentile](/lib/volatility/bbwp/Bbwp.md) + * [CCV - Close-to-Close Volatility](/lib/volatility/ccv/Ccv.md) + * [CV - Conditional Volatility](/lib/volatility/cv/Cv.md) + * [CVI - Chaikin's Volatility](/lib/volatility/cvi/Cvi.md) + * [EWMA - Exponential Weighted MA Volatility](/lib/volatility/ewma/Ewma.md) + * [GKV - Garman-Klass Volatility](/lib/volatility/gkv/Gkv.md) + * [HLV - High-Low Volatility](/lib/volatility/hlv/Hlv.md) + * [HV - Historical Volatility](/lib/volatility/hv/Hv.md) + * [JVOLTY - Jurik Volatility](/lib/volatility/jvolty/Jvolty.md) + * [JVOLTYN - Jurik Volatility Normalized](/lib/volatility/jvoltyn/Jvoltyn.md) + * [MASSI - Mass Index](/lib/volatility/massi/Massi.md) + * [NATR - Normalized ATR](/lib/volatility/natr/Natr.md) + * [RSV - Rogers-Satchell Volatility](/lib/volatility/rsv/Rsv.md) + * [RV - Realized Volatility](/lib/volatility/rv/Rv.md) + * [RVI - Relative Volatility Index](/lib/volatility/rvi/Rvi.md) + * [TR - True Range](/lib/volatility/tr/Tr.md) + * [UI - Ulcer Index](/lib/volatility/ui/Ui.md) + * [VOV - Volatility of Volatility](/lib/volatility/vov/Vov.md) + * [VR - Volatility Ratio](/lib/volatility/vr/Vr.md) + * [YZV - Yang-Zhang Volatility](/lib/volatility/yzv/Yzv.md) * **Volume** - * [Overview](lib/volume/_index.md) - * [ADL - Accumulation/Distribution Line](lib/volume/adl/Adl.md) - * [ADOSC - Chaikin A/D Oscillator](lib/volume/adosc/Adosc.md) - * [AOBV - Archer On-Balance Volume](lib/volume/aobv/Aobv.md) - * [CMF - Chaikin Money Flow](lib/volume/cmf/Cmf.md) - * [EFI - Elder's Force Index](lib/volume/efi/Efi.md) - * [EOM - Ease of Movement](lib/volume/eom/Eom.md) - * [III - Intraday Intensity Index](lib/volume/iii/Iii.md) - * [KVO - Klinger Volume Oscillator](lib/volume/kvo/Kvo.md) - * [MFI - Money Flow Index](lib/volume/mfi/Mfi.md) - * [NVI - Negative Volume Index](lib/volume/nvi/Nvi.md) - * [OBV - On Balance Volume](lib/volume/obv/Obv.md) - * [PVD - Price Volume Divergence](lib/volume/pvd/Pvd.md) - * [PVI - Positive Volume Index](lib/volume/pvi/Pvi.md) - * [PVO - Percentage Volume Oscillator](lib/volume/pvo/Pvo.md) - * [PVR - Price Volume Rank](lib/volume/pvr/Pvr.md) - * [PVT - Price Volume Trend](lib/volume/pvt/Pvt.md) - * [TVI - Trade Volume Index](lib/volume/tvi/Tvi.md) - * [TWAP - Time Weighted Average Price](lib/volume/twap/Twap.md) - * [VA - Volume Accumulation](lib/volume/va/Va.md) - * [VF - Volume Force](lib/volume/vf/Vf.md) - * [VO - Volume Oscillator](lib/volume/vo/Vo.md) - * [VROC - Volume Rate of Change](lib/volume/vroc/Vroc.md) - * [VWAD - Volume Weighted A/D](lib/volume/vwad/Vwad.md) - * [VWAP - Volume Weighted Average Price](lib/volume/vwap/Vwap.md) - * [VWMA - Volume Weighted MA](lib/volume/vwma/Vwma.md) - * [WAD - Williams A/D](lib/volume/wad/Wad.md) + * [Overview](/lib/volume/_index.md) + * [ADL - Accumulation/Distribution Line](/lib/volume/adl/Adl.md) + * [ADOSC - Chaikin A/D Oscillator](/lib/volume/adosc/Adosc.md) + * [AOBV - Archer On-Balance Volume](/lib/volume/aobv/Aobv.md) + * [CMF - Chaikin Money Flow](/lib/volume/cmf/Cmf.md) + * [EFI - Elder's Force Index](/lib/volume/efi/Efi.md) + * [EOM - Ease of Movement](/lib/volume/eom/Eom.md) + * [III - Intraday Intensity Index](/lib/volume/iii/Iii.md) + * [KVO - Klinger Volume Oscillator](/lib/volume/kvo/Kvo.md) + * [MFI - Money Flow Index](/lib/volume/mfi/Mfi.md) + * [NVI - Negative Volume Index](/lib/volume/nvi/Nvi.md) + * [OBV - On Balance Volume](/lib/volume/obv/Obv.md) + * [PVD - Price Volume Divergence](/lib/volume/pvd/Pvd.md) + * [PVI - Positive Volume Index](/lib/volume/pvi/Pvi.md) + * [PVO - Percentage Volume Oscillator](/lib/volume/pvo/Pvo.md) + * [PVR - Price Volume Rank](/lib/volume/pvr/Pvr.md) + * [PVT - Price Volume Trend](/lib/volume/pvt/Pvt.md) + * [TVI - Trade Volume Index](/lib/volume/tvi/Tvi.md) + * [TWAP - Time Weighted Average Price](/lib/volume/twap/Twap.md) + * [VA - Volume Accumulation](/lib/volume/va/Va.md) + * [VF - Volume Force](/lib/volume/vf/Vf.md) + * [VO - Volume Oscillator](/lib/volume/vo/Vo.md) + * [VROC - Volume Rate of Change](/lib/volume/vroc/Vroc.md) + * [VWAD - Volume Weighted A/D](/lib/volume/vwad/Vwad.md) + * [VWAP - Volume Weighted Average Price](/lib/volume/vwap/Vwap.md) + * [VWMA - Volume Weighted MA](/lib/volume/vwma/Vwma.md) + * [WAD - Williams A/D](/lib/volume/wad/Wad.md) * **Channels** - * [Overview](lib/channels/_index.md) - * [ABBER - Aberration Bands](lib/channels/abber/abber.md) - * [ACCBANDS - Acceleration Bands](lib/channels/accbands/accbands.md) - * [APCHANNEL - Andrews' Pitchfork](lib/channels/apchannel/Apchannel.md) - * [APZ - Adaptive Price Zone](lib/channels/apz/apz.md) - * [ATRBANDS - ATR Bands](lib/channels/atrbands/Atrbands.md) - * [BBANDS - Bollinger Bands](lib/channels/bbands/Bbands.md) - * [DCHANNEL - Donchian Channels](lib/channels/dchannel/Dchannel.md) - * [DECAYCHANNEL - Decay Min-Max Channel](lib/channels/decaychannel/decaychannel.md) - * [FCB - Fractal Chaos Bands](lib/channels/fcb/Fcb.md) - * [JBANDS - Jurik Volatility Bands](lib/channels/jbands/Jbands.md) - * [KCHANNEL - Keltner Channel](lib/channels/kchannel/Kchannel.md) - * [MAENV - Moving Average Envelope](lib/channels/maenv/Maenv.md) - * [MMCHANNEL - Min-Max Channel](lib/channels/mmchannel/Mmchannel.md) - * [PCHANNEL - Price Channel](lib/channels/pchannel/Pchannel.md) - * [REGCHANNEL - Regression Channels](lib/channels/regchannel/Regchannel.md) - * [SDCHANNEL - Standard Deviation Channel](lib/channels/sdchannel/Sdchannel.md) - * [STARCHANNEL - Stoller Average Range Channel](lib/channels/starchannel/Starchannel.md) - * [STBANDS - Super Trend Bands](lib/channels/stbands/Stbands.md) - * [UBANDS - Ultimate Bands](lib/channels/ubands/Ubands.md) - * [UCHANNEL - Ultimate Channel](lib/channels/uchannel/Uchannel.md) - * [VWAPBANDS - VWAP Bands](lib/channels/vwapbands/Vwapbands.md) - * [VWAPSD - VWAP with Standard Deviation Bands](lib/channels/vwapsd/Vwapsd.md) + * [Overview](/lib/channels/_index.md) + * [ABBER - Aberration Bands](/lib/channels/abber/abber.md) + * [ACCBANDS - Acceleration Bands](/lib/channels/accbands/accbands.md) + * [APCHANNEL - Andrews' Pitchfork](/lib/channels/apchannel/Apchannel.md) + * [APZ - Adaptive Price Zone](/lib/channels/apz/apz.md) + * [ATRBANDS - ATR Bands](/lib/channels/atrbands/Atrbands.md) + * [BBANDS - Bollinger Bands](/lib/channels/bbands/Bbands.md) + * [DCHANNEL - Donchian Channels](/lib/channels/dchannel/Dchannel.md) + * [DECAYCHANNEL - Decay Min-Max Channel](/lib/channels/decaychannel/decaychannel.md) + * [FCB - Fractal Chaos Bands](/lib/channels/fcb/Fcb.md) + * [JBANDS - Jurik Volatility Bands](/lib/channels/jbands/Jbands.md) + * [KCHANNEL - Keltner Channel](/lib/channels/kchannel/Kchannel.md) + * [MAENV - Moving Average Envelope](/lib/channels/maenv/Maenv.md) + * [MMCHANNEL - Min-Max Channel](/lib/channels/mmchannel/Mmchannel.md) + * [PCHANNEL - Price Channel](/lib/channels/pchannel/Pchannel.md) + * [REGCHANNEL - Regression Channels](/lib/channels/regchannel/Regchannel.md) + * [SDCHANNEL - Standard Deviation Channel](/lib/channels/sdchannel/Sdchannel.md) + * [STARCHANNEL - Stoller Average Range Channel](/lib/channels/starchannel/Starchannel.md) + * [STBANDS - Super Trend Bands](/lib/channels/stbands/Stbands.md) + * [TTM_LRC - TTM Linear Regression Channel](/lib/channels/ttm_lrc/TtmLrc.md) + * [UBANDS - Ultimate Bands](/lib/channels/ubands/Ubands.md) + * [UCHANNEL - Ultimate Channel](/lib/channels/uchannel/Uchannel.md) + * [VWAPBANDS - VWAP Bands](/lib/channels/vwapbands/Vwapbands.md) + * [VWAPSD - VWAP with Standard Deviation Bands](/lib/channels/vwapsd/Vwapsd.md) * **Statistics** - * [Overview](lib/statistics/_index.md) - * [BETA - Beta Coefficient](lib/statistics/beta/Beta.md) - * [BIAS - Bias](lib/statistics/bias/Bias.md) - * [CMA - Cumulative MA](lib/statistics/cma/Cma.md) - * [COINTEGRATION - Cointegration](lib/statistics/cointegration/Cointegration.md) - * [CORRELATION - Correlation](lib/statistics/correlation/Correlation.md) - * [COVARIANCE - Covariance](lib/statistics/covariance/Covariance.md) - * [ENTROPY - Shannon Entropy](lib/statistics/entropy/Entropy.md) - * [GEOMEAN - Geometric Mean](lib/statistics/geomean/Geomean.md) - * [GRANGER - Granger Causality](lib/statistics/granger/Granger.md) - * [HARMEAN - Harmonic Mean](lib/statistics/harmean/Harmean.md) - * [HURST - Hurst Exponent](lib/statistics/hurst/Hurst.md) - * [IQR - Interquartile Range](lib/statistics/iqr/Iqr.md) - * [JB - Jarque-Bera Test](lib/statistics/jb/Jb.md) - * [KENDALL - Kendall Rank Correlation](lib/statistics/kendall/Kendall.md) - * [KURTOSIS - Kurtosis](lib/statistics/kurtosis/Kurtosis.md) - * [LINREG - Linear Regression Curve](lib/statistics/linreg/LinReg.md) - * [MEDIAN - Rolling Median](lib/statistics/median/Median.md) - * [MODE - Mode](lib/statistics/mode/Mode.md) - * [PERCENTILE - Percentile](lib/statistics/percentile/Percentile.md) - * [QUANTILE - Quantile](lib/statistics/quantile/Quantile.md) - * [SKEW - Skewness](lib/statistics/skew/Skew.md) - * [SPEARMAN - Spearman Rank Correlation](lib/statistics/spearman/Spearman.md) - * [STDDEV - Standard Deviation](lib/statistics/stddev/StdDev.md) - * [SUM - Rolling Sum](lib/statistics/sum/Sum.md) - * [THEIL - Theil Index](lib/statistics/theil/Theil.md) - * [VARIANCE - Population and Sample Variance](lib/statistics/variance/Variance.md) - * [ZSCORE - Z-score](lib/statistics/zscore/Zscore.md) - * [ZTEST - Z-Test](lib/statistics/ztest/Ztest.md) + * [Overview](/lib/statistics/_index.md) + * [ACF - Autocorrelation Function](/lib/statistics/acf/Acf.md) + * [BETA - Beta Coefficient](/lib/statistics/beta/Beta.md) + * [BIAS - Bias](/lib/statistics/bias/Bias.md) + * [CMA - Cumulative MA](/lib/statistics/cma/Cma.md) + * [COINTEGRATION - Cointegration](/lib/statistics/cointegration/Cointegration.md) + * [CORRELATION - Correlation](/lib/statistics/correlation/Correlation.md) + * [COVARIANCE - Covariance](/lib/statistics/covariance/Covariance.md) + * [ENTROPY - Shannon Entropy](/lib/statistics/entropy/Entropy.md) + * [GEOMEAN - Geometric Mean](/lib/statistics/geomean/Geomean.md) + * [GRANGER - Granger Causality](/lib/statistics/granger/Granger.md) + * [HARMEAN - Harmonic Mean](/lib/statistics/harmean/Harmean.md) + * [HURST - Hurst Exponent](/lib/statistics/hurst/Hurst.md) + * [IQR - Interquartile Range](/lib/statistics/iqr/Iqr.md) + * [JB - Jarque-Bera Test](/lib/statistics/jb/Jb.md) + * [KENDALL - Kendall Rank Correlation](/lib/statistics/kendall/Kendall.md) + * [KURTOSIS - Kurtosis](/lib/statistics/kurtosis/Kurtosis.md) + * [LINREG - Linear Regression Curve](/lib/statistics/linreg/LinReg.md) + * [MEDIAN - Rolling Median](/lib/statistics/median/Median.md) + * [PACF - Partial Autocorrelation Function](/lib/statistics/pacf/Pacf.md) + * [MODE - Mode](/lib/statistics/mode/Mode.md) + * [PERCENTILE - Percentile](/lib/statistics/percentile/Percentile.md) + * [QUANTILE - Quantile](/lib/statistics/quantile/Quantile.md) + * [SKEW - Skewness](/lib/statistics/skew/Skew.md) + * [SPEARMAN - Spearman Rank Correlation](/lib/statistics/spearman/Spearman.md) + * [STDDEV - Standard Deviation](/lib/statistics/stddev/StdDev.md) + * [SUM - Rolling Sum](/lib/statistics/sum/Sum.md) + * [THEIL - Theil Index](/lib/statistics/theil/Theil.md) + * [VARIANCE - Population and Sample Variance](/lib/statistics/variance/Variance.md) + * [ZSCORE - Z-score](/lib/statistics/zscore/Zscore.md) + * [ZTEST - Z-Test](/lib/statistics/ztest/Ztest.md) * **Numerics** - * [Overview](lib/numerics/_index.md) + * [Overview](/lib/numerics/_index.md) + * [ACCEL - Acceleration](/lib/numerics/accel/Accel.md) + * [CHANGE - Percentage Change](/lib/numerics/change/Change.md) + * [EXPTRANS - Exponential Transform](/lib/numerics/exptrans/Exptrans.md) + * [HIGHEST - Rolling Maximum](/lib/numerics/highest/Highest.md) + * [JERK - Jerk](/lib/numerics/jerk/Jerk.md) + * [LINEARTRANS - Linear Transform](/lib/numerics/lineartrans/Lineartrans.md) + * [LOGTRANS - Logarithmic Transform](/lib/numerics/logtrans/Logtrans.md) + * [LOWEST - Rolling Minimum](/lib/numerics/lowest/Lowest.md) + * [MIDPOINT - Midrange](/lib/numerics/midpoint/Midpoint.md) + * [NORMALIZE - Min-Max Normalization](/lib/numerics/normalize/Normalize.md) + * [RELU - Rectified Linear Unit](/lib/numerics/relu/Relu.md) + * [SIGMOID - Logistic Function](/lib/numerics/sigmoid/Sigmoid.md) + * [SLOPE - Rate of Change](/lib/numerics/slope/Slope.md) + * [SQRTTRANS - Square Root Transform](/lib/numerics/sqrttrans/Sqrttrans.md) + * [STANDARDIZE - Z-Score Normalization](/lib/numerics/standardize/Standardize.cs) * **Errors** - * [Overview](lib/errors/_index.md) - * [HUBER - Huber Loss](lib/errors/huber/Huber.md) - * [LOGCOSH - Log-Cosh Loss](lib/errors/logcosh/LogCosh.md) - * [MAE - Mean Absolute Error](lib/errors/mae/Mae.md) - * [MAAPE - Mean Arctangent Absolute Percentage Error](lib/errors/maape/Maape.md) - * [MAPD - Mean Absolute Percentage Deviation](lib/errors/mapd/Mapd.md) - * [MAPE - Mean Absolute Percentage Error](lib/errors/mape/Mape.md) - * [MASE - Mean Absolute Scaled Error](lib/errors/mase/Mase.md) - * [MDAE - Median Absolute Error](lib/errors/mdae/Mdae.md) - * [MDAPE - Median Absolute Percentage Error](lib/errors/mdape/Mdape.md) - * [ME - Mean Error](lib/errors/me/Me.md) - * [MPE - Mean Percentage Error](lib/errors/mpe/Mpe.md) - * [MRAE - Mean Relative Absolute Error](lib/errors/mrae/Mrae.md) - * [MSE - Mean Squared Error](lib/errors/mse/Mse.md) - * [MSLE - Mean Squared Logarithmic Error](lib/errors/msle/Msle.md) - * [PSEUDOHUBER - Pseudo-Huber Loss](lib/errors/pseudohuber/PseudoHuber.md) - * [QUANTILE - Quantile Loss](lib/errors/quantile/QuantileLoss.md) - * [RAE - Relative Absolute Error](lib/errors/rae/Rae.md) - * [RMSE - Root Mean Squared Error](lib/errors/rmse/Rmse.md) - * [RMSLE - Root Mean Squared Logarithmic Error](lib/errors/rmsle/Rmsle.md) - * [RSE - Relative Squared Error](lib/errors/rse/Rse.md) - * [RSQUARED - Coefficient of Determination](lib/errors/rsquared/Rsquared.md) - * [SMAPE - Symmetric Mean Absolute Percentage Error](lib/errors/smape/Smape.md) - * [THEILU - Theil's U Statistic](lib/errors/theilu/TheilU.md) - * [TUKEY - Tukey Biweight Loss](lib/errors/tukey/TukeyBiweight.md) - * [WMAPE - Weighted Mean Absolute Percentage Error](lib/errors/wmape/Wmape.md) + * [Overview](/lib/errors/_index.md) + * [HUBER - Huber Loss](/lib/errors/huber/Huber.md) + * [LOGCOSH - Log-Cosh Loss](/lib/errors/logcosh/LogCosh.md) + * [MAE - Mean Absolute Error](/lib/errors/mae/Mae.md) + * [MAAPE - Mean Arctangent Absolute Percentage Error](/lib/errors/maape/Maape.md) + * [MAPD - Mean Absolute Percentage Deviation](/lib/errors/mapd/Mapd.md) + * [MAPE - Mean Absolute Percentage Error](/lib/errors/mape/Mape.md) + * [MASE - Mean Absolute Scaled Error](/lib/errors/mase/Mase.md) + * [MDAE - Median Absolute Error](/lib/errors/mdae/Mdae.md) + * [MDAPE - Median Absolute Percentage Error](/lib/errors/mdape/Mdape.md) + * [ME - Mean Error](/lib/errors/me/Me.md) + * [MPE - Mean Percentage Error](/lib/errors/mpe/Mpe.md) + * [MRAE - Mean Relative Absolute Error](/lib/errors/mrae/Mrae.md) + * [MSE - Mean Squared Error](/lib/errors/mse/Mse.md) + * [MSLE - Mean Squared Logarithmic Error](/lib/errors/msle/Msle.md) + * [PSEUDOHUBER - Pseudo-Huber Loss](/lib/errors/pseudohuber/PseudoHuber.md) + * [QUANTILE - Quantile Loss](/lib/errors/quantile/QuantileLoss.md) + * [RAE - Relative Absolute Error](/lib/errors/rae/Rae.md) + * [RMSE - Root Mean Squared Error](/lib/errors/rmse/Rmse.md) + * [RMSLE - Root Mean Squared Logarithmic Error](/lib/errors/rmsle/Rmsle.md) + * [RSE - Relative Squared Error](/lib/errors/rse/Rse.md) + * [RSQUARED - Coefficient of Determination](/lib/errors/rsquared/Rsquared.md) + * [SMAPE - Symmetric Mean Absolute Percentage Error](/lib/errors/smape/Smape.md) + * [THEILU - Theil's U Statistic](/lib/errors/theilu/TheilU.md) + * [TUKEY - Tukey Biweight Loss](/lib/errors/tukey/TukeyBiweight.md) + * [WMAPE - Weighted Mean Absolute Percentage Error](/lib/errors/wmape/Wmape.md) + * [WRMSE - Weighted RMSE](/lib/errors/wrmse/Wrmse.md) * **Forecasts** - * [Overview](lib/forecasts/_index.md) - * [AFIRMA - Adaptive FIR MA](lib/forecasts/afirma/Afirma.md) - * [MLP - Multilayer Perceptron](lib/forecasts/mlp/Mlp.md) + * [Overview](/lib/forecasts/_index.md) + * [AFIRMA - Adaptive FIR MA](/lib/forecasts/afirma/Afirma.md) * **Cycles** - * [Overview](lib/cycles/_index.md) - * [CG - Center of Gravity](lib/cycles/cg/Cg.md) - * [DSP - Detrended Synthetic Price](lib/cycles/dsp/Dsp.md) - * [EACP - Ehlers Autocorrelation Periodogram](lib/cycles/eacp/Eacp.md) - * [EBSW - Ehlers Even Better Sinewave](lib/cycles/ebsw/Ebsw.md) - * [HOMOD - Homodyne Discriminator](lib/cycles/homod/Homod.md) - * [HT_DCPERIOD - Hilbert Transform Dominant Cycle Period](lib/cycles/ht_dcperiod/HtDcperiod.md) - * [HT_DCPHASE - Hilbert Transform Dominant Cycle Phase](lib/cycles/ht_dcphase/HtDcphase.md) - * [HT_PHASOR - Hilbert Transform Phasor](lib/cycles/ht_phasor/HtPhasor.md) - * [HT_SINE - Hilbert Transform SineWave](lib/cycles/ht_sine/HtSine.md) - * [LUNAR - Lunar Phase](lib/cycles/lunar/Lunar.md) - * [PHASOR - Phasor Analysis](lib/cycles/phasor/Phasor.md) - * [SINE - Sine Wave](lib/cycles/sine/Sine.md) - * [SOLAR - Solar Activity Cycle](lib/cycles/solar/Solar.md) - * [SSFDSP - SSF-Based Detrended Synthetic Price](lib/cycles/ssfdsp/Ssfdsp.md) - * [STC - Schaff Trend Cycle](lib/cycles/stc/Stc.md) + * [Overview](/lib/cycles/_index.md) + * [CG - Center of Gravity](/lib/cycles/cg/Cg.md) + * [DSP - Detrended Synthetic Price](/lib/cycles/dsp/Dsp.md) + * [EACP - Ehlers Autocorrelation Periodogram](/lib/cycles/eacp/Eacp.md) + * [EBSW - Ehlers Even Better Sinewave](/lib/cycles/ebsw/Ebsw.md) + * [HOMOD - Homodyne Discriminator](/lib/cycles/homod/Homod.md) + * [HT_DCPERIOD - Hilbert Transform Dominant Cycle Period](/lib/cycles/ht_dcperiod/HtDcperiod.md) + * [HT_DCPHASE - Hilbert Transform Dominant Cycle Phase](/lib/cycles/ht_dcphase/HtDcphase.md) + * [HT_PHASOR - Hilbert Transform Phasor](/lib/cycles/ht_phasor/HtPhasor.md) + * [HT_SINE - Hilbert Transform SineWave](/lib/cycles/ht_sine/HtSine.md) + * [LUNAR - Lunar Phase](/lib/cycles/lunar/Lunar.md) + * [SINE - Sine Wave](/lib/cycles/sine/Sine.md) + * [SOLAR - Solar Activity Cycle](/lib/cycles/solar/Solar.md) + * [SSFDSP - SSF-Based Detrended Synthetic Price](/lib/cycles/ssfdsp/Ssfdsp.md) + * [STC - Schaff Trend Cycle](/lib/cycles/stc/Stc.md) * **Reversals** - * [Overview](lib/reversals/_index.md) - * [FRACTALS - Williams Fractals](lib/reversals/fractals/Fractals.md) - * [PIVOT - Pivot Points](lib/reversals/pivot/Pivot.md) - * [PIVOTCAM - Camarilla Pivot Points](lib/reversals/pivotcam/Pivotcam.md) - * [PIVOTDEM - DeMark Pivot Points](lib/reversals/pivotdem/Pivotdem.md) - * [PIVOTEXT - Extended Traditional Pivots](lib/reversals/pivotext/Pivotext.md) - * [PIVOTFIB - Fibonacci Pivot Points](lib/reversals/pivotfib/Pivotfib.md) - * [PIVOTWOOD - Woodie's Pivot Points](lib/reversals/pivotwood/Pivotwood.md) - * [PSAR - Parabolic Stop And Reverse](lib/reversals/psar/Psar.md) - * [SWINGS - Swing High/Low Detection](lib/reversals/swings/Swings.md) + * [Overview](/lib/reversals/_index.md) + * [FRACTALS - Williams Fractals](/lib/reversals/fractals/Fractals.md) + * [PIVOT - Pivot Points](/lib/reversals/pivot/Pivot.md) + * [PIVOTCAM - Camarilla Pivot Points](/lib/reversals/pivotcam/Pivotcam.md) + * [PIVOTDEM - DeMark Pivot Points](/lib/reversals/pivotdem/Pivotdem.md) + * [PIVOTEXT - Extended Traditional Pivots](/lib/reversals/pivotext/Pivotext.md) + * [PIVOTFIB - Fibonacci Pivot Points](/lib/reversals/pivotfib/Pivotfib.md) + * [PIVOTWOOD - Woodie's Pivot Points](/lib/reversals/pivotwood/Pivotwood.md) + * [PSAR - Parabolic Stop And Reverse](/lib/reversals/psar/Psar.md) + * [SWINGS - Swing High/Low Detection](/lib/reversals/swings/Swings.md) + * [TTM_SCALPER - TTM Scalper Alert](/lib/reversals/ttm_scalper/TtmScalper.md) diff --git a/docs/indicators.md b/docs/indicators.md index ae344770..3c942c56 100644 --- a/docs/indicators.md +++ b/docs/indicators.md @@ -132,7 +132,11 @@ Rate of change and velocity measurements. First derivatives of price. | [**BOP**](../lib/momentum/bop/Bop.md) | Balance of Power | Close position in range | | [**CFB**](../lib/momentum/cfb/Cfb.md) | Composite Fractal Behavior | Jurik fractal momentum | | [**ROC**](../lib/momentum/roc/Roc.md) | Rate of Change | Absolute price change over N periods | +| [**ROCP**](../lib/momentum/rocp/Rocp.md) | Rate of Change Percentage | Percentage price change over N periods | +| [**ROCR**](../lib/momentum/rocr/Rocr.md) | Rate of Change Ratio | Price ratio over N periods | +| [**PRS**](../lib/momentum/prs/Prs.md) | Price Relative Strength | Dual-input ratio comparison | | [**RSX**](../lib/momentum/rsx/Rsx.md) | Jurik RSX | Smoothed RSI variant | +| [**TSI**](../lib/momentum/tsi/Tsi.md) | True Strength Index | Double-smoothed momentum oscillator | | [**VEL**](../lib/momentum/vel/Vel.md) | Jurik Velocity | Adaptive velocity | ### Volatility diff --git a/index.html b/index.html index 465255dd..4cff8eb3 100644 --- a/index.html +++ b/index.html @@ -174,10 +174,10 @@ name: 'QuanTAlib', repo: 'https://github.com/mihakralj/QuanTAlib', loadSidebar: true, - subMaxLevel: 2, + subMaxLevel: 0, auto2top: true, homepage: 'README.md', - relativePath: false, + relativePath: true, } diff --git a/lib/_index.md b/lib/_index.md index 29058949..73404371 100644 --- a/lib/_index.md +++ b/lib/_index.md @@ -301,7 +301,11 @@ | [TRIX](oscillators/trix/Trix.md) | Triple Exponential Average | Oscillators | | TSF | Time Series Forecast | Statistics | | [TSI](momentum/tsi/Tsi.md) | True Strength Index | Momentum | -| [TTM](dynamics/ttm/Ttm.md) | TTM Trend | Dynamics | +| [TTM_LRC](channels/ttm_lrc/TtmLrc.md) | TTM Linear Regression Channel | Channels | +| [TTM_SCALPER](reversals/ttm_scalper/TtmScalper.md) | TTM Scalper Alert | Reversals | +| [TTM_SQUEEZE](dynamics/ttm_squeeze/TtmSqueeze.md) | TTM Squeeze | Dynamics | +| [TTM_TREND](dynamics/ttm_trend/TtmTrend.md) | TTM Trend | Dynamics | +| [TTM_WAVE](oscillators/ttm_wave/TtmWave.md) | TTM Wave | Oscillators | | [TUKEY](errors/tukey/Tukey.md) | Tukey Biweight Loss | Errors | | [TVI](volume/tvi/Tvi.md) | Trade Volume Index | Volume | | [TWAP](volume/twap/Twap.md) | Time Weighted Average Price | Volume | diff --git a/lib/channels/abber/Abber.cs b/lib/channels/abber/Abber.cs index 0205d777..0117422e 100644 --- a/lib/channels/abber/Abber.cs +++ b/lib/channels/abber/Abber.cs @@ -30,13 +30,15 @@ namespace QuanTAlib; /// Pine Script implementation: https://github.com/mihakralj/pinescript/blob/main/indicators/channels/abber.pine /// [SkipLocalsInit] -public sealed class Abber : ITValuePublisher +public sealed class Abber : ITValuePublisher, IDisposable { private readonly int _period; private readonly double _multiplier; private readonly RingBuffer _sourceBuffer; private readonly RingBuffer _deviationBuffer; private readonly TValuePublishedHandler _handler; + private ITValuePublisher? _source; + private bool _disposed; private const int ResyncInterval = 1000; @@ -116,8 +118,9 @@ public sealed class Abber : ITValuePublisher /// public Abber(TSeries source, int period, double multiplier = 2.0) : this(period, multiplier) { + _source = source ?? throw new ArgumentNullException(nameof(source)); Prime(source); - source.Pub += _handler; + _source.Pub += _handler; } /// @@ -125,7 +128,8 @@ public sealed class Abber : ITValuePublisher /// public Abber(ITValuePublisher source, int period, double multiplier = 2.0) : this(period, multiplier) { - source.Pub += _handler; + _source = source ?? throw new ArgumentNullException(nameof(source)); + _source.Pub += _handler; } private void HandleValue(object? sender, in TValueEventArgs e) => Update(e.Value, e.IsNew); @@ -673,4 +677,21 @@ public sealed class Abber : ITValuePublisher var results = abber.Update(source); return (results, abber); } + + /// + /// Disposes the Abber instance, unsubscribing from the source publisher. + /// This method is idempotent. + /// + public void Dispose() + { + if (!_disposed) + { + if (_source != null) + { + _source.Pub -= _handler; + _source = null; + } + _disposed = true; + } + } } diff --git a/lib/channels/apchannel/apchannel.Tests.cs b/lib/channels/apchannel/apchannel.Tests.cs index 764b02a2..55a501de 100644 --- a/lib/channels/apchannel/apchannel.Tests.cs +++ b/lib/channels/apchannel/apchannel.Tests.cs @@ -347,22 +347,22 @@ public class ApchannelTests // Alpha must be > 0 and <= 1 Assert.Throws(() => - Apchannel.Calculate(high, low, upperBand, lowerBand, 0.0)); + Apchannel.Batch(high, low, upperBand, lowerBand, 0.0)); Assert.Throws(() => - Apchannel.Calculate(high, low, upperBand, lowerBand, 1.5)); + Apchannel.Batch(high, low, upperBand, lowerBand, 1.5)); // Arrays must be same length double[] wrongSizeLow = new double[3]; Assert.Throws(() => - Apchannel.Calculate(high, wrongSizeLow, upperBand, lowerBand, 0.2)); + Apchannel.Batch(high, wrongSizeLow, upperBand, lowerBand, 0.2)); double[] wrongSizeUpper = new double[3]; Assert.Throws(() => - Apchannel.Calculate(high, low, wrongSizeUpper, lowerBand, 0.2)); + Apchannel.Batch(high, low, wrongSizeUpper, lowerBand, 0.2)); double[] wrongSizeLower = new double[3]; Assert.Throws(() => - Apchannel.Calculate(high, low, upperBand, wrongSizeLower, 0.2)); + Apchannel.Batch(high, low, upperBand, wrongSizeLower, 0.2)); } [Fact] @@ -377,7 +377,7 @@ public class ApchannelTests double[] lowerBandSpan = new double[100]; // Calculate using span - Apchannel.Calculate(high, low, upperBandSpan, lowerBandSpan, 0.2); + Apchannel.Batch(high, low, upperBandSpan, lowerBandSpan, 0.2); // Calculate iteratively var apc = new Apchannel(0.2); @@ -407,7 +407,7 @@ public class ApchannelTests double[] upperBand = new double[5]; double[] lowerBand = new double[5]; - Apchannel.Calculate(high, low, upperBand, lowerBand, 0.2); + Apchannel.Batch(high, low, upperBand, lowerBand, 0.2); foreach (var val in upperBand) { @@ -437,7 +437,7 @@ public class ApchannelTests } // Warm up - Apchannel.Calculate(high, low, upperBand, lowerBand, 0.2); + Apchannel.Batch(high, low, upperBand, lowerBand, 0.2); // Verify method completes without OOM or stack overflow Assert.True(double.IsFinite(upperBand[^1])); diff --git a/lib/channels/apchannel/apchannel.Validation.Tests.cs b/lib/channels/apchannel/apchannel.Validation.Tests.cs index 7ed971dc..60af7a5a 100644 --- a/lib/channels/apchannel/apchannel.Validation.Tests.cs +++ b/lib/channels/apchannel/apchannel.Validation.Tests.cs @@ -66,7 +66,7 @@ public sealed class ApchannelValidationTests : IDisposable double[] spanUpper = new double[bars.Count]; double[] spanLower = new double[bars.Count]; - Apchannel.Calculate(high, low, spanUpper, spanLower, alpha); + Apchannel.Batch(high, low, spanUpper, spanLower, alpha); // 3. Batch Mode (Calculate) var (batchResults, _) = Apchannel.Calculate(bars, alpha); @@ -156,7 +156,7 @@ public sealed class ApchannelValidationTests : IDisposable double[] apchannelUpper = new double[high.Length]; double[] apchannelLower = new double[low.Length]; - Apchannel.Calculate(high, low, apchannelUpper, apchannelLower, alpha); + Apchannel.Batch(high, low, apchannelUpper, apchannelLower, alpha); // Calculate using Skender EMA for comparison var skenderQuotesForHigh = bars.Select(b => new Quote @@ -333,7 +333,7 @@ public sealed class ApchannelValidationTests : IDisposable double[] low = bars.Select(b => b.Low).ToArray(); double[] spanUpper = new double[size]; double[] spanLower = new double[size]; - Apchannel.Calculate(high, low, spanUpper, spanLower, alpha); + Apchannel.Batch(high, low, spanUpper, spanLower, alpha); // Compare last values Assert.Equal(streamingApc.UpperBand, spanUpper[^1], ValidationHelper.SkenderTolerance); diff --git a/lib/channels/apchannel/apchannel.cs b/lib/channels/apchannel/apchannel.cs index d70e302d..576e00cc 100644 --- a/lib/channels/apchannel/apchannel.cs +++ b/lib/channels/apchannel/apchannel.cs @@ -283,7 +283,7 @@ public sealed class Apchannel : AbstractBase /// Calculates the Adaptive Price Channel using span-based batch processing. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void Calculate( + public static void Batch( ReadOnlySpan sourceHigh, ReadOnlySpan sourceLow, Span upperBand, diff --git a/lib/channels/bbands/Bbands.Tests.cs b/lib/channels/bbands/Bbands.Tests.cs index 002a346f..c91ac4d1 100644 --- a/lib/channels/bbands/Bbands.Tests.cs +++ b/lib/channels/bbands/Bbands.Tests.cs @@ -214,7 +214,7 @@ public class BbandsTests TSeries source = bars.Close; // Act - TSeries result = Bbands.Calculate(source, period: 5, multiplier: 2.0); + TSeries result = Bbands.Batch(source, period: 5, multiplier: 2.0); // Assert Assert.Equal(source.Count, result.Count); @@ -236,9 +236,9 @@ public class BbandsTests double[] middleArray = new double[source.Count]; double[] upperArray = new double[source.Count]; double[] lowerArray = new double[source.Count]; - Bbands.Calculate(sourceArray.AsSpan(), middleArray, upperArray, lowerArray, period, multiplier); + Bbands.Batch(sourceArray.AsSpan(), middleArray, upperArray, lowerArray, period, multiplier); - TSeries seriesResult = Bbands.Calculate(source, period, multiplier); + TSeries seriesResult = Bbands.Batch(source, period, multiplier); // Assert - Compare last 10 values for (int i = source.Count - 10; i < source.Count; i++) @@ -258,7 +258,7 @@ public class BbandsTests // Act & Assert ArgumentException exception = Assert.Throws( - () => Bbands.Calculate(sourceArr.AsSpan(), middleArr.AsSpan(), upperArr.AsSpan(), lowerArr.AsSpan())); + () => Bbands.Batch(sourceArr.AsSpan(), middleArr.AsSpan(), upperArr.AsSpan(), lowerArr.AsSpan())); Assert.Equal("source", exception.ParamName); } @@ -305,14 +305,14 @@ public class BbandsTests } // Batch - TSeries batchResult = Bbands.Calculate(source, period, multiplier); + TSeries batchResult = Bbands.Batch(source, period, multiplier); // Span - Copy arrays before using to avoid ref local lambda issue double[] sourceArray = source.Values.ToArray(); double[] middleArray = new double[source.Count]; double[] upperArray = new double[source.Count]; double[] lowerArray = new double[source.Count]; - Bbands.Calculate(sourceArray.AsSpan(), middleArray, upperArray, lowerArray, period, multiplier); + Bbands.Batch(sourceArray.AsSpan(), middleArray, upperArray, lowerArray, period, multiplier); // Assert - Compare last 50 values (streaming only has last value) Assert.Equal(batchResult[^1].Value, streamingBbands.Middle.Value, precision: 8); diff --git a/lib/channels/bbands/Bbands.Validation.Tests.cs b/lib/channels/bbands/Bbands.Validation.Tests.cs index 40b21a33..e35fc625 100644 --- a/lib/channels/bbands/Bbands.Validation.Tests.cs +++ b/lib/channels/bbands/Bbands.Validation.Tests.cs @@ -100,7 +100,7 @@ public sealed class BbandsValidationTests : IDisposable double[] qMiddle = new double[sourceData.Length]; double[] qUpper = new double[sourceData.Length]; double[] qLower = new double[sourceData.Length]; - Bbands.Calculate(sourceData.AsSpan(), qMiddle.AsSpan(), qUpper.AsSpan(), qLower.AsSpan(), period, multiplier); + Bbands.Batch(sourceData.AsSpan(), qMiddle.AsSpan(), qUpper.AsSpan(), qLower.AsSpan(), period, multiplier); // Calculate Skender Bollinger Bands var sResult = _testData.SkenderQuotes.GetBollingerBands(period, multiplier).ToList(); @@ -215,7 +215,7 @@ public sealed class BbandsValidationTests : IDisposable double[] qMiddle = new double[sourceData.Length]; double[] qUpper = new double[sourceData.Length]; double[] qLower = new double[sourceData.Length]; - Bbands.Calculate(sourceData.AsSpan(), qMiddle.AsSpan(), qUpper.AsSpan(), qLower.AsSpan(), period, multiplier); + Bbands.Batch(sourceData.AsSpan(), qMiddle.AsSpan(), qUpper.AsSpan(), qLower.AsSpan(), period, multiplier); // Calculate TA-Lib Bollinger Bands var retCode = Functions.Bbands( @@ -321,7 +321,7 @@ public sealed class BbandsValidationTests : IDisposable double[] qMiddle = new double[sourceData.Length]; double[] qUpper = new double[sourceData.Length]; double[] qLower = new double[sourceData.Length]; - Bbands.Calculate(sourceData.AsSpan(), qMiddle.AsSpan(), qUpper.AsSpan(), qLower.AsSpan(), period, multiplier); + Bbands.Batch(sourceData.AsSpan(), qMiddle.AsSpan(), qUpper.AsSpan(), qLower.AsSpan(), period, multiplier); // Calculate Tulip Bollinger Bands var bbandsIndicator = Tulip.Indicators.bbands; diff --git a/lib/channels/bbands/Bbands.cs b/lib/channels/bbands/Bbands.cs index 235d8eba..9dcd434b 100644 --- a/lib/channels/bbands/Bbands.cs +++ b/lib/channels/bbands/Bbands.cs @@ -181,7 +181,7 @@ public sealed class Bbands : AbstractBase Span upperSpan = upperRented.AsSpan(0, len); Span lowerSpan = lowerRented.AsSpan(0, len); - Calculate(sourceSpan, middleSpan, upperSpan, lowerSpan, _period, _multiplier); + Batch(sourceSpan, middleSpan, upperSpan, lowerSpan, _period, _multiplier); for (int i = 0; i < len; i++) { @@ -230,7 +230,7 @@ public sealed class Bbands : AbstractBase /// /// Calculates Bollinger Bands for the entire series and returns the middle band series. /// - public static TSeries Calculate(TSeries source, int period = DefaultPeriod, double multiplier = DefaultMultiplier) + public static TSeries Batch(TSeries source, int period = DefaultPeriod, double multiplier = DefaultMultiplier) { Bbands bbands = new(period, multiplier); return bbands.Update(source); @@ -239,7 +239,7 @@ public sealed class Bbands : AbstractBase /// /// Calculates Bollinger Bands across all input values using SIMD-optimized operations where possible. /// - public static void Calculate( + public static void Batch( ReadOnlySpan source, Span middle, Span upper, @@ -360,4 +360,12 @@ public sealed class Bbands : AbstractBase } } } + + public static (TSeries Results, Bbands Indicator) Calculate(TSeries source, int period = DefaultPeriod, double multiplier = DefaultMultiplier) + { + var indicator = new Bbands(period, multiplier); + TSeries results = indicator.Update(source); + return (results, indicator); + } + } diff --git a/lib/channels/dchannel/Dchannel.cs b/lib/channels/dchannel/Dchannel.cs index 8984757c..64631797 100644 --- a/lib/channels/dchannel/Dchannel.cs +++ b/lib/channels/dchannel/Dchannel.cs @@ -269,8 +269,8 @@ public sealed class Dchannel : ITValuePublisher try { - QuanTAlib.Highest.Calculate(high, top.AsSpan(0, len), period); - QuanTAlib.Lowest.Calculate(low, bot.AsSpan(0, len), period); + QuanTAlib.Highest.Batch(high, top.AsSpan(0, len), period); + QuanTAlib.Lowest.Batch(low, bot.AsSpan(0, len), period); for (int i = 0; i < len; i++) { diff --git a/lib/channels/decaychannel/Decaychannel.cs b/lib/channels/decaychannel/Decaychannel.cs index dd4607d3..16528f31 100644 --- a/lib/channels/decaychannel/Decaychannel.cs +++ b/lib/channels/decaychannel/Decaychannel.cs @@ -428,8 +428,8 @@ public sealed class Decaychannel : ITValuePublisher try { - QuanTAlib.Highest.Calculate(high, rawMaxArr.AsSpan(0, len), period); - QuanTAlib.Lowest.Calculate(low, rawMinArr.AsSpan(0, len), period); + QuanTAlib.Highest.Batch(high, rawMaxArr.AsSpan(0, len), period); + QuanTAlib.Lowest.Batch(low, rawMinArr.AsSpan(0, len), period); double currentMax = double.NaN; double currentMin = double.NaN; diff --git a/lib/channels/jbands/Jbands.Tests.cs b/lib/channels/jbands/Jbands.Tests.cs index 4ad4b017..941462cb 100644 --- a/lib/channels/jbands/Jbands.Tests.cs +++ b/lib/channels/jbands/Jbands.Tests.cs @@ -202,11 +202,11 @@ public class JbandsTests double[] shortOut = new double[2]; Assert.Throws(() => - Jbands.Calculate(source.AsSpan(), shortOut.AsSpan(), upper.AsSpan(), lower.AsSpan(), 14)); + Jbands.Batch(source.AsSpan(), shortOut.AsSpan(), upper.AsSpan(), lower.AsSpan(), 14)); Assert.Throws(() => - Jbands.Calculate(source.AsSpan(), middle.AsSpan(), shortOut.AsSpan(), lower.AsSpan(), 14)); + Jbands.Batch(source.AsSpan(), middle.AsSpan(), shortOut.AsSpan(), lower.AsSpan(), 14)); Assert.Throws(() => - Jbands.Calculate(source.AsSpan(), middle.AsSpan(), upper.AsSpan(), shortOut.AsSpan(), 14)); + Jbands.Batch(source.AsSpan(), middle.AsSpan(), upper.AsSpan(), shortOut.AsSpan(), 14)); } [Fact] @@ -223,7 +223,7 @@ public class JbandsTests double[] upper = new double[100]; double[] lower = new double[100]; - Jbands.Calculate(source.AsSpan(), middle.AsSpan(), upper.AsSpan(), lower.AsSpan(), 14); + Jbands.Batch(source.AsSpan(), middle.AsSpan(), upper.AsSpan(), lower.AsSpan(), 14); var jStream = new Jbands(14); for (int i = 0; i < source.Length; i++) diff --git a/lib/channels/jbands/Jbands.Validation.Tests.cs b/lib/channels/jbands/Jbands.Validation.Tests.cs index 595e677e..8f9f68d3 100644 --- a/lib/channels/jbands/Jbands.Validation.Tests.cs +++ b/lib/channels/jbands/Jbands.Validation.Tests.cs @@ -103,7 +103,7 @@ public class JbandsValidationTests double[] upper = new double[200]; double[] lower = new double[200]; - Jbands.Calculate(source.AsSpan(), middle.AsSpan(), upper.AsSpan(), lower.AsSpan(), 14); + Jbands.Batch(source.AsSpan(), middle.AsSpan(), upper.AsSpan(), lower.AsSpan(), 14); var jStream = new Jbands(14); for (int i = 0; i < source.Length; i++) @@ -144,7 +144,7 @@ public class JbandsValidationTests double[] middleSpan = new double[150]; double[] upperSpan = new double[150]; double[] lowerSpan = new double[150]; - Jbands.Calculate(rawValues.AsSpan(), middleSpan.AsSpan(), upperSpan.AsSpan(), lowerSpan.AsSpan(), 14, 25, 0.45); + Jbands.Batch(rawValues.AsSpan(), middleSpan.AsSpan(), upperSpan.AsSpan(), lowerSpan.AsSpan(), 14, 25, 0.45); // Mode 4: Event-based var jEvent = new Jbands(14, 25, 0.45); @@ -240,7 +240,7 @@ public class JbandsValidationTests double[] middle = new double[50]; double[] upper = new double[50]; double[] lower = new double[50]; - Jbands.Calculate(rawValues.AsSpan(), middle.AsSpan(), upper.AsSpan(), lower.AsSpan(), 14); + Jbands.Batch(rawValues.AsSpan(), middle.AsSpan(), upper.AsSpan(), lower.AsSpan(), 14); // After warmup, values should be stable and match for (int i = warmupPeriod; i < rawValues.Length; i++) diff --git a/lib/channels/jbands/Jbands.cs b/lib/channels/jbands/Jbands.cs index d2e5c280..f0e2d485 100644 --- a/lib/channels/jbands/Jbands.cs +++ b/lib/channels/jbands/Jbands.cs @@ -386,7 +386,7 @@ public sealed class Jbands : ITValuePublisher, IDisposable return jbands.Update(source); } - public static void Calculate( + public static void Batch( ReadOnlySpan source, Span middle, Span upper, diff --git a/lib/channels/mmchannel/Mmchannel.cs b/lib/channels/mmchannel/Mmchannel.cs index 1814422d..c7de7538 100644 --- a/lib/channels/mmchannel/Mmchannel.cs +++ b/lib/channels/mmchannel/Mmchannel.cs @@ -252,8 +252,8 @@ public sealed class Mmchannel : ITValuePublisher return; } - Highest.Calculate(high, upper, period); - Lowest.Calculate(low, lower, period); + Highest.Batch(high, upper, period); + Lowest.Batch(low, lower, period); } public static (TSeries Upper, TSeries Lower) Batch(TBarSeries source, int period) diff --git a/lib/channels/pchannel/Pchannel.cs b/lib/channels/pchannel/Pchannel.cs index 92bc7ea2..9911679b 100644 --- a/lib/channels/pchannel/Pchannel.cs +++ b/lib/channels/pchannel/Pchannel.cs @@ -376,8 +376,8 @@ public sealed class Pchannel : ITValuePublisher try { - Highest.Calculate(high, top.AsSpan(0, len), period); - Lowest.Calculate(low, bot.AsSpan(0, len), period); + Highest.Batch(high, top.AsSpan(0, len), period); + Lowest.Batch(low, bot.AsSpan(0, len), period); for (int i = 0; i < len; i++) { diff --git a/lib/channels/stbands/Stbands.Tests.cs b/lib/channels/stbands/Stbands.Tests.cs index c832b37a..e725727b 100644 --- a/lib/channels/stbands/Stbands.Tests.cs +++ b/lib/channels/stbands/Stbands.Tests.cs @@ -279,7 +279,7 @@ public class StbandsTests TBarSeries bars = gbm.Fetch(50, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1)); // Act - TSeries result = Stbands.Calculate(bars, period: 5, multiplier: 2.0); + TSeries result = Stbands.Batch(bars, period: 5, multiplier: 2.0); // Assert Assert.Equal(bars.Count, result.Count); @@ -302,7 +302,7 @@ public class StbandsTests double[] trend = new double[bars.Count]; // Act - Stbands.Calculate(high.AsSpan(), low.AsSpan(), close.AsSpan(), upper.AsSpan(), lower.AsSpan(), trend.AsSpan(), period, multiplier); + Stbands.Batch(high.AsSpan(), low.AsSpan(), close.AsSpan(), upper.AsSpan(), lower.AsSpan(), trend.AsSpan(), period, multiplier); // Assert for (int i = 0; i < bars.Count; i++) @@ -327,7 +327,7 @@ public class StbandsTests // Act & Assert ArgumentException exception = Assert.Throws( - () => Stbands.Calculate(high.AsSpan(), low.AsSpan(), close.AsSpan(), upper.AsSpan(), lower.AsSpan(), trend.AsSpan())); + () => Stbands.Batch(high.AsSpan(), low.AsSpan(), close.AsSpan(), upper.AsSpan(), lower.AsSpan(), trend.AsSpan())); Assert.Equal("high", exception.ParamName); } @@ -348,7 +348,7 @@ public class StbandsTests } // Batch - TSeries batchResult = Stbands.Calculate(bars, period, multiplier); + TSeries batchResult = Stbands.Batch(bars, period, multiplier); // Assert - Last values should match Assert.Equal(batchResult[^1].Value, streamingStbands.Last.Value, precision: 8); @@ -377,7 +377,7 @@ public class StbandsTests double[] upper = new double[bars.Count]; double[] lower = new double[bars.Count]; double[] trend = new double[bars.Count]; - Stbands.Calculate(high.AsSpan(), low.AsSpan(), close.AsSpan(), upper.AsSpan(), lower.AsSpan(), trend.AsSpan(), period, multiplier); + Stbands.Batch(high.AsSpan(), low.AsSpan(), close.AsSpan(), upper.AsSpan(), lower.AsSpan(), trend.AsSpan(), period, multiplier); // Assert - Last values should match Assert.Equal(upper[^1], streamingStbands.Upper.Value, precision: 8); diff --git a/lib/channels/stbands/Stbands.Validation.Tests.cs b/lib/channels/stbands/Stbands.Validation.Tests.cs index 0e213371..e796525d 100644 --- a/lib/channels/stbands/Stbands.Validation.Tests.cs +++ b/lib/channels/stbands/Stbands.Validation.Tests.cs @@ -69,7 +69,7 @@ public sealed class StbandsValidationTests : IDisposable } // Batch mode - var batchResult = Stbands.Calculate(bars, period, multiplier); + var batchResult = Stbands.Batch(bars, period, multiplier); // Compare last 100 values int compareCount = Math.Min(100, bars.Count - period); @@ -118,7 +118,7 @@ public sealed class StbandsValidationTests : IDisposable double[] spanLower = new double[bars.Count]; double[] spanTrend = new double[bars.Count]; - Stbands.Calculate(high.AsSpan(), low.AsSpan(), close.AsSpan(), + Stbands.Batch(high.AsSpan(), low.AsSpan(), close.AsSpan(), spanUpper.AsSpan(), spanLower.AsSpan(), spanTrend.AsSpan(), period, multiplier); // Compare last 100 values @@ -155,7 +155,7 @@ public sealed class StbandsValidationTests : IDisposable double[] lower = new double[bars.Count]; double[] trend = new double[bars.Count]; - Stbands.Calculate(high.AsSpan(), low.AsSpan(), close.AsSpan(), + Stbands.Batch(high.AsSpan(), low.AsSpan(), close.AsSpan(), upper.AsSpan(), lower.AsSpan(), trend.AsSpan(), period, multiplier); // Verify Upper >= Lower for all points @@ -192,7 +192,7 @@ public sealed class StbandsValidationTests : IDisposable double[] lower = new double[bars.Count]; double[] trend = new double[bars.Count]; - Stbands.Calculate(high.AsSpan(), low.AsSpan(), close.AsSpan(), + Stbands.Batch(high.AsSpan(), low.AsSpan(), close.AsSpan(), upper.AsSpan(), lower.AsSpan(), trend.AsSpan(), period, multiplier); int trendChanges = 0; diff --git a/lib/channels/stbands/Stbands.cs b/lib/channels/stbands/Stbands.cs index 385582f1..24da1669 100644 --- a/lib/channels/stbands/Stbands.cs +++ b/lib/channels/stbands/Stbands.cs @@ -286,7 +286,7 @@ public sealed class Stbands : AbstractBase /// /// Calculates Super Trend Bands for the entire bar series. /// - public static TSeries Calculate(TBarSeries source, int period = DefaultPeriod, double multiplier = DefaultMultiplier) + public static TSeries Batch(TBarSeries source, int period = DefaultPeriod, double multiplier = DefaultMultiplier) { Stbands stbands = new(period, multiplier); return stbands.Update(source); @@ -295,7 +295,7 @@ public sealed class Stbands : AbstractBase /// /// Calculates Super Trend Bands across OHLC data using spans. /// - public static void Calculate( + public static void Batch( ReadOnlySpan high, ReadOnlySpan low, ReadOnlySpan close, @@ -399,4 +399,12 @@ public sealed class Stbands : AbstractBase prevClose = c; } } + + public static (TSeries Results, Stbands Indicator) Calculate(TBarSeries source, int period = DefaultPeriod, double multiplier = DefaultMultiplier) + { + var indicator = new Stbands(period, multiplier); + TSeries results = indicator.Update(source); + return (results, indicator); + } + } diff --git a/lib/channels/ubands/Ubands.Tests.cs b/lib/channels/ubands/Ubands.Tests.cs index b3f43631..cfefebbf 100644 --- a/lib/channels/ubands/Ubands.Tests.cs +++ b/lib/channels/ubands/Ubands.Tests.cs @@ -285,7 +285,7 @@ public class UbandsTests TSeries series = bars.Close; // 1. Batch Mode - var batchResult = Ubands.Calculate(series, period, multiplier); + var batchResult = Ubands.Batch(series, period, multiplier); double batchLast = batchResult.Last.Value; // 2. Span Mode @@ -293,7 +293,7 @@ public class UbandsTests double[] spanUpper = new double[source.Length]; double[] spanMiddle = new double[source.Length]; double[] spanLower = new double[source.Length]; - Ubands.Calculate(source.AsSpan(), spanUpper.AsSpan(), spanMiddle.AsSpan(), spanLower.AsSpan(), period, multiplier); + Ubands.Batch(source.AsSpan(), spanUpper.AsSpan(), spanMiddle.AsSpan(), spanLower.AsSpan(), period, multiplier); double spanLast = spanMiddle[^1]; // 3. Streaming Mode @@ -319,13 +319,13 @@ public class UbandsTests // Period must be >= 1 Assert.Throws(() => - Ubands.Calculate(source.AsSpan(), upper.AsSpan(), middle.AsSpan(), lower.AsSpan(), 0)); + Ubands.Batch(source.AsSpan(), upper.AsSpan(), middle.AsSpan(), lower.AsSpan(), 0)); Assert.Throws(() => - Ubands.Calculate(source.AsSpan(), upper.AsSpan(), middle.AsSpan(), lower.AsSpan(), -1)); + Ubands.Batch(source.AsSpan(), upper.AsSpan(), middle.AsSpan(), lower.AsSpan(), -1)); // All arrays must be same length Assert.Throws(() => - Ubands.Calculate(source.AsSpan(), wrongSize.AsSpan(), middle.AsSpan(), lower.AsSpan(), 3)); + Ubands.Batch(source.AsSpan(), wrongSize.AsSpan(), middle.AsSpan(), lower.AsSpan(), 3)); } [Fact] @@ -336,7 +336,7 @@ public class UbandsTests double[] middle = new double[5]; double[] lower = new double[5]; - Ubands.Calculate(source.AsSpan(), upper.AsSpan(), middle.AsSpan(), lower.AsSpan(), 3, 1.0); + Ubands.Batch(source.AsSpan(), upper.AsSpan(), middle.AsSpan(), lower.AsSpan(), 3, 1.0); foreach (var val in middle) { @@ -404,7 +404,7 @@ public class UbandsTests var bars = gbm.Fetch(50, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1)); TSeries series = bars.Close; - var result = Ubands.Calculate(series, 10, 1.0); + var result = Ubands.Batch(series, 10, 1.0); Assert.Equal(50, result.Count); Assert.True(double.IsFinite(result.Last.Value)); diff --git a/lib/channels/ubands/Ubands.Validation.Tests.cs b/lib/channels/ubands/Ubands.Validation.Tests.cs index 414734d8..7b2bd7a3 100644 --- a/lib/channels/ubands/Ubands.Validation.Tests.cs +++ b/lib/channels/ubands/Ubands.Validation.Tests.cs @@ -71,7 +71,7 @@ public sealed class UbandsValidationTests : IDisposable } // Batch mode - var batchResult = Ubands.Calculate(series, period, multiplier); + var batchResult = Ubands.Batch(series, period, multiplier); // Compare last 100 values int compareCount = Math.Min(100, series.Count - period); @@ -119,7 +119,7 @@ public sealed class UbandsValidationTests : IDisposable double[] spanMiddle = new double[series.Count]; double[] spanLower = new double[series.Count]; - Ubands.Calculate(source.AsSpan(), spanUpper.AsSpan(), spanMiddle.AsSpan(), + Ubands.Batch(source.AsSpan(), spanUpper.AsSpan(), spanMiddle.AsSpan(), spanLower.AsSpan(), period, multiplier); // Compare last 100 values diff --git a/lib/channels/ubands/Ubands.cs b/lib/channels/ubands/Ubands.cs index 828c87a4..2ee43905 100644 --- a/lib/channels/ubands/Ubands.cs +++ b/lib/channels/ubands/Ubands.cs @@ -268,7 +268,7 @@ public sealed class Ubands : AbstractBase /// /// Calculates Ultimate Bands for the entire series. /// - public static TSeries Calculate(TSeries source, int period = DefaultPeriod, double multiplier = DefaultMultiplier) + public static TSeries Batch(TSeries source, int period = DefaultPeriod, double multiplier = DefaultMultiplier) { Ubands ubands = new(period, multiplier); return ubands.Update(source); @@ -277,7 +277,7 @@ public sealed class Ubands : AbstractBase /// /// Calculates Ultimate Bands across data using spans. /// - public static void Calculate( + public static void Batch( ReadOnlySpan source, Span upper, Span middle, @@ -397,4 +397,12 @@ public sealed class Ubands : AbstractBase lower[i] = usf - bandOffset; } } -} \ No newline at end of file + + public static (TSeries Results, Ubands Indicator) Calculate(TSeries source, int period = DefaultPeriod, double multiplier = DefaultMultiplier) + { + var indicator = new Ubands(period, multiplier); + TSeries results = indicator.Update(source); + return (results, indicator); + } + +} diff --git a/lib/channels/uchannel/Uchannel.Tests.cs b/lib/channels/uchannel/Uchannel.Tests.cs index fbc8cd5b..59a1ffab 100644 --- a/lib/channels/uchannel/Uchannel.Tests.cs +++ b/lib/channels/uchannel/Uchannel.Tests.cs @@ -306,7 +306,7 @@ public class UchannelTests double[] spanUpper = new double[highArr.Length]; double[] spanMiddle = new double[highArr.Length]; double[] spanLower = new double[highArr.Length]; - Uchannel.Calculate(highArr.AsSpan(), lowArr.AsSpan(), closeArr.AsSpan(), + Uchannel.Batch(highArr.AsSpan(), lowArr.AsSpan(), closeArr.AsSpan(), spanUpper.AsSpan(), spanMiddle.AsSpan(), spanLower.AsSpan(), strPeriod, centerPeriod, multiplier); double spanLast = spanMiddle[^1]; @@ -336,15 +336,15 @@ public class UchannelTests // Period must be >= 1 Assert.Throws(() => - Uchannel.Calculate(high.AsSpan(), low.AsSpan(), close.AsSpan(), + Uchannel.Batch(high.AsSpan(), low.AsSpan(), close.AsSpan(), upper.AsSpan(), middle.AsSpan(), lower.AsSpan(), 0)); Assert.Throws(() => - Uchannel.Calculate(high.AsSpan(), low.AsSpan(), close.AsSpan(), + Uchannel.Batch(high.AsSpan(), low.AsSpan(), close.AsSpan(), upper.AsSpan(), middle.AsSpan(), lower.AsSpan(), -1)); // All arrays must be same length Assert.Throws(() => - Uchannel.Calculate(high.AsSpan(), wrongSize.AsSpan(), close.AsSpan(), + Uchannel.Batch(high.AsSpan(), wrongSize.AsSpan(), close.AsSpan(), upper.AsSpan(), middle.AsSpan(), lower.AsSpan(), 3)); } @@ -358,7 +358,7 @@ public class UchannelTests double[] middle = new double[5]; double[] lower = new double[5]; - Uchannel.Calculate(high.AsSpan(), low.AsSpan(), close.AsSpan(), + Uchannel.Batch(high.AsSpan(), low.AsSpan(), close.AsSpan(), upper.AsSpan(), middle.AsSpan(), lower.AsSpan(), 3, 3, 1.0); foreach (var val in middle) diff --git a/lib/channels/uchannel/Uchannel.Validation.Tests.cs b/lib/channels/uchannel/Uchannel.Validation.Tests.cs index 42cf6e51..a3413bc1 100644 --- a/lib/channels/uchannel/Uchannel.Validation.Tests.cs +++ b/lib/channels/uchannel/Uchannel.Validation.Tests.cs @@ -70,7 +70,7 @@ public class UchannelValidationTests double[] spanUpper = new double[highArr.Length]; double[] spanMiddle = new double[highArr.Length]; double[] spanLower = new double[highArr.Length]; - Uchannel.Calculate(highArr.AsSpan(), lowArr.AsSpan(), closeArr.AsSpan(), + Uchannel.Batch(highArr.AsSpan(), lowArr.AsSpan(), closeArr.AsSpan(), spanUpper.AsSpan(), spanMiddle.AsSpan(), spanLower.AsSpan(), DefaultStrPeriod, DefaultCenterPeriod, DefaultMultiplier); diff --git a/lib/channels/uchannel/Uchannel.cs b/lib/channels/uchannel/Uchannel.cs index d7226b01..b4232058 100644 --- a/lib/channels/uchannel/Uchannel.cs +++ b/lib/channels/uchannel/Uchannel.cs @@ -377,7 +377,7 @@ public sealed class Uchannel : AbstractBase /// Period for STR smoothing. /// Period for centerline smoothing. /// Band multiplier. - public static void Calculate( + public static void Batch( ReadOnlySpan high, ReadOnlySpan low, ReadOnlySpan close, diff --git a/lib/channels/vwapbands/Vwapbands.Tests.cs b/lib/channels/vwapbands/Vwapbands.Tests.cs index f71adb1a..13dde4ad 100644 --- a/lib/channels/vwapbands/Vwapbands.Tests.cs +++ b/lib/channels/vwapbands/Vwapbands.Tests.cs @@ -446,12 +446,12 @@ public class VwapbandsTests // Multiplier must be >= MinMultiplier Assert.Throws(() => - Vwapbands.Calculate(price.AsSpan(), volume.AsSpan(), + Vwapbands.Batch(price.AsSpan(), volume.AsSpan(), upper1.AsSpan(), lower1.AsSpan(), upper2.AsSpan(), lower2.AsSpan(), vwap.AsSpan(), stdDev.AsSpan(), 0)); // All arrays must be same length Assert.Throws(() => - Vwapbands.Calculate(price.AsSpan(), volume.AsSpan(), + Vwapbands.Batch(price.AsSpan(), volume.AsSpan(), wrongSize.AsSpan(), lower1.AsSpan(), upper2.AsSpan(), lower2.AsSpan(), vwap.AsSpan(), stdDev.AsSpan(), 1.0)); } @@ -467,7 +467,7 @@ public class VwapbandsTests double[] vwap = new double[5]; double[] stdDev = new double[5]; - Vwapbands.Calculate(price.AsSpan(), volume.AsSpan(), + Vwapbands.Batch(price.AsSpan(), volume.AsSpan(), upper1.AsSpan(), lower1.AsSpan(), upper2.AsSpan(), lower2.AsSpan(), vwap.AsSpan(), stdDev.AsSpan(), 1.0); foreach (var val in vwap) diff --git a/lib/channels/vwapbands/Vwapbands.Validation.Tests.cs b/lib/channels/vwapbands/Vwapbands.Validation.Tests.cs index e071ab89..69e2cb3e 100644 --- a/lib/channels/vwapbands/Vwapbands.Validation.Tests.cs +++ b/lib/channels/vwapbands/Vwapbands.Validation.Tests.cs @@ -126,7 +126,7 @@ public sealed class VwapbandsValidationTests : IDisposable double[] spanLower2 = new double[bars.Count]; double[] spanStdDev = new double[bars.Count]; - Vwapbands.Calculate(price.AsSpan(), volume.AsSpan(), + Vwapbands.Batch(price.AsSpan(), volume.AsSpan(), spanUpper1.AsSpan(), spanLower1.AsSpan(), spanUpper2.AsSpan(), spanLower2.AsSpan(), spanVwap.AsSpan(), spanStdDev.AsSpan(), multiplier); diff --git a/lib/channels/vwapbands/Vwapbands.cs b/lib/channels/vwapbands/Vwapbands.cs index 7954ccc5..4af73f48 100644 --- a/lib/channels/vwapbands/Vwapbands.cs +++ b/lib/channels/vwapbands/Vwapbands.cs @@ -364,7 +364,7 @@ public sealed class Vwapbands : AbstractBase /// Output span for VWAP values /// Output span for standard deviation values /// Band multiplier (default 1.0) - public static void Calculate( + public static void Batch( ReadOnlySpan price, ReadOnlySpan volume, Span upper1, diff --git a/lib/channels/vwapsd/Vwapsd.Tests.cs b/lib/channels/vwapsd/Vwapsd.Tests.cs index a1d8c10b..d6710686 100644 --- a/lib/channels/vwapsd/Vwapsd.Tests.cs +++ b/lib/channels/vwapsd/Vwapsd.Tests.cs @@ -472,17 +472,17 @@ public class VwapsdTests // NumDevs must be >= MinNumDevs Assert.Throws(() => - Vwapsd.Calculate(price.AsSpan(), volume.AsSpan(), + Vwapsd.Batch(price.AsSpan(), volume.AsSpan(), upper.AsSpan(), lower.AsSpan(), vwap.AsSpan(), stdDev.AsSpan(), 0)); // NumDevs must be <= MaxNumDevs Assert.Throws(() => - Vwapsd.Calculate(price.AsSpan(), volume.AsSpan(), + Vwapsd.Batch(price.AsSpan(), volume.AsSpan(), upper.AsSpan(), lower.AsSpan(), vwap.AsSpan(), stdDev.AsSpan(), 6.0)); // All arrays must be same length Assert.Throws(() => - Vwapsd.Calculate(price.AsSpan(), volume.AsSpan(), + Vwapsd.Batch(price.AsSpan(), volume.AsSpan(), wrongSize.AsSpan(), lower.AsSpan(), vwap.AsSpan(), stdDev.AsSpan(), 1.0)); } @@ -496,7 +496,7 @@ public class VwapsdTests double[] vwap = new double[5]; double[] stdDev = new double[5]; - Vwapsd.Calculate(price.AsSpan(), volume.AsSpan(), + Vwapsd.Batch(price.AsSpan(), volume.AsSpan(), upper.AsSpan(), lower.AsSpan(), vwap.AsSpan(), stdDev.AsSpan(), 1.0); foreach (var val in vwap) diff --git a/lib/channels/vwapsd/Vwapsd.Validation.Tests.cs b/lib/channels/vwapsd/Vwapsd.Validation.Tests.cs index 3eba528e..ca8cc8d3 100644 --- a/lib/channels/vwapsd/Vwapsd.Validation.Tests.cs +++ b/lib/channels/vwapsd/Vwapsd.Validation.Tests.cs @@ -120,7 +120,7 @@ public sealed class VwapsdValidationTests : IDisposable double[] spanLower = new double[bars.Count]; double[] spanStdDev = new double[bars.Count]; - Vwapsd.Calculate(price.AsSpan(), volume.AsSpan(), + Vwapsd.Batch(price.AsSpan(), volume.AsSpan(), spanUpper.AsSpan(), spanLower.AsSpan(), spanVwap.AsSpan(), spanStdDev.AsSpan(), numDevs); diff --git a/lib/channels/vwapsd/Vwapsd.cs b/lib/channels/vwapsd/Vwapsd.cs index 043363e5..7e40d2c4 100644 --- a/lib/channels/vwapsd/Vwapsd.cs +++ b/lib/channels/vwapsd/Vwapsd.cs @@ -344,7 +344,7 @@ public sealed class Vwapsd : AbstractBase /// Output span for VWAP values /// Output span for standard deviation values /// Number of standard deviations for bands (default 2.0) - public static void Calculate( + public static void Batch( ReadOnlySpan price, ReadOnlySpan volume, Span upper, diff --git a/lib/cycles/cg/Cg.Tests.cs b/lib/cycles/cg/Cg.Tests.cs index 8289d989..513e7501 100644 --- a/lib/cycles/cg/Cg.Tests.cs +++ b/lib/cycles/cg/Cg.Tests.cs @@ -425,7 +425,7 @@ public class CgTests var batchResult = batchIndicator.Update(tSeries); // Mode 3: Static Calculate - var staticResult = Cg.Calculate(tSeries, period); + var staticResult = Cg.Batch(tSeries, period); // Mode 4: Span-based Batch double[] sourceArray = new double[dataLen]; diff --git a/lib/cycles/cg/Cg.Validation.Tests.cs b/lib/cycles/cg/Cg.Validation.Tests.cs index 326790bb..0c34df56 100644 --- a/lib/cycles/cg/Cg.Validation.Tests.cs +++ b/lib/cycles/cg/Cg.Validation.Tests.cs @@ -219,7 +219,7 @@ public class CgValidationTests tSeries.Add(new TValue(bar.Time, bar.Close)); } - var batch = Cg.Calculate(tSeries, period); + var batch = Cg.Batch(tSeries, period); // Compare last values Assert.Equal(batch[^1].Value, streaming.Last.Value, Tolerance); @@ -241,7 +241,7 @@ public class CgValidationTests tSeries.Add(new TValue(bar.Time, bar.Close)); } - var tSeriesResult = Cg.Calculate(tSeries, period); + var tSeriesResult = Cg.Batch(tSeries, period); // Span approach double[] source = new double[dataLen]; diff --git a/lib/cycles/cg/Cg.cs b/lib/cycles/cg/Cg.cs index 3bfbd012..7cba9311 100644 --- a/lib/cycles/cg/Cg.cs +++ b/lib/cycles/cg/Cg.cs @@ -213,7 +213,7 @@ public sealed class Cg : AbstractBase /// /// Calculates CG for a time series. /// - public static TSeries Calculate(TSeries source, int period = 10) + public static TSeries Batch(TSeries source, int period = 10) { var cg = new Cg(period); return cg.Update(source); @@ -244,6 +244,13 @@ public sealed class Cg : AbstractBase CalculateScalarCore(source, output, period); } + public static (TSeries Results, Cg Indicator) Calculate(TSeries source, int period = 10) + { + var indicator = new Cg(period); + TSeries results = indicator.Update(source); + return (results, indicator); + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] private static void CalculateScalarCore(ReadOnlySpan source, Span output, int period) { diff --git a/lib/cycles/dsp/Dsp.Tests.cs b/lib/cycles/dsp/Dsp.Tests.cs index 1251d712..2d10ec94 100644 --- a/lib/cycles/dsp/Dsp.Tests.cs +++ b/lib/cycles/dsp/Dsp.Tests.cs @@ -298,7 +298,7 @@ public class DspTests tSeries.Add(new TValue(bar.Time, bar.Close)); } - var batch = Dsp.Calculate(tSeries, period); + var batch = Dsp.Batch(tSeries, period); // Compare last values Assert.Equal(batch[^1].Value, streaming.Last.Value, Tolerance); diff --git a/lib/cycles/dsp/Dsp.Validation.Tests.cs b/lib/cycles/dsp/Dsp.Validation.Tests.cs index 50902004..317114b3 100644 --- a/lib/cycles/dsp/Dsp.Validation.Tests.cs +++ b/lib/cycles/dsp/Dsp.Validation.Tests.cs @@ -205,7 +205,7 @@ public class DspValidationTests tSeries.Add(new TValue(bar.Time, bar.Close)); } - var batch = Dsp.Calculate(tSeries, period); + var batch = Dsp.Batch(tSeries, period); // Compare last values Assert.Equal(batch[^1].Value, streaming.Last.Value, Tolerance); @@ -227,7 +227,7 @@ public class DspValidationTests tSeries.Add(new TValue(bar.Time, bar.Close)); } - var tSeriesResult = Dsp.Calculate(tSeries, period); + var tSeriesResult = Dsp.Batch(tSeries, period); // Span approach double[] source = new double[dataLen]; diff --git a/lib/cycles/dsp/Dsp.cs b/lib/cycles/dsp/Dsp.cs index 27e72274..19939b7a 100644 --- a/lib/cycles/dsp/Dsp.cs +++ b/lib/cycles/dsp/Dsp.cs @@ -210,7 +210,7 @@ public sealed class Dsp : AbstractBase /// /// Calculates DSP for a time series. /// - public static TSeries Calculate(TSeries source, int period = 40) + public static TSeries Batch(TSeries source, int period = 40) { var dsp = new Dsp(period); return dsp.Update(source); @@ -290,4 +290,11 @@ public sealed class Dsp : AbstractBase output[i] = emaFast - emaSlow; } } + + public static (TSeries Results, Dsp Indicator) Calculate(TSeries source, int period = 40) + { + var indicator = new Dsp(period); + TSeries results = indicator.Update(source); + return (results, indicator); + } } \ No newline at end of file diff --git a/lib/cycles/eacp/Eacp.Tests.cs b/lib/cycles/eacp/Eacp.Tests.cs index 538e21d4..15c7c172 100644 --- a/lib/cycles/eacp/Eacp.Tests.cs +++ b/lib/cycles/eacp/Eacp.Tests.cs @@ -321,7 +321,7 @@ public class EacpTests tSeries.Add(new TValue(bar.Time, bar.Close)); } - var batch = Eacp.Calculate(tSeries, minPeriod, maxPeriod); + var batch = Eacp.Batch(tSeries, minPeriod, maxPeriod); // Compare last values Assert.Equal(batch[^1].Value, streaming.Last.Value, Tolerance); diff --git a/lib/cycles/eacp/Eacp.Validation.Tests.cs b/lib/cycles/eacp/Eacp.Validation.Tests.cs index 63596874..e2badd10 100644 --- a/lib/cycles/eacp/Eacp.Validation.Tests.cs +++ b/lib/cycles/eacp/Eacp.Validation.Tests.cs @@ -196,7 +196,7 @@ public class EacpValidationTests tSeries.Add(new TValue(bar.Time, bar.Close)); } - var batch = Eacp.Calculate(tSeries, minPeriod, maxPeriod); + var batch = Eacp.Batch(tSeries, minPeriod, maxPeriod); // Compare last values Assert.Equal(batch[^1].Value, streaming.Last.Value, Tolerance); @@ -219,7 +219,7 @@ public class EacpValidationTests tSeries.Add(new TValue(bar.Time, bar.Close)); } - var tSeriesResult = Eacp.Calculate(tSeries, minPeriod, maxPeriod); + var tSeriesResult = Eacp.Batch(tSeries, minPeriod, maxPeriod); // Span approach double[] source = new double[dataLen]; diff --git a/lib/cycles/eacp/Eacp.cs b/lib/cycles/eacp/Eacp.cs index b9c426b9..94aa44db 100644 --- a/lib/cycles/eacp/Eacp.cs +++ b/lib/cycles/eacp/Eacp.cs @@ -405,7 +405,7 @@ public sealed class Eacp : AbstractBase /// /// Calculates EACP for a time series. /// - public static TSeries Calculate(TSeries source, int minPeriod = 8, int maxPeriod = 48, + public static TSeries Batch(TSeries source, int minPeriod = 8, int maxPeriod = 48, int avgLength = 3, bool enhance = true) { var eacp = new Eacp(minPeriod, maxPeriod, avgLength, enhance); @@ -447,4 +447,11 @@ public sealed class Eacp : AbstractBase output[i] = result.Value; } } + + public static (TSeries Results, Eacp Indicator) Calculate(TSeries source, int minPeriod = 8, int maxPeriod = 48, int avgLength = 3, bool enhance = true) + { + var indicator = new Eacp(minPeriod, maxPeriod, avgLength, enhance); + TSeries results = indicator.Update(source); + return (results, indicator); + } } \ No newline at end of file diff --git a/lib/cycles/ebsw/Ebsw.Tests.cs b/lib/cycles/ebsw/Ebsw.Tests.cs index 357e72f9..451bbf91 100644 --- a/lib/cycles/ebsw/Ebsw.Tests.cs +++ b/lib/cycles/ebsw/Ebsw.Tests.cs @@ -320,7 +320,7 @@ public class EbswTests tSeries.Add(new TValue(bar.Time, bar.Close)); } - var batch = Ebsw.Calculate(tSeries, hpLength, ssfLength); + var batch = Ebsw.Batch(tSeries, hpLength, ssfLength); // Compare last values Assert.Equal(batch[^1].Value, streaming.Last.Value, Tolerance); diff --git a/lib/cycles/ebsw/Ebsw.Validation.Tests.cs b/lib/cycles/ebsw/Ebsw.Validation.Tests.cs index 253401b7..89387074 100644 --- a/lib/cycles/ebsw/Ebsw.Validation.Tests.cs +++ b/lib/cycles/ebsw/Ebsw.Validation.Tests.cs @@ -296,7 +296,7 @@ public class EbswValidationTests tSeries.Add(new TValue(bar.Time, bar.Close)); } - var batch = Ebsw.Calculate(tSeries, hpLength, ssfLength); + var batch = Ebsw.Batch(tSeries, hpLength, ssfLength); // Compare last values Assert.Equal(batch[^1].Value, streaming.Last.Value, Tolerance); @@ -319,7 +319,7 @@ public class EbswValidationTests tSeries.Add(new TValue(bar.Time, bar.Close)); } - var tSeriesResult = Ebsw.Calculate(tSeries, hpLength, ssfLength); + var tSeriesResult = Ebsw.Batch(tSeries, hpLength, ssfLength); // Span approach double[] source = new double[dataLen]; diff --git a/lib/cycles/ebsw/Ebsw.cs b/lib/cycles/ebsw/Ebsw.cs index 47602f79..bbfce1a4 100644 --- a/lib/cycles/ebsw/Ebsw.cs +++ b/lib/cycles/ebsw/Ebsw.cs @@ -246,7 +246,7 @@ public sealed class Ebsw : AbstractBase /// /// Calculates EBSW for a time series. /// - public static TSeries Calculate(TSeries source, int hpLength = 40, int ssfLength = 10) + public static TSeries Batch(TSeries source, int hpLength = 40, int ssfLength = 10) { var ebsw = new Ebsw(hpLength, ssfLength); return ebsw.Update(source); @@ -337,4 +337,11 @@ public sealed class Ebsw : AbstractBase filt1 = filt0; } } + + public static (TSeries Results, Ebsw Indicator) Calculate(TSeries source, int hpLength = 40, int ssfLength = 10) + { + var indicator = new Ebsw(hpLength, ssfLength); + TSeries results = indicator.Update(source); + return (results, indicator); + } } \ No newline at end of file diff --git a/lib/cycles/homod/Homod.Tests.cs b/lib/cycles/homod/Homod.Tests.cs index 55c3b7a2..5c55be22 100644 --- a/lib/cycles/homod/Homod.Tests.cs +++ b/lib/cycles/homod/Homod.Tests.cs @@ -297,7 +297,7 @@ public class HomodTests tSeries.Add(new TValue(bar.Time, bar.Close)); } - var batch = Homod.Calculate(tSeries, minPeriod, maxPeriod); + var batch = Homod.Batch(tSeries, minPeriod, maxPeriod); // Compare last values Assert.Equal(batch[^1].Value, streaming.Last.Value, Tolerance); diff --git a/lib/cycles/homod/Homod.Validation.Tests.cs b/lib/cycles/homod/Homod.Validation.Tests.cs index bf4494f1..31b7b506 100644 --- a/lib/cycles/homod/Homod.Validation.Tests.cs +++ b/lib/cycles/homod/Homod.Validation.Tests.cs @@ -160,7 +160,7 @@ public class HomodValidationTests { tSeries.Add(new TValue(bar.Time, bar.Close)); } - var tSeriesResult = Homod.Calculate(tSeries, 6, 50); + var tSeriesResult = Homod.Batch(tSeries, 6, 50); // Compare all values for (int i = 0; i < bars.Count; i++) diff --git a/lib/cycles/homod/Homod.cs b/lib/cycles/homod/Homod.cs index 82bebea3..c440fe55 100644 --- a/lib/cycles/homod/Homod.cs +++ b/lib/cycles/homod/Homod.cs @@ -381,7 +381,7 @@ public sealed class Homod : AbstractBase /// /// Calculates Homodyne Discriminator for a time series. /// - public static TSeries Calculate(TSeries source, double minPeriod = 6.0, double maxPeriod = 50.0) + public static TSeries Batch(TSeries source, double minPeriod = 6.0, double maxPeriod = 50.0) { var homod = new Homod(minPeriod, maxPeriod); return homod.Update(source); @@ -421,4 +421,11 @@ public sealed class Homod : AbstractBase output[i] = result.Value; } } + + public static (TSeries Results, Homod Indicator) Calculate(TSeries source, double minPeriod = 6.0, double maxPeriod = 50.0) + { + var indicator = new Homod(minPeriod, maxPeriod); + TSeries results = indicator.Update(source); + return (results, indicator); + } } \ No newline at end of file diff --git a/lib/cycles/ht_dcperiod/HtDcperiod.cs b/lib/cycles/ht_dcperiod/HtDcperiod.cs index 86c6d712..8794e1e9 100644 --- a/lib/cycles/ht_dcperiod/HtDcperiod.cs +++ b/lib/cycles/ht_dcperiod/HtDcperiod.cs @@ -402,7 +402,7 @@ public sealed class HtDcperiod : AbstractBase } } - public static void Calculate(ReadOnlySpan source, Span output) + public static void Batch(ReadOnlySpan source, Span output) { if (output.Length < source.Length) { @@ -416,9 +416,16 @@ public sealed class HtDcperiod : AbstractBase } } - public static TSeries Calculate(TSeries source) + public static TSeries Batch(TSeries source) { var ht = new HtDcperiod(); return ht.Update(source); } + + public static (TSeries Results, HtDcperiod Indicator) Calculate(TSeries source) + { + var indicator = new HtDcperiod(); + TSeries results = indicator.Update(source); + return (results, indicator); + } } diff --git a/lib/cycles/ht_dcphase/HtDcphase.cs b/lib/cycles/ht_dcphase/HtDcphase.cs index ffca1f14..f2baf2b9 100644 --- a/lib/cycles/ht_dcphase/HtDcphase.cs +++ b/lib/cycles/ht_dcphase/HtDcphase.cs @@ -472,7 +472,7 @@ public sealed class HtDcphase : AbstractBase } } - public static void Calculate(ReadOnlySpan source, Span output) + public static void Batch(ReadOnlySpan source, Span output) { if (output.Length < source.Length) { @@ -486,9 +486,16 @@ public sealed class HtDcphase : AbstractBase } } - public static TSeries Calculate(TSeries source) + public static TSeries Batch(TSeries source) { var ht = new HtDcphase(); return ht.Update(source); } + + public static (TSeries Results, HtDcphase Indicator) Calculate(TSeries source) + { + var indicator = new HtDcphase(); + TSeries results = indicator.Update(source); + return (results, indicator); + } } diff --git a/lib/cycles/ht_phasor/HtPhasor.cs b/lib/cycles/ht_phasor/HtPhasor.cs index 514531ff..af742d91 100644 --- a/lib/cycles/ht_phasor/HtPhasor.cs +++ b/lib/cycles/ht_phasor/HtPhasor.cs @@ -415,7 +415,7 @@ public sealed class HtPhasor : AbstractBase /// /// Calculates HT_PHASOR for a time series. /// - public static TSeries Calculate(TSeries source) + public static TSeries Batch(TSeries source) { var htPhasor = new HtPhasor(); return htPhasor.Update(source); @@ -453,4 +453,11 @@ public sealed class HtPhasor : AbstractBase quadrature[i] = htPhasor.Quadrature; } } -} + + public static (TSeries Results, HtPhasor Indicator) Calculate(TSeries source) + { + var indicator = new HtPhasor(); + TSeries results = indicator.Update(source); + return (results, indicator); + } +} \ No newline at end of file diff --git a/lib/cycles/ht_sine/HtSine.Tests.cs b/lib/cycles/ht_sine/HtSine.Tests.cs index 3080080f..0370f729 100644 --- a/lib/cycles/ht_sine/HtSine.Tests.cs +++ b/lib/cycles/ht_sine/HtSine.Tests.cs @@ -300,7 +300,7 @@ public class HtSineTests tSeries.Add(new TValue(bar.Time, bar.Close)); } - var batch = HtSine.Calculate(tSeries); + var batch = HtSine.Batch(tSeries); // Compare last values Assert.Equal(batch[^1].Value, streaming.Last.Value, Tolerance); diff --git a/lib/cycles/ht_sine/HtSine.cs b/lib/cycles/ht_sine/HtSine.cs index e43c28bb..c44262f1 100644 --- a/lib/cycles/ht_sine/HtSine.cs +++ b/lib/cycles/ht_sine/HtSine.cs @@ -496,7 +496,7 @@ public sealed class HtSine : AbstractBase /// /// Calculates HT_SINE for a time series. /// - public static TSeries Calculate(TSeries source) + public static TSeries Batch(TSeries source) { var htSine = new HtSine(); return htSine.Update(source); @@ -534,4 +534,11 @@ public sealed class HtSine : AbstractBase leadSine[i] = htSine.LeadSine; } } + + public static (TSeries Results, HtSine Indicator) Calculate(TSeries source) + { + var indicator = new HtSine(); + TSeries results = indicator.Update(source); + return (results, indicator); + } } \ No newline at end of file diff --git a/lib/cycles/lunar/Lunar.Tests.cs b/lib/cycles/lunar/Lunar.Tests.cs index bd27c34a..b1768131 100644 --- a/lib/cycles/lunar/Lunar.Tests.cs +++ b/lib/cycles/lunar/Lunar.Tests.cs @@ -171,7 +171,7 @@ public class LunarTests series.Add(new TValue(startDate.AddDays(i), 100.0 + i)); } - var result = Lunar.Calculate(series); + var result = Lunar.Batch(series); Assert.Equal(30, result.Count); diff --git a/lib/cycles/lunar/Lunar.cs b/lib/cycles/lunar/Lunar.cs index 850e1f5a..509e3f0d 100644 --- a/lib/cycles/lunar/Lunar.cs +++ b/lib/cycles/lunar/Lunar.cs @@ -91,7 +91,7 @@ public sealed class Lunar : AbstractBase /// /// Creates a new Lunar indicator and calculates phases for the source series. /// - public static TSeries Calculate(TSeries source) + public static TSeries Batch(TSeries source) { var lunar = new Lunar(); return lunar.Update(source); @@ -114,6 +114,13 @@ public sealed class Lunar : AbstractBase } } + public static (TSeries Results, Lunar Indicator) Calculate(TSeries source) + { + var indicator = new Lunar(); + TSeries results = indicator.Update(source); + return (results, indicator); + } + /// /// Calculates lunar phase for a specific DateTime. /// diff --git a/lib/cycles/sine/Sine.Tests.cs b/lib/cycles/sine/Sine.Tests.cs index 63dd6fcc..eb3c1e12 100644 --- a/lib/cycles/sine/Sine.Tests.cs +++ b/lib/cycles/sine/Sine.Tests.cs @@ -185,7 +185,7 @@ public class SineTests series.Add(new TValue(bar.Time, bar.Close)); } - var result = Sine.Calculate(series); + var result = Sine.Batch(series); Assert.Equal(100, result.Count); } @@ -201,7 +201,7 @@ public class SineTests series.Add(new TValue(bar.Time, bar.Close)); } - var result = Sine.Calculate(series, hpPeriod: 20, ssfPeriod: 5); + var result = Sine.Batch(series, hpPeriod: 20, ssfPeriod: 5); Assert.Equal(100, result.Count); } @@ -252,7 +252,7 @@ public class SineTests } // Batch calculation - var batchResult = Sine.Calculate(series); + var batchResult = Sine.Batch(series); // Compare last 100 values (after warmup) for (int i = 100; i < 200; i++) diff --git a/lib/cycles/sine/Sine.cs b/lib/cycles/sine/Sine.cs index 699b3069..bfd4387d 100644 --- a/lib/cycles/sine/Sine.cs +++ b/lib/cycles/sine/Sine.cs @@ -215,12 +215,19 @@ public sealed class Sine : AbstractBase /// /// Creates a new Sine indicator and calculates for the source series. /// - public static TSeries Calculate(TSeries source, int hpPeriod = 40, int ssfPeriod = 10) + public static TSeries Batch(TSeries source, int hpPeriod = 40, int ssfPeriod = 10) { var sine = new Sine(hpPeriod, ssfPeriod); return sine.Update(source); } + public static (TSeries Results, Sine Indicator) Calculate(TSeries source, int hpPeriod = 40, int ssfPeriod = 10) + { + var indicator = new Sine(hpPeriod, ssfPeriod); + TSeries results = indicator.Update(source); + return (results, indicator); + } + public override void Reset() { _srcBuffer.Clear(); @@ -240,4 +247,4 @@ public sealed class Sine : AbstractBase Update(new TValue(baseTime + (interval * i), source[i]), true); } } -} \ No newline at end of file +} diff --git a/lib/cycles/solar/Solar.Tests.cs b/lib/cycles/solar/Solar.Tests.cs index 78a0b876..7ede4003 100644 --- a/lib/cycles/solar/Solar.Tests.cs +++ b/lib/cycles/solar/Solar.Tests.cs @@ -160,7 +160,7 @@ public class SolarTests series.Add(new TValue(startDate.AddDays(i), 100.0 + i)); } - var result = Solar.Calculate(series); + var result = Solar.Batch(series); Assert.Equal(30, result.Count); diff --git a/lib/cycles/solar/Solar.cs b/lib/cycles/solar/Solar.cs index 976e822a..65258cab 100644 --- a/lib/cycles/solar/Solar.cs +++ b/lib/cycles/solar/Solar.cs @@ -91,7 +91,7 @@ public sealed class Solar : AbstractBase /// /// Creates a new Solar indicator and calculates cycles for the source series. /// - public static TSeries Calculate(TSeries source) + public static TSeries Batch(TSeries source) { var solar = new Solar(); return solar.Update(source); @@ -114,6 +114,13 @@ public sealed class Solar : AbstractBase } } + public static (TSeries Results, Solar Indicator) Calculate(TSeries source) + { + var indicator = new Solar(); + TSeries results = indicator.Update(source); + return (results, indicator); + } + /// /// Calculates solar cycle for a specific DateTime. /// diff --git a/lib/cycles/ssfdsp/Ssfdsp.Tests.cs b/lib/cycles/ssfdsp/Ssfdsp.Tests.cs index 27392ae2..b03ea65f 100644 --- a/lib/cycles/ssfdsp/Ssfdsp.Tests.cs +++ b/lib/cycles/ssfdsp/Ssfdsp.Tests.cs @@ -307,7 +307,7 @@ public class SsfdspTests tSeries.Add(new TValue(bar.Time, bar.Close)); } - var batch = Ssfdsp.Calculate(tSeries, period); + var batch = Ssfdsp.Batch(tSeries, period); // Compare last values Assert.Equal(batch[^1].Value, streaming.Last.Value, Tolerance); diff --git a/lib/cycles/ssfdsp/Ssfdsp.Validation.Tests.cs b/lib/cycles/ssfdsp/Ssfdsp.Validation.Tests.cs index 59d5249f..e7493b48 100644 --- a/lib/cycles/ssfdsp/Ssfdsp.Validation.Tests.cs +++ b/lib/cycles/ssfdsp/Ssfdsp.Validation.Tests.cs @@ -292,7 +292,7 @@ public class SsfdspValidationTests } // TSeries Calculate - var tsResult = Ssfdsp.Calculate(tSeries, period); + var tsResult = Ssfdsp.Batch(tSeries, period); // Streaming var streaming = new Ssfdsp(period); diff --git a/lib/cycles/ssfdsp/Ssfdsp.cs b/lib/cycles/ssfdsp/Ssfdsp.cs index ca0e6f74..3744d9a5 100644 --- a/lib/cycles/ssfdsp/Ssfdsp.cs +++ b/lib/cycles/ssfdsp/Ssfdsp.cs @@ -229,7 +229,7 @@ public sealed class Ssfdsp : AbstractBase /// /// Calculates SSF-DSP for a time series. /// - public static TSeries Calculate(TSeries source, int period = 40) + public static TSeries Batch(TSeries source, int period = 40) { var ssfdsp = new Ssfdsp(period); return ssfdsp.Update(source); @@ -328,4 +328,11 @@ public sealed class Ssfdsp : AbstractBase output[i] = ssfFast - ssfSlow; } } + + public static (TSeries Results, Ssfdsp Indicator) Calculate(TSeries source, int period = 40) + { + var indicator = new Ssfdsp(period); + TSeries results = indicator.Update(source); + return (results, indicator); + } } \ No newline at end of file diff --git a/lib/cycles/stc/Stc.Tests.cs b/lib/cycles/stc/Stc.Tests.cs index ee380946..7391d39f 100644 --- a/lib/cycles/stc/Stc.Tests.cs +++ b/lib/cycles/stc/Stc.Tests.cs @@ -93,7 +93,7 @@ public class StcTests var batchStc = CreateDefaultStc(); var tseriesResult = batchStc.Update(series); - Stc.Calculate(input.AsSpan(), output.AsSpan(), kPeriod: CycleLength, dPeriod: CycleLength, fastLength: FastLength, slowLength: SlowLength, smoothing: StcSmoothing.Sigmoid); + Stc.Batch(input.AsSpan(), output.AsSpan(), kPeriod: CycleLength, dPeriod: CycleLength, fastLength: FastLength, slowLength: SlowLength, smoothing: StcSmoothing.Sigmoid); // Compare last value Assert.Equal(tseriesResult.Last.Value, output[^1], 1e-9); diff --git a/lib/cycles/stc/Stc.cs b/lib/cycles/stc/Stc.cs index 6dee398c..69610864 100644 --- a/lib/cycles/stc/Stc.cs +++ b/lib/cycles/stc/Stc.cs @@ -473,7 +473,7 @@ public sealed class Stc : AbstractBase /// /// Static convenience method that creates a new Stc instance and processes the entire series. /// - public static TSeries Calculate(TSeries source, int kPeriod = 10, int dPeriod = 3, int fastLength = 23, int slowLength = 50, StcSmoothing smoothing = StcSmoothing.Ema) + public static TSeries Batch(TSeries source, int kPeriod = 10, int dPeriod = 3, int fastLength = 23, int slowLength = 50, StcSmoothing smoothing = StcSmoothing.Ema) { var indicator = new Stc(kPeriod, dPeriod, fastLength, slowLength, smoothing); return indicator.Update(source); @@ -483,7 +483,7 @@ public sealed class Stc : AbstractBase // replicate the full STC state machine inline for zero-allocation performance. // The sequential MACD→Stoch1→Stoch2→Smoothing pipeline cannot be decomposed // without introducing heap allocations or sacrificing inlining opportunities. - public static void Calculate(ReadOnlySpan source, Span output, + public static void Batch(ReadOnlySpan source, Span output, int kPeriod = 10, int dPeriod = 3, int fastLength = 23, int slowLength = 50, StcSmoothing smoothing = StcSmoothing.Ema) { if (source.Length != output.Length) @@ -665,4 +665,11 @@ public sealed class Stc : AbstractBase } } } -} + + public static (TSeries Results, Stc Indicator) Calculate(TSeries source, int kPeriod = 10, int dPeriod = 3, int fastLength = 23, int slowLength = 50, StcSmoothing smoothing = StcSmoothing.Ema) + { + var indicator = new Stc(kPeriod, dPeriod, fastLength, slowLength, smoothing); + TSeries results = indicator.Update(source); + return (results, indicator); + } +} \ No newline at end of file diff --git a/lib/dynamics/_index.md b/lib/dynamics/_index.md index 299ff55f..976b28fa 100644 --- a/lib/dynamics/_index.md +++ b/lib/dynamics/_index.md @@ -18,11 +18,10 @@ Dynamics indicators measure trend strength, speed, and direction. Unlike momentu | [DMX](dmx/Dmx.md) | Jurik DMX | Smoothed bipolar DMI using Jurik smoothing. Low noise. | | [DX](dx/Dx.md) | Directional Movement Index | Raw directional strength. Unsmoothed ADX component. | | [HT_TRENDMODE](ht_trendmode/Ht_trendmode.md) | HT Trend vs Cycle | Ehlers Hilbert Transform. Binary trend/cycle mode detection. | -| [ICHIMOKU](ichimoku/Ichimoku.md) | Ichimoku Cloud | Five-line system. Cloud defines support/resistance zones. | -| [IMI](imi/Imi.md) | Intraday Momentum Index | RSI variant using open-close range. Intraday overbought/oversold. | +| [ICHIMOKU](ichimoku/Ichimoku.cs) | Ichimoku Cloud | Five-line system. Cloud defines support/resistance zones. | +| [IMI](imi/Imi.cs) | Intraday Momentum Index | RSI variant using open-close range. Intraday overbought/oversold. | | [QSTICK](qstick/Qstick.md) | Qstick | MA of (Close - Open). Positive = buying pressure. | -| SAR | Stop and Reverse | | | [SUPER](super/Super.md) | SuperTrend | ATR-based trailing stop. Flips on breakout. Color-coded direction. | -| [TTM](ttm/Ttm.md) | TTM Trend | Fast 6-period EMA. Color-coded trend from John Carter. | +| [TTM_TREND](ttm_trend/TtmTrend.md) | TTM Trend | Fast 6-period EMA. Color-coded trend from John Carter. | | [TTM_SQUEEZE](ttm_squeeze/TtmSqueeze.md) | TTM Squeeze | BB inside KC squeeze detection with linear regression momentum. John Carter. | | [VORTEX](vortex/Vortex.md) | Vortex Indicator | VI+ and VI- measure positive/negative trend movement. | diff --git a/lib/dynamics/adx/Adx.cs b/lib/dynamics/adx/Adx.cs index ba2b16fb..39bcad5a 100644 --- a/lib/dynamics/adx/Adx.cs +++ b/lib/dynamics/adx/Adx.cs @@ -348,7 +348,7 @@ public sealed class Adx : ITValuePublisher var v = new double[len]; // Use the static Calculate method for performance - Calculate(source.High.Values, source.Low.Values, source.Close.Values, _period, v); + Batch(source.High.Values, source.Low.Values, source.Close.Values, _period, v); // Create lists for TSeries - use collection expression directly var tList = new List(len); @@ -405,7 +405,7 @@ public sealed class Adx : ITValuePublisher } [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void Calculate(ReadOnlySpan high, ReadOnlySpan low, ReadOnlySpan close, int period, Span destination) + public static void Batch(ReadOnlySpan high, ReadOnlySpan low, ReadOnlySpan close, int period, Span destination) { int len = high.Length; if (len < period * 2) @@ -490,7 +490,7 @@ public sealed class Adx : ITValuePublisher var len = source.Count; var v = new double[len]; - Calculate(source.High.Values, source.Low.Values, source.Close.Values, period, v); + Batch(source.High.Values, source.Low.Values, source.Close.Values, period, v); var tList = new List(len); var times = source.Open.Times; @@ -501,4 +501,11 @@ public sealed class Adx : ITValuePublisher return new TSeries(tList, [.. v]); } + + public static (TSeries Results, Adx Indicator) Calculate(TBarSeries source, int period) + { + var indicator = new Adx(period); + TSeries results = indicator.Update(source); + return (results, indicator); + } } diff --git a/lib/dynamics/adxr/Adxr.cs b/lib/dynamics/adxr/Adxr.cs index 2c3813da..d576b585 100644 --- a/lib/dynamics/adxr/Adxr.cs +++ b/lib/dynamics/adxr/Adxr.cs @@ -132,7 +132,7 @@ public sealed class Adxr : ITValuePublisher int len = source.Count; var v = new double[len]; - Calculate(source.High.Values, source.Low.Values, source.Close.Values, _period, v); + Batch(source.High.Values, source.Low.Values, source.Close.Values, _period, v); var tList = new List(len); var vList = new List(v); @@ -153,7 +153,7 @@ public sealed class Adxr : ITValuePublisher } [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void Calculate(ReadOnlySpan high, ReadOnlySpan low, ReadOnlySpan close, int period, Span destination) + public static void Batch(ReadOnlySpan high, ReadOnlySpan low, ReadOnlySpan close, int period, Span destination) { int len = high.Length; if (len == 0 || len != low.Length || len != close.Length || len != destination.Length) @@ -180,7 +180,7 @@ public sealed class Adxr : ITValuePublisher try { - Adx.Calculate(high, low, close, period, adxSpan); + Adx.Batch(high, low, close, period, adxSpan); destination.Clear(); @@ -223,7 +223,7 @@ public sealed class Adxr : ITValuePublisher int len = source.Count; var v = new double[len]; - Calculate(source.High.Values, source.Low.Values, source.Close.Values, period, v); + Batch(source.High.Values, source.Low.Values, source.Close.Values, period, v); var tList = new List(len); var times = source.Open.Times; @@ -234,4 +234,11 @@ public sealed class Adxr : ITValuePublisher return new TSeries(tList, [.. v]); } + + public static (TSeries Results, Adxr Indicator) Calculate(TBarSeries source, int period) + { + var indicator = new Adxr(period); + TSeries results = indicator.Update(source); + return (results, indicator); + } } diff --git a/lib/dynamics/alligator/Alligator.cs b/lib/dynamics/alligator/Alligator.cs index bd6ebfbf..e906cfc3 100644 --- a/lib/dynamics/alligator/Alligator.cs +++ b/lib/dynamics/alligator/Alligator.cs @@ -311,6 +311,14 @@ public sealed class Alligator : ITValuePublisher return alligator.Update(source); } + public static (TSeries Results, Alligator Indicator) Calculate(TBarSeries source) + { + var indicator = new Alligator(); + TSeries results = indicator.Update(source); + return (results, indicator); + } + + /// /// Gets the Jaw period value. /// diff --git a/lib/dynamics/amat/Amat.Tests.cs b/lib/dynamics/amat/Amat.Tests.cs index 1020e365..661198b8 100644 --- a/lib/dynamics/amat/Amat.Tests.cs +++ b/lib/dynamics/amat/Amat.Tests.cs @@ -328,7 +328,7 @@ public class AmatTests var tValues = _testData.Values.ToArray(); var spanInput = new ReadOnlySpan(tValues); var spanOutput = new double[tValues.Length]; - Amat.Calculate(spanInput, spanOutput, fastPeriod, slowPeriod); + Amat.Batch(spanInput, spanOutput, fastPeriod, slowPeriod); double spanResult = spanOutput[^1]; // 3. Streaming Mode (instance, one value at a time) @@ -363,13 +363,13 @@ public class AmatTests double[] wrongSize = new double[3]; Assert.Throws(() => - Amat.Calculate(source.AsSpan(), wrongSize.AsSpan(), strength.AsSpan(), 5, 10)); + Amat.Batch(source.AsSpan(), wrongSize.AsSpan(), strength.AsSpan(), 5, 10)); Assert.Throws(() => - Amat.Calculate(source.AsSpan(), trend.AsSpan(), wrongSize.AsSpan(), 5, 10)); + Amat.Batch(source.AsSpan(), trend.AsSpan(), wrongSize.AsSpan(), 5, 10)); Assert.Throws(() => - Amat.Calculate(source.AsSpan(), trend.AsSpan(), strength.AsSpan(), 0, 10)); + Amat.Batch(source.AsSpan(), trend.AsSpan(), strength.AsSpan(), 0, 10)); Assert.Throws(() => - Amat.Calculate(source.AsSpan(), trend.AsSpan(), strength.AsSpan(), 10, 5)); // fast >= slow + Amat.Batch(source.AsSpan(), trend.AsSpan(), strength.AsSpan(), 10, 5)); // fast >= slow } [Fact] @@ -379,7 +379,7 @@ public class AmatTests double[] trend = new double[source.Length]; var tseriesResult = Amat.Batch(_testData, 10, 30); - Amat.Calculate(source.AsSpan(), trend.AsSpan(), 10, 30); + Amat.Batch(source.AsSpan(), trend.AsSpan(), 10, 30); // Since trend values are discrete (-1, 0, 1), check after warmup where // both methods should converge. Early values may differ due to EMA initialization. @@ -404,7 +404,7 @@ public class AmatTests double[] trend = new double[10]; double[] strength = new double[10]; - Amat.Calculate(source.AsSpan(), trend.AsSpan(), strength.AsSpan(), 3, 5); + Amat.Batch(source.AsSpan(), trend.AsSpan(), strength.AsSpan(), 3, 5); foreach (var val in trend) { diff --git a/lib/dynamics/amat/Amat.Validation.Tests.cs b/lib/dynamics/amat/Amat.Validation.Tests.cs index e863dfc3..2f956b92 100644 --- a/lib/dynamics/amat/Amat.Validation.Tests.cs +++ b/lib/dynamics/amat/Amat.Validation.Tests.cs @@ -351,7 +351,7 @@ public sealed class AmatValidationTests : IDisposable double[] sourceData = _testData.RawData.ToArray(); double[] spanTrend = new double[sourceData.Length]; double[] spanStrength = new double[sourceData.Length]; - Amat.Calculate(sourceData, spanTrend, spanStrength, fastPeriod, slowPeriod); + Amat.Batch(sourceData, spanTrend, spanStrength, fastPeriod, slowPeriod); // Compare trend values (after warmup period) int warmup = slowPeriod * 2; // Allow extra warmup for convergence diff --git a/lib/dynamics/amat/Amat.cs b/lib/dynamics/amat/Amat.cs index 7dcf571f..031b31cd 100644 --- a/lib/dynamics/amat/Amat.cs +++ b/lib/dynamics/amat/Amat.cs @@ -391,7 +391,7 @@ public sealed class Amat : ITValuePublisher, IDisposable /// Fast EMA period /// Slow EMA period [MethodImpl(MethodImplOptions.AggressiveOptimization)] - public static void Calculate(ReadOnlySpan source, Span trend, Span strength, + public static void Batch(ReadOnlySpan source, Span trend, Span strength, int fastPeriod = 10, int slowPeriod = 50) { if (source.Length != trend.Length) @@ -501,7 +501,7 @@ public sealed class Amat : ITValuePublisher, IDisposable /// Fast EMA period /// Slow EMA period [MethodImpl(MethodImplOptions.AggressiveOptimization)] - public static void Calculate(ReadOnlySpan source, Span trend, + public static void Batch(ReadOnlySpan source, Span trend, int fastPeriod = 10, int slowPeriod = 50) { if (source.Length != trend.Length) @@ -585,6 +585,19 @@ public sealed class Amat : ITValuePublisher, IDisposable } } + /// + /// Calculates AMAT for the entire series using a new instance. + /// + /// Input series + /// Fast EMA period + /// Slow EMA period + /// AMAT trend series + public static TSeries Batch(TSeries source, int fastPeriod = 10, int slowPeriod = 50) + { + var amat = new Amat(fastPeriod, slowPeriod); + return amat.Update(source); + } + /// /// Runs a high-performance batch calculation on history and returns /// a "Hot" Amat instance ready to process the next tick immediately. @@ -600,16 +613,4 @@ public sealed class Amat : ITValuePublisher, IDisposable return (results, amat); } - /// - /// Calculates AMAT for the entire series using a new instance. - /// - /// Input series - /// Fast EMA period - /// Slow EMA period - /// AMAT trend series - public static TSeries Batch(TSeries source, int fastPeriod = 10, int slowPeriod = 50) - { - var amat = new Amat(fastPeriod, slowPeriod); - return amat.Update(source); - } } diff --git a/lib/dynamics/aroon/Aroon.cs b/lib/dynamics/aroon/Aroon.cs index e34b8a3d..fc21dc7c 100644 --- a/lib/dynamics/aroon/Aroon.cs +++ b/lib/dynamics/aroon/Aroon.cs @@ -165,7 +165,7 @@ public sealed class Aroon : ITValuePublisher int len = source.Count; var v = new double[len]; - Calculate(source.High.Values, source.Low.Values, _period, v); + Batch(source.High.Values, source.Low.Values, _period, v); var tList = new List(len); var vList = new List(v); @@ -193,7 +193,7 @@ public sealed class Aroon : ITValuePublisher /// Lookback period /// Output oscillator values (Up - Down) [MethodImpl(MethodImplOptions.AggressiveOptimization)] - public static void Calculate(ReadOnlySpan high, ReadOnlySpan low, int period, Span destination) + public static void Batch(ReadOnlySpan high, ReadOnlySpan low, int period, Span destination) { int len = high.Length; if (len == 0 || len != low.Length || len != destination.Length || period <= 0) @@ -293,7 +293,7 @@ public sealed class Aroon : ITValuePublisher int len = source.Count; var v = new double[len]; - Calculate(source.High.Values, source.Low.Values, period, v); + Batch(source.High.Values, source.Low.Values, period, v); var tList = new List(len); var times = source.Open.Times; @@ -304,4 +304,11 @@ public sealed class Aroon : ITValuePublisher return new TSeries(tList, [.. v]); } + + public static (TSeries Results, Aroon Indicator) Calculate(TBarSeries source, int period) + { + var indicator = new Aroon(period); + TSeries results = indicator.Update(source); + return (results, indicator); + } } diff --git a/lib/dynamics/aroonosc/AroonOsc.cs b/lib/dynamics/aroonosc/AroonOsc.cs index 931c409c..97c4f081 100644 --- a/lib/dynamics/aroonosc/AroonOsc.cs +++ b/lib/dynamics/aroonosc/AroonOsc.cs @@ -150,7 +150,7 @@ public sealed class AroonOsc : ITValuePublisher int len = source.Count; var v = new double[len]; - Calculate(source.High.Values, source.Low.Values, period: _period, destination: v); + Batch(source.High.Values, source.Low.Values, period: _period, destination: v); var tList = new List(len); var vList = new List(v); @@ -177,10 +177,10 @@ public sealed class AroonOsc : ITValuePublisher /// Lookback period /// Output oscillator values (Up - Down) [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void Calculate(ReadOnlySpan high, ReadOnlySpan low, int period, Span destination) + public static void Batch(ReadOnlySpan high, ReadOnlySpan low, int period, Span destination) { // Delegate to Aroon's O(n) monotonic deque implementation - Aroon.Calculate(high, low, period, destination); + Aroon.Batch(high, low, period, destination); } [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -194,7 +194,7 @@ public sealed class AroonOsc : ITValuePublisher int len = source.Count; var v = new double[len]; - Calculate(source.High.Values, source.Low.Values, period, v); + Batch(source.High.Values, source.Low.Values, period, v); var tList = new List(len); var times = source.Open.Times; @@ -205,4 +205,11 @@ public sealed class AroonOsc : ITValuePublisher return new TSeries(tList, [.. v]); } + + public static (TSeries Results, AroonOsc Indicator) Calculate(TBarSeries source, int period) + { + var indicator = new AroonOsc(period); + TSeries results = indicator.Update(source); + return (results, indicator); + } } diff --git a/lib/dynamics/chop/Chop.cs b/lib/dynamics/chop/Chop.cs index 1ac1ae8f..10cff82f 100644 --- a/lib/dynamics/chop/Chop.cs +++ b/lib/dynamics/chop/Chop.cs @@ -256,4 +256,12 @@ public sealed class Chop : ITValuePublisher var indicator = new Chop(period); return indicator.Update(source); } + + public static (TSeries Results, Chop Indicator) Calculate(TBarSeries source) + { + var indicator = new Chop(); + TSeries results = indicator.Update(source); + return (results, indicator); + } + } diff --git a/lib/dynamics/dmx/Dmx.cs b/lib/dynamics/dmx/Dmx.cs index e504b315..075166d6 100644 --- a/lib/dynamics/dmx/Dmx.cs +++ b/lib/dynamics/dmx/Dmx.cs @@ -166,7 +166,7 @@ public sealed class Dmx : ITValuePublisher var vSpan = CollectionsMarshal.AsSpan(v); // Span-based batch calculation - Calculate(source.High.Values, source.Low.Values, source.Close.Values, _period, vSpan); + Batch(source.High.Values, source.Low.Values, source.Close.Values, _period, vSpan); source.Close.Times.CopyTo(tSpan); // Restore streaming state by replaying only tail bars (JMA needs ~2*period for full warmup) @@ -182,7 +182,7 @@ public sealed class Dmx : ITValuePublisher } [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void Calculate(ReadOnlySpan high, + public static void Batch(ReadOnlySpan high, ReadOnlySpan low, ReadOnlySpan close, int period, @@ -271,9 +271,9 @@ public sealed class Dmx : ITValuePublisher tr[i] = trRaw; } - Jma.Calculate(dmPlus, dmPlusSmooth, period); - Jma.Calculate(dmMinus, dmMinusSmooth, period); - Jma.Calculate(tr, trSmooth, period); + Jma.Batch(dmPlus, dmPlusSmooth, period); + Jma.Batch(dmMinus, dmMinusSmooth, period); + Jma.Batch(tr, trSmooth, period); for (int i = 0; i < len; i++) { @@ -304,4 +304,11 @@ public sealed class Dmx : ITValuePublisher var dmx = new Dmx(period); return dmx.Update(source); } + + public static (TSeries Results, Dmx Indicator) Calculate(TBarSeries source, int period = 14) + { + var indicator = new Dmx(period); + TSeries results = indicator.Update(source); + return (results, indicator); + } } diff --git a/lib/dynamics/dx/Dx.Validation.Tests.cs b/lib/dynamics/dx/Dx.Validation.Tests.cs index 9e730d94..796fd006 100644 --- a/lib/dynamics/dx/Dx.Validation.Tests.cs +++ b/lib/dynamics/dx/Dx.Validation.Tests.cs @@ -26,44 +26,6 @@ public sealed class DxValidationTests : IDisposable _data.Dispose(); } - /// - /// Validates DX against TA-Lib. Our DX uses the standard formula: - /// DX = 100 × |+DI - -DI| / (+DI + -DI) - /// This matches the Wilder/industry standard formula. - /// - /// NOTE: TA-Lib's DX function produces different results than computing DX - /// from their standalone PlusDI/MinusDI functions. Our implementation matches: - /// - TA-Lib's individual +DI and -DI (verified in DiPlus_MatchesTalib, DiMinus_MatchesTalib) - /// - Tulip's DX (verified in MatchesTulip) - /// - Skender's DI values (verified in MatchesSkender_DiValues) - /// - /// The discrepancy appears to be in TA-Lib's DX function itself, possibly due to - /// internal rounding or unstable period handling that differs from the standalone DI functions. - /// - [Fact(Skip = "TA-Lib DX function differs from standard; we match TA-Lib's PlusDI/MinusDI and Tulip")] - public void MatchesTalib() - { - var dx = new Dx(14); - var results = new List(); - - for (int i = 0; i < _data.Bars.Count; i++) - { - var res = dx.Update(_data.Bars[i]); - results.Add(res.Value); - } - - double[] hData = _data.Bars.High.Select(x => x.Value).ToArray(); - double[] lData = _data.Bars.Low.Select(x => x.Value).ToArray(); - double[] cData = _data.Bars.Close.Select(x => x.Value).ToArray(); - double[] outReal = new double[_data.Bars.Count]; - - var retCode = Functions.Dx(hData, lData, cData, 0..^0, outReal, out var outRange, 14); - Assert.Equal(Core.RetCode.Success, retCode); - - int lookback = Functions.DxLookback(14); - ValidationHelper.VerifyData(results, outReal, outRange, lookback); - } - [Fact] public void MatchesTulip() { diff --git a/lib/dynamics/dx/Dx.cs b/lib/dynamics/dx/Dx.cs index 1fb8a3cf..95444fad 100644 --- a/lib/dynamics/dx/Dx.cs +++ b/lib/dynamics/dx/Dx.cs @@ -299,7 +299,7 @@ public sealed class Dx : ITValuePublisher var v = new double[len]; // Use the static Calculate method for performance - Calculate(source.High.Values, source.Low.Values, source.Close.Values, _period, v); + Batch(source.High.Values, source.Low.Values, source.Close.Values, _period, v); // Create lists for TSeries var tList = new List(len); @@ -356,7 +356,7 @@ public sealed class Dx : ITValuePublisher } [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void Calculate(ReadOnlySpan high, ReadOnlySpan low, ReadOnlySpan close, int period, Span destination) + public static void Batch(ReadOnlySpan high, ReadOnlySpan low, ReadOnlySpan close, int period, Span destination) { int len = high.Length; if (len < period + 1) @@ -419,7 +419,7 @@ public sealed class Dx : ITValuePublisher var len = source.Count; var v = new double[len]; - Calculate(source.High.Values, source.Low.Values, source.Close.Values, period, v); + Batch(source.High.Values, source.Low.Values, source.Close.Values, period, v); var tList = new List(len); var times = source.Open.Times; @@ -430,4 +430,11 @@ public sealed class Dx : ITValuePublisher return new TSeries(tList, [.. v]); } + + public static (TSeries Results, Dx Indicator) Calculate(TBarSeries source, int period = 14) + { + var indicator = new Dx(period); + TSeries results = indicator.Update(source); + return (results, indicator); + } } diff --git a/lib/dynamics/ht_trendmode/HtTrendmode.Tests.cs b/lib/dynamics/ht_trendmode/HtTrendmode.Tests.cs index e7f9ec50..48844ec6 100644 --- a/lib/dynamics/ht_trendmode/HtTrendmode.Tests.cs +++ b/lib/dynamics/ht_trendmode/HtTrendmode.Tests.cs @@ -221,7 +221,7 @@ public class HtTrendmodeTests input[i] = 100.0 + Math.Sin(i * 0.15) * 8; } - HtTrendmode.Calculate(input.AsSpan(), output.AsSpan()); + HtTrendmode.Batch(input.AsSpan(), output.AsSpan()); // After warmup, all values should be 0 or 1 for (int i = 40; i < output.Length; i++) @@ -241,7 +241,7 @@ public class HtTrendmodeTests series.Add(DateTime.UtcNow.AddMinutes(i), 100.0 + Math.Sin(i * 0.25) * 12); } - var result = HtTrendmode.Calculate(series); + var result = HtTrendmode.Batch(series); Assert.Equal(100, result.Count); } diff --git a/lib/dynamics/ht_trendmode/HtTrendmode.Validation.Tests.cs b/lib/dynamics/ht_trendmode/HtTrendmode.Validation.Tests.cs index b16e4bbf..fd8557ae 100644 --- a/lib/dynamics/ht_trendmode/HtTrendmode.Validation.Tests.cs +++ b/lib/dynamics/ht_trendmode/HtTrendmode.Validation.Tests.cs @@ -102,7 +102,7 @@ public sealed class HtTrendmodeValidationTests : IDisposable } // Act - Batch - var batchResult = HtTrendmode.Calculate(series); + var batchResult = HtTrendmode.Batch(series); // Assert Assert.Equal(streamingResults.Count, batchResult.Count); diff --git a/lib/dynamics/ht_trendmode/HtTrendmode.cs b/lib/dynamics/ht_trendmode/HtTrendmode.cs index 491adcbf..e4a6ad23 100644 --- a/lib/dynamics/ht_trendmode/HtTrendmode.cs +++ b/lib/dynamics/ht_trendmode/HtTrendmode.cs @@ -587,7 +587,7 @@ public sealed class HtTrendmode : AbstractBase } } - public static void Calculate(ReadOnlySpan source, Span output) + public static void Batch(ReadOnlySpan source, Span output) { if (output.Length < source.Length) { @@ -601,9 +601,16 @@ public sealed class HtTrendmode : AbstractBase } } - public static TSeries Calculate(TSeries source) + public static TSeries Batch(TSeries source) { var ht = new HtTrendmode(); return ht.Update(source); } + + public static (TSeries Results, HtTrendmode Indicator) Calculate(TSeries source) + { + var indicator = new HtTrendmode(); + TSeries results = indicator.Update(source); + return (results, indicator); + } } diff --git a/lib/dynamics/qstick/Qstick.Quantower.Tests.cs b/lib/dynamics/qstick/Qstick.Quantower.Tests.cs new file mode 100644 index 00000000..ce343d84 --- /dev/null +++ b/lib/dynamics/qstick/Qstick.Quantower.Tests.cs @@ -0,0 +1,144 @@ +using TradingPlatform.BusinessLayer; +using Xunit; + +namespace QuanTAlib.Tests; + +public class QstickIndicatorTests +{ + [Fact] + public void Constructor_CreatesValidIndicator() + { + var indicator = new QstickIndicator(); + Assert.NotNull(indicator); + Assert.Equal("Qstick Indicator", indicator.Name); + } + + [Fact] + public void DefaultPeriod_Is14() + { + var indicator = new QstickIndicator(); + Assert.Equal(14, indicator.Period); + } + + [Fact] + public void DefaultMaType_IsSMA() + { + var indicator = new QstickIndicator(); + Assert.Equal("SMA", indicator.MaType); + } + + [Fact] + public void ShortName_IncludesParameters() + { + var indicator = new QstickIndicator { Period = 20, MaType = "EMA" }; + Assert.Equal("QSTICK(20,EMA)", indicator.ShortName); + } + + [Fact] + public void MinHistoryDepths_EqualsZero() + { + var indicator = new QstickIndicator { Period = 10 }; + Assert.Equal(0, QstickIndicator.MinHistoryDepths); + IWatchlistIndicator watchlistIndicator = indicator; + Assert.Equal(0, watchlistIndicator.MinHistoryDepths); + } + + [Fact] + public void CalculationIntegration_ProducesCorrectValues() + { + var qstickCore = new Qstick(3); + var time = DateTime.UtcNow; + + // Simulate bar data + var bar1 = new TBar(time.Ticks, 100.0, 110.0, 95.0, 105.0, 1000); + var bar2 = new TBar(time.AddMinutes(1).Ticks, 100.0, 105.0, 95.0, 103.0, 1000); + var bar3 = new TBar(time.AddMinutes(2).Ticks, 100.0, 108.0, 95.0, 106.0, 1000); + + qstickCore.Update(bar1); + qstickCore.Update(bar2); + var result = qstickCore.Update(bar3); + + // SMA of (5, 3, 6) = 14/3 ≈ 4.667 + Assert.Equal(14.0 / 3.0, result.Value, 10); + } + + [Fact] + public void EmaMode_CalculatesCorrectly() + { + var qstickCore = new Qstick(3, useEma: true); + var time = DateTime.UtcNow; + + // Bar 1: diff = 5 + qstickCore.Update(new TBar(time.Ticks, 100.0, 110.0, 95.0, 105.0, 1000)); + + // Bar 2: diff = -3, EMA with alpha = 0.5 + var result = qstickCore.Update(new TBar(time.AddMinutes(1).Ticks, 100.0, 105.0, 95.0, 97.0, 1000)); + + // EMA = 0.5 * -3 + 0.5 * 5 = 1.0 + Assert.Equal(1.0, result.Value, 10); + } + + [Fact] + public void BullishBars_ProducePositiveQstick() + { + var qstick = new Qstick(5); + var time = DateTime.UtcNow; + + // All bullish bars (close > open) + for (int i = 0; i < 5; i++) + { + qstick.Update(new TBar(time.AddMinutes(i).Ticks, 100.0, 110.0, 95.0, 105.0, 1000)); + } + + Assert.True(qstick.Last.Value > 0); + Assert.Equal(5.0, qstick.Last.Value, 10); + } + + [Fact] + public void BearishBars_ProduceNegativeQstick() + { + var qstick = new Qstick(5); + var time = DateTime.UtcNow; + + // All bearish bars (close < open) + for (int i = 0; i < 5; i++) + { + qstick.Update(new TBar(time.AddMinutes(i).Ticks, 100.0, 105.0, 90.0, 95.0, 1000)); + } + + Assert.True(qstick.Last.Value < 0); + Assert.Equal(-5.0, qstick.Last.Value, 10); + } + + [Fact] + public void DojiBars_ProduceZeroQstick() + { + var qstick = new Qstick(5); + var time = DateTime.UtcNow; + + // All doji bars (close = open) + for (int i = 0; i < 5; i++) + { + qstick.Update(new TBar(time.AddMinutes(i).Ticks, 100.0, 105.0, 95.0, 100.0, 1000)); + } + + Assert.Equal(0.0, qstick.Last.Value, 10); + } + + [Fact] + public void CoreIndicator_ResetsCorrectly() + { + var qstick = new Qstick(3); + var time = DateTime.UtcNow; + + qstick.Update(new TBar(time.Ticks, 100.0, 110.0, 95.0, 105.0, 1000)); + qstick.Update(new TBar(time.AddMinutes(1).Ticks, 100.0, 105.0, 95.0, 103.0, 1000)); + + Assert.NotEqual(default, qstick.Last); + + qstick.Reset(); + + Assert.False(qstick.IsHot); + Assert.Equal(default, qstick.Last); + } +} diff --git a/lib/dynamics/qstick/Qstick.Quantower.cs b/lib/dynamics/qstick/Qstick.Quantower.cs new file mode 100644 index 00000000..eb610beb --- /dev/null +++ b/lib/dynamics/qstick/Qstick.Quantower.cs @@ -0,0 +1,58 @@ +using System.Drawing; +using System.Runtime.CompilerServices; +using TradingPlatform.BusinessLayer; + +namespace QuanTAlib; + +[SkipLocalsInit] +public sealed class QstickIndicator : Indicator, IWatchlistIndicator +{ + [InputParameter("Period", sortIndex: 0, 1, 1000, 1, 0)] + public int Period { get; set; } = 14; + + [InputParameter("MA Type", sortIndex: 1, variants: new object[] { "SMA", "SMA", "EMA", "EMA" })] + public string MaType { get; set; } = "SMA"; + + [InputParameter("Show cold values", sortIndex: 21)] + public bool ShowColdValues { get; set; } = true; + + public override string ShortName => $"QSTICK({Period},{MaType})"; + + public static int MinHistoryDepths => 0; + int IWatchlistIndicator.MinHistoryDepths => MinHistoryDepths; + + private Qstick _indicator = null!; + private readonly LineSeries _series; + + public QstickIndicator() + { + Name = "Qstick Indicator"; + Description = "Measures average candlestick body direction by calculating the moving average of close minus open."; + _series = new LineSeries("Qstick", Color.Yellow, 2, LineStyle.Solid); + AddLineSeries(_series); + SeparateWindow = true; + } + + protected override void OnInit() + { + bool useEma = string.Equals(MaType, "EMA", StringComparison.Ordinal); + _indicator = new Qstick(Period, useEma); + AddLineLevel(0, "Zero", Color.Gray, 1, LineStyle.Dash); + base.OnInit(); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + protected override void OnUpdate(UpdateArgs args) + { + bool isNew = args.IsNewBar(); + var bar = this.GetInputBar(args); + var result = _indicator.Update(bar, isNew); + _series.SetValue(result.Value, _indicator.IsHot, ShowColdValues); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public override void OnPaintChart(PaintChartEventArgs args) + { + base.OnPaintChart(args); + } +} diff --git a/lib/dynamics/qstick/Qstick.Tests.cs b/lib/dynamics/qstick/Qstick.Tests.cs new file mode 100644 index 00000000..8072e4ab --- /dev/null +++ b/lib/dynamics/qstick/Qstick.Tests.cs @@ -0,0 +1,557 @@ +using Xunit; + +namespace QuanTAlib.Tests; + +public class QstickTests +{ + // ═══════════════════════════════════════════════════════════════════════════ + // Constructor Tests + // ═══════════════════════════════════════════════════════════════════════════ + + [Fact] + public void Constructor_DefaultParameters_CreatesValidIndicator() + { + var qstick = new Qstick(); + Assert.Equal(14, qstick.Period); + Assert.False(qstick.UseEma); + Assert.Equal("QSTICK(14)", qstick.Name); + } + + [Fact] + public void Constructor_CustomPeriod_SetsCorrectly() + { + var qstick = new Qstick(20); + Assert.Equal(20, qstick.Period); + } + + [Fact] + public void Constructor_EmaMode_SetsCorrectly() + { + var qstick = new Qstick(14, useEma: true); + Assert.True(qstick.UseEma); + Assert.Equal("QSTICK(14,EMA)", qstick.Name); + } + + [Fact] + public void Constructor_PeriodLessThanOne_ThrowsException() + { + Assert.Throws(() => new Qstick(0)); + } + + [Fact] + public void Constructor_NegativePeriod_ThrowsException() + { + Assert.Throws(() => new Qstick(-1)); + } + + [Fact] + public void Constructor_PeriodOne_IsValid() + { + var qstick = new Qstick(1); + Assert.Equal(1, qstick.Period); + } + + // ═══════════════════════════════════════════════════════════════════════════ + // Basic Calculation Tests - SMA Mode + // ═══════════════════════════════════════════════════════════════════════════ + + [Fact] + public void Update_SingleBullishBar_ReturnsPositiveValue() + { + var qstick = new Qstick(1); + var bar = new TBar(DateTime.UtcNow.Ticks, 100.0, 105.0, 99.0, 105.0, 1000); + var result = qstick.Update(bar); + Assert.Equal(5.0, result.Value); // close - open = 105 - 100 = 5 + } + + [Fact] + public void Update_SingleBearishBar_ReturnsNegativeValue() + { + var qstick = new Qstick(1); + var bar = new TBar(DateTime.UtcNow.Ticks, 105.0, 105.0, 99.0, 100.0, 1000); + var result = qstick.Update(bar); + Assert.Equal(-5.0, result.Value); // close - open = 100 - 105 = -5 + } + + [Fact] + public void Update_DojiBar_ReturnsZero() + { + var qstick = new Qstick(1); + var bar = new TBar(DateTime.UtcNow.Ticks, 100.0, 105.0, 99.0, 100.0, 1000); + var result = qstick.Update(bar); + Assert.Equal(0.0, result.Value); // close - open = 100 - 100 = 0 + } + + [Fact] + public void Update_ThreeBars_CalculatesCorrectSMA() + { + var qstick = new Qstick(3); + var time = DateTime.UtcNow; + + // Bar 1: +5 (bullish) + qstick.Update(new TBar(time.Ticks, 100.0, 110.0, 95.0, 105.0, 1000)); + + // Bar 2: -3 (bearish) + qstick.Update(new TBar(time.AddMinutes(1).Ticks, 100.0, 105.0, 95.0, 97.0, 1000)); + + // Bar 3: +2 (bullish) + var result = qstick.Update(new TBar(time.AddMinutes(2).Ticks, 100.0, 105.0, 95.0, 102.0, 1000)); + + // SMA = (5 + -3 + 2) / 3 = 4/3 ≈ 1.333 + Assert.Equal(4.0 / 3.0, result.Value, 10); + Assert.True(qstick.IsHot); + } + + [Fact] + public void Update_NotWarmUp_ReturnsPartialAverage() + { + var qstick = new Qstick(5); + var time = DateTime.UtcNow; + + // Bar 1: +5 + qstick.Update(new TBar(time.Ticks, 100.0, 110.0, 95.0, 105.0, 1000)); + + // Bar 2: +3 + var result = qstick.Update(new TBar(time.AddMinutes(1).Ticks, 100.0, 105.0, 95.0, 103.0, 1000)); + + // Partial average = (5 + 3) / 2 = 4 + Assert.Equal(4.0, result.Value, 10); + Assert.False(qstick.IsHot); + } + + // ═══════════════════════════════════════════════════════════════════════════ + // Basic Calculation Tests - EMA Mode + // ═══════════════════════════════════════════════════════════════════════════ + + [Fact] + public void Update_EmaMode_FirstBar_ReturnsDiff() + { + var qstick = new Qstick(14, useEma: true); + var bar = new TBar(DateTime.UtcNow.Ticks, 100.0, 105.0, 99.0, 105.0, 1000); + var result = qstick.Update(bar); + Assert.Equal(5.0, result.Value); + } + + [Fact] + public void Update_EmaMode_MultipleBar_CalculatesEma() + { + var qstick = new Qstick(3, useEma: true); // alpha = 2/(3+1) = 0.5 + var time = DateTime.UtcNow; + + // Bar 1: diff = 5 + qstick.Update(new TBar(time.Ticks, 100.0, 110.0, 95.0, 105.0, 1000)); + + // Bar 2: diff = -3, EMA = 0.5 * -3 + 0.5 * 5 = 1.0 + var result = qstick.Update(new TBar(time.AddMinutes(1).Ticks, 100.0, 105.0, 95.0, 97.0, 1000)); + + Assert.Equal(1.0, result.Value, 10); + } + + [Fact] + public void Update_EmaMode_IsHotFromFirstBar() + { + var qstick = new Qstick(14, useEma: true); + var bar = new TBar(DateTime.UtcNow.Ticks, 100.0, 105.0, 99.0, 105.0, 1000); + qstick.Update(bar); + Assert.True(qstick.IsHot); + } + + // ═══════════════════════════════════════════════════════════════════════════ + // Bar Correction Tests + // ═══════════════════════════════════════════════════════════════════════════ + + [Fact] + public void Update_IsNewFalse_CorrectsPreviousBar() + { + var qstick = new Qstick(3); + var time = DateTime.UtcNow; + + // Bar 1 + qstick.Update(new TBar(time.Ticks, 100.0, 110.0, 95.0, 105.0, 1000)); + + // Bar 2 + qstick.Update(new TBar(time.AddMinutes(1).Ticks, 100.0, 105.0, 95.0, 103.0, 1000)); + + // Bar 3 initial + qstick.Update(new TBar(time.AddMinutes(2).Ticks, 100.0, 105.0, 95.0, 102.0, 1000)); + + // Bar 3 correction (close changes from 102 to 108) + var result = qstick.Update(new TBar(time.AddMinutes(2).Ticks, 100.0, 110.0, 95.0, 108.0, 1000), isNew: false); + + // SMA = (5 + 3 + 8) / 3 = 16/3 ≈ 5.333 + Assert.Equal(16.0 / 3.0, result.Value, 10); + } + + [Fact] + public void Update_MultipleCorrections_ProducesSameResult() + { + var qstick = new Qstick(3); + var time = DateTime.UtcNow; + + qstick.Update(new TBar(time.Ticks, 100.0, 110.0, 95.0, 105.0, 1000)); + qstick.Update(new TBar(time.AddMinutes(1).Ticks, 100.0, 105.0, 95.0, 103.0, 1000)); + qstick.Update(new TBar(time.AddMinutes(2).Ticks, 100.0, 105.0, 95.0, 102.0, 1000)); + + // Multiple corrections to same bar should be idempotent + qstick.Update(new TBar(time.AddMinutes(2).Ticks, 100.0, 110.0, 95.0, 107.0, 1000), isNew: false); + qstick.Update(new TBar(time.AddMinutes(2).Ticks, 100.0, 110.0, 95.0, 107.0, 1000), isNew: false); + var result = qstick.Update(new TBar(time.AddMinutes(2).Ticks, 100.0, 110.0, 95.0, 107.0, 1000), isNew: false); + + // SMA = (5 + 3 + 7) / 3 = 15/3 = 5 + Assert.Equal(5.0, result.Value, 10); + } + + [Fact] + public void Update_EmaMode_BarCorrection_Works() + { + var qstick = new Qstick(3, useEma: true); // alpha = 0.5 + var time = DateTime.UtcNow; + + qstick.Update(new TBar(time.Ticks, 100.0, 110.0, 95.0, 105.0, 1000)); // diff=5, ema=5 + qstick.Update(new TBar(time.AddMinutes(1).Ticks, 100.0, 105.0, 95.0, 103.0, 1000)); // diff=3, ema=0.5*3+0.5*5=4 + qstick.Update(new TBar(time.AddMinutes(2).Ticks, 100.0, 105.0, 95.0, 102.0, 1000)); // diff=2, ema=0.5*2+0.5*4=3 + + // Correct bar 3: diff changes from 2 to 8 + var result = qstick.Update(new TBar(time.AddMinutes(2).Ticks, 100.0, 110.0, 95.0, 108.0, 1000), isNew: false); + + // ema = 0.5*8 + 0.5*4 = 6 + Assert.Equal(6.0, result.Value, 10); + } + + // ═══════════════════════════════════════════════════════════════════════════ + // Edge Case Tests + // ═══════════════════════════════════════════════════════════════════════════ + + [Fact] + public void Update_NaNOpen_ReturnsLastValue() + { + var qstick = new Qstick(3); + var time = DateTime.UtcNow; + + var bar1 = new TBar(time.Ticks, 100.0, 105.0, 99.0, 105.0, 1000); + var result1 = qstick.Update(bar1); + + var bar2 = new TBar(time.AddMinutes(1).Ticks, double.NaN, 105.0, 99.0, 105.0, 1000); + var result2 = qstick.Update(bar2); + + Assert.Equal(result1.Value, result2.Value); + } + + [Fact] + public void Update_NaNClose_ReturnsLastValue() + { + var qstick = new Qstick(3); + var time = DateTime.UtcNow; + + var bar1 = new TBar(time.Ticks, 100.0, 105.0, 99.0, 105.0, 1000); + var result1 = qstick.Update(bar1); + + var bar2 = new TBar(time.AddMinutes(1).Ticks, 100.0, 105.0, 99.0, double.NaN, 1000); + var result2 = qstick.Update(bar2); + + Assert.Equal(result1.Value, result2.Value); + } + + [Fact] + public void Update_InfinityInput_ReturnsLastValue() + { + var qstick = new Qstick(3); + var time = DateTime.UtcNow; + + var bar1 = new TBar(time.Ticks, 100.0, 105.0, 99.0, 105.0, 1000); + var result1 = qstick.Update(bar1); + + var bar2 = new TBar(time.AddMinutes(1).Ticks, double.PositiveInfinity, 105.0, 99.0, 105.0, 1000); + var result2 = qstick.Update(bar2); + + Assert.Equal(result1.Value, result2.Value); + } + + [Fact] + public void Update_LargeValues_CalculatesCorrectly() + { + var qstick = new Qstick(1); + var bar = new TBar(DateTime.UtcNow.Ticks, 1e10, 1.1e10, 0.9e10, 1.05e10, 1000); + var result = qstick.Update(bar); + Assert.Equal(0.05e10, result.Value, 1); + } + + [Fact] + public void Update_SmallValues_CalculatesCorrectly() + { + var qstick = new Qstick(1); + var bar = new TBar(DateTime.UtcNow.Ticks, 0.0001, 0.00015, 0.00009, 0.00012, 1000); + var result = qstick.Update(bar); + Assert.Equal(0.00002, result.Value, 10); + } + + // ═══════════════════════════════════════════════════════════════════════════ + // Reset Tests + // ═══════════════════════════════════════════════════════════════════════════ + + [Fact] + public void Reset_ClearsState() + { + var qstick = new Qstick(3); + var time = DateTime.UtcNow; + + qstick.Update(new TBar(time.Ticks, 100.0, 110.0, 95.0, 105.0, 1000)); + qstick.Update(new TBar(time.AddMinutes(1).Ticks, 100.0, 105.0, 95.0, 103.0, 1000)); + qstick.Update(new TBar(time.AddMinutes(2).Ticks, 100.0, 105.0, 95.0, 102.0, 1000)); + + Assert.True(qstick.IsHot); + + qstick.Reset(); + + Assert.False(qstick.IsHot); + Assert.Equal(default, qstick.Last); + } + + [Fact] + public void Reset_EmaMode_ClearsState() + { + var qstick = new Qstick(3, useEma: true); + var time = DateTime.UtcNow; + + qstick.Update(new TBar(time.Ticks, 100.0, 110.0, 95.0, 105.0, 1000)); + qstick.Update(new TBar(time.AddMinutes(1).Ticks, 100.0, 105.0, 95.0, 103.0, 1000)); + + Assert.True(qstick.IsHot); + + qstick.Reset(); + + Assert.False(qstick.IsHot); + } + + [Fact] + public void Reset_CanReuseAfterReset() + { + var qstick = new Qstick(2); + var time = DateTime.UtcNow; + + qstick.Update(new TBar(time.Ticks, 100.0, 110.0, 95.0, 105.0, 1000)); + qstick.Update(new TBar(time.AddMinutes(1).Ticks, 100.0, 105.0, 95.0, 103.0, 1000)); + + qstick.Reset(); + + // New data after reset + qstick.Update(new TBar(time.AddMinutes(2).Ticks, 100.0, 105.0, 95.0, 98.0, 1000)); // diff = -2 + var result = qstick.Update(new TBar(time.AddMinutes(3).Ticks, 100.0, 105.0, 95.0, 106.0, 1000)); // diff = 6 + + // SMA = (-2 + 6) / 2 = 2 + Assert.Equal(2.0, result.Value, 10); + Assert.True(qstick.IsHot); + } + + // ═══════════════════════════════════════════════════════════════════════════ + // Batch Processing Tests + // ═══════════════════════════════════════════════════════════════════════════ + + [Fact] + public void Update_BarSeries_ReturnsCorrectLength() + { + var qstick = new Qstick(3); + var bars = new TBarSeries(); + var time = DateTime.UtcNow; + + bars.Add(new TBar(time.Ticks, 100.0, 110.0, 95.0, 105.0, 1000)); + bars.Add(new TBar(time.AddMinutes(1).Ticks, 100.0, 105.0, 95.0, 103.0, 1000)); + bars.Add(new TBar(time.AddMinutes(2).Ticks, 100.0, 108.0, 95.0, 106.0, 1000)); + + var result = qstick.Update(bars); + + Assert.Equal(3, result.Count); + } + + [Fact] + public void Update_BarSeries_LastValueMatchesLast() + { + var qstick = new Qstick(3); + var bars = new TBarSeries(); + var time = DateTime.UtcNow; + + bars.Add(new TBar(time.Ticks, 100.0, 110.0, 95.0, 105.0, 1000)); + bars.Add(new TBar(time.AddMinutes(1).Ticks, 100.0, 105.0, 95.0, 103.0, 1000)); + bars.Add(new TBar(time.AddMinutes(2).Ticks, 100.0, 108.0, 95.0, 106.0, 1000)); + + var result = qstick.Update(bars); + + Assert.Equal(qstick.Last.Value, result.Values[^1], 10); + } + + [Fact] + public void Update_EmptyBarSeries_ReturnsEmpty() + { + var qstick = new Qstick(3); + var bars = new TBarSeries(); + + var result = qstick.Update(bars); + + Assert.True(result.Count == 0); + } + + [Fact] + public void Batch_ReturnsCorrectResults() + { + var bars = new TBarSeries(); + var time = DateTime.UtcNow; + + bars.Add(new TBar(time.Ticks, 100.0, 110.0, 95.0, 105.0, 1000)); // diff = 5 + bars.Add(new TBar(time.AddMinutes(1).Ticks, 100.0, 105.0, 95.0, 103.0, 1000)); // diff = 3 + bars.Add(new TBar(time.AddMinutes(2).Ticks, 100.0, 108.0, 95.0, 106.0, 1000)); // diff = 6 + + var result = Qstick.Batch(bars, period: 3); + + // Last value = SMA(5, 3, 6) = 14/3 ≈ 4.667 + Assert.Equal(14.0 / 3.0, result.Values[^1], 10); + } + + [Fact] + public void Calculate_ReturnsIndicatorAndResults() + { + var bars = new TBarSeries(); + var time = DateTime.UtcNow; + + bars.Add(new TBar(time.Ticks, 100.0, 110.0, 95.0, 105.0, 1000)); + bars.Add(new TBar(time.AddMinutes(1).Ticks, 100.0, 105.0, 95.0, 103.0, 1000)); + bars.Add(new TBar(time.AddMinutes(2).Ticks, 100.0, 108.0, 95.0, 106.0, 1000)); + + var (results, indicator) = Qstick.Calculate(bars, period: 3); + + Assert.Equal(3, results.Count); + Assert.True(indicator.IsHot); + Assert.Equal(3, indicator.Period); + } + + // ═══════════════════════════════════════════════════════════════════════════ + // Prime Tests + // ═══════════════════════════════════════════════════════════════════════════ + + [Fact] + public void Prime_WarmUpIndicator() + { + var qstick = new Qstick(3); + var bars = new TBarSeries(); + var time = DateTime.UtcNow; + + bars.Add(new TBar(time.Ticks, 100.0, 110.0, 95.0, 105.0, 1000)); + bars.Add(new TBar(time.AddMinutes(1).Ticks, 100.0, 105.0, 95.0, 103.0, 1000)); + bars.Add(new TBar(time.AddMinutes(2).Ticks, 100.0, 108.0, 95.0, 106.0, 1000)); + + qstick.Prime(bars); + + Assert.True(qstick.IsHot); + Assert.NotEqual(default, qstick.Last); + } + + // ═══════════════════════════════════════════════════════════════════════════ + // Event Tests + // ═══════════════════════════════════════════════════════════════════════════ + + [Fact] + public void Update_RaisesPubEvent() + { + var qstick = new Qstick(3); + var eventRaised = false; + TValue receivedValue = default; + + qstick.Pub += (object? sender, in TValueEventArgs args) => + { + eventRaised = true; + receivedValue = args.Value; + }; + + var bar = new TBar(DateTime.UtcNow.Ticks, 100.0, 110.0, 95.0, 105.0, 1000); + var result = qstick.Update(bar); + + Assert.True(eventRaised); + Assert.Equal(result.Value, receivedValue.Value); + } + + // ═══════════════════════════════════════════════════════════════════════════ + // Property Tests + // ═══════════════════════════════════════════════════════════════════════════ + + [Fact] + public void WarmupPeriod_EqualsPeriod() + { + var qstick = new Qstick(20); + Assert.Equal(20, qstick.WarmupPeriod); + } + + [Fact] + public void IsHot_SmaMode_FalseBeforeFullPeriod() + { + var qstick = new Qstick(5); + var time = DateTime.UtcNow; + + for (int i = 0; i < 4; i++) + { + qstick.Update(new TBar(time.AddMinutes(i).Ticks, 100.0, 105.0, 95.0, 102.0, 1000)); + Assert.False(qstick.IsHot); + } + + qstick.Update(new TBar(time.AddMinutes(4).Ticks, 100.0, 105.0, 95.0, 102.0, 1000)); + Assert.True(qstick.IsHot); + } + + // ═══════════════════════════════════════════════════════════════════════════ + // Consistency Tests + // ═══════════════════════════════════════════════════════════════════════════ + + [Fact] + public void Update_BatchVsStreaming_ProducesSameResults() + { + var bars = new TBarSeries(); + var time = DateTime.UtcNow; + + for (int i = 0; i < 20; i++) + { + double open = 100.0 + i * 0.5; + double close = open + (i % 3 - 1); // varies between -1, 0, 1 + bars.Add(new TBar(time.AddMinutes(i).Ticks, open, open + 2, open - 1, close, 1000)); + } + + // Batch processing + var batchQstick = new Qstick(5); + var batchResult = batchQstick.Update(bars); + + // Streaming processing + var streamQstick = new Qstick(5); + var streamResults = new List(); + for (int i = 0; i < bars.Count; i++) + { + var result = streamQstick.Update(bars[i]); + streamResults.Add(result.Value); + } + + // Compare results + for (int i = 0; i < bars.Count; i++) + { + Assert.Equal(batchResult.Values[i], streamResults[i], 10); + } + } + + [Fact] + public void SmaVsEma_DifferentResults() + { + var bars = new TBarSeries(); + var time = DateTime.UtcNow; + + for (int i = 0; i < 10; i++) + { + double open = 100.0; + double close = 100.0 + (i % 2 == 0 ? 5 : -3); + bars.Add(new TBar(time.AddMinutes(i).Ticks, open, 110.0, 95.0, close, 1000)); + } + + var smaQstick = new Qstick(5, useEma: false); + var emaQstick = new Qstick(5, useEma: true); + + var smaResult = smaQstick.Update(bars); + var emaResult = emaQstick.Update(bars); + + // SMA and EMA should produce different results (EMA weights more recent) + Assert.NotEqual(smaResult.Values[^1], emaResult.Values[^1]); + } +} diff --git a/lib/dynamics/qstick/Qstick.Validation.Tests.cs b/lib/dynamics/qstick/Qstick.Validation.Tests.cs new file mode 100644 index 00000000..8d2855c0 --- /dev/null +++ b/lib/dynamics/qstick/Qstick.Validation.Tests.cs @@ -0,0 +1,353 @@ +using Xunit; + +namespace QuanTAlib.Tests; + +/// +/// Validation tests for Qstick indicator. +/// Validates against manual formula calculations since Qstick is not +/// available in TA-Lib, Skender, Tulip, or Ooples. +/// +public sealed class QstickValidationTests : IDisposable +{ + private readonly ValidationTestData _data; + + public QstickValidationTests() + { + _data = new ValidationTestData(); + } + + public void Dispose() + { + _data.Dispose(); + } + + // ═══════════════════════════════════════════════════════════════════════════ + // Mathematical Correctness Tests + // ═══════════════════════════════════════════════════════════════════════════ + + [Fact] + public void ManualCalculation_MatchesFormula() + { + var bars = new TBarSeries(); + var time = DateTime.UtcNow; + + // Create known bars + bars.Add(new TBar(time.Ticks, 100.0, 110.0, 95.0, 105.0, 1000)); // diff = 5 + bars.Add(new TBar(time.AddMinutes(1).Ticks, 100.0, 108.0, 92.0, 97.0, 1000)); // diff = -3 + bars.Add(new TBar(time.AddMinutes(2).Ticks, 100.0, 106.0, 94.0, 104.0, 1000)); // diff = 4 + bars.Add(new TBar(time.AddMinutes(3).Ticks, 100.0, 107.0, 93.0, 98.0, 1000)); // diff = -2 + bars.Add(new TBar(time.AddMinutes(4).Ticks, 100.0, 109.0, 91.0, 106.0, 1000)); // diff = 6 + + var qstick = new Qstick(5); + for (int i = 0; i < bars.Count; i++) + { + qstick.Update(bars[i]); + } + + // Expected: SMA of (5, -3, 4, -2, 6) = 10/5 = 2.0 + Assert.Equal(2.0, qstick.Last.Value, 10); + } + + [Fact] + public void ManualCalculation_Period3() + { + var qstick = new Qstick(3); + var time = DateTime.UtcNow; + + // Bar 1: diff = 8 + qstick.Update(new TBar(time.Ticks, 100.0, 115.0, 95.0, 108.0, 1000)); + + // Bar 2: diff = -4 + qstick.Update(new TBar(time.AddMinutes(1).Ticks, 100.0, 102.0, 90.0, 96.0, 1000)); + + // Bar 3: diff = 6 + var result = qstick.Update(new TBar(time.AddMinutes(2).Ticks, 100.0, 110.0, 95.0, 106.0, 1000)); + + // Expected: SMA(8, -4, 6) = 10/3 ≈ 3.333 + Assert.Equal(10.0 / 3.0, result.Value, 10); + } + + [Fact] + public void ManualCalculation_Period7() + { + var qstick = new Qstick(7); + var time = DateTime.UtcNow; + + double[] diffs = { 5, -3, 4, -2, 6, -1, 3 }; + + for (int i = 0; i < diffs.Length; i++) + { + double open = 100.0; + double close = 100.0 + diffs[i]; + qstick.Update(new TBar(time.AddMinutes(i).Ticks, open, 110.0, 90.0, close, 1000)); + } + + // Expected: SMA of 5, -3, 4, -2, 6, -1, 3 = 12/7 ≈ 1.714 + double expectedSum = 5 - 3 + 4 - 2 + 6 - 1 + 3; // = 12 + Assert.Equal(expectedSum / 7.0, qstick.Last.Value, 10); + } + + [Fact] + public void EmaCalculation_MatchesFormula() + { + var qstick = new Qstick(3, useEma: true); // alpha = 2/(3+1) = 0.5 + var time = DateTime.UtcNow; + + // Bar 1: diff = 10 + qstick.Update(new TBar(time.Ticks, 100.0, 115.0, 95.0, 110.0, 1000)); + Assert.Equal(10.0, qstick.Last.Value, 10); + + // Bar 2: diff = -6, EMA = 0.5 * -6 + 0.5 * 10 = 2.0 + qstick.Update(new TBar(time.AddMinutes(1).Ticks, 100.0, 102.0, 90.0, 94.0, 1000)); + Assert.Equal(2.0, qstick.Last.Value, 10); + + // Bar 3: diff = 4, EMA = 0.5 * 4 + 0.5 * 2 = 3.0 + qstick.Update(new TBar(time.AddMinutes(2).Ticks, 100.0, 108.0, 95.0, 104.0, 1000)); + Assert.Equal(3.0, qstick.Last.Value, 10); + } + + [Fact] + public void EmaCalculation_Period5() + { + var qstick = new Qstick(5, useEma: true); // alpha = 2/(5+1) = 1/3 + var time = DateTime.UtcNow; + double alpha = 2.0 / 6.0; + + // Bar 1: diff = 6 + qstick.Update(new TBar(time.Ticks, 100.0, 110.0, 95.0, 106.0, 1000)); + double expectedEma = 6.0; + Assert.Equal(expectedEma, qstick.Last.Value, 10); + + // Bar 2: diff = 3, EMA = alpha * 3 + (1-alpha) * 6- + qstick.Update(new TBar(time.AddMinutes(1).Ticks, 100.0, 108.0, 96.0, 103.0, 1000)); + expectedEma = alpha * 3 + (1 - alpha) * expectedEma; + Assert.Equal(expectedEma, qstick.Last.Value, 10); + } + + // ═══════════════════════════════════════════════════════════════════════════ + // Edge Case Validation + // ═══════════════════════════════════════════════════════════════════════════ + + [Fact] + public void ZeroCrossing_IdentifiesCorrectly() + { + var qstick = new Qstick(3); + var time = DateTime.UtcNow; + + // Start bullish + qstick.Update(new TBar(time.Ticks, 100.0, 110.0, 95.0, 106.0, 1000)); // +6 + qstick.Update(new TBar(time.AddMinutes(1).Ticks, 100.0, 108.0, 92.0, 104.0, 1000)); // +4 + qstick.Update(new TBar(time.AddMinutes(2).Ticks, 100.0, 106.0, 94.0, 102.0, 1000)); // +2 + + Assert.True(qstick.Last.Value > 0); + + // Shift to bearish + qstick.Update(new TBar(time.AddMinutes(3).Ticks, 100.0, 105.0, 90.0, 92.0, 1000)); // -8 + qstick.Update(new TBar(time.AddMinutes(4).Ticks, 100.0, 104.0, 88.0, 90.0, 1000)); // -10 + qstick.Update(new TBar(time.AddMinutes(5).Ticks, 100.0, 103.0, 86.0, 88.0, 1000)); // -12 + + Assert.True(qstick.Last.Value < 0); + } + + [Fact] + public void LargeGaps_HandledCorrectly() + { + var qstick = new Qstick(3); + var time = DateTime.UtcNow; + + // Gap up scenario - previous close has no effect on body calculation + qstick.Update(new TBar(time.Ticks, 100.0, 105.0, 95.0, 103.0, 1000)); // diff = 3 + qstick.Update(new TBar(time.AddMinutes(1).Ticks, 110.0, 118.0, 108.0, 115.0, 1000)); // diff = 5 (gap up) + qstick.Update(new TBar(time.AddMinutes(2).Ticks, 120.0, 125.0, 118.0, 122.0, 1000)); // diff = 2 (gap up) + + // SMA = (3 + 5 + 2) / 3 = 10/3 ≈ 3.333 + Assert.Equal(10.0 / 3.0, qstick.Last.Value, 10); + } + + [Fact] + public void AlternatingBullishBearish_AveragesToNearZero() + { + var qstick = new Qstick(4); + var time = DateTime.UtcNow; + + // Alternating pattern + qstick.Update(new TBar(time.Ticks, 100.0, 110.0, 95.0, 105.0, 1000)); // +5 + qstick.Update(new TBar(time.AddMinutes(1).Ticks, 100.0, 105.0, 90.0, 95.0, 1000)); // -5 + qstick.Update(new TBar(time.AddMinutes(2).Ticks, 100.0, 110.0, 95.0, 105.0, 1000)); // +5 + qstick.Update(new TBar(time.AddMinutes(3).Ticks, 100.0, 105.0, 90.0, 95.0, 1000)); // -5 + + // SMA = (5 - 5 + 5 - 5) / 4 = 0 + Assert.Equal(0.0, qstick.Last.Value, 10); + } + + [Fact] + public void AllDoji_ReturnsZero() + { + var qstick = new Qstick(5); + var time = DateTime.UtcNow; + + for (int i = 0; i < 5; i++) + { + qstick.Update(new TBar(time.AddMinutes(i).Ticks, 100.0, 105.0, 95.0, 100.0, 1000)); // diff = 0 + } + + Assert.Equal(0.0, qstick.Last.Value, 10); + } + + // ═══════════════════════════════════════════════════════════════════════════ + // Stability Tests + // ═══════════════════════════════════════════════════════════════════════════ + + [Fact] + public void LongSeries_MaintainsStability() + { + var qstick = new Qstick(14); + var results = new List(); + + for (int i = 0; i < _data.Bars.Count; i++) + { + var result = qstick.Update(_data.Bars[i]); + results.Add(result.Value); + } + + // Verify no NaN or Infinity after warmup + for (int i = 14; i < results.Count; i++) + { + Assert.True(double.IsFinite(results[i]), $"Result at index {i} is not finite: {results[i]}"); + } + } + + [Fact] + public void BatchVsStreaming_MatchesExactly() + { + // Batch processing + var batchResults = Qstick.Batch(_data.Bars, period: 14); + + // Streaming processing + var streamQstick = new Qstick(14); + var streamResults = new List(); + for (int i = 0; i < _data.Bars.Count; i++) + { + var result = streamQstick.Update(_data.Bars[i]); + streamResults.Add(result.Value); + } + + // Compare + Assert.Equal(batchResults.Count, streamResults.Count); + for (int i = 0; i < batchResults.Count; i++) + { + Assert.Equal(batchResults.Values[i], streamResults[i], 12); + } + } + + [Fact] + public void SmaVsEma_ConvergesOverLongPeriod() + { + // With constant input, SMA and EMA should converge + var smaQstick = new Qstick(10, useEma: false); + var emaQstick = new Qstick(10, useEma: true); + var time = DateTime.UtcNow; + + // Feed constant bars (close - open = 5) + for (int i = 0; i < 100; i++) + { + var bar = new TBar(time.AddMinutes(i).Ticks, 100.0, 110.0, 95.0, 105.0, 1000); + smaQstick.Update(bar); + emaQstick.Update(bar); + } + + // Both should converge to 5.0 with constant input + Assert.Equal(5.0, smaQstick.Last.Value, 10); + Assert.Equal(5.0, emaQstick.Last.Value, 4); // EMA converges slower + } + + // ═══════════════════════════════════════════════════════════════════════════ + // Period Boundary Tests + // ═══════════════════════════════════════════════════════════════════════════ + + [Fact] + public void Period1_ReturnsDiffDirectly() + { + var qstick = new Qstick(1); + var time = DateTime.UtcNow; + + var bar = new TBar(time.Ticks, 100.0, 110.0, 95.0, 107.0, 1000); + var result = qstick.Update(bar); + + Assert.Equal(7.0, result.Value, 10); // close - open = 107 - 100 = 7 + } + + [Fact] + public void LargePeriod_CalculatesCorrectly() + { + var qstick = new Qstick(50); + var results = new List(); + + for (int i = 0; i < _data.Bars.Count; i++) + { + var result = qstick.Update(_data.Bars[i]); + results.Add(result.Value); + } + + // Verify indicator is hot after warmup + Assert.True(qstick.IsHot); + + // Verify values are finite after warmup + for (int i = 50; i < results.Count; i++) + { + Assert.True(double.IsFinite(results[i]), $"Result at index {i} is not finite"); + } + } + + // ═══════════════════════════════════════════════════════════════════════════ + // Rolling Window Tests + // ═══════════════════════════════════════════════════════════════════════════ + + [Fact] + public void RollingWindow_DropsOldestValue() + { + var qstick = new Qstick(3); + var time = DateTime.UtcNow; + + // Fill window: +10, +10, +10 + qstick.Update(new TBar(time.Ticks, 100.0, 115.0, 95.0, 110.0, 1000)); + qstick.Update(new TBar(time.AddMinutes(1).Ticks, 100.0, 115.0, 95.0, 110.0, 1000)); + qstick.Update(new TBar(time.AddMinutes(2).Ticks, 100.0, 115.0, 95.0, 110.0, 1000)); + + Assert.Equal(10.0, qstick.Last.Value, 10); + + // Add -20 (replaces oldest +10) + qstick.Update(new TBar(time.AddMinutes(3).Ticks, 100.0, 105.0, 75.0, 80.0, 1000)); + + // Window is now: +10, +10, -20 → SMA = 0/3 = 0 + Assert.Equal(0.0, qstick.Last.Value, 10); + } + + [Fact] + public void RollingWindow_MaintainsCorrectSum() + { + var qstick = new Qstick(5); + var time = DateTime.UtcNow; + + // Create predictable pattern + double[] diffs = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }; + + for (int i = 0; i < diffs.Length; i++) + { + double open = 100.0; + double close = 100.0 + diffs[i]; + qstick.Update(new TBar(time.AddMinutes(i).Ticks, open, 110.0, 90.0, close, 1000)); + + if (i >= 4) // After warmup + { + // Expected: SMA of last 5 values + double expectedSum = 0; + for (int j = i - 4; j <= i; j++) + { + expectedSum += diffs[j]; + } + Assert.Equal(expectedSum / 5.0, qstick.Last.Value, 10); + } + } + } +} diff --git a/lib/dynamics/qstick/Qstick.cs b/lib/dynamics/qstick/Qstick.cs new file mode 100644 index 00000000..fcf291a5 --- /dev/null +++ b/lib/dynamics/qstick/Qstick.cs @@ -0,0 +1,257 @@ +// QSTICK: Qstick Indicator by Tushar Chande +// Measures average candlestick body: MA(Close - Open) +// Positive = bullish (closes above opens), Negative = bearish + +using System.Runtime.CompilerServices; + +namespace QuanTAlib; + +/// +/// Qstick (QSTICK) - Candlestick Momentum Indicator +/// A moving average of the difference between Close and Open prices, +/// measuring the average direction and strength of candlestick bodies. +/// +/// Calculation: Qstick = MA(Close - Open, period) +/// +/// +/// Calculation: +/// +/// diff = Close - Open +/// Qstick = SMA(diff, period) or EMA(diff, period) +/// +/// +/// Key characteristics: +/// - O(1) update complexity per bar +/// - Supports SMA or EMA averaging modes +/// - Positive = average bullish bars +/// - Negative = average bearish bars +/// - Uses RingBuffer for SMA (handles isNew internally) +/// - Uses state rollback for EMA bar correction support +/// +/// Detailed documentation +[SkipLocalsInit] +public sealed class Qstick : ITValuePublisher +{ + private const int DefaultPeriod = 14; + private const bool DefaultUseEma = false; + + private readonly int _period; + private readonly bool _useEma; + private readonly double _alpha; + private readonly RingBuffer _buffer; + + // State for bar correction (EMA mode only) + private double _emaValue; + private double _savedEmaValue; + private int _count; + private int _savedCount; + + /// + /// Display name for the indicator. + /// + public string Name { get; } + + public event TValuePublishedHandler? Pub; + + /// + /// Current Qstick value. + /// + public TValue Last { get; private set; } + + /// + /// True when the indicator has calculated a valid value. + /// For SMA: after receiving 'period' bars + /// For EMA: after receiving at least 1 bar (with bias compensation approximation) + /// + public bool IsHot => _useEma ? _count > 0 : _buffer.IsFull; + + /// + /// The lookback period parameter. + /// + public int Period => _period; + + /// + /// Whether the indicator uses EMA (true) or SMA (false). + /// + public bool UseEma => _useEma; + + /// + /// The number of bars required for the indicator to warm up. + /// + public int WarmupPeriod { get; } + + /// + /// Creates a Qstick indicator with specified period. + /// + /// Lookback period (must be >= 1) + /// Use EMA (true) or SMA (false) + public Qstick(int period = DefaultPeriod, bool useEma = DefaultUseEma) + { + if (period < 1) + { + throw new ArgumentException("Period must be at least 1", nameof(period)); + } + + _period = period; + _useEma = useEma; + _alpha = 2.0 / (period + 1); + Name = useEma ? $"QSTICK({period},EMA)" : $"QSTICK({period})"; + WarmupPeriod = period; + + if (!useEma) + { + _buffer = new RingBuffer(period); + } + else + { + _buffer = null!; + } + + _emaValue = 0; + _savedEmaValue = 0; + _count = 0; + _savedCount = 0; + } + + /// + /// Resets the indicator state. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Reset() + { + _buffer?.Clear(); + _emaValue = 0; + _savedEmaValue = 0; + _count = 0; + _savedCount = 0; + Last = default; + } + + /// + /// Updates the Qstick indicator with a new bar. + /// + /// The price bar (Open and Close required) + /// True for new bar, false for update of current bar + /// The current Qstick value + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public TValue Update(TBar input, bool isNew = true) + { + double open = input.Open; + double close = input.Close; + + // Handle NaN/Infinity inputs + if (!double.IsFinite(open) || !double.IsFinite(close)) + { + Pub?.Invoke(this, new TValueEventArgs { Value = Last, IsNew = isNew }); + return Last; + } + + double diff = close - open; + + double result; + if (_useEma) + { + if (isNew) + { + _savedEmaValue = _emaValue; + _savedCount = _count; + } + else + { + _emaValue = _savedEmaValue; + _count = _savedCount; + } + + // EMA calculation + if (_count == 0) + { + _emaValue = diff; + } + else + { + _emaValue = Math.FusedMultiplyAdd(_alpha, diff - _emaValue, _emaValue); + } + + if (isNew) + { + _count++; + } + + result = _emaValue; + } + else + { + // SMA calculation using RingBuffer + // RingBuffer.Add handles isNew internally: + // - isNew=true: adds new value, removes oldest if full + // - isNew=false: replaces newest value + // RingBuffer.Sum is always accurate after Add + _buffer.Add(diff, isNew); + + int count = _buffer.Count; + result = count > 0 ? _buffer.Sum / count : double.NaN; + } + + Last = new TValue(input.Time, result); + Pub?.Invoke(this, new TValueEventArgs { Value = Last, IsNew = isNew }); + return Last; + } + + /// + /// Updates with a bar series. + /// + public TSeries Update(TBarSeries source) + { + if (source.Count == 0) + { + return new TSeries([], []); + } + + int len = source.Count; + var tList = new List(len); + var vList = new List(len); + + var times = source.Open.Times; + for (int i = 0; i < len; i++) + { + var result = Update(source[i], isNew: true); + tList.Add(times[i]); + vList.Add(result.Value); + } + + return new TSeries(tList, vList); + } + + /// + /// Primes the indicator with historical bar data. + /// + public void Prime(TBarSeries source) + { + for (int i = 0; i < source.Count; i++) + { + Update(source[i], isNew: true); + } + } + + /// + /// Creates and returns results for a bar series. + /// + public static TSeries Batch(TBarSeries source, int period = DefaultPeriod, bool useEma = DefaultUseEma) + { + var indicator = new Qstick(period, useEma); + return indicator.Update(source); + } + + /// + /// Returns the indicator and its results. + /// + public static (TSeries Results, Qstick Indicator) Calculate( + TBarSeries source, + int period = DefaultPeriod, + bool useEma = DefaultUseEma) + { + var indicator = new Qstick(period, useEma); + var results = indicator.Update(source); + return (results, indicator); + } +} diff --git a/lib/dynamics/qstick/Qstick.md b/lib/dynamics/qstick/Qstick.md new file mode 100644 index 00000000..147a7052 --- /dev/null +++ b/lib/dynamics/qstick/Qstick.md @@ -0,0 +1,143 @@ +# QSTICK: Qstick Indicator + +> "The average candlestick body reveals the market's true conviction." + +Developed by Tushar Chande, the Qstick indicator measures the average difference between closing and opening prices over a lookback period. It quantifies whether bars are predominantly bullish (closing above opens) or bearish (closing below opens), providing a smoothed view of candlestick body direction and magnitude. + +## Historical Context + +Tushar Chande introduced the Qstick as part of his work on candlestick pattern quantification in the early 1990s. While traditional candlestick analysis relies on visual pattern recognition, Qstick provides a numerical measure that can be systematically tracked and used for algorithmic trading. + +The indicator addresses a fundamental question: "On average, are prices closing higher or lower than they open?" This simple metric captures intrabar momentum that other indicators measuring close-to-close changes may miss. + +## Architecture + +### 1. Body Difference Calculation + +The core input is the difference between close and open: + +``` +diff = Close - Open +``` + +- **Positive diff**: Bullish bar (white/green candle) +- **Negative diff**: Bearish bar (black/red candle) +- **Zero diff**: Doji (open equals close) + +### 2. Moving Average Smoothing + +The raw differences are smoothed using either SMA or EMA: + +**SMA Mode:** +$$\text{Qstick} = \frac{1}{n} \sum_{i=0}^{n-1} (Close_i - Open_i)$$ + +**EMA Mode:** +$$\text{Qstick}_t = \alpha \cdot diff_t + (1 - \alpha) \cdot \text{Qstick}_{t-1}$$ + +where $\alpha = \frac{2}{period + 1}$ + +### 3. State Management + +For real-time bar correction (isNew=false), the indicator maintains: +- `_sum` / `_savedSum`: Running sum for SMA +- `_emaValue` / `_savedEmaValue`: Current EMA value +- `_count` / `_savedCount`: Bar count for warmup + +## Parameters + +| Parameter | Type | Default | Valid Range | Description | +|-----------|------|---------|-------------|-------------| +| `period` | int | 14 | ≥ 1 | Lookback period for moving average | +| `useEma` | bool | false | true/false | Use EMA (true) or SMA (false) | + +## Mathematical Foundation + +### Formula + +``` +Qstick = MA(Close - Open, period) +``` + +### Interpretation + +| Qstick Value | Market Condition | +|--------------|------------------| +| > 0 | Bullish momentum (closes above opens) | +| < 0 | Bearish momentum (closes below opens) | +| = 0 | Neutral (balanced open/close) | +| Rising | Increasing bullish pressure | +| Falling | Increasing bearish pressure | + +### Signal Generation + +- **Buy Signal**: Qstick crosses above zero +- **Sell Signal**: Qstick crosses below zero +- **Divergence**: Price making new highs while Qstick making lower highs suggests weakening momentum + +## Performance Profile + +### Operation Count (Streaming Mode) + +| Operation | SMA Mode | EMA Mode | +|-----------|----------|----------| +| ADD/SUB | 3 | 2 | +| MUL | 0 | 1 | +| DIV | 1 | 0 | +| FMA | 0 | 1 | +| Memory | O(period) | O(1) | + +### Complexity + +- **Time**: O(1) per bar for both modes +- **Space**: O(period) for SMA, O(1) for EMA + +### Quality Metrics + +| Metric | Score | Notes | +|--------|-------|-------| +| Accuracy | 10/10 | Exact calculation | +| Timeliness | 8/10 | Lag proportional to period | +| Overshoot | 2/10 | Smooth, no overshoot | +| Smoothness | 8/10 | SMA smoother than EMA | + +## Validation + +| Library | Status | Notes | +|---------|--------|-------| +| TA-Lib | ✓ | Not available (implement locally) | +| Skender | ✓ | Validated against Qstick | +| OoplesFinance | ✓ | Validated | + +## Common Pitfalls + +1. **Ignoring Volume**: Qstick weights all bars equally; consider volume-weighted variants for more accuracy +2. **Range Dependence**: Absolute values depend on price scale; normalize for comparison across instruments +3. **Period Selection**: Short periods (5-8) for trading signals; long periods (20+) for trend identification +4. **Gap Sensitivity**: Large gaps (open ≠ previous close) can distort readings +5. **Flat Markets**: Near-zero readings indicate indecision, not necessarily reversal + +## Usage Example + +```csharp +// Create Qstick with 14-period SMA +var qstick = new Qstick(14); + +// Update with bar data +foreach (var bar in bars) +{ + var result = qstick.Update(bar); + if (qstick.IsHot) + { + Console.WriteLine($"Qstick: {result.Value:F4}"); + } +} + +// Or use EMA mode +var qstickEma = new Qstick(14, useEma: true); +``` + +## References + +1. Chande, T. S. (1994). *The New Technical Trader*. John Wiley & Sons. +2. Chande, T. S., & Kroll, S. (1994). *Beyond Technical Analysis*. John Wiley & Sons. +3. Kirkpatrick, C. D., & Dahlquist, J. R. (2015). *Technical Analysis: The Complete Resource for Financial Market Technicians*. FT Press. diff --git a/lib/dynamics/super/Super.cs b/lib/dynamics/super/Super.cs index 866fb095..99cf7373 100644 --- a/lib/dynamics/super/Super.cs +++ b/lib/dynamics/super/Super.cs @@ -270,4 +270,11 @@ public sealed class Super : ITValuePublisher var indicator = new Super(period, multiplier); return indicator.Update(source); } + + public static (TSeries Results, Super Indicator) Calculate(TBarSeries source, int period = 10, double multiplier = 3.0) + { + var indicator = new Super(period, multiplier); + TSeries results = indicator.Update(source); + return (results, indicator); + } } diff --git a/lib/dynamics/ttm_trend/TtmTrend.Quantower.Tests.cs b/lib/dynamics/ttm_trend/TtmTrend.Quantower.Tests.cs new file mode 100644 index 00000000..a680f9cb --- /dev/null +++ b/lib/dynamics/ttm_trend/TtmTrend.Quantower.Tests.cs @@ -0,0 +1,111 @@ +using TradingPlatform.BusinessLayer; +using Xunit; + +namespace QuanTAlib.Tests; + +public class TtmTrendIndicatorTests +{ + [Fact] + public void Constructor_CreatesValidIndicator() + { + var indicator = new TtmTrendIndicator(); + Assert.NotNull(indicator); + Assert.Equal("TTM Trend", indicator.Name); + } + + [Fact] + public void DefaultPeriod_Is6() + { + var indicator = new TtmTrendIndicator(); + Assert.Equal(6, indicator.Period); + } + + [Fact] + public void ShortName_IncludesParameters() + { + var indicator = new TtmTrendIndicator { Period = 10 }; + Assert.Equal("TTM_TREND(10)", indicator.ShortName); + } + + [Fact] + public void MinHistoryDepths_EqualsZero() + { + var indicator = new TtmTrendIndicator { Period = 10 }; + Assert.Equal(0, TtmTrendIndicator.MinHistoryDepths); + IWatchlistIndicator watchlistIndicator = indicator; + Assert.Equal(0, watchlistIndicator.MinHistoryDepths); + } + + [Fact] + public void SeparateWindow_IsFalse() + { + var indicator = new TtmTrendIndicator(); + Assert.False(indicator.SeparateWindow); + } + + [Fact] + public void OnBackGround_IsTrue() + { + var indicator = new TtmTrendIndicator(); + Assert.True(indicator.OnBackGround); + } + + [Fact] + public void CalculationIntegration_ProducesCorrectValues() + { + var ttmCore = new TtmTrend(6); + var time = DateTime.UtcNow; + + var bar1 = new TBar(time.Ticks, 100.0, 105.0, 98.0, 102.0, 1000); + var bar2 = new TBar(time.AddMinutes(1).Ticks, 102.0, 108.0, 100.0, 106.0, 1000); + + ttmCore.Update(bar1); + var result = ttmCore.Update(bar2); + + // After 2 bars, should be hot and have valid value + Assert.True(ttmCore.IsHot); + Assert.True(double.IsFinite(result.Value)); + } + + [Fact] + public void TrendDirection_Bullish_WhenRising() + { + var ttmCore = new TtmTrend(6); + var time = DateTime.UtcNow; + + ttmCore.Update(new TBar(time.Ticks, 100.0, 105.0, 98.0, 102.0, 1000)); + ttmCore.Update(new TBar(time.AddMinutes(1).Ticks, 110.0, 115.0, 108.0, 112.0, 1000)); + + Assert.Equal(1, ttmCore.Trend); + } + + [Fact] + public void TrendDirection_Bearish_WhenFalling() + { + var ttmCore = new TtmTrend(6); + var time = DateTime.UtcNow; + + ttmCore.Update(new TBar(time.Ticks, 100.0, 105.0, 98.0, 102.0, 1000)); + ttmCore.Update(new TBar(time.AddMinutes(1).Ticks, 90.0, 95.0, 88.0, 92.0, 1000)); + + Assert.Equal(-1, ttmCore.Trend); + } + + [Fact] + public void CoreIndicator_ResetsCorrectly() + { + var ttm = new TtmTrend(6); + var time = DateTime.UtcNow; + + ttm.Update(new TBar(time.Ticks, 100.0, 105.0, 98.0, 102.0, 1000)); + ttm.Update(new TBar(time.AddMinutes(1).Ticks, 102.0, 108.0, 100.0, 106.0, 1000)); + + Assert.True(ttm.IsHot); + + ttm.Reset(); + + Assert.False(ttm.IsHot); + Assert.Equal(default, ttm.Last); + Assert.Equal(0, ttm.Trend); + } +} diff --git a/lib/dynamics/ttm_trend/TtmTrend.Quantower.cs b/lib/dynamics/ttm_trend/TtmTrend.Quantower.cs new file mode 100644 index 00000000..7ca130e3 --- /dev/null +++ b/lib/dynamics/ttm_trend/TtmTrend.Quantower.cs @@ -0,0 +1,66 @@ +using System.Drawing; +using System.Runtime.CompilerServices; +using TradingPlatform.BusinessLayer; + +namespace QuanTAlib; + +[SkipLocalsInit] +public sealed class TtmTrendIndicator : Indicator, IWatchlistIndicator +{ + [InputParameter("Period", sortIndex: 0, 1, 100, 1, 0)] + public int Period { get; set; } = 6; + + [InputParameter("Show cold values", sortIndex: 21)] + public bool ShowColdValues { get; set; } = true; + + public override string ShortName => $"TTM_TREND({Period})"; + + public static int MinHistoryDepths => 0; + int IWatchlistIndicator.MinHistoryDepths => MinHistoryDepths; + + private TtmTrend _indicator = null!; + private readonly LineSeries _series; + + public TtmTrendIndicator() + { + Name = "TTM Trend"; + Description = "John Carter's TTM Trend - EMA-based trend indicator with color-coded direction."; + _series = new LineSeries("TTM Trend", Color.Gray, 3, LineStyle.Solid); + AddLineSeries(_series); + SeparateWindow = false; + OnBackGround = true; + } + + protected override void OnInit() + { + _indicator = new TtmTrend(Period); + base.OnInit(); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + protected override void OnUpdate(UpdateArgs args) + { + bool isNew = args.IsNewBar(); + var bar = this.GetInputBar(args); + var result = _indicator.Update(bar, isNew); + _series.SetValue(result.Value, _indicator.IsHot, ShowColdValues); + + // Color based on trend direction + if (_indicator.IsHot) + { + Color trendColor = _indicator.Trend switch + { + 1 => Color.Green, + -1 => Color.Red, + _ => Color.Gray + }; + _series.SetMarker(0, trendColor); + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public override void OnPaintChart(PaintChartEventArgs args) + { + base.OnPaintChart(args); + } +} diff --git a/lib/dynamics/ttm_trend/TtmTrend.Tests.cs b/lib/dynamics/ttm_trend/TtmTrend.Tests.cs new file mode 100644 index 00000000..f48490e0 --- /dev/null +++ b/lib/dynamics/ttm_trend/TtmTrend.Tests.cs @@ -0,0 +1,469 @@ +// TTM_TREND Tests - John Carter's TTM Trend Indicator + +using Xunit; + +namespace QuanTAlib.Tests; + +// ═══════════════════════════════════════════════════════════════════════════ +// Constructor Tests +// ═══════════════════════════════════════════════════════════════════════════ + +public class TtmTrendConstructorTests +{ + [Fact] + public void Constructor_DefaultPeriod_Is6() + { + var ttm = new TtmTrend(); + Assert.Equal(6, ttm.Period); + } + + [Fact] + public void Constructor_CustomPeriod_IsSet() + { + var ttm = new TtmTrend(period: 10); + Assert.Equal(10, ttm.Period); + } + + [Theory] + [InlineData(0)] + [InlineData(-1)] + [InlineData(-10)] + public void Constructor_InvalidPeriod_Throws(int period) + { + Assert.Throws(() => new TtmTrend(period)); + } + + [Fact] + public void Constructor_MinPeriod_IsValid() + { + var ttm = new TtmTrend(period: 1); + Assert.Equal(1, ttm.Period); + } + + [Fact] + public void Name_ContainsPeriod() + { + var ttm = new TtmTrend(period: 10); + Assert.Contains("10", ttm.Name, StringComparison.Ordinal); + Assert.Contains("TTM_TREND", ttm.Name, StringComparison.Ordinal); + } + + [Fact] + public void WarmupPeriod_Is2() + { + Assert.Equal(2, TtmTrend.WarmupPeriod); + } +} + +// ═══════════════════════════════════════════════════════════════════════════ +// Basic Operation Tests +// ═══════════════════════════════════════════════════════════════════════════ + +public class TtmTrendBasicTests +{ + [Fact] + public void Update_FirstBar_ReturnsValue() + { + var ttm = new TtmTrend(); + var result = ttm.Update(new TValue(DateTime.UtcNow.Ticks, 100.0)); + Assert.Equal(100.0, result.Value); + } + + [Fact] + public void Update_SecondBar_CalculatesEma() + { + var ttm = new TtmTrend(period: 6); // alpha = 2/7 ≈ 0.2857 + var time = DateTime.UtcNow; + + ttm.Update(new TValue(time.Ticks, 100.0)); + var result = ttm.Update(new TValue(time.AddMinutes(1).Ticks, 107.0)); + + // EMA = alpha * value + (1 - alpha) * prevEMA + // EMA = 0.2857 * 107 + 0.7143 * 100 = 30.57 + 71.43 = 102.0 + double alpha = 2.0 / 7.0; + double expected = alpha * 107.0 + (1 - alpha) * 100.0; + Assert.Equal(expected, result.Value, 10); + } + + [Fact] + public void IsHot_AfterFirstBar_IsFalse() + { + var ttm = new TtmTrend(); + ttm.Update(new TValue(DateTime.UtcNow.Ticks, 100.0)); + Assert.False(ttm.IsHot); + } + + [Fact] + public void IsHot_AfterSecondBar_IsTrue() + { + var ttm = new TtmTrend(); + var time = DateTime.UtcNow; + ttm.Update(new TValue(time.Ticks, 100.0)); + ttm.Update(new TValue(time.AddMinutes(1).Ticks, 101.0)); + Assert.True(ttm.IsHot); + } +} + +// ═══════════════════════════════════════════════════════════════════════════ +// Trend Direction Tests +// ═══════════════════════════════════════════════════════════════════════════ + +public class TtmTrendDirectionTests +{ + [Fact] + public void Trend_RisingValues_IsBullish() + { + var ttm = new TtmTrend(period: 6); + var time = DateTime.UtcNow; + + ttm.Update(new TValue(time.Ticks, 100.0)); + ttm.Update(new TValue(time.AddMinutes(1).Ticks, 110.0)); + + Assert.Equal(1, ttm.Trend); + } + + [Fact] + public void Trend_FallingValues_IsBearish() + { + var ttm = new TtmTrend(period: 6); + var time = DateTime.UtcNow; + + ttm.Update(new TValue(time.Ticks, 100.0)); + ttm.Update(new TValue(time.AddMinutes(1).Ticks, 90.0)); + + Assert.Equal(-1, ttm.Trend); + } + + [Fact] + public void Trend_SameValue_IsNeutral() + { + var ttm = new TtmTrend(period: 6); + var time = DateTime.UtcNow; + + ttm.Update(new TValue(time.Ticks, 100.0)); + ttm.Update(new TValue(time.AddMinutes(1).Ticks, 100.0)); + + Assert.Equal(0, ttm.Trend); + } + + [Fact] + public void Trend_CanChangeDirection() + { + var ttm = new TtmTrend(period: 2); // Fast EMA + var time = DateTime.UtcNow; + + ttm.Update(new TValue(time.Ticks, 100.0)); + ttm.Update(new TValue(time.AddMinutes(1).Ticks, 110.0)); + Assert.Equal(1, ttm.Trend); + + // Drop significantly to reverse trend + ttm.Update(new TValue(time.AddMinutes(2).Ticks, 90.0)); + Assert.Equal(-1, ttm.Trend); + } +} + +// ═══════════════════════════════════════════════════════════════════════════ +// Strength Tests +// ═══════════════════════════════════════════════════════════════════════════ + +public class TtmTrendStrengthTests +{ + [Fact] + public void Strength_IsPositive() + { + var ttm = new TtmTrend(period: 6); + var time = DateTime.UtcNow; + + ttm.Update(new TValue(time.Ticks, 100.0)); + ttm.Update(new TValue(time.AddMinutes(1).Ticks, 110.0)); + + Assert.True(ttm.Strength > 0); + } + + [Fact] + public void Strength_ZeroOnFirstBar() + { + var ttm = new TtmTrend(period: 6); + var time = DateTime.UtcNow; + + ttm.Update(new TValue(time.Ticks, 100.0)); + + Assert.Equal(0, ttm.Strength); + } + + [Fact] + public void Strength_LargerMoves_HigherStrength() + { + var ttm1 = new TtmTrend(period: 6); + var ttm2 = new TtmTrend(period: 6); + var time = DateTime.UtcNow; + + // Small move + ttm1.Update(new TValue(time.Ticks, 100.0)); + ttm1.Update(new TValue(time.AddMinutes(1).Ticks, 101.0)); + + // Large move + ttm2.Update(new TValue(time.Ticks, 100.0)); + ttm2.Update(new TValue(time.AddMinutes(1).Ticks, 110.0)); + + Assert.True(ttm2.Strength > ttm1.Strength); + } +} + +// ═══════════════════════════════════════════════════════════════════════════ +// Bar Input Tests +// ═══════════════════════════════════════════════════════════════════════════ + +public class TtmTrendBarInputTests +{ + [Fact] + public void Update_Bar_UsesTypicalPrice() + { + var ttm = new TtmTrend(period: 6); + var bar = new TBar(DateTime.UtcNow.Ticks, 100.0, 105.0, 98.0, 102.0, 1000); + + var result = ttm.Update(bar); + + // Typical price = (H + L + C) / 3 = (105 + 98 + 102) / 3 = 101.67 + double typical = (105.0 + 98.0 + 102.0) / 3.0; + Assert.Equal(typical, result.Value, 10); + } + + [Fact] + public void Update_BarSeries_ReturnsCorrectLength() + { + var ttm = new TtmTrend(period: 6); + var bars = new TBarSeries(); + var time = DateTime.UtcNow; + + for (int i = 0; i < 10; i++) + { + bars.Add(new TBar(time.AddMinutes(i).Ticks, 100.0, 105.0, 95.0, 102.0, 1000)); + } + + var result = ttm.Update(bars); + Assert.Equal(10, result.Count); + } +} + +// ═══════════════════════════════════════════════════════════════════════════ +// Edge Case Tests +// ═══════════════════════════════════════════════════════════════════════════ + +public class TtmTrendEdgeCaseTests +{ + [Fact] + public void Update_NaN_ReturnsLastValue() + { + var ttm = new TtmTrend(period: 6); + var time = DateTime.UtcNow; + + var result1 = ttm.Update(new TValue(time.Ticks, 100.0)); + var result2 = ttm.Update(new TValue(time.AddMinutes(1).Ticks, double.NaN)); + + Assert.Equal(result1.Value, result2.Value); + } + + [Fact] + public void Update_Infinity_ReturnsLastValue() + { + var ttm = new TtmTrend(period: 6); + var time = DateTime.UtcNow; + + var result1 = ttm.Update(new TValue(time.Ticks, 100.0)); + var result2 = ttm.Update(new TValue(time.AddMinutes(1).Ticks, double.PositiveInfinity)); + + Assert.Equal(result1.Value, result2.Value); + } + + [Fact] + public void Update_LargeValues_CalculatesCorrectly() + { + var ttm = new TtmTrend(period: 6); + var time = DateTime.UtcNow; + + var result = ttm.Update(new TValue(time.Ticks, 1e10)); + Assert.True(double.IsFinite(result.Value)); + Assert.Equal(1e10, result.Value); + } + + [Fact] + public void Update_SmallValues_CalculatesCorrectly() + { + var ttm = new TtmTrend(period: 6); + var time = DateTime.UtcNow; + + var result = ttm.Update(new TValue(time.Ticks, 1e-10)); + Assert.True(double.IsFinite(result.Value)); + Assert.Equal(1e-10, result.Value); + } +} + +// ═══════════════════════════════════════════════════════════════════════════ +// Reset Tests +// ═══════════════════════════════════════════════════════════════════════════ + +public class TtmTrendResetTests +{ + [Fact] + public void Reset_ClearsState() + { + var ttm = new TtmTrend(period: 6); + var time = DateTime.UtcNow; + + ttm.Update(new TValue(time.Ticks, 100.0)); + ttm.Update(new TValue(time.AddMinutes(1).Ticks, 110.0)); + + Assert.True(ttm.IsHot); + + ttm.Reset(); + + Assert.False(ttm.IsHot); + Assert.Equal(default, ttm.Last); + Assert.Equal(0, ttm.Trend); + Assert.Equal(0, ttm.Strength); + } + + [Fact] + public void Reset_CanReuseAfterReset() + { + var ttm = new TtmTrend(period: 6); + var time = DateTime.UtcNow; + + ttm.Update(new TValue(time.Ticks, 100.0)); + ttm.Update(new TValue(time.AddMinutes(1).Ticks, 110.0)); + ttm.Reset(); + + var result = ttm.Update(new TValue(time.AddMinutes(2).Ticks, 200.0)); + + Assert.Equal(200.0, result.Value); + } +} + +// ═══════════════════════════════════════════════════════════════════════════ +// Bar Correction Tests +// ═══════════════════════════════════════════════════════════════════════════ + +public class TtmTrendBarCorrectionTests +{ + [Fact] + public void Update_IsNewFalse_CorrectsPreviousValue() + { + var ttm = new TtmTrend(period: 6); + var time = DateTime.UtcNow; + + ttm.Update(new TValue(time.Ticks, 100.0)); + ttm.Update(new TValue(time.AddMinutes(1).Ticks, 110.0), isNew: true); + + // Correct the bar with different value + var corrected = ttm.Update(new TValue(time.AddMinutes(1).Ticks, 105.0), isNew: false); + + // Should use 105 instead of 110 + double alpha = 2.0 / 7.0; + double expected = alpha * 105.0 + (1 - alpha) * 100.0; + Assert.Equal(expected, corrected.Value, 10); + } +} + +// ═══════════════════════════════════════════════════════════════════════════ +// Batch Processing Tests +// ═══════════════════════════════════════════════════════════════════════════ + +public class TtmTrendBatchTests +{ + [Fact] + public void Batch_ReturnsCorrectResults() + { + var bars = new TBarSeries(); + var time = DateTime.UtcNow; + + for (int i = 0; i < 10; i++) + { + bars.Add(new TBar(time.AddMinutes(i).Ticks, 100.0 + i, 105.0 + i, 95.0 + i, 102.0 + i, 1000)); + } + + var result = TtmTrend.Batch(bars, period: 6); + + Assert.Equal(10, result.Count); + } + + [Fact] + public void Calculate_ReturnsIndicatorAndResults() + { + var bars = new TBarSeries(); + var time = DateTime.UtcNow; + + for (int i = 0; i < 10; i++) + { + bars.Add(new TBar(time.AddMinutes(i).Ticks, 100.0 + i, 105.0 + i, 95.0 + i, 102.0 + i, 1000)); + } + + var (results, indicator) = TtmTrend.Calculate(bars, period: 6); + + Assert.Equal(10, results.Count); + Assert.True(indicator.IsHot); + Assert.Equal(6, indicator.Period); + } + + [Fact] + public void Update_EmptyBarSeries_ReturnsEmpty() + { + var ttm = new TtmTrend(period: 6); + var bars = new TBarSeries(); + + var result = ttm.Update(bars); + + Assert.True(result.Count == 0); + } +} + +// ═══════════════════════════════════════════════════════════════════════════ +// Event Tests +// ═══════════════════════════════════════════════════════════════════════════ + +public class TtmTrendEventTests +{ + [Fact] + public void Update_RaisesPubEvent() + { + var ttm = new TtmTrend(period: 6); + var eventRaised = false; + TValue receivedValue = default; + + ttm.Pub += (object? sender, in TValueEventArgs args) => + { + eventRaised = true; + receivedValue = args.Value; + }; + + var result = ttm.Update(new TValue(DateTime.UtcNow.Ticks, 100.0)); + + Assert.True(eventRaised); + Assert.Equal(result.Value, receivedValue.Value); + } +} + +// ═══════════════════════════════════════════════════════════════════════════ +// Prime Tests +// ═══════════════════════════════════════════════════════════════════════════ + +public class TtmTrendPrimeTests +{ + [Fact] + public void Prime_WarmUpIndicator() + { + var ttm = new TtmTrend(period: 6); + var bars = new TBarSeries(); + var time = DateTime.UtcNow; + + for (int i = 0; i < 10; i++) + { + bars.Add(new TBar(time.AddMinutes(i).Ticks, 100.0 + i, 105.0 + i, 95.0 + i, 102.0 + i, 1000)); + } + + ttm.Prime(bars); + + Assert.True(ttm.IsHot); + Assert.NotEqual(default, ttm.Last); + } +} diff --git a/lib/dynamics/ttm_trend/TtmTrend.cs b/lib/dynamics/ttm_trend/TtmTrend.cs new file mode 100644 index 00000000..e1e8145a --- /dev/null +++ b/lib/dynamics/ttm_trend/TtmTrend.cs @@ -0,0 +1,270 @@ +// TTM_TREND: John Carter's TTM Trend Indicator +// Color-coded EMA for visual trend identification +// Uses 6-period EMA of HLC/3 (typical price) by default + +using System.Runtime.CompilerServices; + +namespace QuanTAlib; + +/// +/// TTM_TREND: John Carter's TTM Trend Indicator +/// A fast EMA-based trend indicator with color-coded direction and strength measurement. +/// +/// Calculation: EMA(source, period) with trend = sign(EMA - prevEMA) +/// +/// +/// Calculation: +/// +/// alpha = 2 / (period + 1) +/// EMA = alpha * source + (1 - alpha) * prevEMA +/// trend = sign(EMA - prevEMA) +/// strength = |EMA - prevEMA| / prevEMA * 100 +/// +/// +/// Key characteristics: +/// - O(1) update complexity per bar +/// - Uses EMA for smooth, responsive trend following +/// - Trend direction: +1 (bullish), -1 (bearish), 0 (neutral) +/// - Strength measures percent change between EMA values +/// - Default period of 6 for fast trend detection +/// +/// Detailed documentation +[SkipLocalsInit] +public sealed class TtmTrend : ITValuePublisher +{ + private const int DefaultPeriod = 6; + + private readonly int _period; + private readonly double _alpha; + + // Current state + private double _ema; + private double _prevEma; + private int _sampleCount; + + // Saved state for bar correction + private double _p_ema; + private double _p_prevEma; + private int _p_sampleCount; + + /// + /// Display name for the indicator. + /// + public string Name { get; } + + public event TValuePublishedHandler? Pub; + + /// + /// Current TTM Trend EMA value. + /// + public TValue Last { get; private set; } + + /// + /// Current trend direction: +1 (bullish), -1 (bearish), 0 (neutral). + /// + public int Trend { get; private set; } + + /// + /// Current trend strength as percent change between EMA values. + /// + public double Strength { get; private set; } + + /// + /// True when the indicator has calculated a valid value (after 2 bars). + /// + public bool IsHot => _sampleCount > 1; + + /// + /// The lookback period parameter. + /// + public int Period => _period; + + /// + /// The number of bars required for the indicator to warm up. + /// + public static int WarmupPeriod => 2; + + /// + /// Creates a TTM Trend indicator with specified period. + /// + /// Lookback period for EMA (must be >= 1, default 6) + public TtmTrend(int period = DefaultPeriod) + { + if (period < 1) + { + throw new ArgumentException("Period must be at least 1", nameof(period)); + } + + _period = period; + _alpha = 2.0 / (period + 1); + Name = $"TTM_TREND({period})"; + } + + /// + /// Resets the indicator state. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Reset() + { + _ema = 0; + _prevEma = 0; + _sampleCount = 0; + _p_ema = 0; + _p_prevEma = 0; + _p_sampleCount = 0; + Trend = 0; + Strength = 0; + Last = default; + } + + /// + /// Updates the TTM Trend indicator with a new value. + /// + /// Input value (typically HLC/3) + /// True for new bar, false for update of current bar + /// The current TTM Trend EMA value + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public TValue Update(TValue input, bool isNew = true) + { + double value = input.Value; + + // Handle NaN/Infinity inputs + if (!double.IsFinite(value)) + { + Pub?.Invoke(this, new TValueEventArgs { Value = Last, IsNew = isNew }); + return Last; + } + + // State management for bar correction + if (isNew) + { + _p_ema = _ema; + _p_prevEma = _prevEma; + _p_sampleCount = _sampleCount; + } + else + { + _ema = _p_ema; + _prevEma = _p_prevEma; + _sampleCount = _p_sampleCount; + } + + // EMA calculation + if (_sampleCount == 0) + { + _ema = value; + _prevEma = value; + } + else + { + _prevEma = _ema; + _ema = Math.FusedMultiplyAdd(_alpha, value - _ema, _ema); + } + + if (isNew) + { + _sampleCount++; + } + + // Calculate trend and strength + double diff = _ema - _prevEma; + Trend = Math.Sign(diff); + Strength = _prevEma > 1e-10 ? Math.Abs(diff) / _prevEma * 100.0 : 0.0; + + Last = new TValue(input.Time, _ema); + Pub?.Invoke(this, new TValueEventArgs { Value = Last, IsNew = isNew }); + return Last; + } + + /// + /// Updates the TTM Trend indicator with a bar using typical price (HLC/3). + /// + /// The price bar + /// True for new bar, false for update of current bar + /// The current TTM Trend EMA value + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public TValue Update(TBar bar, bool isNew = true) + { + double typical = (bar.High + bar.Low + bar.Close) / 3.0; + return Update(new TValue(bar.Time, typical), isNew); + } + + /// + /// Updates with a value series. + /// + public TSeries Update(TSeries source) + { + if (source.Count == 0) + { + return new TSeries([], []); + } + + int len = source.Count; + var tList = new List(len); + var vList = new List(len); + + for (int i = 0; i < len; i++) + { + var result = Update(source[i], isNew: true); + tList.Add(source.Times[i]); + vList.Add(result.Value); + } + + return new TSeries(tList, vList); + } + + /// + /// Updates with a bar series. + /// + public TSeries Update(TBarSeries source) + { + if (source.Count == 0) + { + return new TSeries([], []); + } + + int len = source.Count; + var tList = new List(len); + var vList = new List(len); + + var times = source.Open.Times; + for (int i = 0; i < len; i++) + { + var result = Update(source[i], isNew: true); + tList.Add(times[i]); + vList.Add(result.Value); + } + + return new TSeries(tList, vList); + } + + /// + /// Primes the indicator with historical bar data. + /// + public void Prime(TBarSeries source) + { + for (int i = 0; i < source.Count; i++) + { + Update(source[i], isNew: true); + } + } + + /// + /// Creates and returns results for a bar series. + /// + public static TSeries Batch(TBarSeries source, int period = DefaultPeriod) + { + var indicator = new TtmTrend(period); + return indicator.Update(source); + } + + /// + /// Returns the indicator and its results. + /// + public static (TSeries Results, TtmTrend Indicator) Calculate(TBarSeries source, int period = DefaultPeriod) + { + var indicator = new TtmTrend(period); + var results = indicator.Update(source); + return (results, indicator); + } +} diff --git a/lib/dynamics/ttm_trend/TtmTrend.md b/lib/dynamics/ttm_trend/TtmTrend.md new file mode 100644 index 00000000..849406ac --- /dev/null +++ b/lib/dynamics/ttm_trend/TtmTrend.md @@ -0,0 +1,113 @@ +# TTM_TREND: TTM Trend Indicator + +> John Carter's TTM Trend - A fast EMA-based trend indicator with color-coded direction. + +## Historical Context + +John Carter developed the TTM (Trade the Markets) Trend indicator as a clean visual tool for identifying short-term trend direction. Popularized through his book *Mastering the Trade* and the thinkorswim platform, it provides a simple but effective way to see trend changes at a glance using color-coded lines. + +## Algorithm + +### Core Calculation +``` +alpha = 2 / (period + 1) +EMA = alpha × source + (1 - alpha) × prevEMA +``` + +Or equivalently: +``` +EMA = alpha × (source - EMA) + EMA +``` + +### Trend Detection +``` +trend = sign(EMA - prevEMA) + +1 = bullish (EMA rising) + -1 = bearish (EMA falling) + 0 = neutral (EMA unchanged) +``` + +### Strength Measurement +``` +strength = |EMA - prevEMA| / prevEMA × 100% +``` + +## Default Parameters + +| Parameter | Value | Description | +|:----------|:------|:------------| +| Period | 6 | EMA lookback period (very fast) | +| Source | HLC/3 | Typical price (High + Low + Close) / 3 | + +## Outputs + +| Output | Type | Description | +|:-------|:-----|:------------| +| Value | double | Current EMA value | +| Trend | int | Trend direction: +1, -1, or 0 | +| Strength | double | Percent change between EMA values | +| IsHot | bool | True after warming up (2 bars) | + +## Color Coding + +| Color | Condition | Meaning | +|:------|:----------|:--------| +| 🟢 Green | Trend > 0 | EMA rising (bullish) | +| 🔴 Red | Trend < 0 | EMA falling (bearish) | +| ⚫ Gray | Trend = 0 | EMA unchanged (neutral) | + +## Performance + +| Metric | Value | +|:-------|:------| +| Time complexity | O(1) per bar | +| Space complexity | O(1) | +| Warmup period | 2 bars | +| Allocations | Zero in hot path | + +## Usage Examples + +### Basic Usage +```csharp +var ttm = new TtmTrend(period: 6); + +// Update with typical price +var result = ttm.Update(new TValue(time, typicalPrice)); + +// Or update with bar (uses HLC/3 automatically) +var result = ttm.Update(bar); + +// Access trend direction +if (ttm.Trend > 0) { /* bullish */ } +else if (ttm.Trend < 0) { /* bearish */ } +``` + +### Batch Processing +```csharp +var results = TtmTrend.Batch(barSeries, period: 6); +``` + +### With Indicator Instance +```csharp +var (results, indicator) = TtmTrend.Calculate(barSeries, period: 6); +bool isBullish = indicator.Trend > 0; +double strength = indicator.Strength; +``` + +## Trading Applications + +1. **Trend Following**: Trade in the direction of the EMA color +2. **Trend Confirmation**: Use with other TTM indicators (Squeeze, Wave) +3. **Entry Timing**: Enter on color change with confirmation +4. **Exit Signal**: Exit when color changes against position + +## Category + +**Dynamics** - Measures trend direction and momentum using fast EMA smoothing. + +## See Also + +- [TTM_SQUEEZE: TTM Squeeze](../ttm_squeeze/TtmSqueeze.md) +- [TTM_WAVE: TTM Wave](../../oscillators/ttm_wave/TtmWave.md) +- [TTM_LRC: TTM Linear Regression Channel](../../channels/ttm_lrc/TtmLrc.md) +- [SUPER: SuperTrend](../super/Super.md) diff --git a/lib/dynamics/ttm/ttm.pine b/lib/dynamics/ttm_trend/TtmTrend.pine similarity index 89% rename from lib/dynamics/ttm/ttm.pine rename to lib/dynamics/ttm_trend/TtmTrend.pine index 13209ec8..70ea4154 100644 --- a/lib/dynamics/ttm/ttm.pine +++ b/lib/dynamics/ttm_trend/TtmTrend.pine @@ -1,14 +1,14 @@ // The MIT License (MIT) // © mihakralj //@version=6 -indicator("TTM Trend", "TTM", overlay=true) +indicator("TTM Trend", "TTM_TREND", overlay=true) //@function Calculates TTM Trend using 6-period moving average with color-coded trend -//@doc https://github.com/mihakralj/pinescript/blob/main/indicators/dynamics/ttm.md -//@param source Series to calculate TTM from +//@doc https://github.com/mihakralj/pinescript/blob/main/indicators/dynamics/ttm_trend.md +//@param source Series to calculate TTM Trend from //@param period Lookback period for moving average //@returns Tuple [ttm_line, trend, strength] where trend is -1/0/1 and strength is percentage change -ttm(series float source, simple int period = 6) => +ttm_trend(series float source, simple int period = 6) => if period <= 0 runtime.error("Period must be greater than 0") @@ -33,7 +33,7 @@ i_source = input.source(hlc3, "Source") i_show_strength = input.bool(true, "Show Trend Strength %") // Calculation -[ttm_line, trend, strength] = ttm(i_source, i_period) +[ttm_line, trend, strength] = ttm_trend(i_source, i_period) // Colors color up_color = color.new(color.green, 0) diff --git a/lib/dynamics/vortex/Vortex.cs b/lib/dynamics/vortex/Vortex.cs index edc146bf..aaa167ba 100644 --- a/lib/dynamics/vortex/Vortex.cs +++ b/lib/dynamics/vortex/Vortex.cs @@ -207,7 +207,7 @@ public sealed class Vortex : ITValuePublisher var viPlusValues = new double[len]; var viMinusValues = new double[len]; - Calculate(source.High.Values, source.Low.Values, source.Close.Values, _period, viPlusValues, viMinusValues); + Batch(source.High.Values, source.Low.Values, source.Close.Values, _period, viPlusValues, viMinusValues); var tList = new List(len); var vList = new List(viPlusValues); @@ -237,7 +237,7 @@ public sealed class Vortex : ITValuePublisher /// Output VI+ values /// Output VI- values [MethodImpl(MethodImplOptions.AggressiveOptimization)] - public static void Calculate(ReadOnlySpan high, ReadOnlySpan low, ReadOnlySpan close, + public static void Batch(ReadOnlySpan high, ReadOnlySpan low, ReadOnlySpan close, int period, Span viPlus, Span viMinus) { int len = high.Length; @@ -313,4 +313,12 @@ public sealed class Vortex : ITValuePublisher var vortex = new Vortex(period); return vortex.Update(source); } + + public static (TSeries Results, Vortex Indicator) Calculate(TBarSeries source) + { + var indicator = new Vortex(); + TSeries results = indicator.Update(source); + return (results, indicator); + } + } diff --git a/lib/errors/huber/Huber.Tests.cs b/lib/errors/huber/Huber.Tests.cs index 70dfb24f..aacf2712 100644 --- a/lib/errors/huber/Huber.Tests.cs +++ b/lib/errors/huber/Huber.Tests.cs @@ -332,7 +332,7 @@ public class HuberTests predicted.Add(now.AddMinutes(i), 100.5); // Small constant error } - var results = Huber.Calculate(actual, predicted, 3); + var results = Huber.Batch(actual, predicted, 3); Assert.Equal(10, results.Count); // Error = 0.5, Huber (small error) = 0.5 * 0.5^2 = 0.125 @@ -355,7 +355,7 @@ public class HuberTests predicted.Add(DateTime.UtcNow, i); } - Assert.Throws(() => Huber.Calculate(actual, predicted, 3)); + Assert.Throws(() => Huber.Batch(actual, predicted, 3)); } [Fact] diff --git a/lib/errors/huber/Huber.cs b/lib/errors/huber/Huber.cs index c06c798f..29e6ef36 100644 --- a/lib/errors/huber/Huber.cs +++ b/lib/errors/huber/Huber.cs @@ -67,7 +67,7 @@ public sealed class Huber : BiInputIndicatorBase /// /// Calculates Huber Loss for two time series. /// - public static TSeries Calculate(TSeries actual, TSeries predicted, int period, double delta = 1.345) + public static TSeries Batch(TSeries actual, TSeries predicted, int period, double delta = 1.345) { if (actual.Count != predicted.Count) { @@ -127,4 +127,11 @@ public sealed class Huber : BiInputIndicatorBase // Apply rolling mean ErrorHelpers.ApplyRollingMean(errors, output, period); } -} + + public static (TSeries Results, Huber Indicator) Calculate(TSeries actual, TSeries predicted, int period, double delta = 1.345) + { + var indicator = new Huber(period, delta); + TSeries results = Batch(actual, predicted, period, delta); + return (results, indicator); + } +} \ No newline at end of file diff --git a/lib/errors/logcosh/LogCosh.Tests.cs b/lib/errors/logcosh/LogCosh.Tests.cs index 135f7b84..0fa8356c 100644 --- a/lib/errors/logcosh/LogCosh.Tests.cs +++ b/lib/errors/logcosh/LogCosh.Tests.cs @@ -239,7 +239,7 @@ public class LogCoshTests iterativeResults[i] = logCoshIterative.Update(actualSeries[i], predictedSeries[i]).Value; } - var batchResults = LogCosh.Calculate(actualSeries, predictedSeries, DefaultPeriod); + var batchResults = LogCosh.Batch(actualSeries, predictedSeries, DefaultPeriod); Assert.Equal(count, batchResults.Count); for (int i = 0; i < count; i++) @@ -283,7 +283,7 @@ public class LogCoshTests predictedArr[i] = pred; } - var tseriesResult = LogCosh.Calculate(actualSeries, predictedSeries, DefaultPeriod); + var tseriesResult = LogCosh.Batch(actualSeries, predictedSeries, DefaultPeriod); LogCosh.Batch(actualArr.AsSpan(), predictedArr.AsSpan(), output.AsSpan(), DefaultPeriod); for (int i = 0; i < 100; i++) @@ -332,7 +332,7 @@ public class LogCoshTests predicted.Add(DateTime.UtcNow.Ticks, 98); - Assert.Throws(() => LogCosh.Calculate(actual, predicted, DefaultPeriod)); + Assert.Throws(() => LogCosh.Batch(actual, predicted, DefaultPeriod)); } [Fact] diff --git a/lib/errors/logcosh/LogCosh.cs b/lib/errors/logcosh/LogCosh.cs index c07d7d3b..1a427d5b 100644 --- a/lib/errors/logcosh/LogCosh.cs +++ b/lib/errors/logcosh/LogCosh.cs @@ -58,7 +58,7 @@ public sealed class LogCosh : BiInputIndicatorBase /// /// Calculates LogCosh for entire series. /// - public static TSeries Calculate(TSeries actual, TSeries predicted, int period) + public static TSeries Batch(TSeries actual, TSeries predicted, int period) => CalculateImpl(actual, predicted, period, Batch); /// @@ -97,4 +97,11 @@ public sealed class LogCosh : BiInputIndicatorBase } } } -} + + public static (TSeries Results, LogCosh Indicator) Calculate(TSeries actual, TSeries predicted, int period) + { + var indicator = new LogCosh(period); + TSeries results = Batch(actual, predicted, period); + return (results, indicator); + } +} \ No newline at end of file diff --git a/lib/errors/maape/Maape.Tests.cs b/lib/errors/maape/Maape.Tests.cs index 87f82bd1..83283f42 100644 --- a/lib/errors/maape/Maape.Tests.cs +++ b/lib/errors/maape/Maape.Tests.cs @@ -212,7 +212,7 @@ public class MaapeTests streamingResults[i] = maapeIterative.Update(actualArr[i], predictedArr[i]).Value; } - var batchResults = Maape.Calculate(actualSeries, predictedSeries, DefaultPeriod); + var batchResults = Maape.Batch(actualSeries, predictedSeries, DefaultPeriod); Assert.Equal(count, batchResults.Count); for (int i = 0; i < count; i++) @@ -256,7 +256,7 @@ public class MaapeTests predictedArr[i] = pred; } - var tseriesResult = Maape.Calculate(actualSeries, predictedSeries, DefaultPeriod); + var tseriesResult = Maape.Batch(actualSeries, predictedSeries, DefaultPeriod); Maape.Batch(actualArr.AsSpan(), predictedArr.AsSpan(), output.AsSpan(), DefaultPeriod); for (int i = 0; i < 100; i++) @@ -305,7 +305,7 @@ public class MaapeTests predicted.Add(DateTime.UtcNow.Ticks, 98); - Assert.Throws(() => Maape.Calculate(actual, predicted, DefaultPeriod)); + Assert.Throws(() => Maape.Batch(actual, predicted, DefaultPeriod)); } [Fact] diff --git a/lib/errors/maape/Maape.cs b/lib/errors/maape/Maape.cs index 4a3ae12b..42cb743a 100644 --- a/lib/errors/maape/Maape.cs +++ b/lib/errors/maape/Maape.cs @@ -49,7 +49,7 @@ public sealed class Maape : BiInputIndicatorBase /// /// Calculates Mean Arctangent Absolute Percentage Error for two time series. /// - public static TSeries Calculate(TSeries actual, TSeries predicted, int period) + public static TSeries Batch(TSeries actual, TSeries predicted, int period) { if (actual.Count != predicted.Count) { @@ -116,6 +116,13 @@ public sealed class Maape : BiInputIndicatorBase } } + public static (TSeries Results, Maape Indicator) Calculate(TSeries actual, TSeries predicted, int period) + { + var indicator = new Maape(period); + TSeries results = Batch(actual, predicted, period); + return (results, indicator); + } + /// /// Computes arctangent percentage errors. /// @@ -178,4 +185,4 @@ public sealed class Maape : BiInputIndicatorBase output[i] = absActual > Epsilon ? Math.Atan(absError / absActual) : Math.PI / 2.0; } } -} +} \ No newline at end of file diff --git a/lib/errors/mae/Mae.Tests.cs b/lib/errors/mae/Mae.Tests.cs index 1c5c4bc7..9f3fb99c 100644 --- a/lib/errors/mae/Mae.Tests.cs +++ b/lib/errors/mae/Mae.Tests.cs @@ -308,7 +308,7 @@ public class MaeTests predicted.Add(now.AddMinutes(i), i * 10 + 5); } - var results = Mae.Calculate(actual, predicted, 3); + var results = Mae.Batch(actual, predicted, 3); Assert.Equal(10, results.Count); // All errors are 5, so MAE should be 5 @@ -331,7 +331,7 @@ public class MaeTests predicted.Add(DateTime.UtcNow, i); } - Assert.Throws(() => Mae.Calculate(actual, predicted, 3)); + Assert.Throws(() => Mae.Batch(actual, predicted, 3)); } [Fact] diff --git a/lib/errors/mae/Mae.cs b/lib/errors/mae/Mae.cs index ab1366b4..101ca02b 100644 --- a/lib/errors/mae/Mae.cs +++ b/lib/errors/mae/Mae.cs @@ -45,7 +45,7 @@ public sealed class Mae : BiInputIndicatorBase /// Predicted values series /// MAE period /// MAE series - public static TSeries Calculate(TSeries actual, TSeries predicted, int period) + public static TSeries Batch(TSeries actual, TSeries predicted, int period) => CalculateImpl(actual, predicted, period, Batch); /// @@ -89,4 +89,11 @@ public sealed class Mae : BiInputIndicatorBase } } } -} + + public static (TSeries Results, Mae Indicator) Calculate(TSeries actual, TSeries predicted, int period) + { + var indicator = new Mae(period); + TSeries results = Batch(actual, predicted, period); + return (results, indicator); + } +} \ No newline at end of file diff --git a/lib/errors/mapd/Mapd.Tests.cs b/lib/errors/mapd/Mapd.Tests.cs index 2291ceeb..df964981 100644 --- a/lib/errors/mapd/Mapd.Tests.cs +++ b/lib/errors/mapd/Mapd.Tests.cs @@ -292,7 +292,7 @@ public class MapdTests predicted.Add(now.AddMinutes(i), 110); } - var results = Mapd.Calculate(actual, predicted, 3); + var results = Mapd.Batch(actual, predicted, 3); Assert.Equal(10, results.Count); // |100-110|/110 * 100 = 9.0909...% @@ -315,7 +315,7 @@ public class MapdTests predicted.Add(DateTime.UtcNow, i + 1); } - Assert.Throws(() => Mapd.Calculate(actual, predicted, 3)); + Assert.Throws(() => Mapd.Batch(actual, predicted, 3)); } [Fact] diff --git a/lib/errors/mapd/Mapd.cs b/lib/errors/mapd/Mapd.cs index e90fdcde..606e142b 100644 --- a/lib/errors/mapd/Mapd.cs +++ b/lib/errors/mapd/Mapd.cs @@ -48,7 +48,7 @@ public sealed class Mapd : BiInputIndicatorBase /// /// Calculates Mean Absolute Percentage Deviation for two time series. /// - public static TSeries Calculate(TSeries actual, TSeries predicted, int period) + public static TSeries Batch(TSeries actual, TSeries predicted, int period) { if (actual.Count != predicted.Count) { @@ -104,6 +104,13 @@ public sealed class Mapd : BiInputIndicatorBase ErrorHelpers.ApplyRollingMean(errors, output, period); } + public static (TSeries Results, Mapd Indicator) Calculate(TSeries actual, TSeries predicted, int period) + { + var indicator = new Mapd(period); + TSeries results = Batch(actual, predicted, period); + return (results, indicator); + } + /// /// Computes MAPD errors (percentage errors divided by predicted). /// @@ -164,4 +171,4 @@ public sealed class Mapd : BiInputIndicatorBase : 0.0; } } -} +} \ No newline at end of file diff --git a/lib/errors/mape/Mape.Tests.cs b/lib/errors/mape/Mape.Tests.cs index 70a96a1a..d42d72b0 100644 --- a/lib/errors/mape/Mape.Tests.cs +++ b/lib/errors/mape/Mape.Tests.cs @@ -308,7 +308,7 @@ public class MapeTests predicted.Add(now.AddMinutes(i), 110); // 10% error } - var results = Mape.Calculate(actual, predicted, 3); + var results = Mape.Batch(actual, predicted, 3); Assert.Equal(10, results.Count); Assert.Equal(10.0, results.Last.Value, 10); @@ -330,7 +330,7 @@ public class MapeTests predicted.Add(DateTime.UtcNow, i + 1); } - Assert.Throws(() => Mape.Calculate(actual, predicted, 3)); + Assert.Throws(() => Mape.Batch(actual, predicted, 3)); } [Fact] diff --git a/lib/errors/mape/Mape.cs b/lib/errors/mape/Mape.cs index 027abbc8..e6e0fef5 100644 --- a/lib/errors/mape/Mape.cs +++ b/lib/errors/mape/Mape.cs @@ -41,7 +41,7 @@ public sealed class Mape : BiInputIndicatorBase /// /// Calculates MAPE for entire series. /// - public static TSeries Calculate(TSeries actual, TSeries predicted, int period) + public static TSeries Batch(TSeries actual, TSeries predicted, int period) => CalculateImpl(actual, predicted, period, Batch); /// @@ -65,4 +65,11 @@ public sealed class Mape : BiInputIndicatorBase ErrorHelpers.ComputePercentageErrors(actual, predicted, percentErrors, Epsilon); ErrorHelpers.ApplyRollingMean(percentErrors, output, period); } -} + + public static (TSeries Results, Mape Indicator) Calculate(TSeries actual, TSeries predicted, int period) + { + var indicator = new Mape(period); + TSeries results = Batch(actual, predicted, period); + return (results, indicator); + } +} \ No newline at end of file diff --git a/lib/errors/mase/Mase.Tests.cs b/lib/errors/mase/Mase.Tests.cs index f032063c..f5eef5e3 100644 --- a/lib/errors/mase/Mase.Tests.cs +++ b/lib/errors/mase/Mase.Tests.cs @@ -241,7 +241,7 @@ public class MaseTests iterativeResults.Add(maseIterative.Update(actual[i], predicted[i]).Value); } - var batchResults = Mase.Calculate(actual, predicted, Period); + var batchResults = Mase.Batch(actual, predicted, Period); Assert.Equal(iterativeResults.Count, batchResults.Count); for (int i = 0; i < iterativeResults.Count; i++) @@ -282,7 +282,7 @@ public class MaseTests double[] predictedArr = predictedSeries.Values.ToArray(); double[] output = new double[100]; - var tseriesResult = Mase.Calculate(actualSeries, predictedSeries, Period); + var tseriesResult = Mase.Batch(actualSeries, predictedSeries, Period); Mase.Batch(actualArr.AsSpan(), predictedArr.AsSpan(), output.AsSpan(), Period); for (int i = 0; i < 100; i++) @@ -303,7 +303,7 @@ public class MaseTests } // 1. Batch Mode (static method) - var batchSeries = Mase.Calculate(actualSeries, predictedSeries, Period); + var batchSeries = Mase.Batch(actualSeries, predictedSeries, Period); double expected = batchSeries.Last.Value; // 2. Span Mode diff --git a/lib/errors/mase/Mase.cs b/lib/errors/mase/Mase.cs index 36384540..e05199bb 100644 --- a/lib/errors/mase/Mase.cs +++ b/lib/errors/mase/Mase.cs @@ -162,7 +162,7 @@ public sealed class Mase : AbstractBase public override TSeries Update(TSeries source) { - throw new NotSupportedException("MASE requires two inputs. Use Calculate(actualSeries, predictedSeries, period)."); + throw new NotSupportedException("MASE requires two inputs. Use Batch(actualSeries, predictedSeries, period)."); } public override void Prime(ReadOnlySpan source, TimeSpan? step = null) @@ -179,7 +179,7 @@ public sealed class Mase : AbstractBase Last = default; } - public static TSeries Calculate(TSeries actual, TSeries predicted, int period) + public static TSeries Batch(TSeries actual, TSeries predicted, int period) { if (actual.Count != predicted.Count) { @@ -352,4 +352,11 @@ public sealed class Mase : AbstractBase } } } -} + + public static (TSeries Results, Mase Indicator) Calculate(TSeries actual, TSeries predicted, int period) + { + var indicator = new Mase(period); + TSeries results = Batch(actual, predicted, period); + return (results, indicator); + } +} \ No newline at end of file diff --git a/lib/errors/mdae/Mdae.Tests.cs b/lib/errors/mdae/Mdae.Tests.cs index 5c72f2fd..1b9c6013 100644 --- a/lib/errors/mdae/Mdae.Tests.cs +++ b/lib/errors/mdae/Mdae.Tests.cs @@ -192,7 +192,7 @@ public class MdaeTests iterativeResults.Add(mdaeIterative.Update(actual, predicted).Value); } - var batchResults = Mdae.Calculate(actualSeries, predictedSeries, DefaultPeriod); + var batchResults = Mdae.Batch(actualSeries, predictedSeries, DefaultPeriod); Assert.Equal(100, iterativeResults.Count); Assert.Equal(iterativeResults.Count, batchResults.Count); @@ -237,7 +237,7 @@ public class MdaeTests predictedArr[i] = pred; } - var tseriesResult = Mdae.Calculate(actualSeries, predictedSeries, DefaultPeriod); + var tseriesResult = Mdae.Batch(actualSeries, predictedSeries, DefaultPeriod); Mdae.Batch(actualArr.AsSpan(), predictedArr.AsSpan(), output.AsSpan(), DefaultPeriod); for (int i = 0; i < 100; i++) @@ -286,7 +286,7 @@ public class MdaeTests predicted.Add(DateTime.UtcNow.Ticks, 98); - Assert.Throws(() => Mdae.Calculate(actual, predicted, DefaultPeriod)); + Assert.Throws(() => Mdae.Batch(actual, predicted, DefaultPeriod)); } [Fact] diff --git a/lib/errors/mdae/Mdae.cs b/lib/errors/mdae/Mdae.cs index 953dc8f0..ea91d3b1 100644 --- a/lib/errors/mdae/Mdae.cs +++ b/lib/errors/mdae/Mdae.cs @@ -114,7 +114,7 @@ public sealed class Mdae : AbstractBase public override TSeries Update(TSeries source) { - throw new NotSupportedException("MdAE requires two inputs. Use Calculate(actualSeries, predictedSeries, period)."); + throw new NotSupportedException("MdAE requires two inputs. Use Batch(actualSeries, predictedSeries, period)."); } public override void Prime(ReadOnlySpan source, TimeSpan? step = null) @@ -161,7 +161,7 @@ public sealed class Mdae : AbstractBase return (_sortBuffer[mid - 1] + _sortBuffer[mid]) * 0.5; } - public static TSeries Calculate(TSeries actual, TSeries predicted, int period) + public static TSeries Batch(TSeries actual, TSeries predicted, int period) { if (actual.Count != predicted.Count) { @@ -299,6 +299,13 @@ public sealed class Mdae : AbstractBase } } + public static (TSeries Results, Mdae Indicator) Calculate(TSeries actual, TSeries predicted, int period) + { + var indicator = new Mdae(period); + TSeries results = Batch(actual, predicted, period); + return (results, indicator); + } + /// /// QuickSelect for Span - finds the k-th smallest element in O(n) average time. /// Uses insertion sort for small arrays and Lomuto partition for larger arrays. @@ -378,4 +385,4 @@ public sealed class Mdae : AbstractBase return span[left]; } -} +} \ No newline at end of file diff --git a/lib/errors/mdape/Mdape.Tests.cs b/lib/errors/mdape/Mdape.Tests.cs index 5f44f4d6..4f9228cf 100644 --- a/lib/errors/mdape/Mdape.Tests.cs +++ b/lib/errors/mdape/Mdape.Tests.cs @@ -194,7 +194,7 @@ public class MdapeTests iterativeResults.Add(mdapeIterative.Update(actualSeries[i], predictedSeries[i])); } - var batchResults = Mdape.Calculate(actualSeries, predictedSeries, DefaultPeriod); + var batchResults = Mdape.Batch(actualSeries, predictedSeries, DefaultPeriod); Assert.Equal(iterativeResults.Count, batchResults.Count); for (int i = 0; i < batchResults.Count; i++) @@ -238,7 +238,7 @@ public class MdapeTests predictedArr[i] = pred; } - var tseriesResult = Mdape.Calculate(actualSeries, predictedSeries, DefaultPeriod); + var tseriesResult = Mdape.Batch(actualSeries, predictedSeries, DefaultPeriod); Mdape.Batch(actualArr.AsSpan(), predictedArr.AsSpan(), output.AsSpan(), DefaultPeriod); for (int i = 0; i < tseriesResult.Count; i++) @@ -287,7 +287,7 @@ public class MdapeTests predicted.Add(DateTime.UtcNow.Ticks, 98); - Assert.Throws(() => Mdape.Calculate(actual, predicted, DefaultPeriod)); + Assert.Throws(() => Mdape.Batch(actual, predicted, DefaultPeriod)); } [Fact] diff --git a/lib/errors/mdape/Mdape.cs b/lib/errors/mdape/Mdape.cs index 34836c2b..e53e895b 100644 --- a/lib/errors/mdape/Mdape.cs +++ b/lib/errors/mdape/Mdape.cs @@ -68,7 +68,7 @@ public sealed class Mdape : AbstractBase public override TSeries Update(TSeries source) { - throw new NotSupportedException("MdAPE requires two inputs. Use Calculate(actualSeries, predictedSeries, period)."); + throw new NotSupportedException("MdAPE requires two inputs. Use Batch(actualSeries, predictedSeries, period)."); } [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -164,7 +164,7 @@ public sealed class Mdape : AbstractBase return (_sortBuffer[mid - 1] + _sortBuffer[mid]) * 0.5; } - public static TSeries Calculate(TSeries actual, TSeries predicted, int period) + public static TSeries Batch(TSeries actual, TSeries predicted, int period) { if (actual.Count != predicted.Count) { @@ -263,6 +263,13 @@ public sealed class Mdape : AbstractBase } } + public static (TSeries Results, Mdape Indicator) Calculate(TSeries actual, TSeries predicted, int period) + { + var indicator = new Mdape(period); + TSeries results = Batch(actual, predicted, period); + return (results, indicator); + } + /// /// Dual-heap based sliding window median calculator. /// Maintains O(log n) insert/remove and O(1) median query. @@ -405,4 +412,4 @@ public sealed class Mdape : AbstractBase } } } -} +} \ No newline at end of file diff --git a/lib/errors/me/Me.Tests.cs b/lib/errors/me/Me.Tests.cs index bbed0415..977ad5d4 100644 --- a/lib/errors/me/Me.Tests.cs +++ b/lib/errors/me/Me.Tests.cs @@ -334,7 +334,7 @@ public class MeTests predicted.Add(now.AddMinutes(i), i * 10 + 5); } - var results = Me.Calculate(actual, predicted, 3); + var results = Me.Batch(actual, predicted, 3); Assert.Equal(10, results.Count); // All errors are -5, so ME should be -5 @@ -357,7 +357,7 @@ public class MeTests predicted.Add(DateTime.UtcNow, i); } - Assert.Throws(() => Me.Calculate(actual, predicted, 3)); + Assert.Throws(() => Me.Batch(actual, predicted, 3)); } [Fact] diff --git a/lib/errors/me/Me.cs b/lib/errors/me/Me.cs index 9d06b631..d81a35dd 100644 --- a/lib/errors/me/Me.cs +++ b/lib/errors/me/Me.cs @@ -40,7 +40,7 @@ public sealed class Me : BiInputIndicatorBase /// /// Calculates ME for entire series. /// - public static TSeries Calculate(TSeries actual, TSeries predicted, int period) + public static TSeries Batch(TSeries actual, TSeries predicted, int period) => CalculateImpl(actual, predicted, period, Batch); /// @@ -78,4 +78,11 @@ public sealed class Me : BiInputIndicatorBase } } } -} + + public static (TSeries Results, Me Indicator) Calculate(TSeries actual, TSeries predicted, int period) + { + var indicator = new Me(period); + TSeries results = Batch(actual, predicted, period); + return (results, indicator); + } +} \ No newline at end of file diff --git a/lib/errors/mpe/Mpe.Tests.cs b/lib/errors/mpe/Mpe.Tests.cs index 5181de7b..13c414ba 100644 --- a/lib/errors/mpe/Mpe.Tests.cs +++ b/lib/errors/mpe/Mpe.Tests.cs @@ -236,7 +236,7 @@ public class MpeTests iterativeResults.Add(mpeIterative.Update(actualSeries[i], predictedSeries[i]).Value); } - var batchResults = Mpe.Calculate(actualSeries, predictedSeries, 10); + var batchResults = Mpe.Batch(actualSeries, predictedSeries, 10); Assert.Equal(iterativeResults.Count, batchResults.Count); for (int i = 0; i < iterativeResults.Count; i++) @@ -280,7 +280,7 @@ public class MpeTests predictedSeries.Add(bar.Time, bar.Close * 0.95); } - var tseriesResult = Mpe.Calculate(actualSeries, predictedSeries, 10); + var tseriesResult = Mpe.Batch(actualSeries, predictedSeries, 10); Mpe.Batch(actualArr.AsSpan(), predictedArr.AsSpan(), output.AsSpan(), 10); for (int i = 0; i < 100; i++) @@ -314,7 +314,7 @@ public class MpeTests actual.Add(DateTime.UtcNow.Ticks + 1, 100); predicted.Add(DateTime.UtcNow.Ticks, 90); - Assert.Throws(() => Mpe.Calculate(actual, predicted, 5)); + Assert.Throws(() => Mpe.Batch(actual, predicted, 5)); } [Fact] diff --git a/lib/errors/mpe/Mpe.cs b/lib/errors/mpe/Mpe.cs index a02609b7..77e08e64 100644 --- a/lib/errors/mpe/Mpe.cs +++ b/lib/errors/mpe/Mpe.cs @@ -52,7 +52,7 @@ public sealed class Mpe : BiInputIndicatorBase /// /// Calculates MPE for entire series. /// - public static TSeries Calculate(TSeries actual, TSeries predicted, int period) + public static TSeries Batch(TSeries actual, TSeries predicted, int period) => CalculateImpl(actual, predicted, period, Batch); /// @@ -78,6 +78,13 @@ public sealed class Mpe : BiInputIndicatorBase ErrorHelpers.ApplyRollingMean(errors, output, period); } + public static (TSeries Results, Mpe Indicator) Calculate(TSeries actual, TSeries predicted, int period) + { + var indicator = new Mpe(period); + TSeries results = Batch(actual, predicted, period); + return (results, indicator); + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] private static void ComputeSignedPercentageErrors(ReadOnlySpan actual, ReadOnlySpan predicted, Span output) { @@ -139,4 +146,4 @@ public sealed class Mpe : BiInputIndicatorBase output[i] = 100.0 * (act - pred) / divisor; } } -} +} \ No newline at end of file diff --git a/lib/errors/mrae/Mrae.Tests.cs b/lib/errors/mrae/Mrae.Tests.cs index 2bf6b1bb..3632c428 100644 --- a/lib/errors/mrae/Mrae.Tests.cs +++ b/lib/errors/mrae/Mrae.Tests.cs @@ -284,7 +284,7 @@ public class MraeTests predicted.Add(now.AddMinutes(i), i * 110); // 10% error } - var results = Mrae.Calculate(actual, predicted, 3); + var results = Mrae.Batch(actual, predicted, 3); Assert.Equal(10, results.Count); Assert.Equal(0.1, results.Last.Value, 10); @@ -306,7 +306,7 @@ public class MraeTests predicted.Add(DateTime.UtcNow, i * 10); } - Assert.Throws(() => Mrae.Calculate(actual, predicted, 3)); + Assert.Throws(() => Mrae.Batch(actual, predicted, 3)); } [Fact] diff --git a/lib/errors/mrae/Mrae.cs b/lib/errors/mrae/Mrae.cs index e3a918e9..98106245 100644 --- a/lib/errors/mrae/Mrae.cs +++ b/lib/errors/mrae/Mrae.cs @@ -49,7 +49,7 @@ public sealed class Mrae : BiInputIndicatorBase /// /// Calculates Mean Relative Absolute Error for two time series. /// - public static TSeries Calculate(TSeries actual, TSeries predicted, int period) + public static TSeries Batch(TSeries actual, TSeries predicted, int period) { if (actual.Count != predicted.Count) { @@ -104,6 +104,13 @@ public sealed class Mrae : BiInputIndicatorBase ErrorHelpers.ApplyRollingMean(errors, output, period); } + public static (TSeries Results, Mrae Indicator) Calculate(TSeries actual, TSeries predicted, int period) + { + var indicator = new Mrae(period); + TSeries results = Batch(actual, predicted, period); + return (results, indicator); + } + /// /// Computes relative errors (0-1 scale, not percentage). /// @@ -163,4 +170,4 @@ public sealed class Mrae : BiInputIndicatorBase : 0.0; } } -} +} \ No newline at end of file diff --git a/lib/errors/mse/Mse.Tests.cs b/lib/errors/mse/Mse.Tests.cs index 82a77a17..16c395fc 100644 --- a/lib/errors/mse/Mse.Tests.cs +++ b/lib/errors/mse/Mse.Tests.cs @@ -287,7 +287,7 @@ public class MseTests predicted.Add(now.AddMinutes(i), i * 10 + 5); } - var results = Mse.Calculate(actual, predicted, 3); + var results = Mse.Batch(actual, predicted, 3); Assert.Equal(10, results.Count); // All errors are 5², so MSE should be 25 diff --git a/lib/errors/mse/Mse.cs b/lib/errors/mse/Mse.cs index c6561b78..4f30b7ea 100644 --- a/lib/errors/mse/Mse.cs +++ b/lib/errors/mse/Mse.cs @@ -46,7 +46,7 @@ public sealed class Mse : BiInputIndicatorBase /// Predicted values series /// MSE period /// MSE series - public static TSeries Calculate(TSeries actual, TSeries predicted, int period) + public static TSeries Batch(TSeries actual, TSeries predicted, int period) => CalculateImpl(actual, predicted, period, Batch); /// @@ -78,4 +78,11 @@ public sealed class Mse : BiInputIndicatorBase // Apply rolling mean using shared helper ErrorHelpers.ApplyRollingMean(sqErrors, output, period); } -} + + public static (TSeries Results, Mse Indicator) Calculate(TSeries actual, TSeries predicted, int period) + { + var indicator = new Mse(period); + TSeries results = Batch(actual, predicted, period); + return (results, indicator); + } +} \ No newline at end of file diff --git a/lib/errors/msle/Msle.Tests.cs b/lib/errors/msle/Msle.Tests.cs index 5b142ebd..5988d121 100644 --- a/lib/errors/msle/Msle.Tests.cs +++ b/lib/errors/msle/Msle.Tests.cs @@ -241,7 +241,7 @@ public class MsleTests iterativeResults.Add(msleIterative.Update(actualSeries[i], predictedSeries[i]).Value); } - var batchResults = Msle.Calculate(actualSeries, predictedSeries, 10); + var batchResults = Msle.Batch(actualSeries, predictedSeries, 10); Assert.Equal(iterativeResults.Count, batchResults.Count); for (int i = 0; i < iterativeResults.Count; i++) @@ -285,7 +285,7 @@ public class MsleTests predictedSeries.Add(bar.Time, bar.Close * 0.95); } - var tseriesResult = Msle.Calculate(actualSeries, predictedSeries, 10); + var tseriesResult = Msle.Batch(actualSeries, predictedSeries, 10); Msle.Batch(actualArr.AsSpan(), predictedArr.AsSpan(), output.AsSpan(), 10); for (int i = 0; i < 100; i++) @@ -319,7 +319,7 @@ public class MsleTests actual.Add(DateTime.UtcNow.Ticks + 1, 100); predicted.Add(DateTime.UtcNow.Ticks, 90); - Assert.Throws(() => Msle.Calculate(actual, predicted, 5)); + Assert.Throws(() => Msle.Batch(actual, predicted, 5)); } [Fact] diff --git a/lib/errors/msle/Msle.cs b/lib/errors/msle/Msle.cs index 0488c421..7c884711 100644 --- a/lib/errors/msle/Msle.cs +++ b/lib/errors/msle/Msle.cs @@ -46,7 +46,7 @@ public sealed class Msle : BiInputIndicatorBase /// /// Calculates MSLE for entire series. /// - public static TSeries Calculate(TSeries actual, TSeries predicted, int period) + public static TSeries Batch(TSeries actual, TSeries predicted, int period) => CalculateImpl(actual, predicted, period, Batch); /// @@ -72,6 +72,13 @@ public sealed class Msle : BiInputIndicatorBase ErrorHelpers.ApplyRollingMean(errors, output, period); } + public static (TSeries Results, Msle Indicator) Calculate(TSeries actual, TSeries predicted, int period) + { + var indicator = new Msle(period); + TSeries results = Batch(actual, predicted, period); + return (results, indicator); + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] private static void ComputeLogSquaredErrors(ReadOnlySpan actual, ReadOnlySpan predicted, Span output) { @@ -127,4 +134,4 @@ public sealed class Msle : BiInputIndicatorBase output[i] = logError * logError; } } -} +} \ No newline at end of file diff --git a/lib/errors/pseudohuber/PseudoHuber.Tests.cs b/lib/errors/pseudohuber/PseudoHuber.Tests.cs index 74dfe88f..748c12b0 100644 --- a/lib/errors/pseudohuber/PseudoHuber.Tests.cs +++ b/lib/errors/pseudohuber/PseudoHuber.Tests.cs @@ -342,7 +342,7 @@ public class PseudoHuberTests } // Calculate batch - var batchResults = PseudoHuber.Calculate(actualSeries, predictedSeries, DefaultPeriod); + var batchResults = PseudoHuber.Batch(actualSeries, predictedSeries, DefaultPeriod); // Compare Assert.Equal(iterativeResults.Count, batchResults.Count); @@ -389,7 +389,7 @@ public class PseudoHuberTests predictedSeries.Add(bar.Time, predictedData[i]); } - var tseriesResult = PseudoHuber.Calculate(actualSeries, predictedSeries, DefaultPeriod); + var tseriesResult = PseudoHuber.Batch(actualSeries, predictedSeries, DefaultPeriod); PseudoHuber.Batch(actualData.AsSpan(), predictedData.AsSpan(), output.AsSpan(), DefaultPeriod); for (int i = 0; i < 100; i++) @@ -445,7 +445,7 @@ public class PseudoHuberTests actual.Add(DateTime.UtcNow, 101); predicted.Add(DateTime.UtcNow, 99); - Assert.Throws(() => PseudoHuber.Calculate(actual, predicted, 5)); + Assert.Throws(() => PseudoHuber.Batch(actual, predicted, 5)); } #endregion diff --git a/lib/errors/pseudohuber/PseudoHuber.cs b/lib/errors/pseudohuber/PseudoHuber.cs index 9bb7d7c2..9ca37aa0 100644 --- a/lib/errors/pseudohuber/PseudoHuber.cs +++ b/lib/errors/pseudohuber/PseudoHuber.cs @@ -64,7 +64,7 @@ public sealed class PseudoHuber : BiInputIndicatorBase /// /// Calculates Pseudo-Huber Loss for two time series. /// - public static TSeries Calculate(TSeries actual, TSeries predicted, int period, double delta = 1.0) + public static TSeries Batch(TSeries actual, TSeries predicted, int period, double delta = 1.0) { if (actual.Count != predicted.Count) { @@ -124,4 +124,11 @@ public sealed class PseudoHuber : BiInputIndicatorBase // Apply rolling mean ErrorHelpers.ApplyRollingMean(errors, output, period); } -} + + public static (TSeries Results, PseudoHuber Indicator) Calculate(TSeries actual, TSeries predicted, int period, double delta = 1.0) + { + var indicator = new PseudoHuber(period, delta); + TSeries results = Batch(actual, predicted, period, delta); + return (results, indicator); + } +} \ No newline at end of file diff --git a/lib/errors/quantile/QuantileLoss.Tests.cs b/lib/errors/quantile/QuantileLoss.Tests.cs index 84ec2767..6187feca 100644 --- a/lib/errors/quantile/QuantileLoss.Tests.cs +++ b/lib/errors/quantile/QuantileLoss.Tests.cs @@ -237,7 +237,7 @@ public class QuantileLossTests var iterativeResults = actualSeries.Zip(predictedSeries, (actual, predicted) => quantileLossIterative.Update(actual.Value, predicted.Value).Value).ToList(); - var batchResults = QuantileLoss.Calculate(actualSeries, predictedSeries, DefaultPeriod, 0.75); + var batchResults = QuantileLoss.Batch(actualSeries, predictedSeries, DefaultPeriod, 0.75); Assert.Equal(iterativeResults.Count, batchResults.Count); int count = iterativeResults.Count; @@ -288,7 +288,7 @@ public class QuantileLossTests predictedArr[i] = pred; } - var tseriesResult = QuantileLoss.Calculate(actualSeries, predictedSeries, DefaultPeriod, 0.75); + var tseriesResult = QuantileLoss.Batch(actualSeries, predictedSeries, DefaultPeriod, 0.75); QuantileLoss.Batch(actualArr.AsSpan(), predictedArr.AsSpan(), output.AsSpan(), DefaultPeriod, 0.75); for (int i = 0; i < 100; i++) @@ -337,7 +337,7 @@ public class QuantileLossTests predicted.Add(DateTime.UtcNow.Ticks, 98); - Assert.Throws(() => QuantileLoss.Calculate(actual, predicted, DefaultPeriod)); + Assert.Throws(() => QuantileLoss.Batch(actual, predicted, DefaultPeriod)); } [Fact] diff --git a/lib/errors/quantile/QuantileLoss.cs b/lib/errors/quantile/QuantileLoss.cs index 9e214abb..ec8e8a4e 100644 --- a/lib/errors/quantile/QuantileLoss.cs +++ b/lib/errors/quantile/QuantileLoss.cs @@ -60,7 +60,7 @@ public sealed class QuantileLoss : BiInputIndicatorBase return diff >= 0 ? Quantile * diff : (Quantile - 1.0) * diff; } - public static TSeries Calculate(TSeries actual, TSeries predicted, int period, double quantile = 0.5) + public static TSeries Batch(TSeries actual, TSeries predicted, int period, double quantile = 0.5) { if (actual.Count != predicted.Count) { @@ -220,4 +220,11 @@ public sealed class QuantileLoss : BiInputIndicatorBase } } } -} + + public static (TSeries Results, QuantileLoss Indicator) Calculate(TSeries actual, TSeries predicted, int period, double quantile = 0.5) + { + var indicator = new QuantileLoss(period, quantile); + TSeries results = Batch(actual, predicted, period, quantile); + return (results, indicator); + } +} \ No newline at end of file diff --git a/lib/errors/rae/Rae.Tests.cs b/lib/errors/rae/Rae.Tests.cs index 37837b3e..801f4396 100644 --- a/lib/errors/rae/Rae.Tests.cs +++ b/lib/errors/rae/Rae.Tests.cs @@ -230,7 +230,7 @@ public class RaeTests iterativeResults.Add(raeIterative.Update(actual[i], predicted[i]).Value); } - var batchResults = Rae.Calculate(actual, predicted, Period); + var batchResults = Rae.Batch(actual, predicted, Period); Assert.Equal(iterativeResults.Count, batchResults.Count); for (int i = 0; i < iterativeResults.Count; i++) @@ -271,7 +271,7 @@ public class RaeTests double[] predictedArr = predictedSeries.Values.ToArray(); double[] output = new double[100]; - var tseriesResult = Rae.Calculate(actualSeries, predictedSeries, Period); + var tseriesResult = Rae.Batch(actualSeries, predictedSeries, Period); Rae.Batch(actualArr.AsSpan(), predictedArr.AsSpan(), output.AsSpan(), Period); for (int i = 0; i < 100; i++) @@ -292,7 +292,7 @@ public class RaeTests } // 1. Batch Mode (static method) - var batchSeries = Rae.Calculate(actualSeries, predictedSeries, Period); + var batchSeries = Rae.Batch(actualSeries, predictedSeries, Period); double expected = batchSeries.Last.Value; // 2. Span Mode diff --git a/lib/errors/rae/Rae.cs b/lib/errors/rae/Rae.cs index a5ed02ba..898ad6f4 100644 --- a/lib/errors/rae/Rae.cs +++ b/lib/errors/rae/Rae.cs @@ -159,7 +159,7 @@ public sealed class Rae : AbstractBase public override TSeries Update(TSeries source) { - throw new NotSupportedException("RAE requires two inputs. Use Calculate(actualSeries, predictedSeries, period)."); + throw new NotSupportedException("RAE requires two inputs. Use Batch(actualSeries, predictedSeries, period)."); } public override void Prime(ReadOnlySpan source, TimeSpan? step = null) @@ -177,7 +177,7 @@ public sealed class Rae : AbstractBase Last = default; } - public static TSeries Calculate(TSeries actual, TSeries predicted, int period) + public static TSeries Batch(TSeries actual, TSeries predicted, int period) { if (actual.Count != predicted.Count) { @@ -355,4 +355,11 @@ public sealed class Rae : AbstractBase } } } + + public static (TSeries Results, Rae Indicator) Calculate(TSeries actual, TSeries predicted, int period) + { + var indicator = new Rae(period); + TSeries results = Batch(actual, predicted, period); + return (results, indicator); + } } \ No newline at end of file diff --git a/lib/errors/rmse/Rmse.Tests.cs b/lib/errors/rmse/Rmse.Tests.cs index c7e6067a..f78d23fd 100644 --- a/lib/errors/rmse/Rmse.Tests.cs +++ b/lib/errors/rmse/Rmse.Tests.cs @@ -241,7 +241,7 @@ public class RmseTests predicted.Add(now.AddMinutes(i), i * 10 + 5); } - var results = Rmse.Calculate(actual, predicted, 3); + var results = Rmse.Batch(actual, predicted, 3); Assert.Equal(10, results.Count); // All errors are 5, MSE = 25, RMSE = 5 diff --git a/lib/errors/rmse/Rmse.cs b/lib/errors/rmse/Rmse.cs index 51fe3c7c..43133cef 100644 --- a/lib/errors/rmse/Rmse.cs +++ b/lib/errors/rmse/Rmse.cs @@ -45,7 +45,7 @@ public sealed class Rmse : BiInputIndicatorBase /// /// Calculates RMSE for entire series. /// - public static TSeries Calculate(TSeries actual, TSeries predicted, int period) + public static TSeries Batch(TSeries actual, TSeries predicted, int period) => CalculateImpl(actual, predicted, period, Batch); /// @@ -70,4 +70,11 @@ public sealed class Rmse : BiInputIndicatorBase ErrorHelpers.ComputeSquaredErrors(actual, predicted, sqErrors); ErrorHelpers.ApplyRollingMeanSqrt(sqErrors, output, period); } -} + + public static (TSeries Results, Rmse Indicator) Calculate(TSeries actual, TSeries predicted, int period) + { + var indicator = new Rmse(period); + TSeries results = Batch(actual, predicted, period); + return (results, indicator); + } +} \ No newline at end of file diff --git a/lib/errors/rmsle/Rmsle.Tests.cs b/lib/errors/rmsle/Rmsle.Tests.cs index a65b99ca..3330715c 100644 --- a/lib/errors/rmsle/Rmsle.Tests.cs +++ b/lib/errors/rmsle/Rmsle.Tests.cs @@ -222,7 +222,7 @@ public class RmsleTests iterativeResults.Add(rmsleIterative.Update(actualSeries[i], predictedSeries[i]).Value); } - var batchResults = Rmsle.Calculate(actualSeries, predictedSeries, 10); + var batchResults = Rmsle.Batch(actualSeries, predictedSeries, 10); Assert.Equal(iterativeResults.Count, batchResults.Count); for (int i = 0; i < iterativeResults.Count; i++) @@ -266,7 +266,7 @@ public class RmsleTests predictedSeries.Add(bar.Time, bar.Close * 0.95); } - var tseriesResult = Rmsle.Calculate(actualSeries, predictedSeries, 10); + var tseriesResult = Rmsle.Batch(actualSeries, predictedSeries, 10); Rmsle.Batch(actualArr.AsSpan(), predictedArr.AsSpan(), output.AsSpan(), 10); for (int i = 0; i < 100; i++) @@ -300,7 +300,7 @@ public class RmsleTests actual.Add(DateTime.UtcNow.Ticks + 1, 100); predicted.Add(DateTime.UtcNow.Ticks, 90); - Assert.Throws(() => Rmsle.Calculate(actual, predicted, 5)); + Assert.Throws(() => Rmsle.Batch(actual, predicted, 5)); } [Fact] diff --git a/lib/errors/rmsle/Rmsle.cs b/lib/errors/rmsle/Rmsle.cs index 07e0b1de..c51bd86b 100644 --- a/lib/errors/rmsle/Rmsle.cs +++ b/lib/errors/rmsle/Rmsle.cs @@ -49,7 +49,7 @@ public sealed class Rmsle : BiInputIndicatorBase /// /// Calculates RMSLE for entire series. /// - public static TSeries Calculate(TSeries actual, TSeries predicted, int period) + public static TSeries Batch(TSeries actual, TSeries predicted, int period) => CalculateImpl(actual, predicted, period, Batch); /// @@ -75,6 +75,13 @@ public sealed class Rmsle : BiInputIndicatorBase ErrorHelpers.ApplyRollingMeanSqrt(errors, output, period); } + public static (TSeries Results, Rmsle Indicator) Calculate(TSeries actual, TSeries predicted, int period) + { + var indicator = new Rmsle(period); + TSeries results = Batch(actual, predicted, period); + return (results, indicator); + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] private static void ComputeLogSquaredErrors(ReadOnlySpan actual, ReadOnlySpan predicted, Span output) { @@ -130,4 +137,4 @@ public sealed class Rmsle : BiInputIndicatorBase output[i] = logError * logError; } } -} +} \ No newline at end of file diff --git a/lib/errors/rse/Rse.Tests.cs b/lib/errors/rse/Rse.Tests.cs index ce2f340d..ec784ad3 100644 --- a/lib/errors/rse/Rse.Tests.cs +++ b/lib/errors/rse/Rse.Tests.cs @@ -253,7 +253,7 @@ public class RseTests iterativeResults.Add(rseIterative.Update(actual[i], predicted[i]).Value); } - var batchResults = Rse.Calculate(actual, predicted, Period); + var batchResults = Rse.Batch(actual, predicted, Period); Assert.Equal(iterativeResults.Count, batchResults.Count); for (int i = 0; i < iterativeResults.Count; i++) @@ -294,7 +294,7 @@ public class RseTests double[] predictedArr = predictedSeries.Values.ToArray(); double[] output = new double[100]; - var tseriesResult = Rse.Calculate(actualSeries, predictedSeries, Period); + var tseriesResult = Rse.Batch(actualSeries, predictedSeries, Period); Rse.Batch(actualArr.AsSpan(), predictedArr.AsSpan(), output.AsSpan(), Period); for (int i = 0; i < 100; i++) @@ -315,7 +315,7 @@ public class RseTests } // 1. Batch Mode (static method) - var batchSeries = Rse.Calculate(actualSeries, predictedSeries, Period); + var batchSeries = Rse.Batch(actualSeries, predictedSeries, Period); double expected = batchSeries.Last.Value; // 2. Span Mode diff --git a/lib/errors/rse/Rse.cs b/lib/errors/rse/Rse.cs index f122e9db..8037f11e 100644 --- a/lib/errors/rse/Rse.cs +++ b/lib/errors/rse/Rse.cs @@ -166,7 +166,7 @@ public sealed class Rse : AbstractBase public override TSeries Update(TSeries source) { - throw new NotSupportedException("RSE requires two inputs. Use Calculate(actualSeries, predictedSeries, period)."); + throw new NotSupportedException("RSE requires two inputs. Use Batch(actualSeries, predictedSeries, period)."); } public override void Prime(ReadOnlySpan source, TimeSpan? step = null) @@ -184,7 +184,7 @@ public sealed class Rse : AbstractBase Last = default; } - public static TSeries Calculate(TSeries actual, TSeries predicted, int period) + public static TSeries Batch(TSeries actual, TSeries predicted, int period) { if (actual.Count != predicted.Count) { @@ -366,4 +366,11 @@ public sealed class Rse : AbstractBase } } } -} + + public static (TSeries Results, Rse Indicator) Calculate(TSeries actual, TSeries predicted, int period) + { + var indicator = new Rse(period); + TSeries results = Batch(actual, predicted, period); + return (results, indicator); + } +} \ No newline at end of file diff --git a/lib/errors/rsquared/Rsquared.Tests.cs b/lib/errors/rsquared/Rsquared.Tests.cs index 86d44f3b..cbab3b15 100644 --- a/lib/errors/rsquared/Rsquared.Tests.cs +++ b/lib/errors/rsquared/Rsquared.Tests.cs @@ -290,7 +290,7 @@ public class RsquaredTests iterativeResults.Add(r2Iterative.Update(actual[i], predicted[i]).Value); } - var batchResults = Rsquared.Calculate(actual, predicted, Period); + var batchResults = Rsquared.Batch(actual, predicted, Period); Assert.Equal(iterativeResults.Count, batchResults.Count); for (int i = 0; i < iterativeResults.Count; i++) @@ -331,7 +331,7 @@ public class RsquaredTests double[] predictedArr = predictedSeries.Values.ToArray(); double[] output = new double[100]; - var tseriesResult = Rsquared.Calculate(actualSeries, predictedSeries, Period); + var tseriesResult = Rsquared.Batch(actualSeries, predictedSeries, Period); Rsquared.Batch(actualArr.AsSpan(), predictedArr.AsSpan(), output.AsSpan(), Period); for (int i = 0; i < 100; i++) @@ -352,7 +352,7 @@ public class RsquaredTests } // 1. Batch Mode (static method) - var batchSeries = Rsquared.Calculate(actualSeries, predictedSeries, Period); + var batchSeries = Rsquared.Batch(actualSeries, predictedSeries, Period); double expected = batchSeries.Last.Value; // 2. Span Mode diff --git a/lib/errors/rsquared/Rsquared.cs b/lib/errors/rsquared/Rsquared.cs index 58be0cd9..addb7218 100644 --- a/lib/errors/rsquared/Rsquared.cs +++ b/lib/errors/rsquared/Rsquared.cs @@ -160,7 +160,7 @@ public sealed class Rsquared : AbstractBase public override TSeries Update(TSeries source) { - throw new NotSupportedException("R² requires two inputs. Use Calculate(actualSeries, predictedSeries, period)."); + throw new NotSupportedException("R² requires two inputs. Use Batch(actualSeries, predictedSeries, period)."); } public override void Prime(ReadOnlySpan source, TimeSpan? step = null) @@ -178,7 +178,7 @@ public sealed class Rsquared : AbstractBase Last = default; } - public static TSeries Calculate(TSeries actual, TSeries predicted, int period) + public static TSeries Batch(TSeries actual, TSeries predicted, int period) { if (actual.Count != predicted.Count) { @@ -360,4 +360,11 @@ public sealed class Rsquared : AbstractBase } } } -} + + public static (TSeries Results, Rsquared Indicator) Calculate(TSeries actual, TSeries predicted, int period) + { + var indicator = new Rsquared(period); + TSeries results = Batch(actual, predicted, period); + return (results, indicator); + } +} \ No newline at end of file diff --git a/lib/errors/smape/Smape.Tests.cs b/lib/errors/smape/Smape.Tests.cs index 389b2eee..4d452388 100644 --- a/lib/errors/smape/Smape.Tests.cs +++ b/lib/errors/smape/Smape.Tests.cs @@ -217,7 +217,7 @@ public class SmapeTests iterativeResults.Add(smapeIterative.Update(actualSeries[i], predictedSeries[i]).Value); } - var batchResults = Smape.Calculate(actualSeries, predictedSeries, 10); + var batchResults = Smape.Batch(actualSeries, predictedSeries, 10); Assert.Equal(iterativeResults.Count, batchResults.Count); for (int i = 0; i < iterativeResults.Count; i++) @@ -261,7 +261,7 @@ public class SmapeTests predictedSeries.Add(bar.Time, bar.Close * 0.95); } - var tseriesResult = Smape.Calculate(actualSeries, predictedSeries, 10); + var tseriesResult = Smape.Batch(actualSeries, predictedSeries, 10); Smape.Batch(actualArr.AsSpan(), predictedArr.AsSpan(), output.AsSpan(), 10); for (int i = 0; i < 100; i++) @@ -295,7 +295,7 @@ public class SmapeTests actual.Add(DateTime.UtcNow.Ticks + 1, 100); predicted.Add(DateTime.UtcNow.Ticks, 90); - Assert.Throws(() => Smape.Calculate(actual, predicted, 5)); + Assert.Throws(() => Smape.Batch(actual, predicted, 5)); } [Fact] diff --git a/lib/errors/smape/Smape.cs b/lib/errors/smape/Smape.cs index 315c1a7f..7e425826 100644 --- a/lib/errors/smape/Smape.cs +++ b/lib/errors/smape/Smape.cs @@ -43,7 +43,7 @@ public sealed class Smape : BiInputIndicatorBase /// /// Calculates SMAPE for entire series. /// - public static TSeries Calculate(TSeries actual, TSeries predicted, int period) + public static TSeries Batch(TSeries actual, TSeries predicted, int period) => CalculateImpl(actual, predicted, period, Batch); /// @@ -70,6 +70,13 @@ public sealed class Smape : BiInputIndicatorBase ErrorHelpers.ApplyRollingMean(symErrors, output, period); } + public static (TSeries Results, Smape Indicator) Calculate(TSeries actual, TSeries predicted, int period) + { + var indicator = new Smape(period); + TSeries results = Batch(actual, predicted, period); + return (results, indicator); + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] private static void ComputeSmapeErrors(ReadOnlySpan actual, ReadOnlySpan predicted, Span output) { @@ -122,4 +129,4 @@ public sealed class Smape : BiInputIndicatorBase output[i] = sumAbs > Epsilon ? 200.0 * absDiff / sumAbs : 0.0; } } -} +} \ No newline at end of file diff --git a/lib/errors/theilu/TheilU.Tests.cs b/lib/errors/theilu/TheilU.Tests.cs index adbdc030..b7b2e5c9 100644 --- a/lib/errors/theilu/TheilU.Tests.cs +++ b/lib/errors/theilu/TheilU.Tests.cs @@ -192,7 +192,7 @@ public class TheilUTests iterativeResults.Add(theilUIterative.Update(new TValue(bar.Time, bar.Close), new TValue(bar.Time, predicted)).Value); } - var batchResults = TheilU.Calculate(actualSeries, predictedSeries, DefaultPeriod); + var batchResults = TheilU.Batch(actualSeries, predictedSeries, DefaultPeriod); Assert.Equal(iterativeResults.Count, batchResults.Count); for (int i = 0; i < iterativeResults.Count; i++) @@ -236,7 +236,7 @@ public class TheilUTests predictedArr[i] = pred; } - var tseriesResult = TheilU.Calculate(actualSeries, predictedSeries, DefaultPeriod); + var tseriesResult = TheilU.Batch(actualSeries, predictedSeries, DefaultPeriod); TheilU.Batch(actualArr.AsSpan(), predictedArr.AsSpan(), output.AsSpan(), DefaultPeriod); for (int i = 0; i < 100; i++) @@ -285,7 +285,7 @@ public class TheilUTests predicted.Add(DateTime.UtcNow.Ticks, 98); - Assert.Throws(() => TheilU.Calculate(actual, predicted, DefaultPeriod)); + Assert.Throws(() => TheilU.Batch(actual, predicted, DefaultPeriod)); } [Fact] diff --git a/lib/errors/theilu/TheilU.cs b/lib/errors/theilu/TheilU.cs index 139c4659..b21ca932 100644 --- a/lib/errors/theilu/TheilU.cs +++ b/lib/errors/theilu/TheilU.cs @@ -75,7 +75,7 @@ public sealed class TheilU : AbstractBase public override TSeries Update(TSeries source) { - throw new NotSupportedException("TheilU requires two inputs. Use Calculate(actualSeries, predictedSeries, period)."); + throw new NotSupportedException("TheilU requires two inputs. Use Batch(actualSeries, predictedSeries, period)."); } [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -170,7 +170,7 @@ public sealed class TheilU : AbstractBase Last = default; } - public static TSeries Calculate(TSeries actual, TSeries predicted, int period) + public static TSeries Batch(TSeries actual, TSeries predicted, int period) { if (actual.Count != predicted.Count) { @@ -352,4 +352,11 @@ public sealed class TheilU : AbstractBase } } } -} + + public static (TSeries Results, TheilU Indicator) Calculate(TSeries actual, TSeries predicted, int period) + { + var indicator = new TheilU(period); + TSeries results = Batch(actual, predicted, period); + return (results, indicator); + } +} \ No newline at end of file diff --git a/lib/errors/tukey/TukeyBiweight.Tests.cs b/lib/errors/tukey/TukeyBiweight.Tests.cs index ecd70ce0..d0863d0c 100644 --- a/lib/errors/tukey/TukeyBiweight.Tests.cs +++ b/lib/errors/tukey/TukeyBiweight.Tests.cs @@ -235,7 +235,7 @@ public class TukeyBiweightTests iterativeResults.Add(tukeyIterative.Update(actual, predicted).Value); } - var batchResults = TukeyBiweight.Calculate(actualSeries, predictedSeries, DefaultPeriod); + var batchResults = TukeyBiweight.Batch(actualSeries, predictedSeries, DefaultPeriod); Assert.Equal(iterativeResults.Count, batchResults.Count); for (int i = 0; i < iterativeResults.Count; i++) @@ -282,7 +282,7 @@ public class TukeyBiweightTests predictedArr[i] = pred; } - var tseriesResult = TukeyBiweight.Calculate(actualSeries, predictedSeries, DefaultPeriod); + var tseriesResult = TukeyBiweight.Batch(actualSeries, predictedSeries, DefaultPeriod); TukeyBiweight.Batch(actualArr.AsSpan(), predictedArr.AsSpan(), output.AsSpan(), DefaultPeriod); for (int i = 0; i < 100; i++) @@ -331,7 +331,7 @@ public class TukeyBiweightTests predicted.Add(DateTime.UtcNow.Ticks, 98); - Assert.Throws(() => TukeyBiweight.Calculate(actual, predicted, DefaultPeriod)); + Assert.Throws(() => TukeyBiweight.Batch(actual, predicted, DefaultPeriod)); } [Fact] diff --git a/lib/errors/tukey/TukeyBiweight.cs b/lib/errors/tukey/TukeyBiweight.cs index 0768390f..8a3d0bda 100644 --- a/lib/errors/tukey/TukeyBiweight.cs +++ b/lib/errors/tukey/TukeyBiweight.cs @@ -66,7 +66,7 @@ public sealed class TukeyBiweight : BiInputIndicatorBase return _cSquaredOver6 * (1.0 - cubed); } - public static TSeries Calculate(TSeries actual, TSeries predicted, int period, double c = DefaultC) + public static TSeries Batch(TSeries actual, TSeries predicted, int period, double c = DefaultC) { if (actual.Count != predicted.Count) { @@ -129,4 +129,11 @@ public sealed class TukeyBiweight : BiInputIndicatorBase ArrayPool.Shared.Return(rented, clearArray: false); } } -} + + public static (TSeries Results, TukeyBiweight Indicator) Calculate(TSeries actual, TSeries predicted, int period, double c = DefaultC) + { + var indicator = new TukeyBiweight(period, c); + TSeries results = Batch(actual, predicted, period, c); + return (results, indicator); + } +} \ No newline at end of file diff --git a/lib/errors/wmape/Wmape.Tests.cs b/lib/errors/wmape/Wmape.Tests.cs index 42a9e4a7..98b35813 100644 --- a/lib/errors/wmape/Wmape.Tests.cs +++ b/lib/errors/wmape/Wmape.Tests.cs @@ -186,7 +186,7 @@ public class WmapeTests predictedSeries.Add(bar.Time, bar.Close * (1 + (i % 2 == 0 ? 0.02 : -0.02))); } - var batchResults = Wmape.Calculate(actualSeries, predictedSeries, DefaultPeriod); + var batchResults = Wmape.Batch(actualSeries, predictedSeries, DefaultPeriod); var iterativeResults = new List(); for (int i = 0; i < actualSeries.Count; i++) @@ -236,7 +236,7 @@ public class WmapeTests predictedArr[i] = pred; } - var tseriesResult = Wmape.Calculate(actualSeries, predictedSeries, DefaultPeriod); + var tseriesResult = Wmape.Batch(actualSeries, predictedSeries, DefaultPeriod); Wmape.Batch(actualArr.AsSpan(), predictedArr.AsSpan(), output.AsSpan(), DefaultPeriod); for (int i = 0; i < 100; i++) @@ -285,7 +285,7 @@ public class WmapeTests predicted.Add(DateTime.UtcNow.Ticks, 98); - Assert.Throws(() => Wmape.Calculate(actual, predicted, DefaultPeriod)); + Assert.Throws(() => Wmape.Batch(actual, predicted, DefaultPeriod)); } [Fact] diff --git a/lib/errors/wmape/Wmape.cs b/lib/errors/wmape/Wmape.cs index 9363977d..12fdcd56 100644 --- a/lib/errors/wmape/Wmape.cs +++ b/lib/errors/wmape/Wmape.cs @@ -138,7 +138,7 @@ public sealed class Wmape : AbstractBase public override TSeries Update(TSeries source) { - throw new NotSupportedException("WMAPE requires two inputs. Use Calculate(actualSeries, predictedSeries, period)."); + throw new NotSupportedException("WMAPE requires two inputs. Use Batch(actualSeries, predictedSeries, period)."); } public override void Prime(ReadOnlySpan source, TimeSpan? step = null) @@ -155,7 +155,7 @@ public sealed class Wmape : AbstractBase Last = default; } - public static TSeries Calculate(TSeries actual, TSeries predicted, int period) + public static TSeries Batch(TSeries actual, TSeries predicted, int period) { if (actual.Count != predicted.Count) { @@ -345,4 +345,11 @@ public sealed class Wmape : AbstractBase } } } -} + + public static (TSeries Results, Wmape Indicator) Calculate(TSeries actual, TSeries predicted, int period) + { + var indicator = new Wmape(period); + TSeries results = Batch(actual, predicted, period); + return (results, indicator); + } +} \ No newline at end of file diff --git a/lib/errors/wrmse/Wrmse.Tests.cs b/lib/errors/wrmse/Wrmse.Tests.cs index dd22b0c2..b84ce29e 100644 --- a/lib/errors/wrmse/Wrmse.Tests.cs +++ b/lib/errors/wrmse/Wrmse.Tests.cs @@ -320,7 +320,7 @@ public class WrmseTests predicted.Add(now.AddMinutes(i), i * 10 + 5); } - var results = Wrmse.Calculate(actual, predicted, 3); + var results = Wrmse.Batch(actual, predicted, 3); Assert.Equal(10, results.Count); // All errors are 5, MSE = 25, RMSE = 5 @@ -342,7 +342,7 @@ public class WrmseTests weights.Add(now.AddMinutes(i), 2.0); // Weight = 2 } - var results = Wrmse.Calculate(actual, predicted, weights, 3); + var results = Wrmse.Batch(actual, predicted, weights, 3); Assert.Equal(10, results.Count); // Weighted error = 2 * 100 = 200 per point, sum weights = 6 (period=3) @@ -366,7 +366,7 @@ public class WrmseTests } } - Assert.Throws(() => Wrmse.Calculate(actual, predicted, 3)); + Assert.Throws(() => Wrmse.Batch(actual, predicted, 3)); } [Fact] @@ -387,7 +387,7 @@ public class WrmseTests } } - Assert.Throws(() => Wrmse.Calculate(actual, predicted, weights, 3)); + Assert.Throws(() => Wrmse.Batch(actual, predicted, weights, 3)); } [Fact] diff --git a/lib/errors/wrmse/Wrmse.cs b/lib/errors/wrmse/Wrmse.cs index e917ae8d..397e3706 100644 --- a/lib/errors/wrmse/Wrmse.cs +++ b/lib/errors/wrmse/Wrmse.cs @@ -197,7 +197,7 @@ public sealed class Wrmse : AbstractBase /// public override TSeries Update(TSeries source) { - throw new NotSupportedException("WRMSE requires two inputs. Use Calculate(actualSeries, predictedSeries, period) or Calculate(actualSeries, predictedSeries, weightsSeries, period)."); + throw new NotSupportedException("WRMSE requires two inputs. Use Batch(actualSeries, predictedSeries, period) or Batch(actualSeries, predictedSeries, weightsSeries, period)."); } /// @@ -221,7 +221,7 @@ public sealed class Wrmse : AbstractBase /// /// Calculates WRMSE for entire series with uniform weights. /// - public static TSeries Calculate(TSeries actual, TSeries predicted, int period) + public static TSeries Batch(TSeries actual, TSeries predicted, int period) { if (actual.Count != predicted.Count) { @@ -246,7 +246,7 @@ public sealed class Wrmse : AbstractBase /// /// Calculates WRMSE for entire series with custom weights. /// - public static TSeries Calculate(TSeries actual, TSeries predicted, TSeries weights, int period) + public static TSeries Batch(TSeries actual, TSeries predicted, TSeries weights, int period) { if (actual.Count != predicted.Count || actual.Count != weights.Count) { @@ -332,4 +332,11 @@ public sealed class Wrmse : AbstractBase ErrorHelpers.ComputeWeightedErrors(actual, predicted, weights, weightedErrors); ErrorHelpers.ApplyRollingWeightedMeanSqrt(weightedErrors, weights, output, period); } -} + + public static (TSeries Results, Wrmse Indicator) Calculate(TSeries actual, TSeries predicted, int period) + { + var indicator = new Wrmse(period); + TSeries results = Batch(actual, predicted, period); + return (results, indicator); + } +} \ No newline at end of file diff --git a/lib/filters/bessel/Bessel.Tests.cs b/lib/filters/bessel/Bessel.Tests.cs index beaca57a..5567e84d 100644 --- a/lib/filters/bessel/Bessel.Tests.cs +++ b/lib/filters/bessel/Bessel.Tests.cs @@ -28,11 +28,11 @@ public class BesselTests double[] output = new double[5]; var exLength = Assert.Throws(() => - Bessel.Calculate(source.AsSpan(), output.AsSpan(), 1)); + Bessel.Batch(source.AsSpan(), output.AsSpan(), 1)); Assert.Equal("length", exLength.ParamName); var exLengthZero = Assert.Throws(() => - Bessel.Calculate(source.AsSpan(), output.AsSpan(), 0)); + Bessel.Batch(source.AsSpan(), output.AsSpan(), 0)); Assert.Equal("length", exLengthZero.ParamName); } @@ -43,7 +43,7 @@ public class BesselTests double[] wrongSizeOutput = new double[3]; var ex = Assert.Throws(() => - Bessel.Calculate(source.AsSpan(), wrongSizeOutput.AsSpan(), 14)); + Bessel.Batch(source.AsSpan(), wrongSizeOutput.AsSpan(), 14)); Assert.Equal("output", ex.ParamName); } @@ -252,7 +252,7 @@ public class BesselTests var tseriesResult = Bessel.Calculate(series, 14).Results; - Bessel.Calculate(source.AsSpan(), output.AsSpan(), 14); + Bessel.Batch(source.AsSpan(), output.AsSpan(), 14); for (int i = 0; i < 100; i++) { @@ -276,7 +276,7 @@ public class BesselTests var tValues = series.Values.ToArray(); var spanInput = new ReadOnlySpan(tValues); var spanOutput = new double[tValues.Length]; - Bessel.Calculate(spanInput, spanOutput, length); + Bessel.Batch(spanInput, spanOutput, length); double spanResult = spanOutput[^1]; // 3. Streaming Mode diff --git a/lib/filters/bessel/Bessel.Validation.Tests.cs b/lib/filters/bessel/Bessel.Validation.Tests.cs index ab034657..1ab978ac 100644 --- a/lib/filters/bessel/Bessel.Validation.Tests.cs +++ b/lib/filters/bessel/Bessel.Validation.Tests.cs @@ -47,7 +47,7 @@ public sealed class BesselValidationTests : IDisposable // Same data via Span API var src = _testData.Data.Values.ToArray(); var outSpan = new double[src.Length]; - Bessel.Calculate(src.AsSpan(), outSpan.AsSpan(), length); + Bessel.Batch(src.AsSpan(), outSpan.AsSpan(), length); // Verify last window for convergence and consistency ValidationHelper.VerifyData(qResult, outSpan, lookback: 0, skip: length, tolerance: ValidationHelper.DefaultTolerance); diff --git a/lib/filters/bessel/Bessel.cs b/lib/filters/bessel/Bessel.cs index ee61f6c0..6e7aa77a 100644 --- a/lib/filters/bessel/Bessel.cs +++ b/lib/filters/bessel/Bessel.cs @@ -481,12 +481,6 @@ public sealed class Bessel : AbstractBase /// Complexity: O(n) where n = source.Count /// The returned indicator maintains state and can continue processing new values. /// - public static (TSeries Results, Bessel Indicator) Calculate(TSeries source, int length) - { - var bessel = new Bessel(length); - TSeries results = bessel.Update(source); - return (results, bessel); - } /// /// Calculates filtered values for a span of doubles (stateless batch processing). @@ -504,7 +498,7 @@ public sealed class Bessel : AbstractBase /// SIMD optimization is not applicable due to IIR recursive data dependency. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void Calculate(ReadOnlySpan source, Span output, int length) + public static void Batch(ReadOnlySpan source, Span output, int length) { if (length < 2) { @@ -531,6 +525,12 @@ public sealed class Bessel : AbstractBase CalculateCore(source, output, c1, c2, c3, length, ref state); } + public static (TSeries Results, Bessel Indicator) Calculate(TSeries source, int length) + { + var bessel = new Bessel(length); + TSeries results = bessel.Update(source); + return (results, bessel); + } /// /// Event handler for reactive updates from subscribed publishers. @@ -567,4 +567,4 @@ public sealed class Bessel : AbstractBase } base.Dispose(disposing); } -} +} \ No newline at end of file diff --git a/lib/filters/bilateral/Bilateral.Tests.cs b/lib/filters/bilateral/Bilateral.Tests.cs index e1d70cd0..b09bf0aa 100644 --- a/lib/filters/bilateral/Bilateral.Tests.cs +++ b/lib/filters/bilateral/Bilateral.Tests.cs @@ -151,7 +151,7 @@ public class BilateralTests var tValues = series.Values.ToArray(); var spanInput = new ReadOnlySpan(tValues); var spanOutput = new double[tValues.Length]; - Bilateral.Calculate(spanInput, spanOutput, period); + Bilateral.Batch(spanInput, spanOutput, period); double spanResult = spanOutput[^1]; // 3. Streaming Mode diff --git a/lib/filters/bilateral/Bilateral.Validation.Tests.cs b/lib/filters/bilateral/Bilateral.Validation.Tests.cs index e64aed48..0b9563f0 100644 --- a/lib/filters/bilateral/Bilateral.Validation.Tests.cs +++ b/lib/filters/bilateral/Bilateral.Validation.Tests.cs @@ -96,7 +96,7 @@ public sealed class BilateralValidationTests : IDisposable { // Calculate QuanTAlib Bilateral (Span API) double[] qOutput = new double[sourceData.Length]; - global::QuanTAlib.Bilateral.Calculate(sourceData.AsSpan(), qOutput.AsSpan(), period, sigmaSRatio, sigmaRMult); + global::QuanTAlib.Bilateral.Batch(sourceData.AsSpan(), qOutput.AsSpan(), period, sigmaSRatio, sigmaRMult); // Calculate Reference Bilateral var refResult = GetReferenceData(period, sigmaSRatio, sigmaRMult); diff --git a/lib/filters/bilateral/Bilateral.cs b/lib/filters/bilateral/Bilateral.cs index 71ffa93a..0c46e767 100644 --- a/lib/filters/bilateral/Bilateral.cs +++ b/lib/filters/bilateral/Bilateral.cs @@ -332,17 +332,11 @@ public sealed class Bilateral : AbstractBase /// /// Calculates bilateral filter values for a TSeries and returns both results and a primed indicator. /// - public static (TSeries Results, Bilateral Indicator) Calculate(TSeries source, int period, double sigmaSRatio = 0.5, double sigmaRMult = 1.0) - { - var indicator = new Bilateral(period, sigmaSRatio, sigmaRMult); - var results = indicator.Update(source); - return (results, indicator); - } /// /// Calculates bilateral filter values using spans (zero allocation in hot path). /// - public static void Calculate(ReadOnlySpan source, Span destination, int period, double sigmaSRatio = 0.5, double sigmaRMult = 1.0) + private static void BatchCore(ReadOnlySpan source, Span destination, int period, double sigmaSRatio = 0.5, double sigmaRMult = 1.0) { if (period <= 0) { @@ -520,7 +514,13 @@ public sealed class Bilateral : AbstractBase /// public static void Batch(ReadOnlySpan source, Span destination, int period, double sigmaSRatio = 0.5, double sigmaRMult = 1.0) { - Calculate(source, destination, period, sigmaSRatio, sigmaRMult); + BatchCore(source, destination, period, sigmaSRatio, sigmaRMult); + } + public static (TSeries Results, Bilateral Indicator) Calculate(TSeries source, int period, double sigmaSRatio = 0.5, double sigmaRMult = 1.0) + { + var indicator = new Bilateral(period, sigmaSRatio, sigmaRMult); + var results = indicator.Update(source); + return (results, indicator); } /// @@ -534,4 +534,4 @@ public sealed class Bilateral : AbstractBase } base.Dispose(disposing); } -} +} \ No newline at end of file diff --git a/lib/filters/bpf/Bpf.Tests.cs b/lib/filters/bpf/Bpf.Tests.cs index 28abce10..b24dabcc 100644 --- a/lib/filters/bpf/Bpf.Tests.cs +++ b/lib/filters/bpf/Bpf.Tests.cs @@ -39,7 +39,7 @@ public class BpfTests // 1. Span Mode double[] spanOutput = new double[series.Count]; - Bpf.Calculate(series.Values.ToArray(), spanOutput, lowerPeriod, upperPeriod); + Bpf.Batch(series.Values.ToArray(), spanOutput, lowerPeriod, upperPeriod); // 2. TSeries Batch Mode var bpfBatch = new Bpf(lowerPeriod, upperPeriod); @@ -111,7 +111,7 @@ public class BpfTests double[] input = Enumerable.Repeat(100.0, 500).ToArray(); double[] output = new double[500]; - Bpf.Calculate(input, output, 10, 20); + Bpf.Batch(input, output, 10, 20); // Last value should be close to 0 Assert.True(Math.Abs(output[^1]) < 1e-6); diff --git a/lib/filters/bpf/Bpf.Validation.Tests.cs b/lib/filters/bpf/Bpf.Validation.Tests.cs index 32a094ee..159eda8d 100644 --- a/lib/filters/bpf/Bpf.Validation.Tests.cs +++ b/lib/filters/bpf/Bpf.Validation.Tests.cs @@ -32,9 +32,9 @@ public class BpfValidationTests double[] out100 = new double[T]; // Instantiate BPF with LowerPeriod=40 (HP cutoff), UpperPeriod=10 (LP cutoff) - Bpf.Calculate(sine5, out5, 40, 10); - Bpf.Calculate(sine15, out15, 40, 10); - Bpf.Calculate(sine100, out100, 40, 10); + Bpf.Batch(sine5, out5, 40, 10); + Bpf.Batch(sine15, out15, 40, 10); + Bpf.Batch(sine100, out100, 40, 10); // Analysis of results (last 100 samples to avoid warmup) double amp5 = GetAmplitude(out5); diff --git a/lib/filters/bpf/Bpf.cs b/lib/filters/bpf/Bpf.cs index 80e9ebcd..4cd0bf5a 100644 --- a/lib/filters/bpf/Bpf.cs +++ b/lib/filters/bpf/Bpf.cs @@ -115,7 +115,7 @@ public sealed class Bpf : AbstractBase double[] values = source.Values.ToArray(); double[] results = new double[values.Length]; - Calculate(values, results, LowerPeriod, UpperPeriod); + Batch(values, results, LowerPeriod, UpperPeriod); TSeries output = []; for (int i = 0; i < values.Length; i++) @@ -181,24 +181,15 @@ public sealed class Bpf : AbstractBase return Last; } - public override void Reset() + public static TSeries Batch(TSeries source, int lowerPeriod, int upperPeriod) { - _state = default; - _state.LastValid = double.NaN; - _p_state = default; - Last = default; + var indicator = new Bpf(lowerPeriod, upperPeriod); + return indicator.Update(source); } - public override void Prime(ReadOnlySpan source, TimeSpan? step = null) - { - foreach (double val in source) - { - Update(new TValue(DateTime.UtcNow, val), isNew: true); - } - } [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void Calculate(ReadOnlySpan source, Span output, int lowerPeriod, int upperPeriod) + public static void Batch(ReadOnlySpan source, Span output, int lowerPeriod, int upperPeriod) { if (source.Length != output.Length) { @@ -267,6 +258,29 @@ public sealed class Bpf : AbstractBase } } + public override void Reset() + { + _state = default; + _state.LastValid = double.NaN; + _p_state = default; + Last = default; + } + + public override void Prime(ReadOnlySpan source, TimeSpan? step = null) + { + foreach (double val in source) + { + Update(new TValue(DateTime.UtcNow, val), isNew: true); + } + } + + public static (TSeries Results, Bpf Indicator) Calculate(TSeries source, int lowerPeriod, int upperPeriod) + { + var indicator = new Bpf(lowerPeriod, upperPeriod); + TSeries results = indicator.Update(source); + return (results, indicator); + } + /// /// Unsubscribes from the source publisher if one was provided during construction. /// diff --git a/lib/filters/butter/Butter.Tests.cs b/lib/filters/butter/Butter.Tests.cs index 67b20dec..4da9bc66 100644 --- a/lib/filters/butter/Butter.Tests.cs +++ b/lib/filters/butter/Butter.Tests.cs @@ -21,7 +21,7 @@ public class ButterTests { var source = new double[10]; var destination = new double[5]; - Assert.Throws(() => Butter.Calculate(source, destination, 5, double.NaN)); + Assert.Throws(() => Butter.Batch(source, destination, 5, double.NaN)); } [Fact] @@ -79,7 +79,7 @@ public class ButterTests var tValues = series.Values.ToArray(); var spanInput = new ReadOnlySpan(tValues); var spanOutput = new double[tValues.Length]; - Butter.Calculate(spanInput, spanOutput, period, double.NaN); + Butter.Batch(spanInput, spanOutput, period, double.NaN); double spanResult = spanOutput[^1]; // 3. Streaming Mode diff --git a/lib/filters/butter/Butter.cs b/lib/filters/butter/Butter.cs index 3daa1054..0d7fff7f 100644 --- a/lib/filters/butter/Butter.cs +++ b/lib/filters/butter/Butter.cs @@ -3,6 +3,7 @@ using System.Runtime.InteropServices; namespace QuanTAlib; +[SkipLocalsInit] public sealed class Butter : AbstractBase { private readonly int _period; @@ -146,7 +147,7 @@ public sealed class Butter : AbstractBase { var result = new TSeries(); Span output = new double[source.Count]; - Calculate(source.Values, output, _period, double.NaN); + Batch(source.Values, output, _period, double.NaN); for (int i = 0; i < source.Count; i++) { @@ -168,7 +169,13 @@ public sealed class Butter : AbstractBase return result; } - public static void Calculate(ReadOnlySpan source, Span destination, int period, double initialLast) + public static TSeries Batch(TSeries source, int period) + { + var indicator = new Butter(period); + return indicator.Update(source); + } + + public static void Batch(ReadOnlySpan source, Span destination, int period, double initialLast) { if (period < 2) { @@ -216,6 +223,12 @@ public sealed class Butter : AbstractBase destination[i] = y; } } + public static (TSeries Results, Butter Indicator) Calculate(TSeries source, int period) + { + var indicator = new Butter(period); + TSeries results = indicator.Update(source); + return (results, indicator); + } /// /// Unsubscribes from the source publisher if one was provided during construction. @@ -228,4 +241,4 @@ public sealed class Butter : AbstractBase } base.Dispose(disposing); } -} +} \ No newline at end of file diff --git a/lib/filters/cheby1/Cheby1.Tests.cs b/lib/filters/cheby1/Cheby1.Tests.cs index 8d2560e1..1f2a8f68 100644 --- a/lib/filters/cheby1/Cheby1.Tests.cs +++ b/lib/filters/cheby1/Cheby1.Tests.cs @@ -103,7 +103,7 @@ public class Cheby1Tests // Span var spanResults = new double[count]; - Cheby1.Calculate(values, spanResults, period, 1.0); + Cheby1.Batch(values, spanResults, period, 1.0); // Compare for (int i = 0; i < count; i++) diff --git a/lib/filters/cheby1/Cheby1.cs b/lib/filters/cheby1/Cheby1.cs index 01167a1b..fe3ff41b 100644 --- a/lib/filters/cheby1/Cheby1.cs +++ b/lib/filters/cheby1/Cheby1.cs @@ -118,7 +118,7 @@ public sealed class Cheby1 : AbstractBase double[] values = source.Values.ToArray(); double[] results = new double[values.Length]; - Calculate(values, results, Period, Ripple); + Batch(values, results, Period, Ripple); TSeries output = []; for (int i = 0; i < values.Length; i++) @@ -196,15 +196,15 @@ public sealed class Cheby1 : AbstractBase return Last; } - public override void Reset() + public static TSeries Batch(TSeries source, int period, double ripple = 1.0) { - _state = default; - _p_state = default; - Last = default; + var indicator = new Cheby1(period, ripple); + return indicator.Update(source); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void Calculate(ReadOnlySpan source, Span output, int period, double ripple = 1.0) + public static void Batch(ReadOnlySpan source, Span output, int period, double ripple = 1.0) { if (source.Length != output.Length) { @@ -294,6 +294,20 @@ public sealed class Cheby1 : AbstractBase } } + public override void Reset() + { + _state = default; + _p_state = default; + Last = default; + } + + public static (TSeries Results, Cheby1 Indicator) Calculate(TSeries source, int period, double ripple = 1.0) + { + var indicator = new Cheby1(period, ripple); + TSeries results = indicator.Update(source); + return (results, indicator); + } + /// /// Unsubscribes from the source publisher if one was provided during construction. /// diff --git a/lib/filters/cheby2/Cheby2.Tests.cs b/lib/filters/cheby2/Cheby2.Tests.cs index ccbf72c9..3a81850c 100644 --- a/lib/filters/cheby2/Cheby2.Tests.cs +++ b/lib/filters/cheby2/Cheby2.Tests.cs @@ -101,7 +101,7 @@ public class Cheby2Tests // Span var spanResults = new double[count]; - Cheby2.Calculate(values, spanResults, period, 5.0); + Cheby2.Batch(values, spanResults, period, 5.0); // Compare for (int i = 0; i < count; i++) diff --git a/lib/filters/cheby2/Cheby2.cs b/lib/filters/cheby2/Cheby2.cs index b485dbef..ce4cf8f6 100644 --- a/lib/filters/cheby2/Cheby2.cs +++ b/lib/filters/cheby2/Cheby2.cs @@ -136,7 +136,7 @@ public sealed class Cheby2 : AbstractBase double[] values = source.Values.ToArray(); double[] results = new double[values.Length]; - Calculate(values, results, Period, Attenuation); + Batch(values, results, Period, Attenuation); TSeries output = []; for (int i = 0; i < values.Length; i++) @@ -217,8 +217,14 @@ public sealed class Cheby2 : AbstractBase Last = default; } + public static TSeries Batch(TSeries source, int period, double attenuation = 5.0) + { + var indicator = new Cheby2(period, attenuation); + return indicator.Update(source); + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void Calculate(ReadOnlySpan source, Span output, int period, double attenuation = 5.0) + public static void Batch(ReadOnlySpan source, Span output, int period, double attenuation = 5.0) { if (source.Length != output.Length) { @@ -322,6 +328,12 @@ public sealed class Cheby2 : AbstractBase filt1 = filt; } } + public static (TSeries Results, Cheby2 Indicator) Calculate(TSeries source, int period, double attenuation = 5.0) + { + var indicator = new Cheby2(period, attenuation); + TSeries results = indicator.Update(source); + return (results, indicator); + } /// /// Unsubscribes from the source publisher if one was provided during construction. @@ -334,4 +346,4 @@ public sealed class Cheby2 : AbstractBase } base.Dispose(disposing); } -} +} \ No newline at end of file diff --git a/lib/filters/elliptic/Elliptic.Tests.cs b/lib/filters/elliptic/Elliptic.Tests.cs index fd77d7e8..158bcb07 100644 --- a/lib/filters/elliptic/Elliptic.Tests.cs +++ b/lib/filters/elliptic/Elliptic.Tests.cs @@ -41,7 +41,7 @@ public class EllipticTests // 3. Span double[] spanInput = series.Values.ToArray(); double[] spanOutput = new double[spanInput.Length]; - Elliptic.Calculate(spanInput, spanOutput, period); + Elliptic.Batch(spanInput, spanOutput, period); // Assert for (int i = 0; i < series.Count; i++) diff --git a/lib/filters/elliptic/Elliptic.cs b/lib/filters/elliptic/Elliptic.cs index 0ae84734..b0a3594c 100644 --- a/lib/filters/elliptic/Elliptic.cs +++ b/lib/filters/elliptic/Elliptic.cs @@ -201,6 +201,19 @@ public sealed class Elliptic : AbstractBase return Last; } + public static TSeries Batch(TSeries source, int period) + { + var indicator = new Elliptic(period); + return indicator.Update(source); + } + + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Batch(ReadOnlySpan source, Span output, int period) + { + CalculateWithState(source, output, period, out _); + } + public override void Reset() { _state = default; @@ -209,10 +222,11 @@ public sealed class Elliptic : AbstractBase Last = default; } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void Calculate(ReadOnlySpan source, Span output, int period) + public static (TSeries Results, Elliptic Indicator) Calculate(TSeries source, int period) { - CalculateWithState(source, output, period, out _); + var indicator = new Elliptic(period); + TSeries results = indicator.Update(source); + return (results, indicator); } [MethodImpl(MethodImplOptions.AggressiveInlining)] diff --git a/lib/filters/gauss/Gauss.Tests.cs b/lib/filters/gauss/Gauss.Tests.cs index 4778b9b0..f5676b24 100644 --- a/lib/filters/gauss/Gauss.Tests.cs +++ b/lib/filters/gauss/Gauss.Tests.cs @@ -61,7 +61,7 @@ public class GaussTests // Static calculate comparison double[] output = new double[source.Count]; - Gauss.Calculate(source.Values, output, 1.0); + Gauss.Batch(source.Values, output, 1.0); for (int i = 0; i < source.Count; i++) { Assert.Equal(seriesResult[i].Value, output[i], 1e-9); diff --git a/lib/filters/gauss/Gauss.Validation.Tests.cs b/lib/filters/gauss/Gauss.Validation.Tests.cs index 90e2282c..281390a2 100644 --- a/lib/filters/gauss/Gauss.Validation.Tests.cs +++ b/lib/filters/gauss/Gauss.Validation.Tests.cs @@ -163,7 +163,7 @@ public class GaussValidationTests : IDisposable foreach (var sigma in sigmas) { double[] output = new double[source.Length]; - Gauss.Calculate(source.AsSpan(), output.AsSpan(), sigma); + Gauss.Batch(source.AsSpan(), output.AsSpan(), sigma); var expected = CalculateExpectedGauss(source, sigma); diff --git a/lib/filters/gauss/Gauss.cs b/lib/filters/gauss/Gauss.cs index 8ea15e39..649b3037 100644 --- a/lib/filters/gauss/Gauss.cs +++ b/lib/filters/gauss/Gauss.cs @@ -199,7 +199,7 @@ public sealed class Gauss : AbstractBase // Calculate using static method for performance var resultValues = new double[source.Count]; - Calculate(source.Values, resultValues, _sigma); + Batch(source.Values, resultValues, _sigma); // Convert to TSeries var result = new TSeries(); @@ -219,12 +219,10 @@ public sealed class Gauss : AbstractBase return result; } - public override void Prime(ReadOnlySpan source, TimeSpan? step = null) + public static TSeries Batch(TSeries source, double sigma = 1.0) { - foreach (double value in source) - { - Update(new TValue(DateTime.MinValue, value), isNew: true); - } + var indicator = new Gauss(sigma); + return indicator.Update(source); } /// @@ -234,7 +232,7 @@ public sealed class Gauss : AbstractBase /// Output buffer (must be same length as source) /// Standard deviation [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void Calculate(ReadOnlySpan source, Span output, double sigma) + public static void Batch(ReadOnlySpan source, Span output, double sigma) { const int StackallocThreshold = 256; @@ -337,6 +335,21 @@ public sealed class Gauss : AbstractBase } } + public override void Prime(ReadOnlySpan source, TimeSpan? step = null) + { + foreach (double value in source) + { + Update(new TValue(DateTime.MinValue, value), isNew: true); + } + } + + public static (TSeries Results, Gauss Indicator) Calculate(TSeries source, double sigma = 1.0) + { + var indicator = new Gauss(sigma); + TSeries results = indicator.Update(source); + return (results, indicator); + } + /// /// Unsubscribes from the source publisher if one was provided during construction. /// diff --git a/lib/filters/hann/Hann.Tests.cs b/lib/filters/hann/Hann.Tests.cs index ea62fb2e..d3c19058 100644 --- a/lib/filters/hann/Hann.Tests.cs +++ b/lib/filters/hann/Hann.Tests.cs @@ -125,7 +125,7 @@ public class HannTests } var tseriesResult = new Hann(length).Update(series); - Hann.Calculate(input.AsSpan(), output.AsSpan(), length); + Hann.Batch(input.AsSpan(), output.AsSpan(), length); for (int i = 0; i < 100; i++) { diff --git a/lib/filters/hann/Hann.Validation.Tests.cs b/lib/filters/hann/Hann.Validation.Tests.cs index 9243bea8..33138d2c 100644 --- a/lib/filters/hann/Hann.Validation.Tests.cs +++ b/lib/filters/hann/Hann.Validation.Tests.cs @@ -136,7 +136,7 @@ public class HannValidationTests : IDisposable foreach (var len in lengths) { double[] output = new double[source.Length]; - Hann.Calculate(source.AsSpan(), output.AsSpan(), len); + Hann.Batch(source.AsSpan(), output.AsSpan(), len); var expected = CalculateExpectedHann(source, len); diff --git a/lib/filters/hann/Hann.cs b/lib/filters/hann/Hann.cs index 31b1cafe..27a7f27b 100644 --- a/lib/filters/hann/Hann.cs +++ b/lib/filters/hann/Hann.cs @@ -9,6 +9,7 @@ namespace QuanTAlib; /// This filter provides strong smoothing properties but introduces lag, as the weights /// typically start and end at zero. /// +[SkipLocalsInit] public sealed class Hann : AbstractBase { private readonly double[] _weights; @@ -156,7 +157,7 @@ public sealed class Hann : AbstractBase } var resultValues = new double[source.Count]; - Calculate(source.Values, resultValues, Length); + Batch(source.Values, resultValues, Length); // Convert to TSeries var result = new TSeries(); @@ -176,12 +177,10 @@ public sealed class Hann : AbstractBase return result; } - public override void Prime(ReadOnlySpan source, TimeSpan? step = null) + public static TSeries Batch(TSeries source, int length) { - foreach (double value in source) - { - Update(new TValue(DateTime.MinValue, value), isNew: true); - } + var indicator = new Hann(length); + return indicator.Update(source); } /// @@ -191,7 +190,7 @@ public sealed class Hann : AbstractBase /// Output buffer (must be same length as source) /// Lookback length [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void Calculate(ReadOnlySpan source, Span output, int length) + public static void Batch(ReadOnlySpan source, Span output, int length) { if (length <= 1) { @@ -249,6 +248,21 @@ public sealed class Hann : AbstractBase } } + public override void Prime(ReadOnlySpan source, TimeSpan? step = null) + { + foreach (double value in source) + { + Update(new TValue(DateTime.MinValue, value), isNew: true); + } + } + + public static (TSeries Results, Hann Indicator) Calculate(TSeries source, int length) + { + var indicator = new Hann(length); + TSeries results = indicator.Update(source); + return (results, indicator); + } + /// /// Unsubscribes from the source publisher if one was provided during construction. /// diff --git a/lib/filters/hp/Hp.Tests.cs b/lib/filters/hp/Hp.Tests.cs index 03e96d03..7fcdf977 100644 --- a/lib/filters/hp/Hp.Tests.cs +++ b/lib/filters/hp/Hp.Tests.cs @@ -68,7 +68,7 @@ public class HpTests var tseriesResult = hp.Update(source); var spanOutput = new double[source.Count]; - Hp.Calculate(source.Values, spanOutput, 1600); + Hp.Batch(source.Values, spanOutput, 1600); for (int i = 0; i < source.Count; i++) { @@ -116,6 +116,6 @@ public class HpTests { double[] src = new double[10]; double[] dst = new double[5]; - Assert.Throws(() => Hp.Calculate(src, dst, 1600)); + Assert.Throws(() => Hp.Batch(src, dst, 1600)); } } diff --git a/lib/filters/hp/Hp.cs b/lib/filters/hp/Hp.cs index 5329f8f7..8e9a7d8b 100644 --- a/lib/filters/hp/Hp.cs +++ b/lib/filters/hp/Hp.cs @@ -165,7 +165,7 @@ public sealed class Hp : AbstractBase } var resultValues = new double[source.Count]; - Calculate(source.Values, resultValues, Lambda); + Batch(source.Values, resultValues, Lambda); var result = new TSeries(); var times = source.Times; @@ -196,10 +196,16 @@ public sealed class Hp : AbstractBase return result; } + public static TSeries Batch(TSeries source, double lambda = 1600.0) + { + var indicator = new Hp(lambda); + return indicator.Update(source); + } + /// /// Static calculation of HP Filter on a span. /// - public static void Calculate(ReadOnlySpan source, Span output, double lambda) + public static void Batch(ReadOnlySpan source, Span output, double lambda) { if (source.Length != output.Length) { @@ -236,6 +242,12 @@ public sealed class Hp : AbstractBase prevTrend = val; } } + public static (TSeries Results, Hp Indicator) Calculate(TSeries source, double lambda = 1600.0) + { + var indicator = new Hp(lambda); + TSeries results = indicator.Update(source); + return (results, indicator); + } /// /// Unsubscribes from the source publisher if one was provided during construction. @@ -248,4 +260,4 @@ public sealed class Hp : AbstractBase } base.Dispose(disposing); } -} +} \ No newline at end of file diff --git a/lib/filters/hpf/Hpf.Tests.cs b/lib/filters/hpf/Hpf.Tests.cs index 97a98070..a4da9add 100644 --- a/lib/filters/hpf/Hpf.Tests.cs +++ b/lib/filters/hpf/Hpf.Tests.cs @@ -70,7 +70,7 @@ public class HpfTests var tseriesResult = hpf.Update(source); var spanOutput = new double[source.Count]; - Hpf.Calculate(source.Values, spanOutput, 40, out _); + Hpf.Batch(source.Values, spanOutput, 40, out _); for (int i = 0; i < source.Count; i++) { @@ -118,6 +118,6 @@ public class HpfTests { double[] src = new double[10]; double[] dst = new double[5]; - Assert.Throws(() => Hpf.Calculate(src, dst, 40, out _)); + Assert.Throws(() => Hpf.Batch(src, dst, 40, out _)); } } diff --git a/lib/filters/hpf/Hpf.cs b/lib/filters/hpf/Hpf.cs index d88e92de..eef979f7 100644 --- a/lib/filters/hpf/Hpf.cs +++ b/lib/filters/hpf/Hpf.cs @@ -181,7 +181,7 @@ public sealed class Hpf : AbstractBase var output = new double[source.Count]; - Calculate(source.Values, output, Length, out var endState); + Batch(source.Values, output, Length, out var endState); _state = new State { @@ -206,17 +206,22 @@ public sealed class Hpf : AbstractBase return result; } - public static void Calculate(ReadOnlySpan source, Span output, int length) + public static TSeries Batch(TSeries source, int length = 40) { - Calculate(source, output, length, out _); + var indicator = new Hpf(length); + return indicator.Update(source); } + public static void Batch(ReadOnlySpan source, Span output, int length) + { + Batch(source, output, length, out _); + } /// /// Batch HPF. Returns end state so callers can restore streaming state without replay. /// NaN/Inf => carry-forward last finite source. /// Outputs 0 for the first two finite samples, then runs the 2-pole recursion. /// - public static void Calculate( + public static void Batch( ReadOnlySpan source, Span output, int length, @@ -304,6 +309,14 @@ public sealed class Hpf : AbstractBase state = (hp1, hp2, src1, src2, samples, hasSrc); } + public static (TSeries Results, Hpf Indicator) Calculate(TSeries source, int length = 40) + { + var indicator = new Hpf(length); + TSeries results = indicator.Update(source); + return (results, indicator); + } + + /// /// Unsubscribes from the source publisher if one was provided during construction. /// diff --git a/lib/filters/kalman/Kalman.Tests.cs b/lib/filters/kalman/Kalman.Tests.cs index 1107f712..aab3627e 100644 --- a/lib/filters/kalman/Kalman.Tests.cs +++ b/lib/filters/kalman/Kalman.Tests.cs @@ -120,7 +120,7 @@ public class KalmanTests // 2. Span Calculate var spanOutput = new double[series.Count]; - Kalman.Calculate(series.Values.ToArray(), spanOutput, 0.01, 0.1); + Kalman.Batch(series.Values.ToArray(), spanOutput, 0.01, 0.1); // 3. Streaming Update filter.Reset(); diff --git a/lib/filters/kalman/Kalman.cs b/lib/filters/kalman/Kalman.cs index 00583be3..23ee4e37 100644 --- a/lib/filters/kalman/Kalman.cs +++ b/lib/filters/kalman/Kalman.cs @@ -186,7 +186,7 @@ public sealed class Kalman : AbstractBase var output = new double[source.Count]; - Calculate(source.Values, output, ProcessNoise, MeasurementNoise, + Batch(source.Values, output, ProcessNoise, MeasurementNoise, out double endX, out double endP, out int endSamples); var result = new TSeries(); @@ -203,19 +203,17 @@ public sealed class Kalman : AbstractBase return result; } - public override void Prime(ReadOnlySpan source, TimeSpan? step = null) + public static TSeries Batch(TSeries source, double q = 0.01, double r = 0.1) { - foreach (double v in source) - { - Update(new TValue(DateTime.MinValue, v), isNew: true); - } + var indicator = new Kalman(q, r); + return indicator.Update(source); } /// /// Batch KF. Returns final state so instance can restore without replay. /// NaN/Inf => prediction-only (hold x, p += q) once initialized. /// - public static void Calculate( + public static void Batch( ReadOnlySpan source, Span output, double q, @@ -278,15 +276,30 @@ public sealed class Kalman : AbstractBase // Overload for Calculate without out params to maintain API compatibility if needed, // although the original Calculate signature was different anyway (returned void). - // The previous implementation had: public static void Calculate(ReadOnlySpan source, Span output, double q, double r) + // The previous implementation had: public static void Batch(ReadOnlySpan source, Span output, double q, double r) // We should keep this signature valid. /// /// Static calculation of Kalman Filter on a span. /// - public static void Calculate(ReadOnlySpan source, Span output, double q, double r) + public static void Batch(ReadOnlySpan source, Span output, double q, double r) { - Calculate(source, output, q, r, out _, out _, out _); + Batch(source, output, q, r, out _, out _, out _); + } + + public override void Prime(ReadOnlySpan source, TimeSpan? step = null) + { + foreach (double v in source) + { + Update(new TValue(DateTime.MinValue, v), isNew: true); + } + } + + public static (TSeries Results, Kalman Indicator) Calculate(TSeries source, double q = 0.01, double r = 0.1) + { + var indicator = new Kalman(q, r); + TSeries results = indicator.Update(source); + return (results, indicator); } /// diff --git a/lib/filters/loess/Loess.Tests.cs b/lib/filters/loess/Loess.Tests.cs index 3e69f8d0..911d265b 100644 --- a/lib/filters/loess/Loess.Tests.cs +++ b/lib/filters/loess/Loess.Tests.cs @@ -106,7 +106,7 @@ public sealed class LoessTests // 2. Span Batch var resSpan = new double[data.Length]; - Loess.Calculate(data.AsSpan(), resSpan.AsSpan(), period); + Loess.Batch(data.AsSpan(), resSpan.AsSpan(), period); // 3. Streaming var loessStream = new Loess(period); diff --git a/lib/filters/loess/Loess.cs b/lib/filters/loess/Loess.cs index 236b6b0d..2dcc9a57 100644 --- a/lib/filters/loess/Loess.cs +++ b/lib/filters/loess/Loess.cs @@ -163,7 +163,7 @@ public sealed class Loess : AbstractBase // Use static Calculate for performance on the whole series var resultValues = new double[source.Count]; - Calculate(source.Values, resultValues, Period); + Batch(source.Values, resultValues, Period); var result = new TSeries(); var times = source.Times; @@ -200,6 +200,48 @@ public sealed class Loess : AbstractBase return result; } + public static TSeries Batch(TSeries source, int period) + { + var indicator = new Loess(period); + return indicator.Update(source); + } + + /// + /// Static stateless calculation optimized for SIMD. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Batch(ReadOnlySpan source, Span output, int period) + { + if (source.Length != output.Length) + { + throw new ArgumentException("Source and output spans must be of equal length.", nameof(output)); + } + + if (period < 3) + { + throw new ArgumentOutOfRangeException(nameof(period), "Period must be at least 3."); + } + + int adjPeriod = (period & 1) == 0 ? period + 1 : period; + + double[] kernel = new double[adjPeriod]; + GenerateKernelOldestFirst(adjPeriod, kernel); + + ReadOnlySpan kSpan = new ReadOnlySpan(kernel); + + for (int i = 0; i < source.Length; i++) + { + if (i < adjPeriod - 1) + { + output[i] = source[i]; + continue; + } + + var window = source.Slice(i - adjPeriod + 1, adjPeriod); + output[i] = DotProduct(window, kSpan); + } + } + public override void Prime(ReadOnlySpan source, TimeSpan? step = null) { foreach (double value in source) @@ -310,40 +352,11 @@ public sealed class Loess : AbstractBase return sum; } - /// - /// Static stateless calculation optimized for SIMD. - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void Calculate(ReadOnlySpan source, Span output, int period) + public static (TSeries Results, Loess Indicator) Calculate(TSeries source, int period) { - if (source.Length != output.Length) - { - throw new ArgumentException("Source and output spans must be of equal length.", nameof(output)); - } - - if (period < 3) - { - throw new ArgumentOutOfRangeException(nameof(period), "Period must be at least 3."); - } - - int adjPeriod = (period & 1) == 0 ? period + 1 : period; - - double[] kernel = new double[adjPeriod]; - GenerateKernelOldestFirst(adjPeriod, kernel); - - ReadOnlySpan kSpan = new ReadOnlySpan(kernel); - - for (int i = 0; i < source.Length; i++) - { - if (i < adjPeriod - 1) - { - output[i] = source[i]; - continue; - } - - var window = source.Slice(i - adjPeriod + 1, adjPeriod); - output[i] = DotProduct(window, kSpan); - } + var indicator = new Loess(period); + TSeries results = indicator.Update(source); + return (results, indicator); } /// diff --git a/lib/filters/notch/Notch.Tests.cs b/lib/filters/notch/Notch.Tests.cs index a5f19947..4967d4bd 100644 --- a/lib/filters/notch/Notch.Tests.cs +++ b/lib/filters/notch/Notch.Tests.cs @@ -37,12 +37,12 @@ public class NotchTests var data = _gbm.Fetch(100, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1)); var series = data.Close; - // 1. Static Calculate (TSeries) - var staticResult = Notch.Calculate(series, period, q); + // 1. Static Batch(TSeries) + var staticResult = Notch.Batch(series, period, q); - // 2. Static Calculate (Span) + // 2. Static Batch(Span) double[] spanResult = new double[series.Count]; - Notch.Calculate(series.Values, spanResult.AsSpan(), period, q); + Notch.Batch(series.Values, spanResult.AsSpan(), period, q); // 3. Instance Update (TSeries) var instance = new Notch(period, q); diff --git a/lib/filters/notch/Notch.cs b/lib/filters/notch/Notch.cs index c3492f4e..04d2f4b3 100644 --- a/lib/filters/notch/Notch.cs +++ b/lib/filters/notch/Notch.cs @@ -138,7 +138,7 @@ public sealed class Notch : AbstractBase ReadOnlySpan srcSpan = source.Values; double[] outArray = new double[srcSpan.Length]; - Calculate(srcSpan, outArray.AsSpan(), NotchFreq, Bandwidth); + Batch(srcSpan, outArray.AsSpan(), NotchFreq, Bandwidth); for (int i = 0; i < outArray.Length; i++) { @@ -159,7 +159,7 @@ public sealed class Notch : AbstractBase return result; } - public static void Calculate(ReadOnlySpan source, Span output, int period, double q = 1.0) + public static void Batch(ReadOnlySpan source, Span output, int period, double q = 1.0) { if (source.Length != output.Length) { @@ -210,13 +210,13 @@ public sealed class Notch : AbstractBase } } - public static TSeries Calculate(TSeries source, int period, double q = 1.0) + public static TSeries Batch(TSeries source, int period, double q = 1.0) { var result = new TSeries(); ReadOnlySpan srcSpan = source.Values; double[] outArray = new double[srcSpan.Length]; - Calculate(srcSpan, outArray.AsSpan(), period, q); + Batch(srcSpan, outArray.AsSpan(), period, q); for (int i = 0; i < outArray.Length; i++) { @@ -226,6 +226,13 @@ public sealed class Notch : AbstractBase return result; } + public static (TSeries Results, Notch Indicator) Calculate(TSeries source, int period, double q = 1.0) + { + var indicator = new Notch(period, q); + TSeries results = indicator.Update(source); + return (results, indicator); + } + /// /// Unsubscribes from the source publisher if one was provided during construction. /// diff --git a/lib/filters/sgf/Sgf.Tests.cs b/lib/filters/sgf/Sgf.Tests.cs index 5400256e..cb1e7cf6 100644 --- a/lib/filters/sgf/Sgf.Tests.cs +++ b/lib/filters/sgf/Sgf.Tests.cs @@ -44,7 +44,7 @@ public class SgfTests // 3. Span Mode double[] spanInput = series.Values.ToArray(); double[] spanOutput = new double[spanInput.Length]; - Sgf.Calculate(spanInput, spanOutput, period, polyOrder); + Sgf.Batch(spanInput, spanOutput, period, polyOrder); // Assert for (int i = 0; i < series.Count; i++) diff --git a/lib/filters/sgf/Sgf.Validation.Tests.cs b/lib/filters/sgf/Sgf.Validation.Tests.cs index 8e4ad409..4fcf01e2 100644 --- a/lib/filters/sgf/Sgf.Validation.Tests.cs +++ b/lib/filters/sgf/Sgf.Validation.Tests.cs @@ -201,7 +201,7 @@ public class SgfValidationTests : IDisposable foreach (var (period, order) in scenarios) { double[] output = new double[source.Length]; - Sgf.Calculate(source.AsSpan(), output.AsSpan(), period, order); + Sgf.Batch(source.AsSpan(), output.AsSpan(), period, order); var expected = CalculateExpectedSgf(source, period, order); diff --git a/lib/filters/sgf/Sgf.cs b/lib/filters/sgf/Sgf.cs index bbd12059..65b736e7 100644 --- a/lib/filters/sgf/Sgf.cs +++ b/lib/filters/sgf/Sgf.cs @@ -177,7 +177,7 @@ public sealed class Sgf : AbstractBase } var resultValues = new double[source.Count]; - Calculate(source.Values, resultValues, _period, _polyOrder); + Batch(source.Values, resultValues, _period, _polyOrder); var result = new TSeries(); var times = source.Times; @@ -197,16 +197,14 @@ public sealed class Sgf : AbstractBase return result; } - public override void Prime(ReadOnlySpan source, TimeSpan? step = null) + public static TSeries Batch(TSeries source, int period, int polyOrder = 2) { - foreach (double value in source) - { - Update(new TValue(DateTime.MinValue, value), isNew: true); - } + var indicator = new Sgf(period, polyOrder); + return indicator.Update(source); } [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void Calculate(ReadOnlySpan source, Span output, int period, int polyOrder = 2) + public static void Batch(ReadOnlySpan source, Span output, int period, int polyOrder = 2) { if (source.Length != output.Length) { @@ -262,6 +260,21 @@ public sealed class Sgf : AbstractBase } } + public override void Prime(ReadOnlySpan source, TimeSpan? step = null) + { + foreach (double value in source) + { + Update(new TValue(DateTime.MinValue, value), isNew: true); + } + } + + public static (TSeries Results, Sgf Indicator) Calculate(TSeries source, int period, int polyOrder = 2) + { + var indicator = new Sgf(period, polyOrder); + TSeries results = indicator.Update(source); + return (results, indicator); + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] private static void CalculateWeightsSpan(Span weights, int period, int polyOrder) { diff --git a/lib/filters/ssf/Ssf.Tests.cs b/lib/filters/ssf/Ssf.Tests.cs index c962462c..c5faf4e1 100644 --- a/lib/filters/ssf/Ssf.Tests.cs +++ b/lib/filters/ssf/Ssf.Tests.cs @@ -231,7 +231,7 @@ public class SsfTests var tseriesResult = Ssf.Calculate(series, 10).Results; // Calculate with Span API - Ssf.Calculate(source.AsSpan(), output.AsSpan(), 10); + Ssf.Batch(source.AsSpan(), output.AsSpan(), 10); // Compare results for (int i = 0; i < 100; i++) @@ -257,7 +257,7 @@ public class SsfTests var tValues = series.Values.ToArray(); var spanInput = new ReadOnlySpan(tValues); var spanOutput = new double[tValues.Length]; - Ssf.Calculate(spanInput, spanOutput, period); + Ssf.Batch(spanInput, spanOutput, period); double spanResult = spanOutput[^1]; // 3. Streaming Mode diff --git a/lib/filters/ssf/Ssf.cs b/lib/filters/ssf/Ssf.cs index f4994e38..36b0b944 100644 --- a/lib/filters/ssf/Ssf.cs +++ b/lib/filters/ssf/Ssf.cs @@ -315,15 +315,8 @@ public sealed class Ssf : AbstractBase } } - public static (TSeries Results, Ssf Indicator) Calculate(TSeries source, int period) - { - var ssf = new Ssf(period); - TSeries results = ssf.Update(source); - return (results, ssf); - } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void Calculate(ReadOnlySpan source, Span output, int period) + public static void Batch(ReadOnlySpan source, Span output, int period) { if (period <= 0) { @@ -353,6 +346,13 @@ public sealed class Ssf : AbstractBase CalculateCore(source, output, c1, c2, c3, period, ref state); } + public static (TSeries Results, Ssf Indicator) Calculate(TSeries source, int period) + { + var ssf = new Ssf(period); + TSeries results = ssf.Update(source); + return (results, ssf); + } + public override void Reset() { _state = State.New(); @@ -371,4 +371,4 @@ public sealed class Ssf : AbstractBase } base.Dispose(disposing); } -} +} \ No newline at end of file diff --git a/lib/filters/usf/Usf.Tests.cs b/lib/filters/usf/Usf.Tests.cs index 5a20b257..ed024039 100644 --- a/lib/filters/usf/Usf.Tests.cs +++ b/lib/filters/usf/Usf.Tests.cs @@ -317,7 +317,7 @@ public class UsfTests var tValues = series.Values.ToArray(); var spanInput = new ReadOnlySpan(tValues); var spanOutput = new double[tValues.Length]; - Usf.Calculate(spanInput, spanOutput, period); + Usf.Batch(spanInput, spanOutput, period); double spanResult = spanOutput[^1]; // 3. Streaming Mode @@ -370,11 +370,11 @@ public class UsfTests double[] wrongSizeOutput = new double[3]; // Period must be > 0 - Assert.Throws(() => Usf.Calculate(source.AsSpan(), output.AsSpan(), 0)); - Assert.Throws(() => Usf.Calculate(source.AsSpan(), output.AsSpan(), -1)); + Assert.Throws(() => Usf.Batch(source.AsSpan(), output.AsSpan(), 0)); + Assert.Throws(() => Usf.Batch(source.AsSpan(), output.AsSpan(), -1)); // Output must be same length as source - Assert.Throws(() => Usf.Calculate(source.AsSpan(), wrongSizeOutput.AsSpan(), 3)); + Assert.Throws(() => Usf.Batch(source.AsSpan(), wrongSizeOutput.AsSpan(), 3)); } [Fact] @@ -396,7 +396,7 @@ public class UsfTests var (tseriesResult, _) = Usf.Calculate(series, 10); // Calculate with Span API - Usf.Calculate(source.AsSpan(), output.AsSpan(), 10); + Usf.Batch(source.AsSpan(), output.AsSpan(), 10); // Compare results for (int i = 0; i < 100; i++) @@ -418,7 +418,7 @@ public class UsfTests } // Warm up - Usf.Calculate(source.AsSpan(), output.AsSpan(), 100); + Usf.Batch(source.AsSpan(), output.AsSpan(), 100); // This test verifies the method runs without throwing Assert.True(double.IsFinite(output[^1])); @@ -430,7 +430,7 @@ public class UsfTests double[] source = [100, 110, double.NaN, 120, 130]; double[] output = new double[5]; - Usf.Calculate(source.AsSpan(), output.AsSpan(), 3); + Usf.Batch(source.AsSpan(), output.AsSpan(), 3); // All outputs should be finite foreach (var val in output) diff --git a/lib/filters/usf/Usf.Validation.Tests.cs b/lib/filters/usf/Usf.Validation.Tests.cs index d8603f31..4faaf684 100644 --- a/lib/filters/usf/Usf.Validation.Tests.cs +++ b/lib/filters/usf/Usf.Validation.Tests.cs @@ -68,7 +68,7 @@ public sealed class UsfValidationTests : IDisposable // 3. Span Mode double[] sourceData = _testData.RawData.ToArray(); double[] spanOutput = new double[sourceData.Length]; - Usf.Calculate(sourceData.AsSpan(), spanOutput.AsSpan(), period); + Usf.Batch(sourceData.AsSpan(), spanOutput.AsSpan(), period); // Compare batch vs streaming Assert.Equal(batchResult.Count, streamingResults.Count); diff --git a/lib/filters/usf/Usf.cs b/lib/filters/usf/Usf.cs index b0b00869..5922f1a9 100644 --- a/lib/filters/usf/Usf.cs +++ b/lib/filters/usf/Usf.cs @@ -315,15 +315,8 @@ public sealed class Usf : AbstractBase } } - public static (TSeries Results, Usf Indicator) Calculate(TSeries source, int period) - { - var usf = new Usf(period); - TSeries results = usf.Update(source); - return (results, usf); - } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void Calculate(ReadOnlySpan source, Span output, int period) + public static void Batch(ReadOnlySpan source, Span output, int period) { if (period <= 0) { @@ -353,6 +346,13 @@ public sealed class Usf : AbstractBase CalculateCore(source, output, c1, c2, c3, period, ref state); } + public static (TSeries Results, Usf Indicator) Calculate(TSeries source, int period) + { + var usf = new Usf(period); + TSeries results = usf.Update(source); + return (results, usf); + } + public override void Reset() { _state = State.New(); @@ -371,4 +371,4 @@ public sealed class Usf : AbstractBase } base.Dispose(disposing); } -} +} \ No newline at end of file diff --git a/lib/filters/wiener/Wiener.Tests.cs b/lib/filters/wiener/Wiener.Tests.cs index 04c4c764..4788fb1b 100644 --- a/lib/filters/wiener/Wiener.Tests.cs +++ b/lib/filters/wiener/Wiener.Tests.cs @@ -44,7 +44,7 @@ public class WienerTests // 3. Span Mode double[] spanInput = series.Values.ToArray(); double[] spanOutput = new double[spanInput.Length]; - Wiener.Calculate(spanInput, spanOutput, period, smoothPeriod); + Wiener.Batch(spanInput, spanOutput, period, smoothPeriod); // Assert for (int i = 0; i < series.Count; i++) diff --git a/lib/filters/wiener/Wiener.Validation.Tests.cs b/lib/filters/wiener/Wiener.Validation.Tests.cs index 4dd46098..033f7e3e 100644 --- a/lib/filters/wiener/Wiener.Validation.Tests.cs +++ b/lib/filters/wiener/Wiener.Validation.Tests.cs @@ -178,7 +178,7 @@ public class WienerValidationTests : IDisposable foreach (var (period, smooth) in scenarios) { double[] output = new double[source.Length]; - Wiener.Calculate(source.AsSpan(), output.AsSpan(), period, smooth); + Wiener.Batch(source.AsSpan(), output.AsSpan(), period, smooth); var expected = CalculateExpectedWiener(source, period, smooth); diff --git a/lib/filters/wiener/Wiener.cs b/lib/filters/wiener/Wiener.cs index 9df96026..83e15c05 100644 --- a/lib/filters/wiener/Wiener.cs +++ b/lib/filters/wiener/Wiener.cs @@ -77,6 +77,26 @@ public sealed class Wiener : AbstractBase return result; } + public static TSeries Batch(TSeries source, int period, int smoothPeriod = 10) + { + var indicator = new Wiener(period, smoothPeriod); + return indicator.Update(source); + } + + public static void Batch(ReadOnlySpan source, Span destination, int period, int smoothPeriod = 10) + { + if (destination.Length < source.Length) + { + throw new ArgumentException("Destination span is shorter than source span.", nameof(destination)); + } + + var filter = new Wiener(period, smoothPeriod); + for (int i = 0; i < source.Length; i++) + { + destination[i] = filter.Update(new TValue(0, source[i])).Value; + } + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] private double Calc() { @@ -154,17 +174,10 @@ public sealed class Wiener : AbstractBase } } - public static void Calculate(ReadOnlySpan source, Span destination, int period, int smoothPeriod = 10) + public static (TSeries Results, Wiener Indicator) Calculate(TSeries source, int period, int smoothPeriod = 10) { - if (destination.Length < source.Length) - { - throw new ArgumentException("Destination span is shorter than source span.", nameof(destination)); - } - - var filter = new Wiener(period, smoothPeriod); - for (int i = 0; i < source.Length; i++) - { - destination[i] = filter.Update(new TValue(0, source[i])).Value; - } + var indicator = new Wiener(period, smoothPeriod); + TSeries results = indicator.Update(source); + return (results, indicator); } } diff --git a/lib/momentum/_index.md b/lib/momentum/_index.md index 9c16b649..75b4cb32 100644 --- a/lib/momentum/_index.md +++ b/lib/momentum/_index.md @@ -8,15 +8,14 @@ Momentum indicators measure the velocity and acceleration of price changes. Unli | Indicator | Full Name | Description | | :--- | :--- | :--- | -| [APO](apo/Apo.md) | Absolute Price Oscillator | Absolute difference between two EMAs. | | [BOP](bop/Bop.md) | Balance of Power | Measures buyer/seller strength by comparing close to open relative to range. | | [CCI](cci/Cci.md) | Commodity Channel Index | Measures price deviation from statistical mean, identifies cyclical turns. | | [CFB](cfb/Cfb.md) | Composite Fractal Behavior | Measures trend duration and quality via fractal efficiency across 96 time scales. | | [CMO](cmo/Cmo.md) | Chande Momentum Oscillator | Momentum using both up and down changes, bounded but not clamped like RSI. | | [MACD](macd/Macd.md) | Moving Average Convergence Divergence | Relationship between two EMAs, identifies momentum and trend direction. | -| [MOM](mom/Mom.md) | Momentum | Raw price change over specified period. | -| [PMO](pmo/Pmo.md) | Price Momentum Oscillator | Double-smoothed ROC oscillator. | -| [PPO](ppo/Ppo.md) | Percentage Price Oscillator | MACD expressed as percentage for cross-instrument comparison. | +| MOM | Momentum | Raw price change over specified period. | +| PMO | Price Momentum Oscillator | Double-smoothed ROC oscillator. | +| PPO | Percentage Price Oscillator | MACD expressed as percentage for cross-instrument comparison. | | [PRS](prs/Prs.md) | Price Relative Strength | Performance ratio between two assets. | | [ROC](roc/Roc.md) | Rate of Change | Absolute price change over N periods. | | [ROCP](rocp/Rocp.md) | Rate of Change Percentage | Percentage price change over N periods. | diff --git a/lib/momentum/apo/apo.pine b/lib/momentum/apo/apo.pine deleted file mode 100644 index ef8f48df..00000000 --- a/lib/momentum/apo/apo.pine +++ /dev/null @@ -1,56 +0,0 @@ -// The MIT License (MIT) -// © mihakralj -//@version=6 -indicator("Absolute Price Oscillator (APO)", "APO", overlay=false) - -//@function Calculates Absolute Price Oscillator using compensated EMAs -//@doc https://github.com/mihakralj/pinescript/blob/main/indicators/momentum/apo.md -//@param src Source series to calculate APO for -//@param fast_len Fast EMA period -//@param slow_len Slow EMA period -//@returns APO value (difference between fast and slow EMAs) -//@optimized Uses embedded EMA with unified warmup compensation for accuracy from bar 1 -apo(series float src, simple int fast_len, simple int slow_len) => - if fast_len <= 0 or slow_len <= 0 - runtime.error("Lengths must be greater than 0") - if fast_len >= slow_len - runtime.error("Fast length must be less than slow length") - float alpha_fast = 2.0 / (fast_len + 1) - float beta_fast = 1.0 - alpha_fast - float alpha_slow = 2.0 / (slow_len + 1) - float beta_slow = 1.0 - alpha_slow - var bool warmup = true - var float e_fast = 1.0 - var float e_slow = 1.0 - var float ema_fast = 0.0 - var float ema_slow = 0.0 - var float result_fast = src - var float result_slow = src - ema_fast := alpha_fast * (src - ema_fast) + ema_fast - ema_slow := alpha_slow * (src - ema_slow) + ema_slow - if warmup - e_fast *= beta_fast - e_slow *= beta_slow - float c_fast = 1.0 / (1.0 - e_fast) - float c_slow = 1.0 / (1.0 - e_slow) - result_fast := c_fast * ema_fast - result_slow := c_slow * ema_slow - warmup := e_fast > 1e-10 or e_slow > 1e-10 - else - result_fast := ema_fast - result_slow := ema_slow - result_fast - result_slow - -// ---------- Main loop ---------- - -// Inputs -i_source = input.source(close, "Source") -i_fast_len = input.int(12, "Fast Length", minval=1) -i_slow_len = input.int(26, "Slow Length", minval=1) - -// Calculation -apo_value = apo(i_source, i_fast_len, i_slow_len) - -// Plot -plot(apo_value, "APO", color=color.yellow, linewidth=2) -hline(0, "Zero", color=color.gray) diff --git a/lib/momentum/bop/Bop.Tests.cs b/lib/momentum/bop/Bop.Tests.cs index 028c1410..c50fa4dd 100644 --- a/lib/momentum/bop/Bop.Tests.cs +++ b/lib/momentum/bop/Bop.Tests.cs @@ -195,7 +195,7 @@ public class BopTests var batchResult = Bop.Batch(bars); var output = new double[bars.Count]; - Bop.Calculate(bars.Open.Values, bars.High.Values, bars.Low.Values, bars.Close.Values, output); + Bop.Batch(bars.Open.Values, bars.High.Values, bars.Low.Values, bars.Close.Values, output); Assert.Equal(batchResult[0].Value, output[0]); Assert.Equal(batchResult[1].Value, output[1]); @@ -213,7 +213,7 @@ public class BopTests // 2. Span Mode var spanOutput = new double[bars.Count]; - Bop.Calculate(bars.Open.Values, bars.High.Values, bars.Low.Values, bars.Close.Values, spanOutput); + Bop.Batch(bars.Open.Values, bars.High.Values, bars.Low.Values, bars.Close.Values, spanOutput); double spanResult = spanOutput[^1]; // 3. Streaming Mode @@ -241,7 +241,7 @@ public class BopTests double[] smallOutput = new double[2]; // This should process 2 elements (minimum of all array lengths) - Bop.Calculate(open, high, low, close, smallOutput); + Bop.Batch(open, high, low, close, smallOutput); // Verify values are calculated for the first 2 elements Assert.Equal(0.25, smallOutput[0], 6); // (1.5 - 1) / (2 - 0) = 0.5/2 = 0.25 diff --git a/lib/momentum/bop/Bop.cs b/lib/momentum/bop/Bop.cs index e94b726f..fcbfa18c 100644 --- a/lib/momentum/bop/Bop.cs +++ b/lib/momentum/bop/Bop.cs @@ -98,7 +98,7 @@ public sealed class Bop : ITValuePublisher /// Calculates BOP for a series of bars. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void Calculate(ReadOnlySpan open, ReadOnlySpan high, ReadOnlySpan low, ReadOnlySpan close, Span destination) + public static void Batch(ReadOnlySpan open, ReadOnlySpan high, ReadOnlySpan low, ReadOnlySpan close, Span destination) { int len = Math.Min(open.Length, Math.Min(high.Length, Math.Min(low.Length, close.Length))); if (destination.Length < len) @@ -170,8 +170,15 @@ public sealed class Bop : ITValuePublisher var vSpan = CollectionsMarshal.AsSpan(v); source.Open.Times.CopyTo(tSpan); - Calculate(source.Open.Values, source.High.Values, source.Low.Values, source.Close.Values, vSpan); + Batch(source.Open.Values, source.High.Values, source.Low.Values, source.Close.Values, vSpan); return new TSeries(t, v); } + + public static (TSeries Results, Bop Indicator) Calculate(TBarSeries source) + { + var indicator = new Bop(); + TSeries results = Bop.Update(source); + return (results, indicator); + } } diff --git a/lib/momentum/cci/Cci.Quantower.Tests.cs b/lib/momentum/cci/Cci.Quantower.Tests.cs new file mode 100644 index 00000000..d91bff0d --- /dev/null +++ b/lib/momentum/cci/Cci.Quantower.Tests.cs @@ -0,0 +1,132 @@ +using TradingPlatform.BusinessLayer; +using Xunit; + +namespace QuanTAlib.Tests; + +public class CciIndicatorTests +{ + [Fact] + public void CciIndicator_Constructor_SetsDefaultPeriod() + { + var indicator = new CciIndicator(); + Assert.Equal(20, indicator.Period); + } + + [Fact] + public void CciIndicator_DefaultName_IsCCI() + { + var indicator = new CciIndicator(); + Assert.Equal("CCI", indicator.Name); + } + + [Fact] + public void CciIndicator_SeparateWindow_IsTrue() + { + var indicator = new CciIndicator(); + Assert.True(indicator.SeparateWindow); + } + + [Fact] + public void CciIndicator_MinHistoryDepths_EqualsPeriod() + { + var indicator = new CciIndicator { Period = 14 }; + Assert.Equal(14, indicator.MinHistoryDepths); + } + + [Fact] + public void CciIndicator_Initialize_CreatesWithCustomPeriod() + { + var indicator = new CciIndicator { Period = 14 }; + indicator.Initialize(); + + Assert.Equal(14, indicator.Period); + } + + [Fact] + public void CciIndicator_OnUpdate_ProcessesHistoricalData() + { + var indicator = new CciIndicator { Period = 10 }; + var historicalData = new HistoricalData(); + + // Add OHLC bars + var baseTime = DateTime.UtcNow.Date; + for (int i = 0; i < 30; i++) + { + historicalData.AddBar( + baseTime.AddDays(i), + open: 100 + i, + high: 102 + i, + low: 98 + i, + close: 101 + i, + volume: 10000); + } + + indicator.HistoricalData = historicalData; + indicator.Initialize(); + + // Process all bars + for (int i = 0; i < historicalData.Count; i++) + { + var args = new UpdateArgs(UpdateReason.HistoricalBar); + indicator.ProcessUpdate(args); + } + + // Should complete without error + Assert.True(true); + } + + [Fact] + public void CciIndicator_OnUpdate_ProcessesNewBar() + { + var indicator = new CciIndicator { Period = 10 }; + var historicalData = new HistoricalData(); + + var baseTime = DateTime.UtcNow.Date; + for (int i = 0; i < 20; i++) + { + historicalData.AddBar( + baseTime.AddDays(i), + open: 100 + i, + high: 102 + i, + low: 98 + i, + close: 101 + i, + volume: 10000); + } + + indicator.HistoricalData = historicalData; + indicator.Initialize(); + + // Simulate new bar + var newBarArgs = new UpdateArgs(UpdateReason.NewBar); + indicator.ProcessUpdate(newBarArgs); + + Assert.True(true); + } + + [Fact] + public void CciIndicator_Description_IsSet() + { + var indicator = new CciIndicator(); + Assert.True(indicator.Description.Contains("Commodity Channel Index", StringComparison.Ordinal) || + indicator.Description.Contains("CCI", StringComparison.Ordinal) || + indicator.Description.Contains("momentum", StringComparison.Ordinal)); + } + + [Fact] + public void CciIndicator_PeriodRange_IsValidated() + { + // Period should accept values from 2 to 200 + var indicator = new CciIndicator { Period = 2 }; + Assert.Equal(2, indicator.Period); + + indicator.Period = 200; + Assert.Equal(200, indicator.Period); + } + + [Fact] + public void CciIndicator_ImplementsIWatchlistIndicator() + { + var indicator = new CciIndicator(); + Assert.True(indicator is IWatchlistIndicator); + } +} diff --git a/lib/momentum/cci/Cci.Quantower.cs b/lib/momentum/cci/Cci.Quantower.cs new file mode 100644 index 00000000..17e1765e --- /dev/null +++ b/lib/momentum/cci/Cci.Quantower.cs @@ -0,0 +1,59 @@ +using System.Drawing; +using System.Runtime.CompilerServices; +using TradingPlatform.BusinessLayer; + +namespace QuanTAlib; + +[SkipLocalsInit] +public sealed class CciIndicator : Indicator, IWatchlistIndicator +{ + [InputParameter("Period", sortIndex: 0, minimum: 2, maximum: 200)] + public int Period { get; set; } = 20; + + public int MinHistoryDepths => Period; + + private Cci? _cci; + private readonly LineSeries _series; + + public CciIndicator() + { + Name = "CCI"; + Description = "Commodity Channel Index - momentum oscillator measuring price deviation from mean"; + SeparateWindow = true; + + _series = new LineSeries("CCI", Color.Yellow, 2, LineStyle.Solid); + } + + protected override void OnInit() + { + _cci = new Cci(Period); + AddLineSeries(_series); + AddLineLevel(100, "Overbought", Color.Red, 1, LineStyle.Dash); + AddLineLevel(-100, "Oversold", Color.Green, 1, LineStyle.Dash); + AddLineLevel(0, "Zero", Color.Gray, 1, LineStyle.Dot); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + protected override void OnUpdate(UpdateArgs args) + { + if (_cci == null) + { + return; + } + + TBar bar = this.GetInputBar(args); + bool isNew = args.Reason != UpdateReason.HistoricalBar || HistoricalData.Count == 1; + var result = _cci.Update(bar, isNew); + _series.SetValue(result.Value); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public override void OnPaintChart(PaintChartEventArgs args) + { + if (_cci == null) + { + return; + } + this.PaintSmoothCurve(args, _series, _cci.Period, showColdValues: true, tension: 0.5); + } +} diff --git a/lib/momentum/cci/Cci.Tests.cs b/lib/momentum/cci/Cci.Tests.cs new file mode 100644 index 00000000..40d2fc79 --- /dev/null +++ b/lib/momentum/cci/Cci.Tests.cs @@ -0,0 +1,330 @@ +using Xunit; + +namespace QuanTAlib.Tests; + +public class CciTests +{ + private readonly TBarSeries _bars; + + public CciTests() + { + _bars = new TBarSeries(capacity: 100); + var baseTime = DateTime.UtcNow.Date; + // Create test bars with known patterns + for (int i = 0; i < 50; i++) + { + double high = 100 + i + 2; + double low = 100 + i - 2; + double open = 100 + i - 1; + double close = 100 + i + 1; + _bars.Add(new TBar(baseTime.AddDays(i), open, high, low, close, 1000)); + } + } + + #region Constructor Tests + + [Fact] + public void Constructor_DefaultPeriod_Is20() + { + var cci = new Cci(); + Assert.Equal(20, cci.Period); + } + + [Fact] + public void Constructor_CustomPeriod_IsStored() + { + var cci = new Cci(14); + Assert.Equal(14, cci.Period); + } + + [Fact] + public void Constructor_PeriodLessThan2_ThrowsArgumentException() + { + Assert.Throws(() => new Cci(1)); + } + + [Fact] + public void WarmupPeriod_IsDefault() + { + Assert.Equal(20, Cci.WarmupPeriod); + } + + #endregion + + #region Basic Calculation Tests + + [Fact] + public void Update_SingleBar_ReturnsZero() + { + var cci = new Cci(5); + var bar = new TBar(DateTime.UtcNow, 100, 105, 95, 102, 1000); + var result = cci.Update(bar); + + // With only one bar, CCI should be 0 (no deviation possible) + Assert.Equal(0, result.Value); + } + + [Fact] + public void Update_FlatMarket_ReturnsZero() + { + var cci = new Cci(5); + var baseTime = DateTime.UtcNow; + + // All bars have same OHLC values - zero deviation + for (int i = 0; i < 10; i++) + { + var bar = new TBar(baseTime.AddDays(i), 100, 100, 100, 100, 1000); + cci.Update(bar); + } + + Assert.Equal(0, cci.Last.Value); + } + + [Fact] + public void Update_UpwardTrend_ReturnsPositive() + { + var cci = new Cci(5); + + // Feed upward trending bars + for (int i = 0; i < 20; i++) + { + var bar = _bars[i]; + cci.Update(bar); + } + + // CCI should be positive in uptrend + Assert.True(cci.Last.Value > 0); + } + + [Fact] + public void Update_DownwardTrend_ReturnsNegative() + { + var cci = new Cci(5); + var baseTime = DateTime.UtcNow; + + // Create downward trending bars + for (int i = 0; i < 20; i++) + { + double high = 200 - i + 2; + double low = 200 - i - 2; + double close = 200 - i + 1; + var bar = new TBar(baseTime.AddDays(i), 200 - i, high, low, close, 1000); + cci.Update(bar); + } + + // CCI should be negative in downtrend + Assert.True(cci.Last.Value < 0); + } + + #endregion + + #region IsHot Tests + + [Fact] + public void IsHot_BeforeWarmup_ReturnsFalse() + { + var cci = new Cci(10); + + for (int i = 0; i < 5; i++) + { + cci.Update(_bars[i]); + } + + Assert.False(cci.IsHot); + } + + [Fact] + public void IsHot_AfterWarmup_ReturnsTrue() + { + var cci = new Cci(10); + + for (int i = 0; i < 15; i++) + { + cci.Update(_bars[i]); + } + + Assert.True(cci.IsHot); + } + + #endregion + + #region Bar Correction Tests + + [Fact] + public void Update_BarCorrection_HandlesIsNewFalse() + { + var cci = new Cci(5); + + // Process some bars + for (int i = 0; i < 10; i++) + { + cci.Update(_bars[i]); + } + + double valueAfterBars = cci.Last.Value; + + // Simulate bar correction (isNew = false) + var correctedBar = new TBar( + _bars[9].Time, + _bars[9].Open, + _bars[9].High + 5, // Higher high + _bars[9].Low, + _bars[9].Close + 5, // Higher close + _bars[9].Volume); + + cci.Update(correctedBar, isNew: false); + + // Value should change after correction + Assert.NotEqual(valueAfterBars, cci.Last.Value); + } + + [Fact] + public void Update_RepeatedCorrection_ProducesSameResult() + { + var cci = new Cci(5); + + for (int i = 0; i < 10; i++) + { + cci.Update(_bars[i]); + } + + var correctionBar = new TBar(DateTime.UtcNow, 105, 110, 100, 108, 1000); + + // Multiple corrections should give same result + cci.Update(correctionBar, isNew: false); + double firstCorrection = cci.Last.Value; + + cci.Update(correctionBar, isNew: false); + double secondCorrection = cci.Last.Value; + + Assert.Equal(firstCorrection, secondCorrection); + } + + #endregion + + #region Reset Tests + + [Fact] + public void Reset_ClearsState() + { + var cci = new Cci(5); + + for (int i = 0; i < 20; i++) + { + cci.Update(_bars[i]); + } + + cci.Reset(); + + Assert.False(cci.IsHot); + Assert.Equal(DateTime.MinValue.Ticks, cci.Last.Time); + } + + #endregion + + #region Batch Processing Tests + + [Fact] + public void Batch_ReturnsCorrectCount() + { + var results = Cci.Batch(_bars, period: 10); + Assert.Equal(_bars.Count, results.Count); + } + + [Fact] + public void Calculate_ReturnsResultsAndIndicator() + { + var (results, indicator) = Cci.Calculate(_bars, period: 14); + + Assert.Equal(_bars.Count, results.Count); + Assert.True(indicator.IsHot); + } + + [Fact] + public void Update_TSeries_MatchesBatch() + { + var cci = new Cci(10); + var results = cci.Update(_bars); + var batchResults = Cci.Batch(_bars, 10); + + for (int i = 0; i < results.Count; i++) + { + Assert.Equal(batchResults[i].Value, results[i].Value, 10); + } + } + + #endregion + + #region Prime Tests + + [Fact] + public void Prime_WarmupIndicator() + { + var cci = new Cci(10); + + cci.Prime(_bars); + + Assert.True(cci.IsHot); + } + + #endregion + + #region Event Tests + + [Fact] + public void Pub_FiresOnUpdate() + { + var cci = new Cci(5); + int eventCount = 0; + + cci.Pub += (object? sender, in TValueEventArgs args) => eventCount++; + + for (int i = 0; i < 10; i++) + { + cci.Update(_bars[i]); + } + + Assert.Equal(10, eventCount); + } + + #endregion + + #region Edge Case Tests + + [Fact] + public void Update_NaNValue_UsesLastValid() + { + var cci = new Cci(5); + + // Add some valid bars + for (int i = 0; i < 10; i++) + { + cci.Update(_bars[i]); + } + + // Add bar with NaN + var nanBar = new TBar(DateTime.UtcNow, double.NaN, double.NaN, double.NaN, double.NaN, 1000); + var result = cci.Update(nanBar); + + // Should handle gracefully + Assert.True(double.IsFinite(result.Value)); + } + + [Fact] + public void Update_InfinityValue_UsesLastValid() + { + var cci = new Cci(5); + + for (int i = 0; i < 10; i++) + { + cci.Update(_bars[i]); + } + + var infBar = new TBar(DateTime.UtcNow, double.PositiveInfinity, double.PositiveInfinity, double.NegativeInfinity, 100, 1000); + var result = cci.Update(infBar); + + Assert.True(double.IsFinite(result.Value)); + } + + #endregion +} diff --git a/lib/momentum/cci/Cci.Validation.Tests.cs b/lib/momentum/cci/Cci.Validation.Tests.cs new file mode 100644 index 00000000..652e38ed --- /dev/null +++ b/lib/momentum/cci/Cci.Validation.Tests.cs @@ -0,0 +1,328 @@ +using Xunit; +using Xunit.Abstractions; + +namespace QuanTAlib.Tests; + +/// +/// CCI Validation Tests against Tulip library. +/// CCI = (Typical Price - SMA of TP) / (0.015 × Mean Deviation) +/// where TP = (High + Low + Close) / 3 +/// +public sealed class CciValidationTests : IDisposable +{ + private readonly ValidationTestData _testData; + private readonly ITestOutputHelper _output; + private bool _disposed; + + public CciValidationTests(ITestOutputHelper output) + { + _output = output; + _testData = new ValidationTestData(); + } + + public void Dispose() + { + Dispose(true); + } + + private void Dispose(bool disposing) + { + if (_disposed) + { + return; + } + + _disposed = true; + + if (disposing) + { + _testData?.Dispose(); + } + } + + #region Tulip Validation + + [Fact] + public void Cci_MatchesTulip_DefaultPeriod() + { + int period = 20; + + // Get QuanTAlib result + var cci = new Cci(period); + var qResult = cci.Update(_testData.Bars); + + // Calculate Tulip CCI + double[] high = _testData.Bars.Select(b => b.High).ToArray(); + double[] low = _testData.Bars.Select(b => b.Low).ToArray(); + double[] close = _testData.Bars.Select(b => b.Close).ToArray(); + + var cciIndicator = Tulip.Indicators.cci; + double[][] inputs = [high, low, close]; + double[] options = [period]; + int lookback = cciIndicator.Start(options); + double[][] outputs = [new double[high.Length - lookback]]; + + cciIndicator.Run(inputs, options, outputs); + double[] tulipResult = outputs[0]; + + // Compare after warmup + double maxDiff = 0; + + for (int i = 0; i < tulipResult.Length; i++) + { + int qIdx = i + lookback; + double diff = Math.Abs(tulipResult[i] - qResult[qIdx].Value); + if (diff > maxDiff) + { + maxDiff = diff; + } + } + + _output.WriteLine($"Tulip CCI period={period}: Max difference = {maxDiff:E3}"); + + // Tulip uses same formula - should match closely + for (int i = 0; i < tulipResult.Length; i++) + { + int qIdx = i + lookback; + Assert.Equal(tulipResult[i], qResult[qIdx].Value, 1e-6); + } + } + + [Theory] + [InlineData(5)] + [InlineData(10)] + [InlineData(14)] + [InlineData(20)] + [InlineData(50)] + public void Cci_MatchesTulip_DifferentPeriods(int period) + { + var cci = new Cci(period); + var qResult = cci.Update(_testData.Bars); + + double[] high = _testData.Bars.Select(b => b.High).ToArray(); + double[] low = _testData.Bars.Select(b => b.Low).ToArray(); + double[] close = _testData.Bars.Select(b => b.Close).ToArray(); + + var cciIndicator = Tulip.Indicators.cci; + double[][] inputs = [high, low, close]; + double[] options = [period]; + int lookback = cciIndicator.Start(options); + double[][] outputs = [new double[high.Length - lookback]]; + + cciIndicator.Run(inputs, options, outputs); + double[] tulipResult = outputs[0]; + + for (int i = 0; i < tulipResult.Length; i++) + { + int qIdx = i + lookback; + Assert.Equal(tulipResult[i], qResult[qIdx].Value, 1e-6); + } + + _output.WriteLine($"Tulip CCI period={period}: Validated successfully"); + } + + [Fact] + public void Cci_StreamingMatchesTulip() + { + int period = 20; + + double[] high = _testData.Bars.Select(b => b.High).ToArray(); + double[] low = _testData.Bars.Select(b => b.Low).ToArray(); + double[] close = _testData.Bars.Select(b => b.Close).ToArray(); + + // Calculate Tulip CCI + var cciIndicator = Tulip.Indicators.cci; + double[][] inputs = [high, low, close]; + double[] options = [period]; + int lookback = cciIndicator.Start(options); + double[][] outputs = [new double[high.Length - lookback]]; + + cciIndicator.Run(inputs, options, outputs); + double[] tulipResult = outputs[0]; + + // Calculate QuanTAlib streaming + var cci = new Cci(period); + var streamingResults = new List(); + + foreach (var bar in _testData.Bars) + { + streamingResults.Add(cci.Update(bar).Value); + } + + // Compare after warmup + for (int i = 0; i < tulipResult.Length; i++) + { + int qIdx = i + lookback; + Assert.Equal(tulipResult[i], streamingResults[qIdx], 1e-6); + } + + _output.WriteLine($"Tulip CCI streaming: Validated successfully"); + } + + #endregion + + #region Manual Calculation Validation + + [Fact] + public void Cci_MatchesManualCalculation() + { + int period = 5; + + // Create simple test data + var bars = new TBarSeries(); + var baseTime = DateTime.UtcNow.Ticks; + var timeStep = TimeSpan.FromMinutes(1).Ticks; + + // Create bars with known values for manual verification + double[] highs = [22, 24, 23, 25, 26, 27, 26, 28, 27, 29]; + double[] lows = [20, 22, 21, 23, 24, 25, 24, 26, 25, 27]; + double[] closes = [21, 23, 22, 24, 25, 26, 25, 27, 26, 28]; + + for (int i = 0; i < highs.Length; i++) + { + bars.Add(new TBar( + baseTime + (i * timeStep), + 21.0 + i, // open + highs[i], + lows[i], + closes[i], + 1000)); // volume + } + + // Calculate using our CCI + var cci = new Cci(period); + var qResult = cci.Update(bars); + + // Manual calculation for last value (index 9) + // TP values for last 5 bars (indices 5-9): + // TP[5] = (27 + 25 + 26) / 3 = 26 + // TP[6] = (26 + 24 + 25) / 3 = 25 + // TP[7] = (28 + 26 + 27) / 3 = 27 + // TP[8] = (27 + 25 + 26) / 3 = 26 + // TP[9] = (29 + 27 + 28) / 3 = 28 + + double tp5 = (27.0 + 25.0 + 26.0) / 3.0; + double tp6 = (26.0 + 24.0 + 25.0) / 3.0; + double tp7 = (28.0 + 26.0 + 27.0) / 3.0; + double tp8 = (27.0 + 25.0 + 26.0) / 3.0; + double tp9 = (29.0 + 27.0 + 28.0) / 3.0; + + double smaTP = (tp5 + tp6 + tp7 + tp8 + tp9) / 5.0; + double meanDev = (Math.Abs(tp5 - smaTP) + Math.Abs(tp6 - smaTP) + Math.Abs(tp7 - smaTP) + Math.Abs(tp8 - smaTP) + Math.Abs(tp9 - smaTP)) / 5.0; + double expectedCci = (tp9 - smaTP) / (0.015 * meanDev); + + _output.WriteLine($"Manual CCI calculation:"); + _output.WriteLine($" TP[5-9] = {tp5:F4}, {tp6:F4}, {tp7:F4}, {tp8:F4}, {tp9:F4}"); + _output.WriteLine($" SMA(TP) = {smaTP:F4}"); + _output.WriteLine($" Mean Dev = {meanDev:F4}"); + _output.WriteLine($" Expected CCI = {expectedCci:F4}"); + _output.WriteLine($" QuanTAlib CCI = {qResult[9].Value:F4}"); + + Assert.Equal(expectedCci, qResult[9].Value, 1e-10); + } + + #endregion + + #region Streaming vs Batch Validation + + [Fact] + public void Cci_StreamingMatchesBatch() + { + int period = 14; + + // Batch + var batchResult = Cci.Batch(_testData.Bars, period); + + // Streaming + var cci = new Cci(period); + var streamingResults = new List(); + + foreach (var bar in _testData.Bars) + { + streamingResults.Add(cci.Update(bar).Value); + } + + Assert.Equal(batchResult.Count, streamingResults.Count); + + for (int i = 0; i < batchResult.Count; i++) + { + Assert.Equal(batchResult[i].Value, streamingResults[i], 1e-10); + } + + _output.WriteLine($"CCI Streaming matches Batch: Validated {batchResult.Count} values"); + } + + #endregion + + #region Edge Cases + + [Fact] + public void Cci_FlatMarket_ReturnsZero() + { + // Create flat market data where all prices are the same + var bars = new TBarSeries(); + var baseTime = DateTime.UtcNow.Ticks; + var timeStep = TimeSpan.FromMinutes(1).Ticks; + + for (int i = 0; i < 30; i++) + { + bars.Add(new TBar( + baseTime + (i * timeStep), + 100, // open + 100, // high + 100, // low + 100, // close + 1000)); // volume + } + + var cci = new Cci(10); + var result = cci.Update(bars); + + // In flat market, TP = SMA(TP), so deviation = 0 + // CCI = 0 / (0.015 * 0) - should handle gracefully + for (int i = 10; i < result.Count; i++) + { + Assert.True(double.IsFinite(result[i].Value) || result[i].Value == 0, + $"CCI at index {i} should be finite or zero, got {result[i].Value}"); + } + + _output.WriteLine("CCI flat market validation passed"); + } + + [Fact] + public void Cci_MultiplePeriods_AllMatchTulip() + { + int[] periods = [5, 10, 14, 20, 50]; + + double[] high = _testData.Bars.Select(b => b.High).ToArray(); + double[] low = _testData.Bars.Select(b => b.Low).ToArray(); + double[] close = _testData.Bars.Select(b => b.Close).ToArray(); + + foreach (var period in periods) + { + var cci = new Cci(period); + var qResult = cci.Update(_testData.Bars); + + var cciIndicator = Tulip.Indicators.cci; + double[][] inputs = [high, low, close]; + double[] options = [period]; + int lookback = cciIndicator.Start(options); + double[][] outputs = [new double[high.Length - lookback]]; + + cciIndicator.Run(inputs, options, outputs); + double[] tulipResult = outputs[0]; + + // Check last 10 values match + int checkCount = Math.Min(10, tulipResult.Length); + for (int i = tulipResult.Length - checkCount; i < tulipResult.Length; i++) + { + int qIdx = i + lookback; + Assert.Equal(tulipResult[i], qResult[qIdx].Value, 1e-6); + } + } + + _output.WriteLine("All periods validated against Tulip"); + } + + #endregion +} diff --git a/lib/momentum/cci/Cci.cs b/lib/momentum/cci/Cci.cs new file mode 100644 index 00000000..07e75f58 --- /dev/null +++ b/lib/momentum/cci/Cci.cs @@ -0,0 +1,227 @@ +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +namespace QuanTAlib; + +/// +/// CCI: Commodity Channel Index +/// +/// +/// Measures the deviation of price from its statistical mean, normalized by mean +/// absolute deviation. Developed by Donald Lambert to identify cyclical turns. +/// +/// Calculation: +/// +/// TP = (High + Low + Close) / 3 +/// SMA = Simple Moving Average of TP over period +/// Mean Deviation = SUM(|TP - SMA|) / period +/// CCI = (TP - SMA) / (0.015 * Mean Deviation) +/// +/// +/// Key levels: +/// - Above +100: Strong uptrend, potentially overbought +/// - Below -100: Strong downtrend, potentially oversold +/// - Zero line crossover: Trend change signal +/// +/// The 0.015 constant ensures approximately 70-80% of values fall between +100 and -100. +/// +/// Detailed documentation +[SkipLocalsInit] +public sealed class Cci : ITValuePublisher +{ + private const int DefaultPeriod = 20; + private const double LambertConstant = 0.015; + + private readonly int _period; + private readonly RingBuffer _tpBuffer; + private int _sampleCount; + private double _lastValid; + private TValue _last; + + // State for bar correction + [StructLayout(LayoutKind.Auto)] + private record struct State(int SampleCount, double LastValid, double Sum); + private State _state, _p_state; + + /// + /// Event fired when a new CCI value is calculated. + /// + public event TValuePublishedHandler? Pub; + + /// + /// Most recently calculated CCI value. + /// + public TValue Last => _last; + + /// + /// True when the indicator has enough data for valid calculations. + /// + public bool IsHot => _sampleCount >= _period; + + /// + /// The lookback period. + /// + public int Period => _period; + + /// + /// Number of bars required for warmup. + /// + public static int WarmupPeriod => DefaultPeriod; + + /// + /// Creates a CCI indicator with specified period. + /// + /// Lookback period (must be >= 2, default 20) + public Cci(int period = DefaultPeriod) + { + if (period < 2) + { + throw new ArgumentException("Period must be >= 2", nameof(period)); + } + + _period = period; + _tpBuffer = new RingBuffer(period); + _sampleCount = 0; + _lastValid = 0; + _last = new TValue(DateTime.MinValue, 0); + } + + /// + /// Resets the indicator to its initial state. + /// + public void Reset() + { + _tpBuffer.Clear(); + _sampleCount = 0; + _lastValid = 0; + _last = default; + _state = default; + _p_state = default; + } + + /// + /// Updates the CCI with a new bar. + /// + /// The input bar with OHLC data + /// True for a new bar, false for updating current bar + /// The updated CCI value + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public TValue Update(TBar bar, bool isNew = true) + { + // State management for bar correction + if (isNew) + { + _p_state = _state; + _sampleCount++; + } + else + { + _state = _p_state; + _sampleCount = _state.SampleCount + 1; + } + + // Calculate typical price + double tp = (bar.High + bar.Low + bar.Close) / 3.0; + + // Handle invalid values + if (!double.IsFinite(tp)) + { + tp = _lastValid; + } + else + { + _lastValid = tp; + } + + // Add to buffer + _tpBuffer.Add(tp, isNew); + + // Calculate CCI + double result = CalculateCci(tp); + + // Save state + _state = new State(_sampleCount, _lastValid, 0); + + _last = new TValue(bar.Time, result); + Pub?.Invoke(this, new TValueEventArgs { Value = _last, IsNew = isNew }); + return _last; + } + + /// + /// Updates CCI from a TBarSeries. + /// + public TSeries Update(TBarSeries source) + { + var result = new TSeries(source.Count); + for (int i = 0; i < source.Count; i++) + { + var tv = Update(source[i], true); + result.Add(tv, true); + } + return result; + } + + /// + /// Primes the indicator with historical bars. + /// + public void Prime(TBarSeries source) + { + for (int i = 0; i < source.Count; i++) + { + Update(source[i], true); + } + } + + /// + /// Convenience method for batch processing. + /// + public static TSeries Batch(TBarSeries source, int period = DefaultPeriod) + { + var indicator = new Cci(period); + return indicator.Update(source); + } + + /// + /// Calculates CCI and returns both the result and the indicator instance. + /// + public static (TSeries Results, Cci Indicator) Calculate(TBarSeries source, int period = DefaultPeriod) + { + var indicator = new Cci(period); + var results = indicator.Update(source); + return (results, indicator); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private double CalculateCci(double currentTp) + { + int count = _tpBuffer.Count; + if (count == 0) + { + return 0.0; + } + + // Calculate SMA of typical prices + double sum = 0.0; + for (int i = 0; i < count; i++) + { + sum += _tpBuffer[i]; + } + double sma = sum / count; + + // Calculate mean deviation + double devSum = 0.0; + for (int i = 0; i < count; i++) + { + devSum += Math.Abs(_tpBuffer[i] - sma); + } + double meanDev = devSum / count; + + // Calculate CCI + if (meanDev <= double.Epsilon) + { + return 0.0; + } + + return (currentTp - sma) / (LambertConstant * meanDev); + } +} diff --git a/lib/momentum/cci/Cci.md b/lib/momentum/cci/Cci.md new file mode 100644 index 00000000..fcc964d3 --- /dev/null +++ b/lib/momentum/cci/Cci.md @@ -0,0 +1,95 @@ +# CCI - Commodity Channel Index + +## Overview + +The Commodity Channel Index (CCI) is a versatile momentum-based oscillator developed by Donald Lambert in 1980. Originally designed for commodity trading, it measures the deviation of price from its statistical mean, normalized by mean absolute deviation. + +## Formula + +``` +TP = (High + Low + Close) / 3 +SMA = Simple Moving Average of TP over period +Mean Deviation = SUM(|TP - SMA|) / period +CCI = (TP - SMA) / (0.015 × Mean Deviation) +``` + +## Key Characteristics + +| Property | Value | +|----------|-------| +| Default Period | 20 | +| Lambert Constant | 0.015 | +| Returns | Unbounded oscillator (typically -300 to +300) | +| Warmup Period | Equal to period | +| Input Data | OHLC bars (uses Typical Price) | + +## Signal Interpretation + +### Primary Levels +- **Above +100**: Strong uptrend, potentially overbought +- **Below -100**: Strong downtrend, potentially oversold +- **Zero Line**: Centerline crossover indicates trend change + +### Trading Strategies +1. **Trend Identification**: Values consistently above/below zero indicate trend direction +2. **Overbought/Oversold**: Extreme readings (+200/-200) suggest reversal potential +3. **Divergence**: Price making new high/low while CCI fails to confirm +4. **Zero-Line Cross**: Bullish when crossing above, bearish when crossing below + +## Lambert Constant (0.015) + +The 0.015 constant was chosen by Lambert to ensure that approximately 70-80% of CCI values fall between +100 and -100 under normal market conditions. This provides a statistical framework where: +- Values outside ±100 indicate significant price movement +- Extended readings suggest strong trends +- Extreme values (±200 or beyond) are relatively rare + +## Usage + +### Basic Construction +```csharp +// Create CCI with default 20-period +var cci = new Cci(); + +// Create CCI with custom period +var cci = new Cci(14); +``` + +### Streaming Updates +```csharp +foreach (var bar in realTimeData) +{ + TValue result = cci.Update(bar); + double cciValue = result.Value; + + if (cciValue > 100) + Console.WriteLine("Overbought territory"); + else if (cciValue < -100) + Console.WriteLine("Oversold territory"); +} +``` + +### Batch Processing +```csharp +TSeries results = Cci.Batch(barSeries, period: 20); +``` + +## Comparison with Other Oscillators + +| Indicator | Bounds | Best For | +|-----------|--------|----------| +| CCI | Unbounded | Trend strength, divergence | +| RSI | 0-100 | Overbought/oversold levels | +| Stochastic | 0-100 | Price position within range | + +## Historical Context + +- Developed by Donald Lambert (1980) +- Originally published in *Commodities* magazine +- Early application for identifying cyclical trends in commodities +- Now widely used across all asset classes + +## References + +- Lambert, D.R. (1980). "Commodity Channel Index: Tool for Trading Cyclic Trends" +- [TradingView CCI Documentation](https://www.tradingview.com/support/solutions/43000502001/) +- [Investopedia CCI Guide](https://www.investopedia.com/terms/c/commoditychannelindex.asp) diff --git a/lib/momentum/cfb/Cfb.cs b/lib/momentum/cfb/Cfb.cs index dc7d41b9..622890c3 100644 --- a/lib/momentum/cfb/Cfb.cs +++ b/lib/momentum/cfb/Cfb.cs @@ -433,4 +433,11 @@ public sealed class Cfb : ITValuePublisher, IDisposable prevCfb = cfb; } } -} + + public static (TSeries Results, Cfb Indicator) Calculate(TSeries source, int[]? lengths = null) + { + var indicator = new Cfb(lengths); + TSeries results = indicator.Update(source); + return (results, indicator); + } +} \ No newline at end of file diff --git a/lib/momentum/cmo/Cmo.Quantower.Tests.cs b/lib/momentum/cmo/Cmo.Quantower.Tests.cs new file mode 100644 index 00000000..7c638d01 --- /dev/null +++ b/lib/momentum/cmo/Cmo.Quantower.Tests.cs @@ -0,0 +1,129 @@ +using TradingPlatform.BusinessLayer; +using QuanTAlib; + +namespace QuanTAlib.Tests; + +public class CmoIndicatorTests +{ + [Fact] + public void CmoIndicator_Constructor_SetsDefaults() + { + var indicator = new CmoIndicator(); + + Assert.Equal("CMO - Chande Momentum Oscillator", indicator.Name); + Assert.True(indicator.SeparateWindow); + Assert.True(indicator.OnBackGround); + Assert.Equal(14, indicator.Period); + } + + [Fact] + public void CmoIndicator_MinHistoryDepths_IsStatic() + { + var indicator = new CmoIndicator + { + Period = 20, + }; + + Assert.Equal(0, CmoIndicator.MinHistoryDepths); + IWatchlistIndicator watchlistIndicator = indicator; + Assert.Equal(0, watchlistIndicator.MinHistoryDepths); + } + + [Fact] + public void CmoIndicator_ShortName_IncludesPeriod() + { + var indicator = new CmoIndicator + { + Period = 20, + }; + indicator.Initialize(); + + Assert.Contains("CMO(20)", indicator.ShortName, StringComparison.Ordinal); + } + + [Fact] + public void CmoIndicator_SourceCodeLink_IsValid() + { + var indicator = new CmoIndicator(); + + Assert.Contains("github.com", indicator.SourceCodeLink, StringComparison.Ordinal); + Assert.Contains("Cmo.Quantower.cs", indicator.SourceCodeLink, StringComparison.Ordinal); + } + + [Fact] + public void CmoIndicator_Initialize_CreatesInternalCmo() + { + var indicator = new CmoIndicator(); + + // Initialize should not throw + indicator.Initialize(); + + // After init, line series should exist (CMO) + Assert.Single(indicator.LinesSeries); + } + + [Fact] + public void CmoIndicator_ProcessUpdate_HistoricalBar_ComputesValue() + { + var indicator = new CmoIndicator + { + Period = 2, // Short period for testing + }; + indicator.Initialize(); + + // Add historical data + var now = DateTime.UtcNow; + indicator.HistoricalData.AddBar(now, 100, 105, 95, 100); + indicator.HistoricalData.AddBar(now.AddMinutes(1), 100, 105, 95, 102); // Gain 2 + indicator.HistoricalData.AddBar(now.AddMinutes(2), 100, 105, 95, 101); // Loss 1 + + // Process updates + var args = new UpdateArgs(UpdateReason.HistoricalBar); + + indicator.ProcessUpdate(args); // Bar 0 + indicator.ProcessUpdate(args); // Bar 1 + indicator.ProcessUpdate(args); // Bar 2 + + // Line series should have a value + double cmo = indicator.LinesSeries[0].GetValue(0); + + // CMO ranges from -100 to +100 + Assert.True(cmo >= -100 && cmo <= 100); + } + + [Fact] + public void CmoIndicator_Period_CanBeSet() + { + var indicator = new CmoIndicator { Period = 20 }; + Assert.Equal(20, indicator.Period); + } + + [Fact] + public void CmoIndicator_Source_CanBeSet() + { + var indicator = new CmoIndicator { Source = SourceType.HLC3 }; + Assert.Equal(SourceType.HLC3, indicator.Source); + } + + [Fact] + public void CmoIndicator_OnUpdate_ProducesValidRange() + { + var indicator = new CmoIndicator { Period = 2 }; + indicator.Initialize(); + + var now = DateTime.UtcNow; + indicator.HistoricalData.AddBar(now, 100, 105, 95, 100); + indicator.HistoricalData.AddBar(now.AddMinutes(1), 100, 106, 96, 103); + indicator.HistoricalData.AddBar(now.AddMinutes(2), 100, 104, 94, 101); + + var args = new UpdateArgs(UpdateReason.HistoricalBar); + indicator.ProcessUpdate(args); + indicator.ProcessUpdate(args); + indicator.ProcessUpdate(args); + + double cmo = indicator.LinesSeries[0].GetValue(0); + + // CMO ranges from -100 to +100 + Assert.True(cmo >= -100 && cmo <= 100); + } +} diff --git a/lib/momentum/cmo/Cmo.Quantower.cs b/lib/momentum/cmo/Cmo.Quantower.cs new file mode 100644 index 00000000..17e74a9e --- /dev/null +++ b/lib/momentum/cmo/Cmo.Quantower.cs @@ -0,0 +1,59 @@ +using System.Drawing; +using System.Runtime.CompilerServices; +using TradingPlatform.BusinessLayer; + +namespace QuanTAlib; + +[SkipLocalsInit] +public sealed class CmoIndicator : Indicator, IWatchlistIndicator +{ + [InputParameter("Period", sortIndex: 1, 1, 2000, 1, 0)] + public int Period { get; set; } = 14; + + [IndicatorExtensions.DataSourceInput] + public SourceType Source { get; set; } = SourceType.Close; + + [InputParameter("Show cold values", sortIndex: 21)] + public bool ShowColdValues { get; set; } = true; + + private Cmo _cmo = null!; + private readonly LineSeries _series; + private string _sourceName = null!; + private Func _priceSelector = null!; + + public static int MinHistoryDepths => 0; + int IWatchlistIndicator.MinHistoryDepths => MinHistoryDepths; + + public override string ShortName => $"CMO({Period}):{_sourceName}"; + public override string SourceCodeLink => "https://github.com/mihakralj/QuanTAlib/blob/main/lib/momentum/cmo/Cmo.Quantower.cs"; + + public CmoIndicator() + { + OnBackGround = true; + SeparateWindow = true; + _sourceName = Source.ToString(); + Name = "CMO - Chande Momentum Oscillator"; + Description = "Measures the momentum of price movements using the difference between gains and losses"; + + _series = new LineSeries(name: "CMO", color: Color.Yellow, width: 2, style: LineStyle.Solid); + AddLineSeries(_series); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + protected override void OnInit() + { + _cmo = new Cmo(Period); + _sourceName = Source.ToString(); + _priceSelector = Source.GetPriceSelector(); + base.OnInit(); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + protected override void OnUpdate(UpdateArgs args) + { + TValue result = _cmo.Update(new TValue(this.GetInputBar(args).Time, _priceSelector(HistoricalData[Count - 1, SeekOriginHistory.Begin])), args.IsNewBar()); + + _series.SetValue(result.Value, _cmo.IsHot, ShowColdValues); + _series.SetMarker(0, Color.Transparent); + } +} diff --git a/lib/momentum/cmo/Cmo.Tests.cs b/lib/momentum/cmo/Cmo.Tests.cs new file mode 100644 index 00000000..eb8a9416 --- /dev/null +++ b/lib/momentum/cmo/Cmo.Tests.cs @@ -0,0 +1,463 @@ +using Xunit; + +namespace QuanTAlib.Tests; + +public class CmoTests +{ + private const double Epsilon = 1e-10; + + // ═══════════════════════════════════════════════════════════════════════════ + // Ctor Tests + // ═══════════════════════════════════════════════════════════════════════════ + + [Fact] + public void Ctor_WithValidPeriod_CreatesIndicator() + { + var cmo = new Cmo(14); + Assert.Equal("Cmo(14)", cmo.Name); + Assert.Equal(15, cmo.WarmupPeriod); + } + + [Fact] + public void Ctor_WithDefaultPeriod_UsesDefault() + { + var cmo = new Cmo(); + Assert.Equal("Cmo(14)", cmo.Name); + } + + [Fact] + public void Ctor_WithPeriodOne_IsValid() + { + var cmo = new Cmo(1); + Assert.Equal("Cmo(1)", cmo.Name); + } + + [Fact] + public void Ctor_WithZeroPeriod_ThrowsArgumentException() + { + Assert.Throws(() => new Cmo(0)); + } + + [Fact] + public void Ctor_WithNegativePeriod_ThrowsArgumentException() + { + Assert.Throws(() => new Cmo(-1)); + } + + // ═══════════════════════════════════════════════════════════════════════════ + // Warmup/IsHot Tests + // ═══════════════════════════════════════════════════════════════════════════ + + [Fact] + public void IsHot_BeforeWarmup_ReturnsFalse() + { + var cmo = new Cmo(5); + + for (int i = 0; i < 4; i++) + { + cmo.Update(new TValue(DateTime.Now.Ticks + i, 100 + i)); + } + + Assert.False(cmo.IsHot); + } + + [Fact] + public void IsHot_AfterWarmup_ReturnsTrue() + { + var cmo = new Cmo(5); + + for (int i = 0; i < 6; i++) + { + cmo.Update(new TValue(DateTime.Now.Ticks + i, 100 + i)); + } + + Assert.True(cmo.IsHot); + } + + // ═══════════════════════════════════════════════════════════════════════════ + // Calculation Tests + // ═══════════════════════════════════════════════════════════════════════════ + + [Fact] + public void Update_WithSingleValue_ReturnsZero() + { + var cmo = new Cmo(5); + + var result = cmo.Update(new TValue(DateTime.Now.Ticks, 100.0)); + + Assert.Equal(0, result.Value); + } + + [Fact] + public void Update_AllUpMoves_ReturnsPositive100() + { + var cmo = new Cmo(5); + + // All upward moves should give CMO = 100 + cmo.Update(new TValue(DateTime.Now.Ticks, 100)); + cmo.Update(new TValue(DateTime.Now.Ticks + 1, 101)); + cmo.Update(new TValue(DateTime.Now.Ticks + 2, 102)); + cmo.Update(new TValue(DateTime.Now.Ticks + 3, 103)); + cmo.Update(new TValue(DateTime.Now.Ticks + 4, 104)); + var result = cmo.Update(new TValue(DateTime.Now.Ticks + 5, 105)); + + Assert.Equal(100.0, result.Value, Epsilon); + } + + [Fact] + public void Update_AllDownMoves_ReturnsNegative100() + { + var cmo = new Cmo(5); + + // All downward moves should give CMO = -100 + cmo.Update(new TValue(DateTime.Now.Ticks, 105)); + cmo.Update(new TValue(DateTime.Now.Ticks + 1, 104)); + cmo.Update(new TValue(DateTime.Now.Ticks + 2, 103)); + cmo.Update(new TValue(DateTime.Now.Ticks + 3, 102)); + cmo.Update(new TValue(DateTime.Now.Ticks + 4, 101)); + var result = cmo.Update(new TValue(DateTime.Now.Ticks + 5, 100)); + + Assert.Equal(-100.0, result.Value, Epsilon); + } + + [Fact] + public void Update_EqualUpAndDown_ReturnsZero() + { + var cmo = new Cmo(4); + + // Pattern: up 2, down 2, up 2, down 2 = equal + cmo.Update(new TValue(DateTime.Now.Ticks, 100)); + cmo.Update(new TValue(DateTime.Now.Ticks + 1, 102)); // up 2 + cmo.Update(new TValue(DateTime.Now.Ticks + 2, 100)); // down 2 + cmo.Update(new TValue(DateTime.Now.Ticks + 3, 102)); // up 2 + var result = cmo.Update(new TValue(DateTime.Now.Ticks + 4, 100)); // down 2 + + // sumUp = 4, sumDown = 4, CMO = 0 + Assert.Equal(0.0, result.Value, Epsilon); + } + + [Fact] + public void Update_NoChange_ReturnsZero() + { + var cmo = new Cmo(5); + + // No change in prices + for (int i = 0; i < 10; i++) + { + cmo.Update(new TValue(DateTime.Now.Ticks + i, 100)); + } + + Assert.Equal(0, cmo.Last.Value); + } + + [Fact] + public void Update_MixedMoves_CalculatesCorrectCmo() + { + var cmo = new Cmo(5); + + // Construct specific pattern + cmo.Update(new TValue(DateTime.Now.Ticks, 100)); + cmo.Update(new TValue(DateTime.Now.Ticks + 1, 105)); // up 5 + cmo.Update(new TValue(DateTime.Now.Ticks + 2, 102)); // down 3 + cmo.Update(new TValue(DateTime.Now.Ticks + 3, 107)); // up 5 + cmo.Update(new TValue(DateTime.Now.Ticks + 4, 104)); // down 3 + var result = cmo.Update(new TValue(DateTime.Now.Ticks + 5, 106)); // up 2 + + // sumUp = 5+5+2 = 12, sumDown = 3+3 = 6 + // CMO = 100 * (12-6)/(12+6) = 100 * 6/18 = 33.333... + Assert.Equal(100.0 * 6.0 / 18.0, result.Value, Epsilon); + } + + // ═══════════════════════════════════════════════════════════════════════════ + // Sliding Window Tests + // ═══════════════════════════════════════════════════════════════════════════ + + [Fact] + public void Update_SlidingWindow_DropsOldValues() + { + var cmo = new Cmo(3); + + cmo.Update(new TValue(DateTime.Now.Ticks, 100)); // no change (first value) + cmo.Update(new TValue(DateTime.Now.Ticks + 1, 110)); // up 10 + cmo.Update(new TValue(DateTime.Now.Ticks + 2, 105)); // down 5 + cmo.Update(new TValue(DateTime.Now.Ticks + 3, 108)); // up 3, window now has: up 10, down 5, up 3 + + // Window contains changes from last 3 bars: up 10, down 5, up 3 + // sumUp = 10 + 3 = 13, sumDown = 5 + // CMO = 100 * (13-5)/(13+5) = 100 * 8/18 = 44.444... + var result = cmo.Last; + Assert.Equal(100.0 * 8.0 / 18.0, result.Value, Epsilon); + } + + // ═══════════════════════════════════════════════════════════════════════════ + // Bar Update Tests (isNew = false) + // ═══════════════════════════════════════════════════════════════════════════ + + [Fact] + public void Update_WithIsNewFalse_UpdatesCurrentBar() + { + var cmo = new Cmo(5); + + // Initial values: alternating to mix up/down + cmo.Update(new TValue(DateTime.Now.Ticks, 100)); + cmo.Update(new TValue(DateTime.Now.Ticks + 1, 105)); // up 5 + cmo.Update(new TValue(DateTime.Now.Ticks + 2, 102)); // down 3 + cmo.Update(new TValue(DateTime.Now.Ticks + 3, 106)); // up 4 + cmo.Update(new TValue(DateTime.Now.Ticks + 4, 104)); // down 2 + cmo.Update(new TValue(DateTime.Now.Ticks + 5, 108)); // up 4, isNew + + var beforeUpdate = cmo.Last; + + // Update current bar with different value (isNew = false) + cmo.Update(new TValue(DateTime.Now.Ticks + 5, 100), isNew: false); // now down 4 instead of up 4 + var afterUpdate = cmo.Last; + + // Value should change since we updated the last bar + Assert.NotEqual(beforeUpdate.Value, afterUpdate.Value); + } + + [Fact] + public void Update_MultipleIsNewFalse_DoesNotAdvanceWindow() + { + var cmo = new Cmo(5); + + // Initial values + for (int i = 0; i < 5; i++) + { + cmo.Update(new TValue(DateTime.Now.Ticks + i, 100.0)); + } + + // Multiple updates with isNew = false + cmo.Update(new TValue(DateTime.Now.Ticks + 5, 110), isNew: false); + cmo.Update(new TValue(DateTime.Now.Ticks + 5, 115), isNew: false); + cmo.Update(new TValue(DateTime.Now.Ticks + 5, 120), isNew: false); + + // Final state should reflect the last update only + // Since all previous values were 100, the only "up" is the current bar update + Assert.True(cmo.Last.Value > 0); // Should be positive since we went from 100 to 120 + } + + // ═══════════════════════════════════════════════════════════════════════════ + // Reset Tests + // ═══════════════════════════════════════════════════════════════════════════ + + [Fact] + public void Reset_ClearsState() + { + var cmo = new Cmo(5); + + for (int i = 0; i < 10; i++) + { + cmo.Update(new TValue(DateTime.Now.Ticks + i, 100 + i)); + } + + cmo.Reset(); + + Assert.Equal(0, cmo.Last.Time); + Assert.False(cmo.IsHot); + } + + [Fact] + public void Reset_AllowsReuse() + { + var cmo = new Cmo(5); + + for (int i = 0; i < 10; i++) + { + cmo.Update(new TValue(DateTime.Now.Ticks + i, 100 + i)); + } + + cmo.Reset(); + + // Should work after reset + var result = cmo.Update(new TValue(DateTime.Now.Ticks, 50)); + Assert.Equal(0, result.Value); // First value, no change yet + } + + // ═══════════════════════════════════════════════════════════════════════════ + // Batch Tests + // ═══════════════════════════════════════════════════════════════════════════ + + [Fact] + public void Batch_ProducesCorrectResults() + { + var prices = new TSeries(); + for (int i = 0; i < 20; i++) + { + prices.Add(new TValue(DateTime.Now.Ticks + i, 100 + Math.Sin(i) * 10)); + } + + var results = Cmo.Batch(prices, 5); + + Assert.Equal(prices.Count, results.Count); + } + + [Fact] + public void Batch_MatchesStreamingResults() + { + var prices = new TSeries(); + for (int i = 0; i < 50; i++) + { + prices.Add(new TValue(DateTime.Now.Ticks + i, 100 + Math.Sin(i * 0.5) * 20)); + } + + var batchResults = Cmo.Batch(prices, 14); + + var cmo = new Cmo(14); + var streamingResults = new TSeries(); + foreach (var price in prices) + { + streamingResults.Add(cmo.Update(price)); + } + + Assert.Equal(batchResults.Count, streamingResults.Count); + for (int i = 0; i < batchResults.Count; i++) + { + Assert.Equal(batchResults[i].Value, streamingResults[i].Value, 1e-9); + } + } + + // ═══════════════════════════════════════════════════════════════════════════ + // Edge Case Tests + // ═══════════════════════════════════════════════════════════════════════════ + + [Fact] + public void Update_WithNaN_DoesNotCrash() + { + var cmo = new Cmo(5); + + for (int i = 0; i < 5; i++) + { + cmo.Update(new TValue(DateTime.Now.Ticks + i, 100)); + } + + // NaN input should not throw + var result = cmo.Update(new TValue(DateTime.Now.Ticks + 5, double.NaN)); + + // The result is a valid TValue (struct is never null) + Assert.True(result.Time > 0); + } + + [Fact] + public void Update_WithLargeValues_CalculatesCorrectly() + { + var cmo = new Cmo(5); + + double largeBase = 1e15; + for (int i = 0; i < 6; i++) + { + cmo.Update(new TValue(DateTime.Now.Ticks + i, largeBase + i)); + } + + // All up moves, should be 100 + Assert.Equal(100.0, cmo.Last.Value, 1e-6); + } + + [Fact] + public void Update_WithVerySmallChanges_CalculatesCorrectly() + { + var cmo = new Cmo(5); + + double baseVal = 100.0; + double smallChange = 1e-10; + for (int i = 0; i < 6; i++) + { + cmo.Update(new TValue(DateTime.Now.Ticks + i, baseVal + i * smallChange)); + } + + // All tiny up moves, should still be 100 + Assert.Equal(100.0, cmo.Last.Value, 1e-6); + } + + // ═══════════════════════════════════════════════════════════════════════════ + // Publisher Pattern Tests + // ═══════════════════════════════════════════════════════════════════════════ + + [Fact] + public void Pub_PublishesOnUpdate() + { + var cmo = new Cmo(5); + TValue? received = null; + + cmo.Pub += (object? sender, in TValueEventArgs args) => + { + received = args.Value; + }; + + for (int i = 0; i < 6; i++) + { + cmo.Update(new TValue(DateTime.Now.Ticks + i, 100 + i)); + } + + Assert.NotNull(received); + Assert.Equal(cmo.Last.Value, received!.Value.Value); + } + + [Fact] + public void Ctor_WithSourcePublisher_SubscribesCorrectly() + { + var source = new Sma(5); + var cmo = new Cmo(source, 5); + + for (int i = 0; i < 10; i++) + { + source.Update(new TValue(DateTime.Now.Ticks + i, 100 + i)); + } + + Assert.True(cmo.IsHot); + } + + // ═══════════════════════════════════════════════════════════════════════════ + // Prime Tests + // ═══════════════════════════════════════════════════════════════════════════ + + [Fact] + public void Prime_WarmupIndicator() + { + var cmo = new Cmo(5); + Span data = [100, 101, 102, 103, 104, 105]; + + cmo.Prime(data); + + Assert.True(cmo.IsHot); + Assert.Equal(100.0, cmo.Last.Value, Epsilon); // All up moves + } + + // ═══════════════════════════════════════════════════════════════════════════ + // Calculate Static Method Tests + // ═══════════════════════════════════════════════════════════════════════════ + + [Fact] + public void Calculate_MatchesInstanceMethod() + { + double[] source = [100, 105, 102, 107, 104, 106, 103, 108, 101, 109]; + double[] output = new double[source.Length]; + + Cmo.Batch(source, output, 5); + + var cmo = new Cmo(5); + for (int i = 0; i < source.Length; i++) + { + var result = cmo.Update(new TValue(DateTime.Now.Ticks + i, source[i])); + Assert.Equal(output[i], result.Value, 1e-9); + } + } + + [Fact] + public void Calculate_WithMismatchedLengths_ThrowsArgumentException() + { + double[] source = new double[10]; + double[] output = new double[5]; + + Assert.Throws(() => Cmo.Batch(source, output, 5)); + } + + [Fact] + public void Calculate_WithInvalidPeriod_ThrowsArgumentException() + { + double[] source = new double[10]; + double[] output = new double[10]; + + Assert.Throws(() => Cmo.Batch(source, output, 0)); + } +} diff --git a/lib/momentum/cmo/Cmo.Validation.Tests.cs b/lib/momentum/cmo/Cmo.Validation.Tests.cs new file mode 100644 index 00000000..fb5036b8 --- /dev/null +++ b/lib/momentum/cmo/Cmo.Validation.Tests.cs @@ -0,0 +1,272 @@ +using Xunit; + +namespace QuanTAlib.Tests; + +/// +/// Validation tests for CMO against external libraries. +/// CMO = 100 × (SumUp - SumDown) / (SumUp + SumDown) +/// +public class CmoValidationTests +{ + private const double Epsilon = 1e-9; + + // ═══════════════════════════════════════════════════════════════════════════ + // Tulip Indicators Validation + // ═══════════════════════════════════════════════════════════════════════════ + + [Fact] + public void Cmo_MatchesTulip_StandardData() + { + // Generate test data + double[] prices = new double[50]; + for (int i = 0; i < prices.Length; i++) + { + prices[i] = 100 + Math.Sin(i * 0.3) * 10 + i * 0.1; + } + + int period = 14; + + // Calculate using Tulip + var cmoIndicator = Tulip.Indicators.cmo; + double[][] inputs = [prices]; + double[] options = [period]; + int lookback = cmoIndicator.Start(options); + double[][] outputs = [new double[prices.Length - lookback]]; + cmoIndicator.Run(inputs, options, outputs); + double[] tulipOutput = outputs[0]; + + // Calculate using our CMO + double[] ourOutput = new double[prices.Length]; + Cmo.Batch(prices, ourOutput, period); + + // Compare results - Tulip outputs from index 0 corresponding to our index period + for (int i = 0; i < tulipOutput.Length; i++) + { + Assert.Equal(tulipOutput[i], ourOutput[i + lookback], Epsilon); + } + } + + [Fact] + public void Cmo_MatchesTulip_UpwardTrend() + { + // Steadily increasing prices + double[] prices = new double[30]; + for (int i = 0; i < prices.Length; i++) + { + prices[i] = 100 + i * 2; + } + + int period = 10; + + var cmoIndicator = Tulip.Indicators.cmo; + double[][] inputs = [prices]; + double[] options = [period]; + int lookback = cmoIndicator.Start(options); + double[][] outputs = [new double[prices.Length - lookback]]; + cmoIndicator.Run(inputs, options, outputs); + double[] tulipOutput = outputs[0]; + + double[] ourOutput = new double[prices.Length]; + Cmo.Batch(prices, ourOutput, period); + + for (int i = 0; i < tulipOutput.Length; i++) + { + Assert.Equal(tulipOutput[i], ourOutput[i + lookback], Epsilon); + } + } + + [Fact] + public void Cmo_MatchesTulip_DownwardTrend() + { + // Steadily decreasing prices + double[] prices = new double[30]; + for (int i = 0; i < prices.Length; i++) + { + prices[i] = 200 - i * 2; + } + + int period = 10; + + var cmoIndicator = Tulip.Indicators.cmo; + double[][] inputs = [prices]; + double[] options = [period]; + int lookback = cmoIndicator.Start(options); + double[][] outputs = [new double[prices.Length - lookback]]; + cmoIndicator.Run(inputs, options, outputs); + double[] tulipOutput = outputs[0]; + + double[] ourOutput = new double[prices.Length]; + Cmo.Batch(prices, ourOutput, period); + + for (int i = 0; i < tulipOutput.Length; i++) + { + Assert.Equal(tulipOutput[i], ourOutput[i + lookback], Epsilon); + } + } + + [Fact] + public void Cmo_MatchesTulip_MultiplePeriods() + { + double[] prices = new double[100]; + var random = new Random(42); + for (int i = 0; i < prices.Length; i++) + { + prices[i] = 100 + (random.NextDouble() - 0.5) * 20 + i * 0.05; + } + + int[] periods = [5, 10, 14, 20, 30]; + + foreach (int period in periods) + { + var cmoIndicator = Tulip.Indicators.cmo; + double[][] inputs = [prices]; + double[] options = [period]; + int lookback = cmoIndicator.Start(options); + double[][] outputs = [new double[prices.Length - lookback]]; + cmoIndicator.Run(inputs, options, outputs); + double[] tulipOutput = outputs[0]; + + double[] ourOutput = new double[prices.Length]; + Cmo.Batch(prices, ourOutput, period); + + for (int i = 0; i < tulipOutput.Length; i++) + { + Assert.Equal(tulipOutput[i], ourOutput[i + lookback], Epsilon); + } + } + } + + // ═══════════════════════════════════════════════════════════════════════════ + // Manual Calculation Validation + // ═══════════════════════════════════════════════════════════════════════════ + + [Fact] + public void Cmo_ManualCalculation_AllUpMoves() + { + // All upward moves + double[] prices = [100, 101, 102, 103, 104, 105]; + int period = 5; + + double[] output = new double[prices.Length]; + Cmo.Batch(prices, output, period); + + // After 5 periods: SumUp = 5, SumDown = 0 + // CMO = 100 * (5-0)/(5+0) = 100 + Assert.Equal(100.0, output[5], Epsilon); + } + + [Fact] + public void Cmo_ManualCalculation_AllDownMoves() + { + // All downward moves + double[] prices = [105, 104, 103, 102, 101, 100]; + int period = 5; + + double[] output = new double[prices.Length]; + Cmo.Batch(prices, output, period); + + // After 5 periods: SumUp = 0, SumDown = 5 + // CMO = 100 * (0-5)/(0+5) = -100 + Assert.Equal(-100.0, output[5], Epsilon); + } + + [Fact] + public void Cmo_ManualCalculation_EqualMoves() + { + // Equal up and down moves + double[] prices = [100, 102, 100, 102, 100]; // up 2, down 2, up 2, down 2 + int period = 4; + + double[] output = new double[prices.Length]; + Cmo.Batch(prices, output, period); + + // SumUp = 4, SumDown = 4 + // CMO = 100 * (4-4)/(4+4) = 0 + Assert.Equal(0.0, output[4], Epsilon); + } + + // ═══════════════════════════════════════════════════════════════════════════ + // Streaming vs Batch Validation + // ═══════════════════════════════════════════════════════════════════════════ + + [Fact] + public void Cmo_StreamingMatchesBatch() + { + double[] prices = new double[100]; + var random = new Random(12345); + for (int i = 0; i < prices.Length; i++) + { + prices[i] = 100 + (random.NextDouble() - 0.5) * 30 + Math.Sin(i * 0.2) * 5; + } + + int period = 14; + + // Batch calculation + double[] batchOutput = new double[prices.Length]; + Cmo.Batch(prices, batchOutput, period); + + // Streaming calculation + var cmo = new Cmo(period); + for (int i = 0; i < prices.Length; i++) + { + var result = cmo.Update(new TValue(DateTime.Now.Ticks + i, prices[i])); + Assert.Equal(batchOutput[i], result.Value, Epsilon); + } + } + + // ═══════════════════════════════════════════════════════════════════════════ + // Edge Case Validation + // ═══════════════════════════════════════════════════════════════════════════ + + [Fact] + public void Cmo_NoChange_ReturnsZero() + { + double[] prices = [100, 100, 100, 100, 100, 100]; + int period = 5; + + double[] output = new double[prices.Length]; + Cmo.Batch(prices, output, period); + + // No movement = 0 + Assert.Equal(0.0, output[5]); + } + + [Fact] + public void Cmo_RangeIsBounded() + { + double[] prices = new double[100]; + var random = new Random(54321); + for (int i = 0; i < prices.Length; i++) + { + prices[i] = 100 + (random.NextDouble() - 0.5) * 50; + } + + double[] output = new double[prices.Length]; + Cmo.Batch(prices, output, 14); + + // All values should be in [-100, 100] range + for (int i = 14; i < output.Length; i++) + { + Assert.True(output[i] >= -100.0 && output[i] <= 100.0, + $"CMO at index {i} = {output[i]} is out of range [-100, 100]"); + } + } + + [Fact] + public void Cmo_AlternatingMoves_ConvergesToZero() + { + // Alternating pattern with equal magnitude + double[] prices = new double[50]; + for (int i = 0; i < prices.Length; i++) + { + prices[i] = 100 + (i % 2 == 0 ? 0 : 2); // 100, 102, 100, 102, ... + } + + double[] output = new double[prices.Length]; + Cmo.Batch(prices, output, 10); + + // Result should be close to 0 for balanced oscillation + Assert.True(Math.Abs(output[^1]) < 20, + $"CMO for alternating pattern should be near zero, got {output[^1]}"); + } +} diff --git a/lib/momentum/cmo/Cmo.cs b/lib/momentum/cmo/Cmo.cs new file mode 100644 index 00000000..2b30c929 --- /dev/null +++ b/lib/momentum/cmo/Cmo.cs @@ -0,0 +1,313 @@ +// CMO: Chande Momentum Oscillator +// Developed by Tushar Chande, CMO measures momentum using both up and down changes. +// Unlike RSI which is bounded [0,100], CMO is bounded [-100,+100]. + +using System.Numerics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +namespace QuanTAlib; + +/// +/// CMO: Chande Momentum Oscillator +/// +/// +/// Momentum oscillator measuring overbought/oversold conditions [-100,+100]. +/// Uses sum of gains vs sum of losses over the lookback period. +/// +/// Calculation: CMO = 100 × (SumUp - SumDown) / (SumUp + SumDown) +/// +/// Key differences from RSI: +/// - RSI uses smoothed averages (RMA), CMO uses simple sums +/// - RSI range is [0,100], CMO range is [-100,+100] +/// - CMO is more sensitive to price changes, RSI is smoother +/// +/// Values above +50 indicate overbought, below -50 indicate oversold. +/// Zero crossings can signal momentum shifts. +/// +/// Detailed documentation +[SkipLocalsInit] +public sealed class Cmo : AbstractBase +{ + private const int DefaultPeriod = 14; + private readonly int _period; + private readonly RingBuffer _upBuffer; + private readonly RingBuffer _downBuffer; + private readonly TValuePublishedHandler _handler; + private double _prevValue; + private double _p_prevValue; + + /// + public override bool IsHot => _upBuffer.IsFull; + + /// + /// Initializes a new CMO indicator with the specified period. + /// + /// Lookback period (default: 14) + /// Thrown when period is less than 1. + public Cmo(int period = DefaultPeriod) + { + if (period < 1) + { + throw new ArgumentException("Period must be at least 1", nameof(period)); + } + + _period = period; + _upBuffer = new RingBuffer(period); + _downBuffer = new RingBuffer(period); + _handler = Handle; + _prevValue = double.NaN; + _p_prevValue = double.NaN; + + Name = $"Cmo({period})"; + WarmupPeriod = period + 1; + } + + /// + /// Initializes a CMO indicator with a source publisher. + /// + /// Source indicator providing values. + /// Lookback period (default: 14) + public Cmo(ITValuePublisher source, int period = DefaultPeriod) : this(period) + { + source.Pub += _handler; + } + + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public override TValue Update(TValue input, bool isNew = true) + { + if (isNew) + { + _p_prevValue = _prevValue; + } + else + { + _prevValue = _p_prevValue; + } + + double val = input.Value; + double up = 0; + double down = 0; + + if (!double.IsNaN(_prevValue)) + { + double change = val - _prevValue; + if (change > 0) + { + up = change; + } + else if (change < 0) + { + down = -change; + } + } + + if (isNew) + { + _prevValue = val; + } + + // Update circular buffers - RingBuffer maintains running Sum internally + _upBuffer.Add(up, isNew); + _downBuffer.Add(down, isNew); + + // Calculate CMO using RingBuffer's built-in Sum property + double sumUp = _upBuffer.Sum; + double sumDown = _downBuffer.Sum; + double denom = sumUp + sumDown; + double cmo; + if (denom < 1e-10) + { + cmo = 0; // No movement = neutral + } + else + { + cmo = 100.0 * (sumUp - sumDown) / denom; + } + + Last = new TValue(input.Time, cmo); + PubEvent(Last, isNew); + return Last; + } + + /// + public override TSeries Update(TSeries source) + { + if (source.Count == 0) + { + return []; + } + + int len = source.Count; + var t = new List(len); + var v = new List(len); + CollectionsMarshal.SetCount(t, len); + CollectionsMarshal.SetCount(v, len); + + var tSpan = CollectionsMarshal.AsSpan(t); + var vSpan = CollectionsMarshal.AsSpan(v); + + Batch(source.Values, vSpan, _period); + source.Times.CopyTo(tSpan); + + // Restore state for streaming + Reset(); + for (int i = 0; i < len; i++) + { + Update(new TValue(source.Times[i], source.Values[i])); + } + + Last = new TValue(tSpan[len - 1], vSpan[len - 1]); + return new TSeries(t, v); + } + + private void Handle(object? sender, in TValueEventArgs args) + { + Update(args.Value, args.IsNew); + } + + /// + public override void Prime(ReadOnlySpan source, TimeSpan? step = null) + { + foreach (var value in source) + { + Update(new TValue(DateTime.MinValue, value)); + } + } + + /// + /// Calculates CMO for a batch of data. + /// + /// Input price series. + /// Lookback period (default: 14) + /// TSeries containing CMO values. + public static TSeries Batch(TSeries source, int period = DefaultPeriod) + { + var cmo = new Cmo(period); + return cmo.Update(source); + } + + /// + /// SIMD-optimized batch calculation for CMO. + /// + /// Input price data. + /// Output CMO values. + /// Lookback period. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Batch(ReadOnlySpan source, Span output, int period) + { + if (source.Length != output.Length) + { + throw new ArgumentException("Source and output must have the same length", nameof(output)); + } + + if (period < 1) + { + throw new ArgumentException("Period must be at least 1", nameof(period)); + } + + int len = source.Length; + if (len == 0) + { + return; + } + + double[] ups = System.Buffers.ArrayPool.Shared.Rent(len); + double[] downs = System.Buffers.ArrayPool.Shared.Rent(len); + Span upSpan = ups.AsSpan(0, len); + Span downSpan = downs.AsSpan(0, len); + + // Calculate ups and downs + upSpan[0] = 0; + downSpan[0] = 0; + int i = 1; + + if (Vector.IsHardwareAccelerated && len > Vector.Count) + { + int vectorSize = Vector.Count; + var vZero = Vector.Zero; + + for (; i <= len - vectorSize; i += vectorSize) + { + var vCurrent = new Vector(source.Slice(i, vectorSize)); + var vPrev = new Vector(source.Slice(i - 1, vectorSize)); + var vChange = vCurrent - vPrev; + + var vUp = Vector.Max(vChange, vZero); + var vDown = Vector.Max(-vChange, vZero); + + vUp.CopyTo(upSpan.Slice(i, vectorSize)); + vDown.CopyTo(downSpan.Slice(i, vectorSize)); + } + } + + for (; i < len; i++) + { + double change = source[i] - source[i - 1]; + if (change > 0) + { + upSpan[i] = change; + downSpan[i] = 0; + } + else + { + upSpan[i] = 0; + downSpan[i] = -change; + } + } + + // Calculate rolling sums and CMO + double sumUp = 0; + double sumDown = 0; + + // Warmup phase + for (i = 0; i < Math.Min(period, len); i++) + { + sumUp += upSpan[i]; + sumDown += downSpan[i]; + double denom = sumUp + sumDown; + output[i] = denom > 1e-10 ? 100.0 * (sumUp - sumDown) / denom : 0; + } + + // Sliding window phase + for (; i < len; i++) + { + sumUp += upSpan[i] - upSpan[i - period]; + sumDown += downSpan[i] - downSpan[i - period]; + double denom = sumUp + sumDown; + output[i] = denom > 1e-10 ? 100.0 * (sumUp - sumDown) / denom : 0; + } + + System.Buffers.ArrayPool.Shared.Return(ups); + System.Buffers.ArrayPool.Shared.Return(downs); + } + + public static (TSeries Results, Cmo Indicator) Calculate(TSeries source, int period = DefaultPeriod) + { + var indicator = new Cmo(period); + TSeries results = indicator.Update(source); + return (results, indicator); + } + + /// + public override void Reset() + { + _upBuffer.Clear(); + _downBuffer.Clear(); + _prevValue = double.NaN; + _p_prevValue = double.NaN; + Last = default; + } + + /// + protected override void Dispose(bool disposing) + { + if (disposing) + { + // No external resources to dispose + } + base.Dispose(disposing); + } +} \ No newline at end of file diff --git a/lib/momentum/cmo/Cmo.md b/lib/momentum/cmo/Cmo.md new file mode 100644 index 00000000..e52a36f9 --- /dev/null +++ b/lib/momentum/cmo/Cmo.md @@ -0,0 +1,77 @@ +# CMO (Chande Momentum Oscillator) + +The Chande Momentum Oscillator (CMO) is a momentum indicator developed by Tushar Chande. Unlike RSI which uses smoothed averages of gains and losses, CMO uses raw sums of up and down movements, making it more responsive to price changes. The indicator oscillates between -100 and +100. + +## Formula + +$$CMO = 100 \times \frac{SumUp - SumDown}{SumUp + SumDown}$$ + +Where: +- **SumUp** = Sum of positive price changes over the period +- **SumDown** = Sum of absolute negative price changes over the period + +## Key Characteristics + +| Property | Value | +|----------|-------| +| Output Range | **-100 to +100** | +| Zero Line | Neutral momentum | +| Overbought | Above +50 | +| Oversold | Below -50 | +| Default Period | 14 | + +## Comparison with RSI + +| Feature | CMO | RSI | +|---------|-----|-----| +| Range | [-100, +100] | [0, 100] | +| Smoothing | None (raw sums) | RMA (exponential) | +| Sensitivity | Higher | Lower | +| Zero crossing | Valid signal | N/A (50 is neutral) | + +## Usage + +```csharp +// Create CMO indicator +var cmo = new Cmo(period: 14); + +// Single value update +var result = cmo.Update(new TValue(time, price)); + +// Batch calculation +var results = Cmo.Batch(priceData, period: 14); + +// Subscribe to source +var cmo = new Cmo(sourceIndicator, period: 14); +``` + +## Interpretation + +1. **Overbought/Oversold** + - CMO > +50: Overbought conditions + - CMO < -50: Oversold conditions + - Extreme readings (±70) suggest stronger signals + +2. **Zero Line Crossings** + - Crossing above zero: Bullish momentum + - Crossing below zero: Bearish momentum + +3. **Divergences** + - Price makes new high, CMO doesn't: Bearish divergence + - Price makes new low, CMO doesn't: Bullish divergence + +4. **Signal Line** + - Some traders use a 9-period EMA of CMO as a signal line + +## Implementation Details + +- O(1) streaming updates using circular buffers +- SIMD-optimized batch calculations +- Zero heap allocations in hot paths +- Handles NaN and edge cases gracefully + +## Sources + +- Chande, Tushar S. "The New Technical Trader" (1994) +- Chande, Tushar S. & Kroll, Stanley. "Beyond Technical Analysis" (1997) +- [StockCharts - CMO](https://school.stockcharts.com/doku.php?id=technical_indicators:chande_momentum_oscillator) diff --git a/lib/momentum/macd/Macd.Tests.cs b/lib/momentum/macd/Macd.Tests.cs index b04d9933..54772967 100644 --- a/lib/momentum/macd/Macd.Tests.cs +++ b/lib/momentum/macd/Macd.Tests.cs @@ -201,7 +201,7 @@ public class MacdTests var batchResult = macd.Update(series); var output = new double[series.Count]; - Macd.Calculate(series.Values, output, 12, 26); + Macd.Batch(series.Values, output, 12, 26); for (int i = 0; i < series.Count; i++) { @@ -222,7 +222,7 @@ public class MacdTests // 2. Span Mode var spanOutput = new double[series.Count]; - Macd.Calculate(series.Close.Values, spanOutput, 12, 26); + Macd.Batch(series.Close.Values, spanOutput, 12, 26); double spanResult = spanOutput[^1]; // 3. Streaming Mode @@ -256,8 +256,8 @@ public class MacdTests double[] output = new double[5]; double[] wrongSize = new double[3]; - Assert.Throws(() => Macd.Calculate(source, wrongSize, 12, 26)); - Assert.Throws(() => Macd.Calculate(source, output, 0, 26)); - Assert.Throws(() => Macd.Calculate(source, output, 12, 0)); + Assert.Throws(() => Macd.Batch(source, wrongSize, 12, 26)); + Assert.Throws(() => Macd.Batch(source, output, 0, 26)); + Assert.Throws(() => Macd.Batch(source, output, 12, 0)); } } diff --git a/lib/momentum/macd/Macd.cs b/lib/momentum/macd/Macd.cs index ec6ae04a..0d01b7ed 100644 --- a/lib/momentum/macd/Macd.cs +++ b/lib/momentum/macd/Macd.cs @@ -129,16 +129,17 @@ public sealed class Macd : ITValuePublisher, IDisposable return new TSeries(t, v); } - private void Handle(object? sender, in TValueEventArgs args) + public static TSeries Batch(TSeries source, int fastPeriod = 12, int slowPeriod = 26, int signalPeriod = 9) { - Update(args.Value, args.IsNew); + var indicator = new Macd(fastPeriod, slowPeriod, signalPeriod); + return indicator.Update(source); } /// /// Calculates the MACD Line (Fast EMA - Slow EMA). /// Does not calculate Signal or Histogram. /// - public static void Calculate(ReadOnlySpan source, Span destination, int fastPeriod = 12, int slowPeriod = 26) + public static void Batch(ReadOnlySpan source, Span destination, int fastPeriod = 12, int slowPeriod = 26) { if (source.Length != destination.Length) { @@ -165,4 +166,16 @@ public sealed class Macd : ITValuePublisher, IDisposable ArrayPool.Shared.Return(slowBuffer); } } + + private void Handle(object? sender, in TValueEventArgs args) + { + Update(args.Value, args.IsNew); + } + + public static (TSeries Results, Macd Indicator) Calculate(TSeries source, int fastPeriod = 12, int slowPeriod = 26, int signalPeriod = 9) + { + var indicator = new Macd(fastPeriod, slowPeriod, signalPeriod); + TSeries results = indicator.Update(source); + return (results, indicator); + } } diff --git a/lib/momentum/prs/Prs.Quantower.Tests.cs b/lib/momentum/prs/Prs.Quantower.Tests.cs new file mode 100644 index 00000000..25d4375f --- /dev/null +++ b/lib/momentum/prs/Prs.Quantower.Tests.cs @@ -0,0 +1,363 @@ +using TradingPlatform.BusinessLayer; + +namespace QuanTAlib.Tests; + +public class PrsIndicatorTests +{ + [Fact] + public void PrsIndicator_Constructor_SetsDefaults() + { + var indicator = new PrsIndicator(); + + Assert.Equal(1, indicator.SmoothPeriod); + Assert.Equal(SourceType.Close, indicator.Source); + Assert.Equal(SourceType.Open, indicator.Source2); + Assert.True(indicator.ShowColdValues); + Assert.Equal("PRS - Price Relative Strength", indicator.Name); + Assert.True(indicator.SeparateWindow); + Assert.True(indicator.OnBackGround); + } + + [Fact] + public void PrsIndicator_MinHistoryDepths_EqualsOne() + { + var indicator = new PrsIndicator(); + + Assert.Equal(1, PrsIndicator.MinHistoryDepths); + Assert.Equal(1, ((IWatchlistIndicator)indicator).MinHistoryDepths); + } + + [Fact] + public void PrsIndicator_ShortName_NoSmoothing_ContainsSourceTypes() + { + var indicator = new PrsIndicator { SmoothPeriod = 1 }; + + Assert.Contains("PRS", indicator.ShortName, StringComparison.Ordinal); + Assert.DoesNotContain("(", indicator.ShortName, StringComparison.Ordinal); + } + + [Fact] + public void PrsIndicator_ShortName_WithSmoothing_IncludesPeriod() + { + var indicator = new PrsIndicator { SmoothPeriod = 14 }; + indicator.Initialize(); + + Assert.Contains("PRS", indicator.ShortName, StringComparison.Ordinal); + Assert.Contains("14", indicator.ShortName, StringComparison.Ordinal); + } + + [Fact] + public void PrsIndicator_Initialize_CreatesPrsInstance() + { + var indicator = new PrsIndicator { SmoothPeriod = 10 }; + + // Initialize should not throw + indicator.Initialize(); + + // After init, line series should exist + Assert.Single(indicator.LinesSeries); + } + + [Fact] + public void PrsIndicator_ProcessUpdate_HistoricalBar_ComputesValue() + { + var indicator = new PrsIndicator { SmoothPeriod = 1 }; + indicator.Initialize(); + + // Add historical data + var now = DateTime.UtcNow; + indicator.HistoricalData.AddBar(now, 100, 110, 90, 105); + + // Process update + var args = new UpdateArgs(UpdateReason.HistoricalBar); + indicator.ProcessUpdate(args); + + // Line series should have a value + Assert.Equal(1, indicator.LinesSeries[0].Count); + } + + [Fact] + public void PrsIndicator_ProcessUpdate_NewBar_ComputesValue() + { + var indicator = new PrsIndicator { SmoothPeriod = 5 }; + indicator.Initialize(); + + var now = DateTime.UtcNow; + indicator.HistoricalData.AddBar(now, 100, 110, 90, 105); + indicator.HistoricalData.AddBar(now.AddMinutes(1), 102, 112, 92, 108); + + indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar)); + indicator.ProcessUpdate(new UpdateArgs(UpdateReason.NewBar)); + + Assert.Equal(2, indicator.LinesSeries[0].Count); + } + + [Fact] + public void PrsIndicator_ProcessUpdate_NewTick_ProcessesWithoutError() + { + var indicator = new PrsIndicator { SmoothPeriod = 1 }; + indicator.Initialize(); + + var now = DateTime.UtcNow; + indicator.HistoricalData.AddBar(now, 100, 110, 90, 105); + + indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar)); + double firstValue = indicator.LinesSeries[0].GetValue(0); + + // NewTick should not throw + indicator.ProcessUpdate(new UpdateArgs(UpdateReason.NewTick)); + double secondValue = indicator.LinesSeries[0].GetValue(0); + + // Values should be produced + Assert.True(double.IsNaN(firstValue) || double.IsFinite(firstValue)); + Assert.True(double.IsNaN(secondValue) || double.IsFinite(secondValue)); + } + + [Fact] + public void PrsIndicator_MultipleUpdates_ProducesSequence() + { + var indicator = new PrsIndicator { SmoothPeriod = 3 }; + indicator.Initialize(); + + var now = DateTime.UtcNow; + double[] opens = { 100, 101, 102, 103, 104, 105 }; + double[] closes = { 100, 101, 102, 103, 104, 105 }; + + for (int i = 0; i < opens.Length; i++) + { + indicator.HistoricalData.AddBar(now.AddMinutes(i), opens[i], opens[i] + 5, opens[i] - 5, closes[i]); + indicator.ProcessUpdate(new UpdateArgs(i == 0 ? UpdateReason.HistoricalBar : UpdateReason.NewBar)); + } + + Assert.Equal(opens.Length, indicator.LinesSeries[0].Count); + } + + [Fact] + public void PrsIndicator_CloseVsOpen_ReturnsPositiveRatio() + { + // Test with Close vs Open - Close should be higher than Open in uptrend + var indicator = new PrsIndicator { SmoothPeriod = 1, Source = SourceType.Close, Source2 = SourceType.Open }; + indicator.Initialize(); + + var now = DateTime.UtcNow; + + // Bar where Close > Open (bullish bar) + indicator.HistoricalData.AddBar(now, 100, 110, 95, 108); + indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar)); + + Assert.Equal(1, indicator.LinesSeries[0].Count); + double value = indicator.LinesSeries[0].GetValue(0); + + // Close/Open = 108/100 = 1.08 + Assert.True(value > 1.0, $"Expected ratio > 1.0 for bullish bar, got {value}"); + Assert.Equal(1.08, value, precision: 6); + } + + [Fact] + public void PrsIndicator_OpenVsClose_ReturnsInverseRatio() + { + // Reverse the sources - Open vs Close + var indicator = new PrsIndicator { SmoothPeriod = 1, Source = SourceType.Open, Source2 = SourceType.Close }; + indicator.Initialize(); + + var now = DateTime.UtcNow; + + // Bar where Close > Open (bullish bar) + indicator.HistoricalData.AddBar(now, 100, 110, 95, 108); + indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar)); + + Assert.Equal(1, indicator.LinesSeries[0].Count); + double value = indicator.LinesSeries[0].GetValue(0); + + // Open/Close = 100/108 ≈ 0.926 + Assert.True(value < 1.0, $"Expected ratio < 1.0 when Open < Close, got {value}"); + } + + [Fact] + public void PrsIndicator_DifferentSourceTypes_Work() + { + var sources = new[] { SourceType.Open, SourceType.High, SourceType.Low, SourceType.Close, SourceType.HL2, SourceType.HLC3 }; + + foreach (var source in sources) + { + var indicator = new PrsIndicator { SmoothPeriod = 1, Source = source, Source2 = SourceType.Close }; + indicator.Initialize(); + + var now = DateTime.UtcNow; + indicator.HistoricalData.AddBar(now, 100, 110, 90, 105); + indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar)); + + // Should have computed a value + Assert.Equal(1, indicator.LinesSeries[0].Count); + } + } + + [Fact] + public void PrsIndicator_DifferentSource2Types_Work() + { + var source2Types = new[] { SourceType.Open, SourceType.High, SourceType.Low, SourceType.HL2 }; + + foreach (var source2 in source2Types) + { + var indicator = new PrsIndicator { SmoothPeriod = 1, Source = SourceType.Close, Source2 = source2 }; + indicator.Initialize(); + + var now = DateTime.UtcNow; + indicator.HistoricalData.AddBar(now, 100, 110, 90, 105); + indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar)); + + Assert.Equal(1, indicator.LinesSeries[0].Count); + } + } + + [Fact] + public void PrsIndicator_SmoothPeriod_CanBeChanged() + { + var indicator = new PrsIndicator { SmoothPeriod = 50 }; + + Assert.Equal(50, indicator.SmoothPeriod); + + indicator.SmoothPeriod = 100; + Assert.Equal(100, indicator.SmoothPeriod); + } + + [Fact] + public void PrsIndicator_Source2_CanBeChanged() + { + var indicator = new PrsIndicator { Source2 = SourceType.High }; + + Assert.Equal(SourceType.High, indicator.Source2); + + indicator.Source2 = SourceType.Low; + Assert.Equal(SourceType.Low, indicator.Source2); + } + + [Fact] + public void PrsIndicator_ReInitialize_ResetsState() + { + var indicator = new PrsIndicator { SmoothPeriod = 5 }; + indicator.Initialize(); + + var now = DateTime.UtcNow; + for (int i = 0; i < 10; i++) + { + indicator.HistoricalData.AddBar(now.AddMinutes(i), 100 + i, 105 + i, 95 + i, 102 + i); + indicator.ProcessUpdate(new UpdateArgs(i == 0 ? UpdateReason.HistoricalBar : UpdateReason.NewBar)); + } + + Assert.Equal(10, indicator.LinesSeries[0].Count); + + // Re-initialize with new instance + var indicator2 = new PrsIndicator { SmoothPeriod = 5 }; + indicator2.Initialize(); + indicator2.HistoricalData.AddBar(now.AddMinutes(100), 200, 210, 190, 205); + indicator2.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar)); + + Assert.Equal(1, indicator2.LinesSeries[0].Count); + } + + [Fact] + public void PrsIndicator_HighVsLow_AlwaysGreaterThanOne() + { + // High is always > Low, so ratio should always be > 1 + var indicator = new PrsIndicator { SmoothPeriod = 1, Source = SourceType.High, Source2 = SourceType.Low }; + indicator.Initialize(); + + var now = DateTime.UtcNow; + + for (int i = 0; i < 10; i++) + { + double mid = 100 + i; + indicator.HistoricalData.AddBar(now.AddMinutes(i), mid, mid + 5, mid - 5, mid); + indicator.ProcessUpdate(new UpdateArgs(i == 0 ? UpdateReason.HistoricalBar : UpdateReason.NewBar)); + } + + Assert.Equal(10, indicator.LinesSeries[0].Count); + + // High/Low should always be > 1 + for (int i = 0; i < 10; i++) + { + double value = indicator.LinesSeries[0].GetValue(i); + if (double.IsFinite(value)) + { + Assert.True(value > 1.0, $"Expected ratio > 1 for High/Low at index {i}, got {value}"); + } + } + } + + [Fact] + public void PrsIndicator_Description_IsSet() + { + var indicator = new PrsIndicator(); + + Assert.Contains("relative", indicator.Description, StringComparison.OrdinalIgnoreCase); + Assert.Contains("performance", indicator.Description, StringComparison.OrdinalIgnoreCase); + } + + [Fact] + public void PrsIndicator_SameSourceAndSource2_ReturnsOne() + { + // When comparing a source to itself, ratio should be 1.0 + var indicator = new PrsIndicator { SmoothPeriod = 1, Source = SourceType.Close, Source2 = SourceType.Close }; + indicator.Initialize(); + + var now = DateTime.UtcNow; + + for (int i = 0; i < 5; i++) + { + indicator.HistoricalData.AddBar(now.AddMinutes(i), 100 + i, 110 + i, 90 + i, 105 + i); + indicator.ProcessUpdate(new UpdateArgs(i == 0 ? UpdateReason.HistoricalBar : UpdateReason.NewBar)); + } + + Assert.Equal(5, indicator.LinesSeries[0].Count); + + // Close/Close should be exactly 1.0 + for (int i = 0; i < 5; i++) + { + double value = indicator.LinesSeries[0].GetValue(i); + Assert.Equal(1.0, value, precision: 10); + } + } + + [Fact] + public void PrsIndicator_SmoothingReducesVariance() + { + // Compare unsmoothed vs smoothed - smoothed should have less variance + var unsmoothed = new PrsIndicator { SmoothPeriod = 1, Source = SourceType.Close, Source2 = SourceType.Open }; + var smoothed = new PrsIndicator { SmoothPeriod = 10, Source = SourceType.Close, Source2 = SourceType.Open }; + unsmoothed.Initialize(); + smoothed.Initialize(); + + var now = DateTime.UtcNow; + double[] opens = { 100, 102, 98, 104, 96, 106, 94, 108, 92, 110, 90, 112, 88, 114, 86 }; + double[] closes = { 102, 100, 101, 97, 105, 95, 107, 93, 109, 91, 111, 89, 113, 87, 115 }; + + for (int i = 0; i < opens.Length; i++) + { + unsmoothed.HistoricalData.AddBar(now.AddMinutes(i), opens[i], 120, 80, closes[i]); + smoothed.HistoricalData.AddBar(now.AddMinutes(i), opens[i], 120, 80, closes[i]); + unsmoothed.ProcessUpdate(new UpdateArgs(i == 0 ? UpdateReason.HistoricalBar : UpdateReason.NewBar)); + smoothed.ProcessUpdate(new UpdateArgs(i == 0 ? UpdateReason.HistoricalBar : UpdateReason.NewBar)); + } + + // Calculate variance of last 5 values + double[] unsmoothedVals = new double[5]; + double[] smoothedVals = new double[5]; + + for (int i = 0; i < 5; i++) + { + unsmoothedVals[i] = unsmoothed.LinesSeries[0].GetValue(i); + smoothedVals[i] = smoothed.LinesSeries[0].GetValue(i); + } + + double unsmoothedMean = unsmoothedVals.Average(); + double smoothedMean = smoothedVals.Average(); + + double unsmoothedVariance = unsmoothedVals.Select(v => (v - unsmoothedMean) * (v - unsmoothedMean)).Sum() / 5; + double smoothedVariance = smoothedVals.Select(v => (v - smoothedMean) * (v - smoothedMean)).Sum() / 5; + + // Smoothed should have less variance (or equal if no variation) + Assert.True(smoothedVariance <= unsmoothedVariance + 0.001, + $"Smoothed variance ({smoothedVariance}) should be <= unsmoothed variance ({unsmoothedVariance})"); + } +} diff --git a/lib/momentum/prs/Prs.Quantower.cs b/lib/momentum/prs/Prs.Quantower.cs new file mode 100644 index 00000000..72b1950f --- /dev/null +++ b/lib/momentum/prs/Prs.Quantower.cs @@ -0,0 +1,83 @@ +using System.Drawing; +using System.Runtime.CompilerServices; +using TradingPlatform.BusinessLayer; + +namespace QuanTAlib; + +/// +/// Quantower adapter for PRS (Price Relative Strength) indicator. +/// Compares two price sources from the same symbol to identify relative performance. +/// +/// +/// This adapter compares two different price sources from the same symbol (e.g., Close vs Open, +/// Close vs Volume, High vs Low). For cross-symbol relative strength analysis, use the core +/// PRS class directly with data from multiple symbols. +/// +/// The output is the ratio of Base/Comparison with optional EMA smoothing. +/// Values above 1.0 indicate the base source is higher than comparison. +/// Rising values indicate base is outperforming, falling values indicate underperformance. +/// +[SkipLocalsInit] +public sealed class PrsIndicator : Indicator, IWatchlistIndicator +{ + [InputParameter("Smooth Period", sortIndex: 0, minimum: 1, maximum: 10000)] + public int SmoothPeriod { get; set; } = 1; + + [IndicatorExtensions.DataSourceInput] + public SourceType Source { get; set; } = SourceType.Close; + + [InputParameter("Comparison Source", sortIndex: 2)] + public SourceType Source2 { get; set; } = SourceType.Open; + + [InputParameter("Show cold values", sortIndex: 21)] + public bool ShowColdValues { get; set; } = true; + + private Prs _prs = null!; + private readonly LineSeries _series; + private string _sourceName = null!; + private Func _priceSelector = null!; + private Func _priceSelector2 = null!; + + public static int MinHistoryDepths => 1; + int IWatchlistIndicator.MinHistoryDepths => MinHistoryDepths; + + public override string ShortName => SmoothPeriod == 1 + ? $"PRS:{_sourceName}/{Source2}" + : $"PRS({SmoothPeriod}):{_sourceName}/{Source2}"; + + public PrsIndicator() + { + OnBackGround = true; + SeparateWindow = true; + Name = "PRS - Price Relative Strength"; + Description = "Compares relative performance between two price sources. Ratio > 1 means base is higher than comparison."; + _series = new LineSeries(name: "PRS", color: IndicatorExtensions.Momentum, width: 2, style: LineStyle.Solid); + AddLineSeries(_series); + } + + protected override void OnInit() + { + _priceSelector = Source.GetPriceSelector(); + _priceSelector2 = Source2.GetPriceSelector(); + _sourceName = Source.ToString(); + _prs = new Prs(SmoothPeriod); + base.OnInit(); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + protected override void OnUpdate(UpdateArgs args) + { + bool isNew = args.IsNewBar(); + + // Get both price sources from the same bar + var item = HistoricalData[Count - 1, SeekOriginHistory.Begin]; + double baseValue = _priceSelector(item); + double compValue = _priceSelector2(item); + + var tvalBase = new TValue(item.TimeLeft.Ticks, baseValue); + var tvalComp = new TValue(item.TimeLeft.Ticks, compValue); + + double value = _prs.Update(tvalBase, tvalComp, isNew).Value; + _series.SetValue(value, _prs.IsHot, ShowColdValues); + } +} diff --git a/lib/momentum/prs/Prs.Tests.cs b/lib/momentum/prs/Prs.Tests.cs new file mode 100644 index 00000000..a8f30584 --- /dev/null +++ b/lib/momentum/prs/Prs.Tests.cs @@ -0,0 +1,460 @@ +using Xunit; + +namespace QuanTAlib.Tests; + +public class PrsTests +{ + private const double Epsilon = 1e-10; + + // ==================== CONSTRUCTION ==================== + [Fact] + public void Constructor_DefaultParameters() + { + var prs = new Prs(); + Assert.Equal("Prs", prs.Name); + Assert.Equal(1, prs.SmoothPeriod); + } + + [Fact] + public void Constructor_CustomSmoothPeriod() + { + var prs = new Prs(10); + Assert.Equal("Prs(10)", prs.Name); + Assert.Equal(10, prs.SmoothPeriod); + } + + [Fact] + public void Constructor_ZeroPeriod_ThrowsException() + { + Assert.Throws(() => new Prs(0)); + } + + [Fact] + public void Constructor_NegativePeriod_ThrowsException() + { + Assert.Throws(() => new Prs(-1)); + } + + // ==================== BASIC CALCULATIONS ==================== + [Fact] + public void Update_SimpleRatio_ReturnsCorrectValue() + { + var prs = new Prs(); + var result = prs.Update(100.0, 50.0); + Assert.Equal(2.0, result.Value, 10); + } + + [Fact] + public void Update_FractionRatio_ReturnsCorrectValue() + { + var prs = new Prs(); + var result = prs.Update(50.0, 100.0); + Assert.Equal(0.5, result.Value, 10); + } + + [Fact] + public void Update_EqualValues_ReturnsOne() + { + var prs = new Prs(); + var result = prs.Update(100.0, 100.0); + Assert.Equal(1.0, result.Value, 10); + } + + [Fact] + public void Update_DivisionByZero_ReturnsNaN() + { + var prs = new Prs(); + var result = prs.Update(100.0, 0.0); + Assert.True(double.IsNaN(result.Value)); + } + + [Fact] + public void RawRatio_MatchesUnsmoothedResult() + { + var prs = new Prs(); + prs.Update(200.0, 100.0); + Assert.Equal(2.0, prs.RawRatio, 10); + Assert.Equal(prs.RawRatio, prs.Last.Value, 10); + } + + // ==================== SMOOTHING ==================== + [Fact] + public void Update_WithSmoothing_SmoothsRatio() + { + var prs = new Prs(5); + var results = new List(); + + // Feed increasing prices with 2:1 base ratio + for (int i = 0; i < 20; i++) + { + double basePrice = 200.0 + i; + double compPrice = 100.0 + i * 0.5; + prs.Update(basePrice, compPrice); + results.Add(prs.Last.Value); + } + + // After warmup, smoothed values should be less volatile + Assert.True(prs.IsHot); + Assert.True(results[^1] > 1.5); // Base is outperforming + } + + [Fact] + public void Update_SmoothedVsRaw_DifferAfterMultipleUpdates() + { + var prsRaw = new Prs(1); + var prsSmoothed = new Prs(10); + + var random = new Random(42); + for (int i = 0; i < 30; i++) + { + double basePrice = 100.0 + random.NextDouble() * 10; + double compPrice = 100.0 + random.NextDouble() * 10; + prsRaw.Update(basePrice, compPrice); + prsSmoothed.Update(basePrice, compPrice); + } + + // Smoothed should differ from raw due to EMA averaging + Assert.NotEqual(prsRaw.Last.Value, prsSmoothed.Last.Value, 3); + } + + // ==================== IsHot ==================== + [Fact] + public void IsHot_BeforeWarmup_ReturnsFalse() + { + var prs = new Prs(10); + prs.Update(100.0, 50.0); + Assert.False(prs.IsHot); + } + + [Fact] + public void IsHot_AfterWarmup_ReturnsTrue() + { + var prs = new Prs(5); + for (int i = 0; i < 10; i++) + { + prs.Update(100.0 + i, 50.0 + i); + } + Assert.True(prs.IsHot); + } + + [Fact] + public void IsHot_NoSmoothing_TrueImmediately() + { + var prs = new Prs(1); + prs.Update(100.0, 50.0); + Assert.True(prs.IsHot); + } + + // ==================== BAR CORRECTION ==================== + [Fact] + public void Update_BarCorrection_RestoresState() + { + var prs1 = new Prs(5); + var prs2 = new Prs(5); + + // Feed same initial data + for (int i = 0; i < 10; i++) + { + prs1.Update(100.0 + i, 50.0 + i); + prs2.Update(100.0 + i, 50.0 + i); + } + + // prs1: Add another bar + prs1.Update(120.0, 60.0, true); + + // prs2: Add wrong bar, then correct it + prs2.Update(999.0, 999.0, true); + prs2.Update(120.0, 60.0, false); + + // Values should match + Assert.Equal(prs1.Last.Value, prs2.Last.Value, 9); + } + + [Fact] + public void Update_MultipleCorrections_FinalValueCorrect() + { + var prs = new Prs(3); + + for (int i = 0; i < 5; i++) + { + prs.Update(100.0 + i, 50.0 + i); + } + + // Add bar + prs.Update(110.0, 55.0, true); + + // Multiple corrections + prs.Update(115.0, 60.0, false); + prs.Update(120.0, 65.0, false); + prs.Update(110.0, 55.0, false); // Back to original + + Assert.True(double.IsFinite(prs.Last.Value)); + } + + // ==================== RESET ==================== + [Fact] + public void Reset_ClearsState() + { + var prs = new Prs(5); + + for (int i = 0; i < 10; i++) + { + prs.Update(100.0 + i, 50.0 + i); + } + + Assert.NotEqual(default, prs.Last); + Assert.True(prs.IsHot); + + prs.Reset(); + + Assert.Equal(default, prs.Last); + Assert.False(prs.IsHot); + } + + // ==================== STATIC CALCULATE ==================== + [Fact] + public void Calculate_TSeries_ReturnsCorrectLength() + { + var baseSeries = new TSeries(); + var compSeries = new TSeries(); + + for (int i = 0; i < 20; i++) + { + baseSeries.Add(new TValue(DateTime.Now.AddMinutes(i), 100.0 + i)); + compSeries.Add(new TValue(DateTime.Now.AddMinutes(i), 50.0 + i)); + } + + var result = Prs.Batch(baseSeries, compSeries, 5); + + Assert.Equal(baseSeries.Count, result.Count); + } + + [Fact] + public void Calculate_MismatchedLengths_ThrowsException() + { + var baseSeries = new TSeries(); + var compSeries = new TSeries(); + + for (int i = 0; i < 10; i++) + { + baseSeries.Add(new TValue(DateTime.Now.AddMinutes(i), 100.0 + i)); + } + for (int i = 0; i < 5; i++) + { + compSeries.Add(new TValue(DateTime.Now.AddMinutes(i), 50.0 + i)); + } + + Assert.Throws(() => Prs.Batch(baseSeries, compSeries)); + } + + [Fact] + public void Calculate_Span_MismatchedLengths_ThrowsException() + { + double[] baseArr = new double[10]; + double[] compArr = new double[5]; + double[] output = new double[10]; + + Assert.Throws(() => Prs.Batch(baseArr, compArr, output)); + } + + [Fact] + public void Calculate_Span_OutputMismatch_ThrowsException() + { + double[] baseArr = new double[10]; + double[] compArr = new double[10]; + double[] output = new double[5]; + + Assert.Throws(() => Prs.Batch(baseArr, compArr, output)); + } + + [Fact] + public void Calculate_Span_InvalidPeriod_ThrowsException() + { + double[] baseArr = new double[10]; + double[] compArr = new double[10]; + double[] output = new double[10]; + + Assert.Throws(() => Prs.Batch(baseArr, compArr, output, 0)); + } + + // ==================== EDGE CASES ==================== + [Fact] + public void Update_NaNInput_UsesLastValidValue() + { + var prs = new Prs(); + + prs.Update(100.0, 50.0); + _ = prs.Last.Value; + + var result = prs.Update(double.NaN, double.NaN); + // Should use last valid values (100/50 pattern OR fallback) + Assert.True(double.IsFinite(result.Value)); + } + + [Fact] + public void Update_InfinityInput_UsesLastValidValue() + { + var prs = new Prs(); + + prs.Update(100.0, 50.0); + + var result = prs.Update(double.PositiveInfinity, 50.0); + Assert.True(double.IsFinite(result.Value)); + } + + [Fact] + public void Update_VerySmallComparison_HandlesCorrectly() + { + var prs = new Prs(); + var result = prs.Update(100.0, 1e-15); + + // Values below epsilon (1e-10) are treated as zero -> returns NaN + Assert.True(double.IsNaN(result.Value)); + } + + [Fact] + public void Update_NegativeValues_HandlesCorrectly() + { + var prs = new Prs(); + + // Negative values (like P&L or temperature) + var result = prs.Update(-50.0, -25.0); + Assert.Equal(2.0, result.Value, 10); + } + + // ==================== PRIME ==================== + [Fact] + public void Prime_Single_ThrowsNotSupported() + { + var prs = new Prs(5); + double[] data = [100, 101, 102, 103, 104]; + + Assert.Throws(() => prs.Prime(data)); + } + + [Fact] + public void Prime_Dual_InitializesState() + { + var prs = new Prs(3); + double[] baseData = [100, 102, 104, 106, 108, 110]; + double[] compData = [50, 51, 52, 53, 54, 55]; + + prs.Prime(baseData, compData); + + Assert.NotEqual(default, prs.Last); + } + + [Fact] + public void Prime_MismatchedLengths_ThrowsException() + { + var prs = new Prs(3); + double[] baseData = [100, 102, 104]; + double[] compData = [50, 51]; + + Assert.Throws(() => prs.Prime(baseData, compData)); + } + + // ==================== NOT SUPPORTED ==================== + [Fact] + public void Update_SingleInput_ThrowsNotSupported() + { + var prs = new Prs(); + var input = new TValue(DateTime.Now, 100.0); + + Assert.Throws(() => prs.Update(input)); + } + + [Fact] + public void Update_TSeries_ThrowsNotSupported() + { + var prs = new Prs(); + var source = new TSeries(); + source.Add(new TValue(DateTime.Now, 100.0)); + + Assert.Throws(() => prs.Update(source)); + } + + // ==================== PERFORMANCE SCENARIOS ==================== + [Fact] + public void Update_OutperformanceScenario_IncreasingRatio() + { + var prs = new Prs(5); + + // Base grows faster than comparison + for (int i = 0; i < 20; i++) + { + double basePrice = 100.0 + i * 2; // +2 per bar + double compPrice = 100.0 + i * 1; // +1 per bar + prs.Update(basePrice, compPrice); + } + + // Ratio should be increasing (base outperforming) + Assert.True(prs.Last.Value > 1.0); + } + + [Fact] + public void Update_UnderperformanceScenario_DecreasingRatio() + { + var prs = new Prs(5); + + // Base grows slower than comparison + for (int i = 0; i < 20; i++) + { + double basePrice = 100.0 + i * 1; // +1 per bar + double compPrice = 100.0 + i * 2; // +2 per bar + prs.Update(basePrice, compPrice); + } + + // Ratio should be decreasing (base underperforming) + Assert.True(prs.Last.Value < 1.0); + } + + [Fact] + public void Update_ParallelMovement_StableRatio() + { + var prs = new Prs(5); + + // Both grow at same rate + for (int i = 0; i < 20; i++) + { + double basePrice = 100.0 + i * 2; + double compPrice = 50.0 + i * 1; + prs.Update(basePrice, compPrice); + } + + // Initial ratio was 2.0, should stay around there + Assert.InRange(prs.Last.Value, 1.9, 2.1); + } + + // ==================== BATCH CALCULATION ==================== + [Fact] + public void Calculate_Batch_MatchesStreaming() + { + var baseSeries = new TSeries(); + var compSeries = new TSeries(); + var random = new Random(42); + + for (int i = 0; i < 50; i++) + { + baseSeries.Add(new TValue(DateTime.Now.AddMinutes(i), 100.0 + random.NextDouble() * 10)); + compSeries.Add(new TValue(DateTime.Now.AddMinutes(i), 50.0 + random.NextDouble() * 5)); + } + + // Batch calculation + var batchResult = Prs.Batch(baseSeries, compSeries, 5); + + // Streaming calculation + var prs = new Prs(5); + var streamingResults = new List(); + for (int i = 0; i < baseSeries.Count; i++) + { + streamingResults.Add(prs.Update(baseSeries[i], compSeries[i], true).Value); + } + + // Compare + for (int i = 0; i < baseSeries.Count; i++) + { + Assert.Equal(batchResult.Values[i], streamingResults[i], 6); + } + } +} diff --git a/lib/momentum/prs/Prs.Validation.Tests.cs b/lib/momentum/prs/Prs.Validation.Tests.cs new file mode 100644 index 00000000..574cd2b1 --- /dev/null +++ b/lib/momentum/prs/Prs.Validation.Tests.cs @@ -0,0 +1,507 @@ +using Xunit; + +namespace QuanTAlib.Tests; + +/// +/// Validation tests for PRS (Price Relative Strength) indicator. +/// PRS compares relative performance between two assets via their price ratio. +/// Note: PRS is a unique indicator without direct equivalents in TA-Lib, Skender, etc. +/// These tests validate mathematical correctness and expected behavior. +/// +public class PrsValidationTests +{ + #region Mathematical Validation + + [Fact] + public void Prs_ManualCalculation_MatchesExpected() + { + var prs = new Prs(1); // No smoothing + var time = DateTime.UtcNow; + + // Test data: base and comparison prices + var baseValues = new double[] { 100, 105, 110, 115, 120 }; + var compValues = new double[] { 100, 100, 100, 100, 100 }; + + // Expected: ratios should be 1.0, 1.05, 1.10, 1.15, 1.20 + for (int i = 0; i < baseValues.Length; i++) + { + var result = prs.Update( + new TValue(time.AddSeconds(i), baseValues[i]), + new TValue(time.AddSeconds(i), compValues[i]), + true); + + double expected = baseValues[i] / compValues[i]; + Assert.Equal(expected, result.Value, 10); + Assert.Equal(expected, prs.RawRatio, 10); + } + } + + [Fact] + public void Prs_EqualPrices_ReturnsOne() + { + var prs = new Prs(1); + + var result = prs.Update(50.0, 50.0, true); + + Assert.Equal(1.0, result.Value, 10); + } + + [Fact] + public void Prs_BaseHigherThanComp_ReturnsGreaterThanOne() + { + var prs = new Prs(1); + + var result = prs.Update(120.0, 100.0, true); + + Assert.Equal(1.2, result.Value, 10); + Assert.True(result.Value > 1.0); + } + + [Fact] + public void Prs_BaseLowerThanComp_ReturnsLessThanOne() + { + var prs = new Prs(1); + + var result = prs.Update(80.0, 100.0, true); + + Assert.Equal(0.8, result.Value, 10); + Assert.True(result.Value < 1.0); + } + + [Fact] + public void Prs_DivisionByZero_ReturnsNaN() + { + var prs = new Prs(1); + + var result = prs.Update(100.0, 0.0, true); + + Assert.True(double.IsNaN(result.Value)); + Assert.True(double.IsNaN(prs.RawRatio)); + } + + [Fact] + public void Prs_VerySmallDenominator_ReturnsNaN() + { + var prs = new Prs(1); + + // Value smaller than epsilon (1e-10) should be treated as zero + var result = prs.Update(100.0, 1e-11, true); + + Assert.True(double.IsNaN(result.Value)); + } + + #endregion + + #region Smoothing Validation + + [Fact] + public void Prs_SmoothedFirstValue_EqualsRawRatio() + { + var prs = new Prs(10); + var time = DateTime.UtcNow; + + var result = prs.Update( + new TValue(time, 100.0), + new TValue(time, 50.0), + true); + + // First value should equal raw ratio + Assert.Equal(prs.RawRatio, result.Value, 10); + Assert.Equal(2.0, result.Value, 10); + } + + [Fact] + public void Prs_SmoothedConvergesToRatio_WhenConstant() + { + var prs = new Prs(5); + var time = DateTime.UtcNow; + + // Feed constant ratio (100/50 = 2.0) repeatedly + TValue result = default; + for (int i = 0; i < 50; i++) + { + result = prs.Update( + new TValue(time.AddSeconds(i), 100.0), + new TValue(time.AddSeconds(i), 50.0), + true); + } + + // Should converge to 2.0 + Assert.Equal(2.0, result.Value, 6); + } + + [Fact] + public void Prs_NoSmoothing_RawRatioEqualsSmoothed() + { + var prs = new Prs(1); // No smoothing + + var values = new (double b, double c)[] + { + (100, 50), + (110, 55), + (120, 60), + (130, 65) + }; + + foreach (var (b, c) in values) + { + var result = prs.Update(b, c, true); + Assert.Equal(prs.RawRatio, result.Value, 10); + } + } + + [Fact] + public void Prs_SmoothingReducesVolatility() + { + var prsNoSmooth = new Prs(1); + var prsSmooth = new Prs(10); + + // Create volatile ratio series + var baseVals = new double[] { 100, 120, 80, 130, 70, 140, 60, 150 }; + var compVals = new double[] { 100, 100, 100, 100, 100, 100, 100, 100 }; + + var rawResults = new List(); + var smoothResults = new List(); + + for (int i = 0; i < baseVals.Length; i++) + { + var raw = prsNoSmooth.Update(baseVals[i], compVals[i], true); + var smooth = prsSmooth.Update(baseVals[i], compVals[i], true); + rawResults.Add(raw.Value); + smoothResults.Add(smooth.Value); + } + + // Calculate variance of latter half + var rawVariance = CalculateVariance(rawResults.Skip(4).ToArray()); + var smoothVariance = CalculateVariance(smoothResults.Skip(4).ToArray()); + + Assert.True(smoothVariance <= rawVariance + 0.01, + $"Smoothed variance ({smoothVariance:F4}) should be <= raw variance ({rawVariance:F4})"); + } + + private static double CalculateVariance(double[] values) + { + double mean = values.Average(); + return values.Select(v => (v - mean) * (v - mean)).Sum() / values.Length; + } + + #endregion + + #region Trend Interpretation + + [Fact] + public void Prs_IncreasingRatio_IndicatesOutperformance() + { + var prs = new Prs(1); + + // Base outperforms: grows faster than comparison + var results = new List(); + + for (int i = 0; i < 10; i++) + { + double basePrice = 100 + (i * 5); // 100, 105, 110... + double compPrice = 100 + (i * 2); // 100, 102, 104... + var result = prs.Update(basePrice, compPrice, true); + results.Add(result.Value); + } + + // Each ratio should be larger than the previous (outperformance) + for (int i = 1; i < results.Count; i++) + { + Assert.True(results[i] > results[i - 1], + $"Outperformance: ratio[{i}]={results[i]:F4} should be > ratio[{i - 1}]={results[i - 1]:F4}"); + } + } + + [Fact] + public void Prs_DecreasingRatio_IndicatesUnderperformance() + { + var prs = new Prs(1); + + // Base underperforms: grows slower than comparison + var results = new List(); + + for (int i = 0; i < 10; i++) + { + double basePrice = 100 + (i * 2); // 100, 102, 104... + double compPrice = 100 + (i * 5); // 100, 105, 110... + var result = prs.Update(basePrice, compPrice, true); + results.Add(result.Value); + } + + // Each ratio should be smaller than the previous (underperformance) + for (int i = 1; i < results.Count; i++) + { + Assert.True(results[i] < results[i - 1], + $"Underperformance: ratio[{i}]={results[i]:F4} should be < ratio[{i - 1}]={results[i - 1]:F4}"); + } + } + + [Fact] + public void Prs_SameGrowthRate_ConstantRatio() + { + var prs = new Prs(1); + + // Both grow at same rate - ratio stays constant at 2.0 + var results = new List(); + + for (int i = 0; i < 10; i++) + { + double basePrice = 100 * (1 + i * 0.05); // 5% growth + double compPrice = 50 * (1 + i * 0.05); // 5% growth + var result = prs.Update(basePrice, compPrice, true); + results.Add(result.Value); + } + + // All ratios should be 2.0 (within precision) + foreach (var ratio in results) + { + Assert.Equal(2.0, ratio, 10); + } + } + + #endregion + + #region Edge Cases and Robustness + + [Fact] + public void Prs_NegativeValues_HandlesCorrectly() + { + var prs = new Prs(1); + + // While unusual, PRS should handle negative values mathematically + var result = prs.Update(-100.0, -50.0, true); + + Assert.Equal(2.0, result.Value, 10); // -100/-50 = 2.0 + } + + [Fact] + public void Prs_MixedSigns_HandlesCorrectly() + { + var prs = new Prs(1); + + // Base positive, comp negative + var result = prs.Update(100.0, -50.0, true); + + Assert.Equal(-2.0, result.Value, 10); + } + + [Fact] + public void Prs_VeryLargeValues_MaintainsPrecision() + { + var prs = new Prs(1); + + var result = prs.Update(1e15, 1e14, true); + + // 1e15 / 1e14 = 10 + Assert.Equal(10.0, result.Value, 6); + } + + [Fact] + public void Prs_VerySmallValues_MaintainsPrecision() + { + var prs = new Prs(1); + + var result = prs.Update(1e-5, 1e-6, true); + + // 1e-5 / 1e-6 = 10 + Assert.Equal(10.0, result.Value, 6); + } + + [Fact] + public void Prs_NaNBase_PropagatesNaN() + { + var prs = new Prs(1); + + // Should fallback to last valid or 0, resulting in 0/comp + prs.Update(100.0, 50.0, true); // First valid value + var result = prs.Update(double.NaN, 50.0, true); + + // NaN base with valid comparison should use fallback (previous: 100) or 0 + // Result will depend on sanitization logic - actual behavior uses last valid + Assert.True(double.IsFinite(result.Value) || double.IsNaN(result.Value)); + } + + [Fact] + public void Prs_NaNComp_PropagatesNaN() + { + var prs = new Prs(1); + + prs.Update(100.0, 50.0, true); // First valid value + var result = prs.Update(100.0, double.NaN, true); + + // NaN comp should use fallback (previous: 50) -> 100/50 = 2.0 + Assert.Equal(2.0, result.Value, 6); + } + + [Fact] + public void Prs_InfinityBase_HandledGracefully() + { + var prs = new Prs(1); + + prs.Update(100.0, 50.0, true); // First valid + var result = prs.Update(double.PositiveInfinity, 50.0, true); + + // Should fallback to last valid (100/50 = 2.0) + Assert.Equal(2.0, result.Value, 6); + } + + #endregion + + #region Batch Calculation Validation + + [Fact] + public void Prs_BatchCalculate_MatchesStreaming() + { + int smoothPeriod = 5; + var prsStream = new Prs(smoothPeriod); + + var baseValues = new double[] { 100, 105, 110, 108, 115, 120, 118, 125, 130, 128 }; + var compValues = new double[] { 100, 100, 100, 100, 100, 100, 100, 100, 100, 100 }; + + // Streaming calculation + var streamResults = new double[baseValues.Length]; + for (int i = 0; i < baseValues.Length; i++) + { + streamResults[i] = prsStream.Update(baseValues[i], compValues[i], true).Value; + } + + // Batch calculation + var batchOutput = new double[baseValues.Length]; + Prs.Batch(baseValues, compValues, batchOutput, smoothPeriod); + + // Results should match + for (int i = 0; i < baseValues.Length; i++) + { + Assert.Equal(streamResults[i], batchOutput[i], 10); + } + } + + [Fact] + public void Prs_TSeriesCalculate_MatchesStreaming() + { + int smoothPeriod = 3; + var prsStream = new Prs(smoothPeriod); + var time = DateTime.UtcNow; + + var baseSeries = new TSeries(10); + var compSeries = new TSeries(10); + + var baseValues = new double[] { 100, 110, 105, 115, 120, 125, 118, 130, 128, 135 }; + var compValues = new double[] { 100, 102, 101, 103, 104, 105, 103, 106, 105, 107 }; + + for (int i = 0; i < baseValues.Length; i++) + { + baseSeries.Add(new TValue(time.AddSeconds(i), baseValues[i])); + compSeries.Add(new TValue(time.AddSeconds(i), compValues[i])); + } + + // Streaming + var streamResults = new double[baseValues.Length]; + for (int i = 0; i < baseValues.Length; i++) + { + streamResults[i] = prsStream.Update(baseValues[i], compValues[i], true).Value; + } + + // TSeries batch + var batchResult = Prs.Batch(baseSeries, compSeries, smoothPeriod); + + for (int i = 0; i < baseValues.Length; i++) + { + Assert.Equal(streamResults[i], batchResult[i].Value, 10); + } + } + + #endregion + + #region Properties and State + + [Fact] + public void Prs_IsHot_BecomesTrue_AfterSmoothPeriod() + { + var prs = new Prs(5); + + for (int i = 0; i < 10; i++) + { + prs.Update(100 + i, 100.0, true); + + if (i < 4) // 0-4 = first 5 values + { + Assert.False(prs.IsHot, $"Should not be hot at index {i}"); + } + else + { + Assert.True(prs.IsHot, $"Should be hot at index {i}"); + } + } + } + + [Fact] + public void Prs_Reset_ClearsState() + { + var prs = new Prs(5); + + // Add values + for (int i = 0; i < 10; i++) + { + prs.Update(100 + i, 50.0, true); + } + + Assert.True(prs.IsHot); + Assert.True(prs.RawRatio > 0); + + // Reset + prs.Reset(); + + Assert.False(prs.IsHot); + Assert.Equal(0.0, prs.RawRatio); + Assert.Equal(default(TValue), prs.Last); + } + + [Fact] + public void Prs_Prime_InitializesState() + { + var prs = new Prs(5); + + var baseSource = new double[] { 100, 105, 110, 115, 120, 125, 130 }; + var compSource = new double[] { 100, 100, 100, 100, 100, 100, 100 }; + + prs.Prime(baseSource, compSource); + + Assert.True(prs.IsHot); + Assert.Equal(1.30, prs.RawRatio, 10); + Assert.Equal(130.0 / 100.0, prs.RawRatio, 10); + } + + #endregion + + #region Performance Properties + + [Fact] + public void Prs_SmoothPeriod_ExposesCorrectValue() + { + var prs = new Prs(14); + + Assert.Equal(14, prs.SmoothPeriod); + } + + [Fact] + public void Prs_WarmupPeriod_EqualsSmoothPeriod() + { + var prs = new Prs(20); + + Assert.Equal(20, prs.WarmupPeriod); + } + + [Fact] + public void Prs_Name_IncludesPeriodIfSmoothed() + { + var prsNoSmooth = new Prs(1); + var prsSmooth = new Prs(14); + + Assert.Equal("Prs", prsNoSmooth.Name); + Assert.Equal("Prs(14)", prsSmooth.Name); + } + + #endregion +} diff --git a/lib/momentum/prs/Prs.cs b/lib/momentum/prs/Prs.cs new file mode 100644 index 00000000..cc0582a8 --- /dev/null +++ b/lib/momentum/prs/Prs.cs @@ -0,0 +1,349 @@ +// PRS: Price Relative Strength +// Compares the performance of one asset to another by calculating the ratio +// and optionally applying EMA smoothing for trend identification. + +using System.Runtime.CompilerServices; +using static System.Math; + +namespace QuanTAlib; + +/// +/// PRS: Price Relative Strength +/// +/// +/// Measures relative performance between two assets by calculating their price ratio. +/// A rising PRS indicates the base asset is outperforming the comparison asset. +/// A falling PRS indicates underperformance. Optional EMA smoothing reduces noise. +/// +/// Key characteristics: +/// - Ratio-based: PRS = Base / Comparison +/// - Trend indicator: Rising = outperformance, Falling = underperformance +/// - Smoothing: Optional EMA with bias compensation for warmup +/// - Division by zero: Returns NaN when comparison is zero +/// +/// Calculation: +/// +/// Raw Ratio = Base / Comparison +/// Smoothed = EMA(Raw Ratio, smoothPeriod) with bias compensation +/// +/// +/// Interpretation: +/// - PRS > 1.0: Base asset is worth more per unit +/// - PRS increasing: Base outperforming comparison +/// - PRS decreasing: Base underperforming comparison +/// - Use with baseline (1.0 or initial ratio) for normalized view +/// +/// Detailed documentation +[SkipLocalsInit] +public sealed class Prs : AbstractBase +{ + private const double Epsilon = 1e-10; + private readonly int _smoothPeriod; + private readonly double _alpha; + + // EMA state with bias compensation + private double _ema; + private double _e; // Bias compensation factor + private bool _isEmaInitialized; + private bool _isWarmup; + + // State for bar correction + private double _lastValidBase; + private double _lastValidComp; + private double _p_ema; + private double _p_e; + private bool _p_isEmaInitialized; + private bool _p_isWarmup; + + private int _count; + + /// + /// Gets the raw (unsmoothed) ratio from the last update. + /// + public double RawRatio { get; private set; } + + /// + /// Gets the smoothing period for the EMA. + /// + public int SmoothPeriod => _smoothPeriod; + + public override bool IsHot => _count >= _smoothPeriod; + + /// + /// Creates a new Price Relative Strength indicator. + /// + /// Smoothing period for EMA (1 = no smoothing) + public Prs(int smoothPeriod = 1) + { + if (smoothPeriod < 1) + { + throw new ArgumentException("Smoothing period must be >= 1", nameof(smoothPeriod)); + } + + _smoothPeriod = smoothPeriod; + _alpha = 2.0 / Max(smoothPeriod, 1); + _isWarmup = true; + _e = 1.0; + + Name = smoothPeriod == 1 ? "Prs" : $"Prs({smoothPeriod})"; + WarmupPeriod = smoothPeriod; + } + + /// + /// Updates the PRS indicator with new values from both series. + /// + /// Base asset price + /// Comparison asset price + /// Whether this is a new bar + /// The smoothed relative strength ratio + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public TValue Update(TValue baseValue, TValue compValue, bool isNew = true) + { + double basePrice = SanitizeBase(baseValue.Value); + double compPrice = SanitizeComp(compValue.Value); + + if (isNew) + { + SaveState(); + } + else + { + RestoreState(); + } + + double result; + if (Abs(compPrice) < Epsilon) + { + // Division by zero - return NaN + RawRatio = double.NaN; + result = double.NaN; + } + else + { + double ratio = basePrice / compPrice; + RawRatio = ratio; + result = CalculateSmoothedRatio(ratio); + } + + if (isNew) + { + _count++; + } + + Last = new TValue(baseValue.Time, result); + PubEvent(Last); + return Last; + } + + /// + /// Updates with raw double values. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public TValue Update(double baseValue, double compValue, bool isNew = true) + { + return Update(new TValue(DateTime.UtcNow, baseValue), new TValue(DateTime.UtcNow, compValue), isNew); + } + + /// + /// Not supported for bi-input indicator. Use Update(baseValue, compValue) instead. + public override TValue Update(TValue input, bool isNew = true) + { + throw new NotSupportedException("PRS requires two inputs (base and comparison). Use Update(baseValue, compValue)."); + } + + /// + /// Not supported for bi-input indicator. Use Calculate(baseSeries, compSeries, period) instead. + public override TSeries Update(TSeries source) + { + throw new NotSupportedException("PRS requires two inputs. Use Batch(baseSeries, compSeries, period)."); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private double SanitizeBase(double value) + { + if (double.IsFinite(value)) + { + _lastValidBase = value; + return value; + } + return double.IsFinite(_lastValidBase) ? _lastValidBase : 0.0; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private double SanitizeComp(double value) + { + if (double.IsFinite(value)) + { + _lastValidComp = value; + return value; + } + return double.IsFinite(_lastValidComp) ? _lastValidComp : 0.0; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void SaveState() + { + _p_ema = _ema; + _p_e = _e; + _p_isEmaInitialized = _isEmaInitialized; + _p_isWarmup = _isWarmup; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void RestoreState() + { + _ema = _p_ema; + _e = _p_e; + _isEmaInitialized = _p_isEmaInitialized; + _isWarmup = _p_isWarmup; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private double CalculateSmoothedRatio(double ratio) + { + if (_smoothPeriod == 1) + { + // No smoothing + return ratio; + } + + if (!_isEmaInitialized) + { + // First value: initialize EMA + _ema = 0; + _isEmaInitialized = true; + return ratio; + } + + // EMA calculation with bias compensation + _ema = FusedMultiplyAdd(_alpha, ratio - _ema, _ema); + + if (_isWarmup) + { + _e *= (1 - _alpha); + double compensation = 1.0 / (1.0 - _e); + double result = compensation * _ema; + + if (_e <= 1e-10) + { + _isWarmup = false; + } + + return result; + } + + return _ema; + } + + /// + public override void Prime(ReadOnlySpan source, TimeSpan? step = null) + { + throw new NotSupportedException("PRS requires two inputs. Use Prime(baseSource, compSource)."); + } + + /// + /// Primes the indicator with historical data from both series. + /// + public void Prime(ReadOnlySpan baseSource, ReadOnlySpan compSource, TimeSpan? step = null) + { + if (baseSource.Length != compSource.Length) + { + throw new ArgumentException("Source arrays must have the same length", nameof(compSource)); + } + + TimeSpan interval = step ?? TimeSpan.FromSeconds(1); + DateTime time = DateTime.UtcNow - (interval * baseSource.Length); + + for (int i = 0; i < baseSource.Length; i++) + { + Update(new TValue(time, baseSource[i]), new TValue(time, compSource[i]), true); + time += interval; + } + } + + public override void Reset() + { + _ema = 0; + _e = 1.0; + _isEmaInitialized = false; + _isWarmup = true; + _lastValidBase = 0; + _lastValidComp = 0; + _count = 0; + RawRatio = 0; + Last = default; + + _p_ema = 0; + _p_e = 1.0; + _p_isEmaInitialized = false; + _p_isWarmup = true; + } + + /// + /// Calculates PRS for two time series. + /// + public static TSeries Batch(TSeries baseSeries, TSeries compSeries, int smoothPeriod = 1) + { + if (baseSeries.Count != compSeries.Count) + { + throw new ArgumentException("Series must have the same length", nameof(compSeries)); + } + + var indicator = new Prs(smoothPeriod); + var result = new TSeries(baseSeries.Count); + + var times = baseSeries.Times; + var baseValues = baseSeries.Values; + var compValues = compSeries.Values; + + for (int i = 0; i < baseSeries.Count; i++) + { + var tvalBase = new TValue(times[i], baseValues[i]); + var tvalComp = new TValue(times[i], compValues[i]); + result.Add(indicator.Update(tvalBase, tvalComp, isNew: true)); + } + + return result; + } + + /// + /// Static batch calculation for span-based processing. + /// + public static void Batch( + ReadOnlySpan baseSeries, + ReadOnlySpan compSeries, + Span output, + int smoothPeriod = 1) + { + if (baseSeries.Length != compSeries.Length) + { + throw new ArgumentException("Series must have the same length", nameof(compSeries)); + } + + if (baseSeries.Length != output.Length) + { + throw new ArgumentException("Output must have the same length as input", nameof(output)); + } + + if (smoothPeriod < 1) + { + throw new ArgumentException("Smoothing period must be >= 1", nameof(smoothPeriod)); + } + + var indicator = new Prs(smoothPeriod); + + for (int i = 0; i < baseSeries.Length; i++) + { + var result = indicator.Update(baseSeries[i], compSeries[i], isNew: true); + output[i] = result.Value; + } + } + + public static (TSeries Results, Prs Indicator) Calculate(TSeries baseSeries, TSeries compSeries, int smoothPeriod = 1) + { + var indicator = new Prs(smoothPeriod); + TSeries results = Batch(baseSeries, compSeries, smoothPeriod); + return (results, indicator); + } + +} diff --git a/lib/momentum/prs/Prs.md b/lib/momentum/prs/Prs.md new file mode 100644 index 00000000..60e21bc5 --- /dev/null +++ b/lib/momentum/prs/Prs.md @@ -0,0 +1,90 @@ +# PRS: Price Relative Strength + +**Category:** Momentum +**Also known as:** Relative Strength Comparison, Price Ratio, Performance Ratio + +[Pine Script Implementation of PRS](https://github.com/mihakralj/pinescript/blob/main/indicators/momentum/prs.pine) + +## Overview + +Price Relative Strength (PRS) measures the performance of one asset relative to another by calculating the ratio between their prices. This indicator helps identify which asset is outperforming and is fundamental for sector rotation, pairs trading, and relative performance analysis. + +Unlike the Relative Strength Index (RSI) which measures momentum within a single asset, PRS compares two different price series. A rising PRS indicates the base asset is outperforming the comparison asset; a falling PRS indicates underperformance. + +## Core Concepts + +- **Ratio-based:** Simply divides base price by comparison price +- **Trend interpretation:** Rising = outperformance, Falling = underperformance +- **Optional smoothing:** EMA with bias compensation removes noise while maintaining responsiveness +- **Scale-independent:** Can compare assets with vastly different price levels +- **Cross-market analysis:** Compare stocks, indices, commodities, or any tradeable assets + +## Parameters + +| Parameter | Default | Description | When to Adjust | +|-----------|---------|-------------|----------------| +| **smoothPeriod** | 1 | EMA smoothing period (1 = no smoothing) | Increase for trend analysis, decrease for signal sensitivity | + +**Pro Tip:** Use smoothPeriod=1 for raw ratio analysis, smoothPeriod=10-20 for trend identification, and smoothPeriod=50+ for longer-term relative strength trends. + +## Calculation + +**Simplified Explanation:** +PRS divides the base asset's price by the comparison asset's price, then optionally smooths the result with an EMA. + +**Technical Formula:** + +``` +Raw Ratio = Base Price / Comparison Price +Smoothed = EMA(Raw Ratio, smoothPeriod) +``` + +With bias-compensated EMA for accurate warmup: +``` +α = 2 / (smoothPeriod + 1) +EMA_biased = α × (ratio - EMA_prev) + EMA_prev +compensation = 1 / (1 - (1-α)^n) +Smoothed = compensation × EMA_biased +``` + +> **Implementation Note:** When smoothPeriod=1, the raw ratio is returned without any smoothing. Division by zero (comparison = 0) returns NaN. + +## Interpretation Details + +**Trend Analysis:** +- **PRS rising:** Base asset outperforming comparison (bullish for base) +- **PRS falling:** Base asset underperforming comparison (bearish for base) +- **PRS flat:** Both assets moving in tandem + +**Common Comparisons:** +- Stock vs. sector ETF (stock relative to its sector) +- Sector vs. broad market index (sector rotation) +- Growth vs. Value ETFs (style performance) +- Emerging markets vs. developed markets +- Commodity vs. currency (inflation dynamics) + +**Trading Signals:** +- **PRS crossover above prior high:** Breakout in relative strength +- **PRS crossover below prior low:** Breakdown in relative strength +- **Divergence:** Price makes new high, PRS does not = warning + +## Limitations and Considerations + +- **No absolute measure:** Tells you relative performance, not absolute value +- **Base dependency:** Results depend on which asset is the numerator +- **Lagging indicator:** Smoothed version lags the raw ratio +- **Volume ignored:** Pure price comparison, volume not considered +- **Currency effects:** Cross-currency comparisons may include FX impact + +## References + +- Murphy, J. J. (1999). *Technical Analysis of the Financial Markets*. New York Institute of Finance. +- Pring, M. J. (2002). *Technical Analysis Explained*. McGraw-Hill. +- Sector rotation and relative strength analysis literature + +## See Also + +- **ROC/ROCP/ROCR:** Single-asset rate of change variants +- **RSI:** Single-asset momentum oscillator +- **Beta:** Statistical measure of relative volatility +- **Correlation:** Measures how two assets move together diff --git a/lib/momentum/roc/Roc.Tests.cs b/lib/momentum/roc/Roc.Tests.cs index 34095342..e983fe5f 100644 --- a/lib/momentum/roc/Roc.Tests.cs +++ b/lib/momentum/roc/Roc.Tests.cs @@ -263,7 +263,7 @@ public class RocTests public void AllModes_ProduceSameResults() { // Mode 1: Batch via TSeries - var batchResult = Roc.Calculate(_gbm, TestPeriod); + var batchResult = Roc.Batch(_gbm, TestPeriod); // Mode 2: Streaming var streamingRoc = new Roc(TestPeriod); @@ -276,7 +276,7 @@ public class RocTests // Mode 3: Span-based Span spanOutput = stackalloc double[DataPoints]; - Roc.Calculate(_gbm.Values, spanOutput, TestPeriod); + Roc.Batch(_gbm.Values, spanOutput, TestPeriod); // Mode 4: Event-driven var eventRoc = new Roc(TestPeriod); @@ -308,7 +308,7 @@ public class RocTests { ReadOnlySpan empty = []; Span output = stackalloc double[1]; - Roc.Calculate(empty, output, TestPeriod); + Roc.Batch(empty, output, TestPeriod); }); Assert.Equal("source", ex.ParamName); } @@ -320,7 +320,7 @@ public class RocTests { ReadOnlySpan source = stackalloc double[] { 1, 2, 3, 4, 5 }; Span output = stackalloc double[3]; // too short - Roc.Calculate(source, output, TestPeriod); + Roc.Batch(source, output, TestPeriod); }); Assert.Equal("output", ex.ParamName); } @@ -332,7 +332,7 @@ public class RocTests { ReadOnlySpan source = stackalloc double[] { 1, 2, 3, 4, 5 }; Span output = stackalloc double[5]; - Roc.Calculate(source, output, 0); + Roc.Batch(source, output, 0); }); Assert.Equal("period", ex.ParamName); } @@ -340,10 +340,10 @@ public class RocTests [Fact] public void Calculate_Span_MatchesTSeries() { - var batchResult = Roc.Calculate(_gbm, TestPeriod); + var batchResult = Roc.Batch(_gbm, TestPeriod); Span spanOutput = stackalloc double[DataPoints]; - Roc.Calculate(_gbm.Values, spanOutput, TestPeriod); + Roc.Batch(_gbm.Values, spanOutput, TestPeriod); for (int i = 0; i < DataPoints; i++) { @@ -358,7 +358,7 @@ public class RocTests Span output = stackalloc double[5]; // Should not throw - Roc.Calculate(source, output, 2); + Roc.Batch(source, output, 2); // Output will contain NaN due to input NaN // This is expected for span-based (no state tracking) @@ -378,7 +378,7 @@ public class RocTests } // Should not throw - Roc.Calculate(source, output, TestPeriod); + Roc.Batch(source, output, TestPeriod); Assert.Equal(largeSize, output.Length); } diff --git a/lib/momentum/roc/Roc.Validation.Tests.cs b/lib/momentum/roc/Roc.Validation.Tests.cs index 3b6fb889..7d8dd70f 100644 --- a/lib/momentum/roc/Roc.Validation.Tests.cs +++ b/lib/momentum/roc/Roc.Validation.Tests.cs @@ -23,7 +23,7 @@ public class RocValidationTests double[] tulipInput = source.Values.ToArray(); // Get QuanTAlib ROC result - var quantResult = Roc.Calculate(source, TestPeriod); + var quantResult = Roc.Batch(source, TestPeriod); // Calculate Tulip MOM (momentum = current - past) var momIndicator = Tulip.Indicators.mom; @@ -87,7 +87,7 @@ public class RocValidationTests // Get QuanTAlib ROC result via span var quantOutput = new double[DataPoints]; - Roc.Calculate(source.Values, quantOutput, TestPeriod); + Roc.Batch(source.Values, quantOutput, TestPeriod); // Calculate Tulip MOM var momIndicator = Tulip.Indicators.mom; @@ -123,7 +123,7 @@ public class RocValidationTests var source = bars.Close; double[] tulipInput = source.Values.ToArray(); - var quantResult = Roc.Calculate(source, period); + var quantResult = Roc.Batch(source, period); // Calculate Tulip MOM var momIndicator = Tulip.Indicators.mom; @@ -155,7 +155,7 @@ public class RocValidationTests constantData.Add(new TValue(DateTime.UtcNow.AddSeconds(i), 100.0), true); } - var result = Roc.Calculate(constantData, TestPeriod); + var result = Roc.Batch(constantData, TestPeriod); // Constant values should produce 0 change after warmup for (int i = TestPeriod; i < 100; i++) @@ -173,7 +173,7 @@ public class RocValidationTests linearData.Add(new TValue(DateTime.UtcNow.AddSeconds(i), 100.0 + i), true); } - var result = Roc.Calculate(linearData, TestPeriod); + var result = Roc.Batch(linearData, TestPeriod); // Linear increase by 1 per bar means ROC = period after warmup for (int i = TestPeriod; i < 100; i++) @@ -189,7 +189,7 @@ public class RocValidationTests var source = bars.Close; double[] tulipInput = source.Values.ToArray(); - var quantResult = Roc.Calculate(source, 1); + var quantResult = Roc.Batch(source, 1); // Calculate Tulip MOM with period 1 var momIndicator = Tulip.Indicators.mom; diff --git a/lib/momentum/roc/Roc.cs b/lib/momentum/roc/Roc.cs index b6f02a57..f7bebf1f 100644 --- a/lib/momentum/roc/Roc.cs +++ b/lib/momentum/roc/Roc.cs @@ -114,7 +114,7 @@ public sealed class Roc : AbstractBase } } - public static TSeries Calculate(TSeries source, int period = 9) + public static TSeries Batch(TSeries source, int period = 9) { var indicator = new Roc(period); return indicator.Update(source); @@ -123,7 +123,7 @@ public sealed class Roc : AbstractBase /// /// Calculates absolute change over a span of values. /// - public static void Calculate(ReadOnlySpan source, Span output, int period = 9) + public static void Batch(ReadOnlySpan source, Span output, int period = 9) { if (source.Length == 0) { @@ -146,6 +146,13 @@ public sealed class Roc : AbstractBase } } + public static (TSeries Results, Roc Indicator) Calculate(TSeries source, int period = 9) + { + var indicator = new Roc(period); + TSeries results = indicator.Update(source); + return (results, indicator); + } + public override void Reset() { _buffer.Clear(); @@ -167,4 +174,4 @@ public sealed class Roc : AbstractBase } base.Dispose(disposing); } -} +} \ No newline at end of file diff --git a/lib/momentum/rocp/Rocp.Tests.cs b/lib/momentum/rocp/Rocp.Tests.cs index abda5fdc..378f5084 100644 --- a/lib/momentum/rocp/Rocp.Tests.cs +++ b/lib/momentum/rocp/Rocp.Tests.cs @@ -272,7 +272,7 @@ public class RocpTests public void AllModes_ProduceSameResults() { // Mode 1: Batch via TSeries - var batchResult = Rocp.Calculate(_gbm, TestPeriod); + var batchResult = Rocp.Batch(_gbm, TestPeriod); // Mode 2: Streaming var streamingRocp = new Rocp(TestPeriod); @@ -285,7 +285,7 @@ public class RocpTests // Mode 3: Span-based Span spanOutput = stackalloc double[DataPoints]; - Rocp.Calculate(_gbm.Values, spanOutput, TestPeriod); + Rocp.Batch(_gbm.Values, spanOutput, TestPeriod); // Mode 4: Event-driven var eventRocp = new Rocp(TestPeriod); @@ -316,7 +316,7 @@ public class RocpTests { ReadOnlySpan empty = []; Span output = stackalloc double[1]; - Rocp.Calculate(empty, output, TestPeriod); + Rocp.Batch(empty, output, TestPeriod); }); Assert.Equal("source", ex.ParamName); } @@ -328,7 +328,7 @@ public class RocpTests { ReadOnlySpan source = stackalloc double[] { 1, 2, 3, 4, 5 }; Span output = stackalloc double[3]; - Rocp.Calculate(source, output, TestPeriod); + Rocp.Batch(source, output, TestPeriod); }); Assert.Equal("output", ex.ParamName); } @@ -340,7 +340,7 @@ public class RocpTests { ReadOnlySpan source = stackalloc double[] { 1, 2, 3, 4, 5 }; Span output = stackalloc double[5]; - Rocp.Calculate(source, output, 0); + Rocp.Batch(source, output, 0); }); Assert.Equal("period", ex.ParamName); } @@ -348,10 +348,10 @@ public class RocpTests [Fact] public void Calculate_Span_MatchesTSeries() { - var batchResult = Rocp.Calculate(_gbm, TestPeriod); + var batchResult = Rocp.Batch(_gbm, TestPeriod); Span spanOutput = stackalloc double[DataPoints]; - Rocp.Calculate(_gbm.Values, spanOutput, TestPeriod); + Rocp.Batch(_gbm.Values, spanOutput, TestPeriod); for (int i = 0; i < DataPoints; i++) { @@ -371,7 +371,7 @@ public class RocpTests source[i] = 100.0 + i * 0.1; } - Rocp.Calculate(source, output, TestPeriod); + Rocp.Batch(source, output, TestPeriod); Assert.Equal(largeSize, output.Length); } diff --git a/lib/momentum/rocp/Rocp.Validation.Tests.cs b/lib/momentum/rocp/Rocp.Validation.Tests.cs index 3156573e..0b62bfaa 100644 --- a/lib/momentum/rocp/Rocp.Validation.Tests.cs +++ b/lib/momentum/rocp/Rocp.Validation.Tests.cs @@ -178,7 +178,7 @@ public class RocpValidationTests } // Batch - var batchResult = Rocp.Calculate(source, 5); + var batchResult = Rocp.Batch(source, 5); for (int i = 0; i < source.Count; i++) { diff --git a/lib/momentum/rocp/Rocp.cs b/lib/momentum/rocp/Rocp.cs index 0656230f..50bd1ec4 100644 --- a/lib/momentum/rocp/Rocp.cs +++ b/lib/momentum/rocp/Rocp.cs @@ -115,7 +115,7 @@ public sealed class Rocp : AbstractBase } } - public static TSeries Calculate(TSeries source, int period = 9) + public static TSeries Batch(TSeries source, int period = 9) { var indicator = new Rocp(period); return indicator.Update(source); @@ -124,7 +124,7 @@ public sealed class Rocp : AbstractBase /// /// Calculates rate of change percentage over a span of values. /// - public static void Calculate(ReadOnlySpan source, Span output, int period = 9) + public static void Batch(ReadOnlySpan source, Span output, int period = 9) { if (source.Length == 0) { @@ -155,6 +155,13 @@ public sealed class Rocp : AbstractBase } } + public static (TSeries Results, Rocp Indicator) Calculate(TSeries source, int period = 9) + { + var indicator = new Rocp(period); + TSeries results = indicator.Update(source); + return (results, indicator); + } + public override void Reset() { _buffer.Clear(); @@ -176,4 +183,4 @@ public sealed class Rocp : AbstractBase } base.Dispose(disposing); } -} +} \ No newline at end of file diff --git a/lib/momentum/rocr/Rocr.Tests.cs b/lib/momentum/rocr/Rocr.Tests.cs index a471fe3a..ee6de7ba 100644 --- a/lib/momentum/rocr/Rocr.Tests.cs +++ b/lib/momentum/rocr/Rocr.Tests.cs @@ -277,7 +277,7 @@ public class RocrTests public void AllModes_ProduceSameResults() { // Mode 1: Batch via TSeries - var batchResult = Rocr.Calculate(_gbm, TestPeriod); + var batchResult = Rocr.Batch(_gbm, TestPeriod); // Mode 2: Streaming var streamingRocr = new Rocr(TestPeriod); @@ -290,7 +290,7 @@ public class RocrTests // Mode 3: Span-based Span spanOutput = stackalloc double[DataPoints]; - Rocr.Calculate(_gbm.Values, spanOutput, TestPeriod); + Rocr.Batch(_gbm.Values, spanOutput, TestPeriod); // Mode 4: Event-driven var eventRocr = new Rocr(TestPeriod); @@ -322,7 +322,7 @@ public class RocrTests { ReadOnlySpan empty = []; Span output = stackalloc double[1]; - Rocr.Calculate(empty, output, TestPeriod); + Rocr.Batch(empty, output, TestPeriod); }); Assert.Equal("source", ex.ParamName); } @@ -334,7 +334,7 @@ public class RocrTests { ReadOnlySpan source = stackalloc double[] { 1, 2, 3, 4, 5 }; Span output = stackalloc double[3]; // too short - Rocr.Calculate(source, output, TestPeriod); + Rocr.Batch(source, output, TestPeriod); }); Assert.Equal("output", ex.ParamName); } @@ -346,7 +346,7 @@ public class RocrTests { ReadOnlySpan source = stackalloc double[] { 1, 2, 3, 4, 5 }; Span output = stackalloc double[5]; - Rocr.Calculate(source, output, 0); + Rocr.Batch(source, output, 0); }); Assert.Equal("period", ex.ParamName); } @@ -354,10 +354,10 @@ public class RocrTests [Fact] public void Calculate_Span_MatchesTSeries() { - var batchResult = Rocr.Calculate(_gbm, TestPeriod); + var batchResult = Rocr.Batch(_gbm, TestPeriod); Span spanOutput = stackalloc double[DataPoints]; - Rocr.Calculate(_gbm.Values, spanOutput, TestPeriod); + Rocr.Batch(_gbm.Values, spanOutput, TestPeriod); for (int i = 0; i < DataPoints; i++) { @@ -372,7 +372,7 @@ public class RocrTests Span output = stackalloc double[5]; // Should not throw - Rocr.Calculate(source, output, 2); + Rocr.Batch(source, output, 2); // First element after warmup divides by 0 Assert.Equal(1.0, output[2]); // 102 / 0 = 1.0 (safe default) @@ -391,7 +391,7 @@ public class RocrTests } // Should not throw - Rocr.Calculate(source, output, TestPeriod); + Rocr.Batch(source, output, TestPeriod); Assert.Equal(largeSize, output.Length); } diff --git a/lib/momentum/rocr/Rocr.Validation.Tests.cs b/lib/momentum/rocr/Rocr.Validation.Tests.cs index b577478d..4bf18edb 100644 --- a/lib/momentum/rocr/Rocr.Validation.Tests.cs +++ b/lib/momentum/rocr/Rocr.Validation.Tests.cs @@ -228,7 +228,7 @@ public class RocrValidationTests } // Batch - var batchResult = Rocr.Calculate(source, 5); + var batchResult = Rocr.Batch(source, 5); for (int i = 0; i < source.Count; i++) { diff --git a/lib/momentum/rocr/Rocr.cs b/lib/momentum/rocr/Rocr.cs index 85e24b95..b0476782 100644 --- a/lib/momentum/rocr/Rocr.cs +++ b/lib/momentum/rocr/Rocr.cs @@ -115,7 +115,7 @@ public sealed class Rocr : AbstractBase } } - public static TSeries Calculate(TSeries source, int period = 9) + public static TSeries Batch(TSeries source, int period = 9) { var indicator = new Rocr(period); return indicator.Update(source); @@ -124,7 +124,7 @@ public sealed class Rocr : AbstractBase /// /// Calculates rate of change ratio over a span of values. /// - public static void Calculate(ReadOnlySpan source, Span output, int period = 9) + public static void Batch(ReadOnlySpan source, Span output, int period = 9) { if (source.Length == 0) { @@ -155,6 +155,13 @@ public sealed class Rocr : AbstractBase } } + public static (TSeries Results, Rocr Indicator) Calculate(TSeries source, int period = 9) + { + var indicator = new Rocr(period); + TSeries results = indicator.Update(source); + return (results, indicator); + } + public override void Reset() { _buffer.Clear(); @@ -176,4 +183,4 @@ public sealed class Rocr : AbstractBase } base.Dispose(disposing); } -} +} \ No newline at end of file diff --git a/lib/momentum/rsi/Rsi.Tests.cs b/lib/momentum/rsi/Rsi.Tests.cs index ef56a039..e2eeef44 100644 --- a/lib/momentum/rsi/Rsi.Tests.cs +++ b/lib/momentum/rsi/Rsi.Tests.cs @@ -329,7 +329,7 @@ public class RsiTests var source = new double[10]; var output = new double[5]; - Assert.Throws(() => Rsi.Calculate(source, output, 14)); + Assert.Throws(() => Rsi.Batch(source, output, 14)); } [Fact] @@ -338,8 +338,8 @@ public class RsiTests var source = new double[10]; var output = new double[10]; - Assert.Throws(() => Rsi.Calculate(source, output, 0)); - Assert.Throws(() => Rsi.Calculate(source, output, -1)); + Assert.Throws(() => Rsi.Batch(source, output, 0)); + Assert.Throws(() => Rsi.Batch(source, output, -1)); } [Fact] @@ -351,7 +351,7 @@ public class RsiTests var batchResults = Rsi.Batch(series.Close, 14); var output = new double[series.Count]; - Rsi.Calculate(series.Close.Values, output, 14); + Rsi.Batch(series.Close.Values, output, 14); for (int i = 0; i < output.Length; i++) { @@ -375,7 +375,7 @@ public class RsiTests source[15] = double.NaN; var output = new double[20]; - Rsi.Calculate(source, output, 10); + Rsi.Batch(source, output, 10); // Should not crash and produce finite results for (int i = 0; i < output.Length; i++) @@ -397,7 +397,7 @@ public class RsiTests // 2. Span Mode var spanOutput = new double[series.Count]; - Rsi.Calculate(series.Close.Values, spanOutput, period); + Rsi.Batch(series.Close.Values, spanOutput, period); double spanResult = spanOutput[^1]; // 3. Streaming Mode diff --git a/lib/momentum/rsi/Rsi.Validation.Tests.cs b/lib/momentum/rsi/Rsi.Validation.Tests.cs index 6e30876f..693d74b8 100644 --- a/lib/momentum/rsi/Rsi.Validation.Tests.cs +++ b/lib/momentum/rsi/Rsi.Validation.Tests.cs @@ -61,7 +61,7 @@ public sealed class RsiValidationTests(ITestOutputHelper output) : IDisposable foreach (var period in periods) { double[] qOutput = new double[tData.Length]; - Rsi.Calculate(tData.AsSpan(), qOutput.AsSpan(), period); + Rsi.Batch(tData.AsSpan(), qOutput.AsSpan(), period); double[] tOutput = new double[tData.Length]; var retCode = TALib.Functions.Rsi(tData, 0..^0, tOutput, out var outRange, period); @@ -86,7 +86,7 @@ public sealed class RsiValidationTests(ITestOutputHelper output) : IDisposable { // Calculate QuanTAlib RSI (Span API) double[] qOutput = new double[sourceData.Length]; - global::QuanTAlib.Rsi.Calculate(sourceData.AsSpan(), qOutput.AsSpan(), period); + global::QuanTAlib.Rsi.Batch(sourceData.AsSpan(), qOutput.AsSpan(), period); // Calculate Skender RSI var sResult = _testData.SkenderQuotes.GetRsi(period).ToList(); @@ -106,7 +106,7 @@ public sealed class RsiValidationTests(ITestOutputHelper output) : IDisposable foreach (var period in periods) { double[] qOutput = new double[tData.Length]; - Rsi.Calculate(tData.AsSpan(), qOutput.AsSpan(), period); + Rsi.Batch(tData.AsSpan(), qOutput.AsSpan(), period); var rsiIndicator = Tulip.Indicators.rsi; double[][] inputs = { tData }; diff --git a/lib/momentum/rsi/Rsi.cs b/lib/momentum/rsi/Rsi.cs index ba1932b8..89e3e915 100644 --- a/lib/momentum/rsi/Rsi.cs +++ b/lib/momentum/rsi/Rsi.cs @@ -128,7 +128,7 @@ public sealed class Rsi : AbstractBase var tSpan = CollectionsMarshal.AsSpan(t); var vSpan = CollectionsMarshal.AsSpan(v); - Calculate(source.Values, vSpan, _period); + Batch(source.Values, vSpan, _period); source.Times.CopyTo(tSpan); // Restore state for streaming @@ -166,7 +166,7 @@ public sealed class Rsi : AbstractBase } [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void Calculate(ReadOnlySpan source, Span output, int period) + public static void Batch(ReadOnlySpan source, Span output, int period) { if (source.Length != output.Length) { @@ -293,6 +293,13 @@ public sealed class Rsi : AbstractBase System.Buffers.ArrayPool.Shared.Return(losses); } + public static (TSeries Results, Rsi Indicator) Calculate(TSeries source, int period = 14) + { + var indicator = new Rsi(period); + TSeries results = indicator.Update(source); + return (results, indicator); + } + public override void Reset() { _avgGain.Reset(); @@ -301,4 +308,4 @@ public sealed class Rsi : AbstractBase _p_prevValue = double.NaN; Last = default; } -} +} \ No newline at end of file diff --git a/lib/momentum/rsx/Rsx.cs b/lib/momentum/rsx/Rsx.cs index 7332bf22..6335263c 100644 --- a/lib/momentum/rsx/Rsx.cs +++ b/lib/momentum/rsx/Rsx.cs @@ -321,10 +321,17 @@ public sealed class Rsx : ITValuePublisher } } + public static (TSeries Results, Rsx Indicator) Calculate(TSeries source, int period) + { + var indicator = new Rsx(period); + TSeries results = indicator.Update(source); + return (results, indicator); + } + public void Reset() { _state = default; _p_state = default; Last = default; } -} +} \ No newline at end of file diff --git a/lib/momentum/tsi/Tsi.Tests.cs b/lib/momentum/tsi/Tsi.Tests.cs index 36573464..80d5b37c 100644 --- a/lib/momentum/tsi/Tsi.Tests.cs +++ b/lib/momentum/tsi/Tsi.Tests.cs @@ -346,7 +346,7 @@ public class TsiTests source[i] = 100.0 + random.NextDouble() * 20; } - Tsi.Calculate(source, output, 5, 3); + Tsi.Batch(source, output, 5, 3); var series = new TSeries(); for (int i = 0; i < 50; i++) @@ -368,7 +368,7 @@ public class TsiTests double[] source = new double[10]; double[] output = new double[5]; - Assert.Throws(() => Tsi.Calculate(source, output)); + Assert.Throws(() => Tsi.Batch(source, output)); } [Fact] @@ -377,8 +377,8 @@ public class TsiTests double[] source = new double[10]; double[] output = new double[10]; - Assert.Throws(() => Tsi.Calculate(source, output, 0, 3)); - Assert.Throws(() => Tsi.Calculate(source, output, 5, 0)); + Assert.Throws(() => Tsi.Batch(source, output, 0, 3)); + Assert.Throws(() => Tsi.Batch(source, output, 5, 0)); } [Fact] @@ -387,7 +387,7 @@ public class TsiTests double[] source = []; double[] output = []; - var exception = Record.Exception(() => Tsi.Calculate(source, output)); + var exception = Record.Exception(() => Tsi.Batch(source, output)); Assert.Null(exception); } diff --git a/lib/momentum/tsi/Tsi.cs b/lib/momentum/tsi/Tsi.cs index 7bab2b30..d75f407a 100644 --- a/lib/momentum/tsi/Tsi.cs +++ b/lib/momentum/tsi/Tsi.cs @@ -194,7 +194,7 @@ public sealed class Tsi : AbstractBase var vSpan = CollectionsMarshal.AsSpan(v); // Batch calculate - Calculate(source.Values, vSpan, LongPeriod, ShortPeriod); + Batch(source.Values, vSpan, LongPeriod, ShortPeriod); source.Times.CopyTo(tSpan); // Restore state for streaming by replaying @@ -231,7 +231,7 @@ public sealed class Tsi : AbstractBase /// Batch calculates TSI values (without signal line). /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void Calculate(ReadOnlySpan source, Span output, int longPeriod = DefaultLongPeriod, int shortPeriod = DefaultShortPeriod) + public static void Batch(ReadOnlySpan source, Span output, int longPeriod = DefaultLongPeriod, int shortPeriod = DefaultShortPeriod) { if (source.Length != output.Length) { @@ -299,6 +299,13 @@ public sealed class Tsi : AbstractBase System.Buffers.ArrayPool.Shared.Return(smoothedAbsMom); } + public static (TSeries Results, Tsi Indicator) Calculate(TSeries source, int longPeriod = DefaultLongPeriod, int shortPeriod = DefaultShortPeriod, int signalPeriod = DefaultSignalPeriod) + { + var indicator = new Tsi(longPeriod, shortPeriod, signalPeriod); + TSeries results = indicator.Update(source); + return (results, indicator); + } + public override void Reset() { _emaMomLong.Reset(); @@ -312,4 +319,4 @@ public sealed class Tsi : AbstractBase _p_lastSignal = 0; Last = default; } -} +} \ No newline at end of file diff --git a/lib/momentum/vel/Vel.Validation.Tests.cs b/lib/momentum/vel/Vel.Validation.Tests.cs index 5abf8cd1..01ad5705 100644 --- a/lib/momentum/vel/Vel.Validation.Tests.cs +++ b/lib/momentum/vel/Vel.Validation.Tests.cs @@ -100,7 +100,7 @@ public sealed class VelValidationTests : IDisposable double[] wmaOutput = new double[sourceData.Length]; Vel.Batch(sourceData.AsSpan(), velOutput.AsSpan(), period); - Pwma.Calculate(sourceData.AsSpan(), pwmaOutput.AsSpan(), period); + Pwma.Batch(sourceData.AsSpan(), pwmaOutput.AsSpan(), period); Wma.Batch(sourceData.AsSpan(), wmaOutput.AsSpan(), period); // Verify relationship for all data points diff --git a/lib/momentum/vel/Vel.cs b/lib/momentum/vel/Vel.cs index ebbbb7bc..bd36f293 100644 --- a/lib/momentum/vel/Vel.cs +++ b/lib/momentum/vel/Vel.cs @@ -161,13 +161,20 @@ public sealed class Vel : ITValuePublisher, IDisposable } } + public static (TSeries Results, Vel Indicator) Calculate(TSeries source, int period) + { + var indicator = new Vel(period); + TSeries results = indicator.Update(source); + return (results, indicator); + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] private static void BatchStackalloc(ReadOnlySpan source, Span output, int period, int len) { Span pwma = stackalloc double[len]; Span wma = stackalloc double[len]; - Pwma.Calculate(source, pwma, period); + Pwma.Batch(source, pwma, period); Wma.Batch(source, wma, period); SimdExtensions.Subtract(pwma, wma, output); } @@ -183,7 +190,7 @@ public sealed class Vel : ITValuePublisher, IDisposable Span pwma = rentedPwma.AsSpan(0, len); Span wma = rentedWma.AsSpan(0, len); - Pwma.Calculate(source, pwma, period); + Pwma.Batch(source, pwma, period); Wma.Batch(source, wma, period); SimdExtensions.Subtract(pwma, wma, output); } @@ -201,4 +208,4 @@ public sealed class Vel : ITValuePublisher, IDisposable _wma.Reset(); Last = default; } -} +} \ No newline at end of file diff --git a/lib/numerics/_index.md b/lib/numerics/_index.md index 3caddd7d..93828d12 100644 --- a/lib/numerics/_index.md +++ b/lib/numerics/_index.md @@ -20,4 +20,4 @@ Basic mathematical transforms and utility functions for time series. These build | [SIGMOID](sigmoid/Sigmoid.md) | Logistic Function | 1/(1+e^-x); bounded [0,1] transform. | | [SLOPE](slope/Slope.md) | Rate of Change | First derivative; velocity of price movement. | | [SQRTTRANS](sqrttrans/Sqrttrans.md) | Square Root Transform | Variance-stabilizing transformation. | -| [STANDARDIZE](standardize/Standardize.md) | Z-Score Normalization | (x - mean) / stddev; zero-mean unit-variance transform. | +| [STANDARDIZE](standardize/Standardize.cs) | Z-Score Normalization | (x - mean) / stddev; zero-mean unit-variance transform. | diff --git a/lib/numerics/accel/Accel.Tests.cs b/lib/numerics/accel/Accel.Tests.cs index 74ad5920..983645bc 100644 --- a/lib/numerics/accel/Accel.Tests.cs +++ b/lib/numerics/accel/Accel.Tests.cs @@ -97,7 +97,7 @@ public class AccelTests // Output must be same length as source Assert.Throws(() => - Accel.Calculate(source.AsSpan(), wrongSizeOutput.AsSpan())); + Accel.Batch(source.AsSpan(), wrongSizeOutput.AsSpan())); } [Fact] @@ -110,7 +110,7 @@ public class AccelTests // 1. Batch Mode (static span) var tValues = series.Values.ToArray(); var batchOutput = new double[tValues.Length]; - Accel.Calculate(tValues, batchOutput); + Accel.Batch(tValues, batchOutput); double expected = batchOutput[^1]; // 2. Streaming Mode @@ -122,7 +122,7 @@ public class AccelTests double streamingResult = streamingInd.Last.Value; // 3. TSeries Batch Mode - var batchSeriesResult = Accel.Calculate(series); + var batchSeriesResult = Accel.Batch(series); double tseriesResult = batchSeriesResult.Last.Value; Assert.Equal(expected, streamingResult, precision: 9); @@ -207,7 +207,7 @@ public class AccelTests // Batch var batchResults = new double[count]; - Accel.Calculate(data, batchResults); + Accel.Batch(data, batchResults); // Compare for (int i = 0; i < count; i++) diff --git a/lib/numerics/accel/Accel.Validation.Tests.cs b/lib/numerics/accel/Accel.Validation.Tests.cs index 88a513f6..e389a541 100644 --- a/lib/numerics/accel/Accel.Validation.Tests.cs +++ b/lib/numerics/accel/Accel.Validation.Tests.cs @@ -109,7 +109,7 @@ public class AccelValidationTests double[] expected = [0, 0, 2, 2, 2, 2]; double[] output = new double[data.Length]; - Accel.Calculate(data, output); + Accel.Batch(data, output); for (int i = 0; i < data.Length; i++) { diff --git a/lib/numerics/accel/Accel.cs b/lib/numerics/accel/Accel.cs index f201f6b6..b909743f 100644 --- a/lib/numerics/accel/Accel.cs +++ b/lib/numerics/accel/Accel.cs @@ -129,7 +129,7 @@ public sealed class Accel : AbstractBase var tSpan = CollectionsMarshal.AsSpan(t); var vSpan = CollectionsMarshal.AsSpan(v); - Calculate(sourceValues, vSpan); + Batch(sourceValues, vSpan); sourceTimes.CopyTo(tSpan); // Prime state with last two values using cached span @@ -176,7 +176,7 @@ public sealed class Accel : AbstractBase } } - public static TSeries Calculate(TSeries source) + public static TSeries Batch(TSeries source) { var accel = new Accel(); return accel.Update(source); @@ -187,7 +187,7 @@ public sealed class Accel : AbstractBase /// accel[i] = source[i] - 2*source[i-1] + source[i-2] /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void Calculate(ReadOnlySpan source, Span output) + public static void Batch(ReadOnlySpan source, Span output) { if (source.Length != output.Length) { @@ -314,6 +314,13 @@ public sealed class Accel : AbstractBase } } + public static (TSeries Results, Accel Indicator) Calculate(TSeries source) + { + var indicator = new Accel(); + TSeries results = indicator.Update(source); + return (results, indicator); + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] private static double FindFinite(double a, double b, double c) { @@ -334,4 +341,4 @@ public sealed class Accel : AbstractBase return 0.0; } -} +} \ No newline at end of file diff --git a/lib/numerics/change/Change.Tests.cs b/lib/numerics/change/Change.Tests.cs index 177bfe98..1b87831d 100644 --- a/lib/numerics/change/Change.Tests.cs +++ b/lib/numerics/change/Change.Tests.cs @@ -154,7 +154,7 @@ public class ChangeTests public void Change_Batch_MatchesStreaming() { int period = 5; - var batchResult = Change.Calculate(_source, period); + var batchResult = Change.Batch(_source, period); var indicator = new Change(period); for (int i = 0; i < _source.Count; i++) @@ -185,8 +185,8 @@ public class ChangeTests var values = _source.Values.ToArray(); var output = new double[values.Length]; - Change.Calculate(values, output, period); - var batchResult = Change.Calculate(_source, period); + Change.Batch(values, output, period); + var batchResult = Change.Batch(_source, period); for (int i = 0; i < values.Length; i++) { @@ -200,9 +200,9 @@ public class ChangeTests var source = new double[10]; var output = new double[5]; - Assert.Throws(() => Change.Calculate(ReadOnlySpan.Empty, output, 1)); - Assert.Throws(() => Change.Calculate(source, output, 1)); - Assert.Throws(() => Change.Calculate(source, new double[10], 0)); + Assert.Throws(() => Change.Batch(ReadOnlySpan.Empty, output, 1)); + Assert.Throws(() => Change.Batch(source, output, 1)); + Assert.Throws(() => Change.Batch(source, new double[10], 0)); } [Fact] diff --git a/lib/numerics/change/Change.Validation.Tests.cs b/lib/numerics/change/Change.Validation.Tests.cs index 11e71fbc..8c8c23aa 100644 --- a/lib/numerics/change/Change.Validation.Tests.cs +++ b/lib/numerics/change/Change.Validation.Tests.cs @@ -18,7 +18,7 @@ public class ChangeValidationTests var series = bars.Close; int period = 10; - var result = Change.Calculate(series, period); + var result = Change.Batch(series, period); for (int i = period; i < series.Count; i++) { @@ -63,7 +63,7 @@ public class ChangeValidationTests var output = new double[values.Length]; int period = 10; - Change.Calculate(values, output, period); + Change.Batch(values, output, period); for (int i = period; i < values.Length; i++) { @@ -83,7 +83,7 @@ public class ChangeValidationTests int period = 10; // Calculate QuanTAlib Change - var qResult = Change.Calculate(source, period); + var qResult = Change.Batch(source, period); // Calculate Tulip ROC (returns percentage) var rocIndicator = Tulip.Indicators.roc; @@ -171,7 +171,7 @@ public class ChangeValidationTests var source = bars.Close; // Batch - var batchResult = Change.Calculate(source, period); + var batchResult = Change.Batch(source, period); // Streaming var streamingIndicator = new Change(period); @@ -185,7 +185,7 @@ public class ChangeValidationTests // Span var values = source.Values.ToArray(); var spanOutput = new double[count]; - Change.Calculate(values, spanOutput, period); + Change.Batch(values, spanOutput, period); // Event-driven var eventIndicator = new Change(period); @@ -215,7 +215,7 @@ public class ChangeValidationTests foreach (int period in new[] { 1, 5, 10, 20 }) { - var result = Change.Calculate(source, period); + var result = Change.Batch(source, period); // Calculate Tulip ROC var rocIndicator = Tulip.Indicators.roc; diff --git a/lib/numerics/change/Change.cs b/lib/numerics/change/Change.cs index 00cb071d..1d874433 100644 --- a/lib/numerics/change/Change.cs +++ b/lib/numerics/change/Change.cs @@ -28,7 +28,7 @@ public sealed class Change : AbstractBase public override bool IsHot => _buffer.Count > _period; /// - /// + /// Initializes a new Change indicator with the specified lookback period. /// /// Lookback period (must be >= 1) public Change(int period = 1) @@ -45,7 +45,7 @@ public sealed class Change : AbstractBase } /// - /// + /// Initializes a new Change indicator chained to a source publisher. /// /// Source indicator for chaining /// Lookback period @@ -116,7 +116,7 @@ public sealed class Change : AbstractBase } } - public static TSeries Calculate(TSeries source, int period = 1) + public static TSeries Batch(TSeries source, int period = 1) { var indicator = new Change(period); return indicator.Update(source); @@ -125,7 +125,7 @@ public sealed class Change : AbstractBase /// /// Calculates relative change over a span of values. /// - public static void Calculate(ReadOnlySpan source, Span output, int period = 1) + public static void Batch(ReadOnlySpan source, Span output, int period = 1) { if (source.Length == 0) { @@ -204,6 +204,13 @@ public sealed class Change : AbstractBase } } + public static (TSeries Results, Change Indicator) Calculate(TSeries source, int period = 1) + { + var indicator = new Change(period); + TSeries results = indicator.Update(source); + return (results, indicator); + } + public override void Reset() { _buffer.Clear(); @@ -211,4 +218,4 @@ public sealed class Change : AbstractBase _p_state = default; Last = default; } -} +} \ No newline at end of file diff --git a/lib/numerics/exptrans/Exptrans.Tests.cs b/lib/numerics/exptrans/Exptrans.Tests.cs index 05852ee4..017c1585 100644 --- a/lib/numerics/exptrans/Exptrans.Tests.cs +++ b/lib/numerics/exptrans/Exptrans.Tests.cs @@ -187,7 +187,7 @@ public class ExptransTests var gbm = new GBM(startPrice: 100, mu: 0.05, sigma: 0.2, seed: 40000); var bars = gbm.Fetch(count, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1)); // Use log of close prices to stay in reasonable exp range - var logSource = Logtrans.Calculate(bars.Close); + var logSource = Logtrans.Batch(bars.Close); // Streaming var streaming = new Exptrans(); @@ -199,7 +199,7 @@ public class ExptransTests } // Batch - var batch = Exptrans.Calculate(logSource); + var batch = Exptrans.Batch(logSource); // Compare all values for (int i = 0; i < logSource.Count; i++) @@ -214,15 +214,15 @@ public class ExptransTests int count = 50; var gbm = new GBM(startPrice: 100, mu: 0.05, sigma: 0.2, seed: 40001); var bars = gbm.Fetch(count, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1)); - var logSource = Logtrans.Calculate(bars.Close); + var logSource = Logtrans.Batch(bars.Close); // TSeries batch - var batchResult = Exptrans.Calculate(logSource); + var batchResult = Exptrans.Batch(logSource); // Span calculation var values = logSource.Values.ToArray(); var output = new double[count]; - Exptrans.Calculate(values, output); + Exptrans.Batch(values, output); for (int i = 0; i < logSource.Count; i++) { @@ -236,14 +236,14 @@ public class ExptransTests Assert.Throws(() => { Span output = stackalloc double[10]; - Exptrans.Calculate(ReadOnlySpan.Empty, output); + Exptrans.Batch(ReadOnlySpan.Empty, output); }); Assert.Throws(() => { ReadOnlySpan source = stackalloc double[10]; Span output = stackalloc double[5]; - Exptrans.Calculate(source, output); + Exptrans.Batch(source, output); }); } diff --git a/lib/numerics/exptrans/Exptrans.Validation.Tests.cs b/lib/numerics/exptrans/Exptrans.Validation.Tests.cs index e60a9fe8..7eb41253 100644 --- a/lib/numerics/exptrans/Exptrans.Validation.Tests.cs +++ b/lib/numerics/exptrans/Exptrans.Validation.Tests.cs @@ -16,9 +16,9 @@ public class ExptransValidationTests // Use log-transformed prices to keep exp in reasonable range var gbm = new GBM(startPrice: 100, mu: 0.05, sigma: 0.2, seed: 50000); var bars = gbm.Fetch(count, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1)); - var logSource = Logtrans.Calculate(bars.Close); + var logSource = Logtrans.Batch(bars.Close); - var result = Exptrans.Calculate(logSource); + var result = Exptrans.Batch(logSource); for (int i = 0; i < logSource.Count; i++) { @@ -33,7 +33,7 @@ public class ExptransValidationTests int count = 100; var gbm = new GBM(startPrice: 100, mu: 0.05, sigma: 0.2, seed: 50001); var bars = gbm.Fetch(count, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1)); - var logSource = Logtrans.Calculate(bars.Close); + var logSource = Logtrans.Batch(bars.Close); var indicator = new Exptrans(); @@ -51,11 +51,11 @@ public class ExptransValidationTests int count = 100; var gbm = new GBM(startPrice: 100, mu: 0.05, sigma: 0.2, seed: 50002); var bars = gbm.Fetch(count, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1)); - var logSource = Logtrans.Calculate(bars.Close); + var logSource = Logtrans.Batch(bars.Close); var values = logSource.Values.ToArray(); var output = new double[count]; - Exptrans.Calculate(values, output); + Exptrans.Batch(values, output); for (int i = 0; i < count; i++) { @@ -94,8 +94,8 @@ public class ExptransValidationTests var bars = gbm.Fetch(50, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1)); var source = bars.Close; - var logResult = Logtrans.Calculate(source); - var expResult = Exptrans.Calculate(logResult); + var logResult = Logtrans.Batch(source); + var expResult = Exptrans.Batch(logResult); for (int i = 0; i < source.Count; i++) { diff --git a/lib/numerics/exptrans/Exptrans.cs b/lib/numerics/exptrans/Exptrans.cs index 82b4c9a0..acd0ac2d 100644 --- a/lib/numerics/exptrans/Exptrans.cs +++ b/lib/numerics/exptrans/Exptrans.cs @@ -106,7 +106,7 @@ public sealed class Exptrans : AbstractBase } } - public static TSeries Calculate(TSeries source) + public static TSeries Batch(TSeries source) { var indicator = new Exptrans(); return indicator.Update(source); @@ -115,7 +115,7 @@ public sealed class Exptrans : AbstractBase /// /// Calculates exponential over a span of values. /// - public static void Calculate(ReadOnlySpan source, Span output) + public static void Batch(ReadOnlySpan source, Span output) { if (source.Length == 0) { @@ -152,10 +152,17 @@ public sealed class Exptrans : AbstractBase } } + public static (TSeries Results, Exptrans Indicator) Calculate(TSeries source) + { + var indicator = new Exptrans(); + TSeries results = indicator.Update(source); + return (results, indicator); + } + public override void Reset() { _state = new(1.0); _p_state = new(1.0); Last = default; } -} +} \ No newline at end of file diff --git a/lib/numerics/highest/Highest.Tests.cs b/lib/numerics/highest/Highest.Tests.cs index ad1a173d..e9d9e125 100644 --- a/lib/numerics/highest/Highest.Tests.cs +++ b/lib/numerics/highest/Highest.Tests.cs @@ -209,7 +209,7 @@ public class HighestTests } // Batch - var batch = Highest.Calculate(source, period); + var batch = Highest.Batch(source, period); // Compare last values (after warmup) for (int i = period; i < source.Count; i++) @@ -228,12 +228,12 @@ public class HighestTests var source = bars.Close; // TSeries batch - var batchResult = Highest.Calculate(source, period); + var batchResult = Highest.Batch(source, period); // Span calculation var values = source.Values.ToArray(); var output = new double[count]; - Highest.Calculate(values, output, period); + Highest.Batch(values, output, period); for (int i = 0; i < source.Count; i++) { @@ -247,21 +247,21 @@ public class HighestTests Assert.Throws(() => { Span output = stackalloc double[10]; - Highest.Calculate(ReadOnlySpan.Empty, output, 5); + Highest.Batch(ReadOnlySpan.Empty, output, 5); }); Assert.Throws(() => { ReadOnlySpan source = stackalloc double[10]; Span output = stackalloc double[5]; - Highest.Calculate(source, output, 5); + Highest.Batch(source, output, 5); }); Assert.Throws(() => { ReadOnlySpan source = stackalloc double[10]; Span output = stackalloc double[10]; - Highest.Calculate(source, output, 0); + Highest.Batch(source, output, 0); }); } diff --git a/lib/numerics/highest/Highest.Validation.Tests.cs b/lib/numerics/highest/Highest.Validation.Tests.cs index 72462dcf..592e0622 100644 --- a/lib/numerics/highest/Highest.Validation.Tests.cs +++ b/lib/numerics/highest/Highest.Validation.Tests.cs @@ -103,7 +103,7 @@ public sealed class HighestValidationTests : IDisposable { // Calculate QuanTAlib Highest (Span API) double[] qOutput = new double[sourceData.Length]; - Highest.Calculate(sourceData.AsSpan(), qOutput.AsSpan(), period); + Highest.Batch(sourceData.AsSpan(), qOutput.AsSpan(), period); // Calculate TA-Lib MAX var retCode = TALib.Functions.Max(sourceData, 0..^0, talibOutput, out var outRange, period); diff --git a/lib/numerics/highest/Highest.cs b/lib/numerics/highest/Highest.cs index 7c2c01fe..1c98af04 100644 --- a/lib/numerics/highest/Highest.cs +++ b/lib/numerics/highest/Highest.cs @@ -106,7 +106,7 @@ public sealed class Highest : AbstractBase } } - public static TSeries Calculate(TSeries source, int period) + public static TSeries Batch(TSeries source, int period) { var indicator = new Highest(period); return indicator.Update(source); @@ -115,7 +115,7 @@ public sealed class Highest : AbstractBase /// /// Calculates rolling maximum over a span of values. /// - public static void Calculate(ReadOnlySpan source, Span output, int period) + public static void Batch(ReadOnlySpan source, Span output, int period) { if (source.Length == 0) { @@ -238,6 +238,13 @@ public sealed class Highest : AbstractBase } } + public static (TSeries Results, Highest Indicator) Calculate(TSeries source, int period) + { + var indicator = new Highest(period); + TSeries results = indicator.Update(source); + return (results, indicator); + } + public override void Reset() { _buffer.Clear(); @@ -245,4 +252,4 @@ public sealed class Highest : AbstractBase _p_state = default; Last = default; } -} +} \ No newline at end of file diff --git a/lib/numerics/jerk/Jerk.Tests.cs b/lib/numerics/jerk/Jerk.Tests.cs index 0ed3b1ad..7b42b2cc 100644 --- a/lib/numerics/jerk/Jerk.Tests.cs +++ b/lib/numerics/jerk/Jerk.Tests.cs @@ -102,7 +102,7 @@ public class JerkTests // Output must be same length as source Assert.Throws(() => - Jerk.Calculate(source.AsSpan(), wrongSizeOutput.AsSpan())); + Jerk.Batch(source.AsSpan(), wrongSizeOutput.AsSpan())); } [Fact] @@ -115,7 +115,7 @@ public class JerkTests // 1. Batch Mode (static span) var tValues = series.Values.ToArray(); var batchOutput = new double[tValues.Length]; - Jerk.Calculate(tValues, batchOutput); + Jerk.Batch(tValues, batchOutput); double expected = batchOutput[^1]; // 2. Streaming Mode @@ -127,7 +127,7 @@ public class JerkTests double streamingResult = streamingInd.Last.Value; // 3. TSeries Batch Mode - var batchSeriesResult = Jerk.Calculate(series); + var batchSeriesResult = Jerk.Batch(series); double tseriesResult = batchSeriesResult.Last.Value; Assert.Equal(expected, streamingResult, precision: 9); @@ -211,7 +211,7 @@ public class JerkTests // Batch var batchResults = new double[count]; - Jerk.Calculate(data, batchResults); + Jerk.Batch(data, batchResults); // Compare for (int i = 0; i < count; i++) diff --git a/lib/numerics/jerk/Jerk.Validation.Tests.cs b/lib/numerics/jerk/Jerk.Validation.Tests.cs index 6eebb356..45445c2f 100644 --- a/lib/numerics/jerk/Jerk.Validation.Tests.cs +++ b/lib/numerics/jerk/Jerk.Validation.Tests.cs @@ -128,7 +128,7 @@ public class JerkValidationTests double[] expected = [0, 0, 0, 6, 6, 6]; double[] output = new double[data.Length]; - Jerk.Calculate(data, output); + Jerk.Batch(data, output); for (int i = 0; i < data.Length; i++) { diff --git a/lib/numerics/jerk/Jerk.cs b/lib/numerics/jerk/Jerk.cs index f9e3f20f..a934d8d5 100644 --- a/lib/numerics/jerk/Jerk.cs +++ b/lib/numerics/jerk/Jerk.cs @@ -146,7 +146,7 @@ public sealed class Jerk : AbstractBase var tSpan = CollectionsMarshal.AsSpan(t); var vSpan = CollectionsMarshal.AsSpan(v); - Calculate(sourceValues, vSpan); + Batch(sourceValues, vSpan); sourceTimes.CopyTo(tSpan); // Prime state with last three values using cached span @@ -200,7 +200,7 @@ public sealed class Jerk : AbstractBase } } - public static TSeries Calculate(TSeries source) + public static TSeries Batch(TSeries source) { var jerk = new Jerk(); return jerk.Update(source); @@ -211,7 +211,7 @@ public sealed class Jerk : AbstractBase /// jerk[i] = source[i] - 3*source[i-1] + 3*source[i-2] - source[i-3] /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void Calculate(ReadOnlySpan source, Span output) + public static void Batch(ReadOnlySpan source, Span output) { if (source.Length != output.Length) { @@ -385,6 +385,13 @@ public sealed class Jerk : AbstractBase } } + public static (TSeries Results, Jerk Indicator) Calculate(TSeries source) + { + var indicator = new Jerk(); + TSeries results = indicator.Update(source); + return (results, indicator); + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] private static double FindFinite(double a, double b, double c, double d) { @@ -410,4 +417,4 @@ public sealed class Jerk : AbstractBase return 0.0; } -} +} \ No newline at end of file diff --git a/lib/numerics/lineartrans/Lineartrans.Tests.cs b/lib/numerics/lineartrans/Lineartrans.Tests.cs index 7a7d58a2..a75a682f 100644 --- a/lib/numerics/lineartrans/Lineartrans.Tests.cs +++ b/lib/numerics/lineartrans/Lineartrans.Tests.cs @@ -197,7 +197,7 @@ public class LineartransTests series.Add(new TValue(time.AddSeconds(i), 10.0 * (i + 1)), true); } - var result = Lineartrans.Calculate(series, slope: 0.5, intercept: 5.0); + var result = Lineartrans.Batch(series, slope: 0.5, intercept: 5.0); Assert.Equal(3, result.Count); Assert.Equal(10.0, result[0].Value, 1e-10); // 0.5*10+5 @@ -211,7 +211,7 @@ public class LineartransTests double[] source = [10.0, 20.0, 30.0, 40.0, 50.0]; double[] output = new double[5]; - Lineartrans.Calculate(source, output, slope: 2.0, intercept: -5.0); + Lineartrans.Batch(source, output, slope: 2.0, intercept: -5.0); Assert.Equal(15.0, output[0], 1e-10); // 2*10-5 Assert.Equal(35.0, output[1], 1e-10); // 2*20-5 @@ -226,10 +226,10 @@ public class LineartransTests double[] source = [1.0, 2.0, 3.0]; double[] output = new double[3]; - Assert.Throws(() => Lineartrans.Calculate([], output)); - Assert.Throws(() => Lineartrans.Calculate(source, new double[2])); - Assert.Throws(() => Lineartrans.Calculate(source, output, slope: double.NaN)); - Assert.Throws(() => Lineartrans.Calculate(source, output, intercept: double.PositiveInfinity)); + Assert.Throws(() => Lineartrans.Batch([], output)); + Assert.Throws(() => Lineartrans.Batch(source, new double[2])); + Assert.Throws(() => Lineartrans.Batch(source, output, slope: double.NaN)); + Assert.Throws(() => Lineartrans.Batch(source, output, intercept: double.PositiveInfinity)); } [Fact] @@ -255,7 +255,7 @@ public class LineartransTests double intercept = -20.0; // Batch - var batchResult = Lineartrans.Calculate(series, slope, intercept); + var batchResult = Lineartrans.Batch(series, slope, intercept); // Stream var streamIndicator = new Lineartrans(slope, intercept); @@ -267,7 +267,7 @@ public class LineartransTests // Span var spanOutput = new double[series.Count]; - Lineartrans.Calculate(series.Values, spanOutput, slope, intercept); + Lineartrans.Batch(series.Values, spanOutput, slope, intercept); // Compare last 50 values for (int i = 50; i < series.Count; i++) diff --git a/lib/numerics/lineartrans/Lineartrans.Validation.Tests.cs b/lib/numerics/lineartrans/Lineartrans.Validation.Tests.cs index f7f4df11..d36597d1 100644 --- a/lib/numerics/lineartrans/Lineartrans.Validation.Tests.cs +++ b/lib/numerics/lineartrans/Lineartrans.Validation.Tests.cs @@ -19,7 +19,7 @@ public class LineartransValidationTests double slope = 2.5; double intercept = -15.0; - var result = Lineartrans.Calculate(series, slope, intercept); + var result = Lineartrans.Batch(series, slope, intercept); for (int i = 0; i < series.Count; i++) { @@ -55,7 +55,7 @@ public class LineartransValidationTests double slope = -1.5; double intercept = 50.0; - Lineartrans.Calculate(source, output, slope, intercept); + Lineartrans.Batch(source, output, slope, intercept); for (int i = 0; i < source.Length; i++) { @@ -71,7 +71,7 @@ public class LineartransValidationTests var bars = _gbm.Fetch(50, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1)); var series = bars.Close; - var result = Lineartrans.Calculate(series, slope: 1.0, intercept: 0.0); + var result = Lineartrans.Batch(series, slope: 1.0, intercept: 0.0); for (int i = 0; i < series.Count; i++) { @@ -87,7 +87,7 @@ public class LineartransValidationTests var series = bars.Close; double intercept = 42.0; - var result = Lineartrans.Calculate(series, slope: 0.0, intercept: intercept); + var result = Lineartrans.Batch(series, slope: 0.0, intercept: intercept); for (int i = 0; i < series.Count; i++) { @@ -106,13 +106,13 @@ public class LineartransValidationTests double c = 3.0, d = -10.0; // Second transform // Compose sequentially - var step1 = Lineartrans.Calculate(series, a, b); - var composed = Lineartrans.Calculate(step1, c, d); + var step1 = Lineartrans.Batch(series, a, b); + var composed = Lineartrans.Batch(step1, c, d); // Direct composed transform: y = c*(a*x + b) + d = (a*c)*x + (b*c + d) double composedSlope = a * c; double composedIntercept = b * c + d; - var direct = Lineartrans.Calculate(series, composedSlope, composedIntercept); + var direct = Lineartrans.Batch(series, composedSlope, composedIntercept); for (int i = 0; i < series.Count; i++) { @@ -129,8 +129,8 @@ public class LineartransValidationTests double a = 2.5, b = -15.0; - var transformed = Lineartrans.Calculate(series, a, b); - var recovered = Lineartrans.Calculate(transformed, 1.0 / a, -b / a); + var transformed = Lineartrans.Batch(series, a, b); + var recovered = Lineartrans.Batch(transformed, 1.0 / a, -b / a); for (int i = 0; i < series.Count; i++) { @@ -158,8 +158,8 @@ public class LineartransValidationTests } // a * (x + offset) should equal a*x + a*offset - var scaledSum = Lineartrans.Calculate(shifted, a, 0.0); - var sumOfScaled = Lineartrans.Calculate(series, a, a * offset); + var scaledSum = Lineartrans.Batch(shifted, a, 0.0); + var sumOfScaled = Lineartrans.Batch(series, a, a * offset); for (int i = 0; i < series.Count; i++) { @@ -174,7 +174,7 @@ public class LineartransValidationTests var bars = _gbm.Fetch(50, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1)); var series = bars.Close; - var negated = Lineartrans.Calculate(series, slope: -1.0, intercept: 0.0); + var negated = Lineartrans.Batch(series, slope: -1.0, intercept: 0.0); for (int i = 0; i < series.Count; i++) { @@ -188,8 +188,8 @@ public class LineartransValidationTests var bars = _gbm.Fetch(50, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1)); var series = bars.Close; - var negated = Lineartrans.Calculate(series, slope: -1.0, intercept: 0.0); - var recovered = Lineartrans.Calculate(negated, slope: -1.0, intercept: 0.0); + var negated = Lineartrans.Batch(series, slope: -1.0, intercept: 0.0); + var recovered = Lineartrans.Batch(negated, slope: -1.0, intercept: 0.0); for (int i = 0; i < series.Count; i++) { @@ -208,7 +208,7 @@ public class LineartransValidationTests series.Add(new TValue(time.AddSeconds(3), 100.0), true); // y = 2x + 3 - var result = Lineartrans.Calculate(series, slope: 2.0, intercept: 3.0); + var result = Lineartrans.Batch(series, slope: 2.0, intercept: 3.0); Assert.Equal(3.0, result[0].Value, Tolerance); // 2*0+3 Assert.Equal(5.0, result[1].Value, Tolerance); // 2*1+3 @@ -229,7 +229,7 @@ public class LineartransValidationTests double slope = 2.5; double intercept = 100.0; - var result = Lineartrans.Calculate(series, slope, intercept); + var result = Lineartrans.Batch(series, slope, intercept); // Difference between consecutive values should be scaled by slope double diff_01_input = series[1].Value - series[0].Value; // 20 @@ -257,7 +257,7 @@ public class LineartransValidationTests double slope = 1.0 + 1e-10; double intercept = -1e15; - var result = Lineartrans.Calculate(series, slope, intercept); + var result = Lineartrans.Batch(series, slope, intercept); // Verify each result matches direct computation for (int i = 0; i < series.Count; i++) diff --git a/lib/numerics/lineartrans/Lineartrans.cs b/lib/numerics/lineartrans/Lineartrans.cs index e03321bd..e24af02f 100644 --- a/lib/numerics/lineartrans/Lineartrans.cs +++ b/lib/numerics/lineartrans/Lineartrans.cs @@ -124,7 +124,7 @@ public sealed class Lineartrans : AbstractBase } } - public static TSeries Calculate(TSeries source, double slope = 1.0, double intercept = 0.0) + public static TSeries Batch(TSeries source, double slope = 1.0, double intercept = 0.0) { var indicator = new Lineartrans(slope, intercept); return indicator.Update(source); @@ -134,7 +134,7 @@ public sealed class Lineartrans : AbstractBase /// Calculates linear transformation over a span of values using SIMD when available. /// Uses FMA intrinsics for y = slope * x + intercept. /// - public static void Calculate(ReadOnlySpan source, Span output, + public static void Batch(ReadOnlySpan source, Span output, double slope = 1.0, double intercept = 0.0) { if (source.Length == 0) @@ -250,10 +250,17 @@ public sealed class Lineartrans : AbstractBase } } + public static (TSeries Results, Lineartrans Indicator) Calculate(TSeries source, double slope = 1.0, double intercept = 0.0) + { + var indicator = new Lineartrans(slope, intercept); + TSeries results = indicator.Update(source); + return (results, indicator); + } + public override void Reset() { _state = default; _p_state = default; Last = default; } -} +} \ No newline at end of file diff --git a/lib/numerics/logtrans/Logtrans.Tests.cs b/lib/numerics/logtrans/Logtrans.Tests.cs index e758b10a..e655acf5 100644 --- a/lib/numerics/logtrans/Logtrans.Tests.cs +++ b/lib/numerics/logtrans/Logtrans.Tests.cs @@ -197,7 +197,7 @@ public class LogtransTests } // Batch - var batch = Logtrans.Calculate(source); + var batch = Logtrans.Batch(source); // Compare all values for (int i = 0; i < source.Count; i++) @@ -215,12 +215,12 @@ public class LogtransTests var source = bars.Close; // TSeries batch - var batchResult = Logtrans.Calculate(source); + var batchResult = Logtrans.Batch(source); // Span calculation var values = source.Values.ToArray(); var output = new double[count]; - Logtrans.Calculate(values, output); + Logtrans.Batch(values, output); for (int i = 0; i < source.Count; i++) { @@ -234,14 +234,14 @@ public class LogtransTests Assert.Throws(() => { Span output = stackalloc double[10]; - Logtrans.Calculate(ReadOnlySpan.Empty, output); + Logtrans.Batch(ReadOnlySpan.Empty, output); }); Assert.Throws(() => { ReadOnlySpan source = stackalloc double[10]; Span output = stackalloc double[5]; - Logtrans.Calculate(source, output); + Logtrans.Batch(source, output); }); } diff --git a/lib/numerics/logtrans/Logtrans.Validation.Tests.cs b/lib/numerics/logtrans/Logtrans.Validation.Tests.cs index 9a51881b..2ad6ff31 100644 --- a/lib/numerics/logtrans/Logtrans.Validation.Tests.cs +++ b/lib/numerics/logtrans/Logtrans.Validation.Tests.cs @@ -18,7 +18,7 @@ public class LogtransValidationTests var bars = gbm.Fetch(count, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1)); var source = bars.Close; - var result = Logtrans.Calculate(source); + var result = Logtrans.Batch(source); for (int i = 0; i < source.Count; i++) { @@ -55,7 +55,7 @@ public class LogtransValidationTests var values = source.Values.ToArray(); var output = new double[count]; - Logtrans.Calculate(values, output); + Logtrans.Batch(values, output); for (int i = 0; i < count; i++) { @@ -199,7 +199,7 @@ public class LogtransValidationTests var values = new double[] { 2.0, 0.0, 3.0 }; var output = new double[3]; - Logtrans.Calculate(values, output); + Logtrans.Batch(values, output); Assert.Equal(Math.Log(2.0), output[0], Tolerance); // ln(2) Assert.Equal(Math.Log(2.0), output[1], Tolerance); // zero -> uses last valid (ln(2)) @@ -213,7 +213,7 @@ public class LogtransValidationTests var values = new double[] { 2.0, -5.0, 3.0 }; var output = new double[3]; - Logtrans.Calculate(values, output); + Logtrans.Batch(values, output); Assert.Equal(Math.Log(2.0), output[0], Tolerance); // ln(2) Assert.Equal(Math.Log(2.0), output[1], Tolerance); // negative -> uses last valid (ln(2)) diff --git a/lib/numerics/logtrans/Logtrans.cs b/lib/numerics/logtrans/Logtrans.cs index c33c0c01..f95a1f3c 100644 --- a/lib/numerics/logtrans/Logtrans.cs +++ b/lib/numerics/logtrans/Logtrans.cs @@ -99,7 +99,7 @@ public sealed class Logtrans : AbstractBase } } - public static TSeries Calculate(TSeries source) + public static TSeries Batch(TSeries source) { var indicator = new Logtrans(); return indicator.Update(source); @@ -109,7 +109,7 @@ public sealed class Logtrans : AbstractBase /// Calculates natural logarithm over a span of values. /// Note: Math.Log has no SIMD intrinsic; uses scalar path with last-valid substitution. /// - public static void Calculate(ReadOnlySpan source, Span output) + public static void Batch(ReadOnlySpan source, Span output) { if (source.Length == 0) { @@ -138,10 +138,17 @@ public sealed class Logtrans : AbstractBase } } + public static (TSeries Results, Logtrans Indicator) Calculate(TSeries source) + { + var indicator = new Logtrans(); + TSeries results = indicator.Update(source); + return (results, indicator); + } + public override void Reset() { _state = default; _p_state = default; Last = default; } -} +} \ No newline at end of file diff --git a/lib/numerics/lowest/Lowest.Tests.cs b/lib/numerics/lowest/Lowest.Tests.cs index d14342a0..5abb4fa0 100644 --- a/lib/numerics/lowest/Lowest.Tests.cs +++ b/lib/numerics/lowest/Lowest.Tests.cs @@ -207,7 +207,7 @@ public class LowestTests } // Batch - var batch = Lowest.Calculate(source, period); + var batch = Lowest.Batch(source, period); // Compare last values (after warmup) for (int i = period; i < source.Count; i++) @@ -226,12 +226,12 @@ public class LowestTests var source = bars.Close; // TSeries batch - var batchResult = Lowest.Calculate(source, period); + var batchResult = Lowest.Batch(source, period); // Span calculation var values = source.Values.ToArray(); var output = new double[count]; - Lowest.Calculate(values, output, period); + Lowest.Batch(values, output, period); for (int i = 0; i < source.Count; i++) { @@ -245,21 +245,21 @@ public class LowestTests Assert.Throws(() => { Span output = stackalloc double[10]; - Lowest.Calculate(ReadOnlySpan.Empty, output, 5); + Lowest.Batch(ReadOnlySpan.Empty, output, 5); }); Assert.Throws(() => { ReadOnlySpan source = stackalloc double[10]; Span output = stackalloc double[5]; - Lowest.Calculate(source, output, 5); + Lowest.Batch(source, output, 5); }); Assert.Throws(() => { ReadOnlySpan source = stackalloc double[10]; Span output = stackalloc double[10]; - Lowest.Calculate(source, output, 0); + Lowest.Batch(source, output, 0); }); } diff --git a/lib/numerics/lowest/Lowest.Validation.Tests.cs b/lib/numerics/lowest/Lowest.Validation.Tests.cs index a81b6f74..8ad66bce 100644 --- a/lib/numerics/lowest/Lowest.Validation.Tests.cs +++ b/lib/numerics/lowest/Lowest.Validation.Tests.cs @@ -103,7 +103,7 @@ public sealed class LowestValidationTests : IDisposable { // Calculate QuanTAlib Lowest (Span API) double[] qOutput = new double[sourceData.Length]; - Lowest.Calculate(sourceData.AsSpan(), qOutput.AsSpan(), period); + Lowest.Batch(sourceData.AsSpan(), qOutput.AsSpan(), period); // Calculate TA-Lib MIN var retCode = TALib.Functions.Min(sourceData, 0..^0, talibOutput, out var outRange, period); diff --git a/lib/numerics/lowest/Lowest.cs b/lib/numerics/lowest/Lowest.cs index a20714a6..7ae13742 100644 --- a/lib/numerics/lowest/Lowest.cs +++ b/lib/numerics/lowest/Lowest.cs @@ -106,7 +106,7 @@ public sealed class Lowest : AbstractBase } } - public static TSeries Calculate(TSeries source, int period) + public static TSeries Batch(TSeries source, int period) { var indicator = new Lowest(period); return indicator.Update(source); @@ -115,7 +115,7 @@ public sealed class Lowest : AbstractBase /// /// Calculates rolling minimum over a span of values. /// - public static void Calculate(ReadOnlySpan source, Span output, int period) + public static void Batch(ReadOnlySpan source, Span output, int period) { if (source.Length == 0) { @@ -217,6 +217,13 @@ public sealed class Lowest : AbstractBase } } + public static (TSeries Results, Lowest Indicator) Calculate(TSeries source, int period) + { + var indicator = new Lowest(period); + TSeries results = indicator.Update(source); + return (results, indicator); + } + public override void Reset() { _buffer.Clear(); @@ -224,4 +231,4 @@ public sealed class Lowest : AbstractBase _p_state = default; Last = default; } -} +} \ No newline at end of file diff --git a/lib/numerics/midpoint/Midpoint.Tests.cs b/lib/numerics/midpoint/Midpoint.Tests.cs index fd5f1521..4ac770e7 100644 --- a/lib/numerics/midpoint/Midpoint.Tests.cs +++ b/lib/numerics/midpoint/Midpoint.Tests.cs @@ -215,7 +215,7 @@ public class MidpointTests } // Batch - var batch = Midpoint.Calculate(source, period); + var batch = Midpoint.Batch(source, period); // Compare last values (after warmup) for (int i = period; i < source.Count; i++) @@ -234,12 +234,12 @@ public class MidpointTests var source = bars.Close; // TSeries batch - var batchResult = Midpoint.Calculate(source, period); + var batchResult = Midpoint.Batch(source, period); // Span calculation var sourceArray = source.Values.ToArray(); var output = new double[count]; - Midpoint.Calculate(sourceArray.AsSpan(), output.AsSpan(), period); + Midpoint.Batch(sourceArray.AsSpan(), output.AsSpan(), period); for (int i = 0; i < source.Count; i++) { @@ -253,21 +253,21 @@ public class MidpointTests Assert.Throws(() => { Span output = stackalloc double[10]; - Midpoint.Calculate(ReadOnlySpan.Empty, output, 5); + Midpoint.Batch(ReadOnlySpan.Empty, output, 5); }); Assert.Throws(() => { ReadOnlySpan source = stackalloc double[10]; Span output = stackalloc double[5]; - Midpoint.Calculate(source, output, 5); + Midpoint.Batch(source, output, 5); }); Assert.Throws(() => { ReadOnlySpan source = stackalloc double[10]; Span output = stackalloc double[10]; - Midpoint.Calculate(source, output, 0); + Midpoint.Batch(source, output, 0); }); } diff --git a/lib/numerics/midpoint/Midpoint.Validation.Tests.cs b/lib/numerics/midpoint/Midpoint.Validation.Tests.cs index 67ab6e06..0e9c6f2e 100644 --- a/lib/numerics/midpoint/Midpoint.Validation.Tests.cs +++ b/lib/numerics/midpoint/Midpoint.Validation.Tests.cs @@ -103,7 +103,7 @@ public sealed class MidpointValidationTests : IDisposable { // Calculate QuanTAlib Midpoint (Span API) double[] qOutput = new double[sourceData.Length]; - Midpoint.Calculate(sourceData.AsSpan(), qOutput.AsSpan(), period); + Midpoint.Batch(sourceData.AsSpan(), qOutput.AsSpan(), period); // Calculate TA-Lib MIDPOINT var retCode = TALib.Functions.MidPoint(sourceData, 0..^0, talibOutput, out var outRange, period); diff --git a/lib/numerics/midpoint/Midpoint.cs b/lib/numerics/midpoint/Midpoint.cs index 39692a05..f0b70149 100644 --- a/lib/numerics/midpoint/Midpoint.cs +++ b/lib/numerics/midpoint/Midpoint.cs @@ -23,6 +23,7 @@ public sealed class Midpoint : AbstractBase private readonly Lowest _lowest; private readonly ITValuePublisher? _source; private readonly TValuePublishedHandler? _handler; + private bool _disposed; public override bool IsHot => _highest.IsHot && _lowest.IsHot; @@ -57,9 +58,13 @@ public sealed class Midpoint : AbstractBase protected override void Dispose(bool disposing) { - if (disposing && _source != null && _handler != null) + if (!_disposed) { - _source.Pub -= _handler; + if (disposing && _source != null && _handler != null) + { + _source.Pub -= _handler; + } + _disposed = true; } base.Dispose(disposing); } @@ -106,7 +111,7 @@ public sealed class Midpoint : AbstractBase } } - public static TSeries Calculate(TSeries source, int period) + public static TSeries Batch(TSeries source, int period) { var indicator = new Midpoint(period); return indicator.Update(source); @@ -115,7 +120,7 @@ public sealed class Midpoint : AbstractBase /// /// Calculates rolling midpoint over a span of values. /// - public static void Calculate(ReadOnlySpan source, Span output, int period) + public static void Batch(ReadOnlySpan source, Span output, int period) { if (source.Length == 0) { @@ -150,8 +155,8 @@ public sealed class Midpoint : AbstractBase try { - Highest.Calculate(source, highBuffer, period); - Lowest.Calculate(source, lowBuffer, period); + Highest.Batch(source, highBuffer, period); + Lowest.Batch(source, lowBuffer, period); for (int i = 0; i < len; i++) { @@ -172,10 +177,17 @@ public sealed class Midpoint : AbstractBase } } + public static (TSeries Results, Midpoint Indicator) Calculate(TSeries source, int period) + { + var indicator = new Midpoint(period); + TSeries results = indicator.Update(source); + return (results, indicator); + } + public override void Reset() { _highest.Reset(); _lowest.Reset(); Last = default; } -} +} \ No newline at end of file diff --git a/lib/numerics/normalize/Normalize.Tests.cs b/lib/numerics/normalize/Normalize.Tests.cs index b96cf8da..6a2a5ea7 100644 --- a/lib/numerics/normalize/Normalize.Tests.cs +++ b/lib/numerics/normalize/Normalize.Tests.cs @@ -205,7 +205,7 @@ public class NormalizeTests } // Static calculation - var staticResult = Normalize.Calculate(tseries, 14); + var staticResult = Normalize.Batch(tseries, 14); // Streaming calculation var streamNorm = new Normalize(14); @@ -230,7 +230,7 @@ public class NormalizeTests double[] output = new double[values.Length]; // Span calculation - Normalize.Calculate(values, output, 14); + Normalize.Batch(values, output, 14); // Streaming calculation var norm = new Normalize(14); @@ -247,9 +247,9 @@ public class NormalizeTests double[] source = { 1, 2, 3, 4, 5 }; double[] output = new double[5]; - Assert.Throws(() => Normalize.Calculate(Array.Empty(), output)); - Assert.Throws(() => Normalize.Calculate(source, new double[3])); - Assert.Throws(() => Normalize.Calculate(source, output, 0)); + Assert.Throws(() => Normalize.Batch(Array.Empty(), output)); + Assert.Throws(() => Normalize.Batch(source, new double[3])); + Assert.Throws(() => Normalize.Batch(source, output, 0)); } [Fact] diff --git a/lib/numerics/normalize/Normalize.Validation.Tests.cs b/lib/numerics/normalize/Normalize.Validation.Tests.cs index 278f1d92..89304f1e 100644 --- a/lib/numerics/normalize/Normalize.Validation.Tests.cs +++ b/lib/numerics/normalize/Normalize.Validation.Tests.cs @@ -203,7 +203,7 @@ public class NormalizeValidationTests // Batch double[] batchResults = new double[values.Length]; - Normalize.Calculate(values, batchResults, 14); + Normalize.Batch(values, batchResults, 14); // Compare all values for (int i = 0; i < values.Length; i++) @@ -233,12 +233,12 @@ public class NormalizeValidationTests tseries.Add(new TValue(bar.Time, bar.Close), true); } - var results2 = Normalize.Calculate(tseries, period); + var results2 = Normalize.Batch(tseries, period); // Mode 3: Static span Calculate double[] values = series.Select(b => b.Close).ToArray(); double[] results3 = new double[values.Length]; - Normalize.Calculate(values, results3, period); + Normalize.Batch(values, results3, period); // Mode 4: Event-based chaining var source = new TSeries(); diff --git a/lib/numerics/normalize/Normalize.cs b/lib/numerics/normalize/Normalize.cs index 61c828b8..f80ce9c8 100644 --- a/lib/numerics/normalize/Normalize.cs +++ b/lib/numerics/normalize/Normalize.cs @@ -154,7 +154,7 @@ public sealed class Normalize : AbstractBase } } - public static TSeries Calculate(TSeries source, int period = 14) + public static TSeries Batch(TSeries source, int period = 14) { var indicator = new Normalize(period); return indicator.Update(source); @@ -163,7 +163,7 @@ public sealed class Normalize : AbstractBase /// /// Calculates Min-Max Normalization over a span of values. /// - public static void Calculate(ReadOnlySpan source, Span output, int period = 14) + public static void Batch(ReadOnlySpan source, Span output, int period = 14) { if (source.Length == 0) { @@ -231,6 +231,13 @@ public sealed class Normalize : AbstractBase } } + public static (TSeries Results, Normalize Indicator) Calculate(TSeries source, int period = 14) + { + var indicator = new Normalize(period); + TSeries results = indicator.Update(source); + return (results, indicator); + } + public override void Reset() { _buffer.Clear(); @@ -238,4 +245,4 @@ public sealed class Normalize : AbstractBase _p_state = _state; Last = default; } -} +} \ No newline at end of file diff --git a/lib/numerics/relu/Relu.Tests.cs b/lib/numerics/relu/Relu.Tests.cs index b8a1261b..d935b1f3 100644 --- a/lib/numerics/relu/Relu.Tests.cs +++ b/lib/numerics/relu/Relu.Tests.cs @@ -179,7 +179,7 @@ public class ReluTests var gbm = new GBM(startPrice: 100, mu: 0.05, sigma: 0.2, seed: 42000); var bars = gbm.Fetch(count, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1)); // Use returns (which can be negative) for meaningful ReLU test - var source = Change.Calculate(bars.Close); + var source = Change.Batch(bars.Close); // Streaming var streaming = new Relu(); @@ -191,7 +191,7 @@ public class ReluTests } // Batch - var batch = Relu.Calculate(source); + var batch = Relu.Batch(source); // Compare all values for (int i = 0; i < source.Count; i++) @@ -206,15 +206,15 @@ public class ReluTests int count = 50; var gbm = new GBM(startPrice: 100, mu: 0.05, sigma: 0.2, seed: 42001); var bars = gbm.Fetch(count, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1)); - var source = Change.Calculate(bars.Close); + var source = Change.Batch(bars.Close); // TSeries batch - var batchResult = Relu.Calculate(source); + var batchResult = Relu.Batch(source); // Span calculation var values = source.Values.ToArray(); var output = new double[count]; - Relu.Calculate(values, output); + Relu.Batch(values, output); for (int i = 0; i < source.Count; i++) { @@ -228,14 +228,14 @@ public class ReluTests Assert.Throws(() => { Span output = stackalloc double[10]; - Relu.Calculate(ReadOnlySpan.Empty, output); + Relu.Batch(ReadOnlySpan.Empty, output); }); Assert.Throws(() => { ReadOnlySpan source = stackalloc double[10]; Span output = stackalloc double[5]; - Relu.Calculate(source, output); + Relu.Batch(source, output); }); } diff --git a/lib/numerics/relu/Relu.Validation.Tests.cs b/lib/numerics/relu/Relu.Validation.Tests.cs index d1ea0948..d830f190 100644 --- a/lib/numerics/relu/Relu.Validation.Tests.cs +++ b/lib/numerics/relu/Relu.Validation.Tests.cs @@ -39,7 +39,7 @@ public class ReluValidationTests time = time.AddMinutes(1); } - var result = Relu.Calculate(source); + var result = Relu.Batch(source); for (int i = 0; i < testValues.Length; i++) { @@ -54,7 +54,7 @@ public class ReluValidationTests double[] testValues = { -10.0, -5.0, -1.0, -0.5, 0.0, 0.5, 1.0, 5.0, 10.0 }; double[] output = new double[testValues.Length]; - Relu.Calculate(testValues, output); + Relu.Batch(testValues, output); for (int i = 0; i < testValues.Length; i++) { @@ -70,9 +70,9 @@ public class ReluValidationTests int count = 100; var gbm = new GBM(startPrice: 100, mu: 0.0, sigma: 0.5, seed: 43000); var bars = gbm.Fetch(count, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1)); - var source = Change.Calculate(bars.Close); + var source = Change.Batch(bars.Close); - var result = Relu.Calculate(source); + var result = Relu.Batch(source); for (int i = 0; i < result.Count; i++) { @@ -87,7 +87,7 @@ public class ReluValidationTests double[] positiveValues = { 0.001, 0.1, 1.0, 10.0, 100.0, 1000.0 }; double[] output = new double[positiveValues.Length]; - Relu.Calculate(positiveValues, output); + Relu.Batch(positiveValues, output); for (int i = 0; i < positiveValues.Length; i++) { @@ -102,7 +102,7 @@ public class ReluValidationTests double[] negativeValues = { -0.001, -0.1, -1.0, -10.0, -100.0, -1000.0 }; double[] output = new double[negativeValues.Length]; - Relu.Calculate(negativeValues, output); + Relu.Batch(negativeValues, output); for (int i = 0; i < negativeValues.Length; i++) { @@ -125,7 +125,7 @@ public class ReluValidationTests int count = 100; var gbm = new GBM(startPrice: 100, mu: 0.05, sigma: 0.3, seed: 43001); var bars = gbm.Fetch(count, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1)); - var source = Change.Calculate(bars.Close); + var source = Change.Batch(bars.Close); // Streaming var streaming = new Relu(); @@ -137,11 +137,11 @@ public class ReluValidationTests } // Batch - var batch = Relu.Calculate(source); + var batch = Relu.Batch(source); // Span var spanOutput = new double[source.Count]; - Relu.Calculate(source.Values.ToArray(), spanOutput); + Relu.Batch(source.Values.ToArray(), spanOutput); // All three should match for (int i = 0; i < source.Count; i++) diff --git a/lib/numerics/relu/Relu.cs b/lib/numerics/relu/Relu.cs index 1b5d2091..9dd74558 100644 --- a/lib/numerics/relu/Relu.cs +++ b/lib/numerics/relu/Relu.cs @@ -25,6 +25,7 @@ public sealed class Relu : AbstractBase private State _state, _p_state; private readonly ITValuePublisher? _source; private readonly TValuePublishedHandler? _handler; + private bool _disposed; public override bool IsHot => true; // No warmup needed @@ -35,7 +36,7 @@ public sealed class Relu : AbstractBase } /// - /// + /// Initializes a new ReLU indicator chained to a source publisher. /// /// Source indicator for chaining public Relu(ITValuePublisher source) : this() @@ -47,9 +48,13 @@ public sealed class Relu : AbstractBase protected override void Dispose(bool disposing) { - if (disposing && _source != null && _handler != null) + if (!_disposed) { - _source.Pub -= _handler; + if (disposing && _source != null && _handler != null) + { + _source.Pub -= _handler; + } + _disposed = true; } base.Dispose(disposing); } @@ -104,7 +109,7 @@ public sealed class Relu : AbstractBase var vSpan = System.Runtime.InteropServices.CollectionsMarshal.AsSpan(v); // Use vectorized Calculate for batch processing - Calculate(source.Values, vSpan); + Batch(source.Values, vSpan); source.Times.CopyTo(tSpan); // Restore state from last value @@ -130,7 +135,7 @@ public sealed class Relu : AbstractBase } } - public static TSeries Calculate(TSeries source) + public static TSeries Batch(TSeries source) { var indicator = new Relu(); return indicator.Update(source); @@ -139,7 +144,7 @@ public sealed class Relu : AbstractBase /// /// Calculates ReLU over a span of values with SIMD optimization. /// - public static void Calculate(ReadOnlySpan source, Span output) + public static void Batch(ReadOnlySpan source, Span output) { if (source.Length == 0) { @@ -213,10 +218,17 @@ public sealed class Relu : AbstractBase } } + public static (TSeries Results, Relu Indicator) Calculate(TSeries source) + { + var indicator = new Relu(); + TSeries results = indicator.Update(source); + return (results, indicator); + } + public override void Reset() { _state = default; _p_state = default; Last = default; } -} +} \ No newline at end of file diff --git a/lib/numerics/sigmoid/Sigmoid.Tests.cs b/lib/numerics/sigmoid/Sigmoid.Tests.cs index fee4bf34..c23047fa 100644 --- a/lib/numerics/sigmoid/Sigmoid.Tests.cs +++ b/lib/numerics/sigmoid/Sigmoid.Tests.cs @@ -238,7 +238,7 @@ public class SigmoidTests series.Add(new TValue(DateTime.UtcNow.AddSeconds(i), i - 50), isNew: true); } - var result = Sigmoid.Calculate(series); + var result = Sigmoid.Batch(series); Assert.Equal(series.Count, result.Count); } @@ -251,7 +251,7 @@ public class SigmoidTests public void Calculate_Span_EmptySource_ThrowsArgumentException() { double[] output = new double[10]; - Assert.Throws(() => Sigmoid.Calculate(ReadOnlySpan.Empty, output.AsSpan())); + Assert.Throws(() => Sigmoid.Batch(ReadOnlySpan.Empty, output.AsSpan())); } [Fact] @@ -260,7 +260,7 @@ public class SigmoidTests double[] source = [1, 2, 3, 4, 5]; double[] output = new double[3]; - Assert.Throws(() => Sigmoid.Calculate(source.AsSpan(), output.AsSpan())); + Assert.Throws(() => Sigmoid.Batch(source.AsSpan(), output.AsSpan())); } [Fact] @@ -269,7 +269,7 @@ public class SigmoidTests double[] source = [1, 2, 3, 4, 5]; double[] output = new double[5]; - Assert.Throws(() => Sigmoid.Calculate(source.AsSpan(), output.AsSpan(), k: 0)); + Assert.Throws(() => Sigmoid.Batch(source.AsSpan(), output.AsSpan(), k: 0)); } [Fact] @@ -283,7 +283,7 @@ public class SigmoidTests } double[] spanOutput = new double[source.Length]; - Sigmoid.Calculate(source.AsSpan(), spanOutput.AsSpan()); + Sigmoid.Batch(source.AsSpan(), spanOutput.AsSpan()); var sigmoid = new Sigmoid(); double[] streamOutput = new double[source.Length]; @@ -304,7 +304,7 @@ public class SigmoidTests double[] source = [1.0, double.NaN, 2.0]; double[] output = new double[3]; - Sigmoid.Calculate(source.AsSpan(), output.AsSpan()); + Sigmoid.Batch(source.AsSpan(), output.AsSpan()); Assert.True(double.IsFinite(output[0])); Assert.True(double.IsFinite(output[1])); // NaN replaced with last valid diff --git a/lib/numerics/sigmoid/Sigmoid.Validation.Tests.cs b/lib/numerics/sigmoid/Sigmoid.Validation.Tests.cs index 19e63d5f..7dfd0aa3 100644 --- a/lib/numerics/sigmoid/Sigmoid.Validation.Tests.cs +++ b/lib/numerics/sigmoid/Sigmoid.Validation.Tests.cs @@ -228,7 +228,7 @@ public class SigmoidValidationTests // Span calculation double[] spanOutput = new double[source.Length]; - Sigmoid.Calculate(source.AsSpan(), spanOutput.AsSpan(), k, x0); + Sigmoid.Batch(source.AsSpan(), spanOutput.AsSpan(), k, x0); // Streaming calculation var sigmoid = new Sigmoid(k, x0); diff --git a/lib/numerics/sigmoid/Sigmoid.cs b/lib/numerics/sigmoid/Sigmoid.cs index 5d7d4b4e..6cc04982 100644 --- a/lib/numerics/sigmoid/Sigmoid.cs +++ b/lib/numerics/sigmoid/Sigmoid.cs @@ -136,7 +136,7 @@ public sealed class Sigmoid : AbstractBase } } - public static TSeries Calculate(TSeries source, double k = 1.0, double x0 = 0.0) + public static TSeries Batch(TSeries source, double k = 1.0, double x0 = 0.0) { var indicator = new Sigmoid(k, x0); return indicator.Update(source); @@ -145,7 +145,7 @@ public sealed class Sigmoid : AbstractBase /// /// Calculates Sigmoid over a span of values. /// - public static void Calculate(ReadOnlySpan source, Span output, double k = 1.0, double x0 = 0.0) + public static void Batch(ReadOnlySpan source, Span output, double k = 1.0, double x0 = 0.0) { if (source.Length == 0) { @@ -190,10 +190,17 @@ public sealed class Sigmoid : AbstractBase } } + public static (TSeries Results, Sigmoid Indicator) Calculate(TSeries source, double k = 1.0, double x0 = 0.0) + { + var indicator = new Sigmoid(k, x0); + TSeries results = indicator.Update(source); + return (results, indicator); + } + public override void Reset() { _state = default; _p_state = default; Last = default; } -} +} \ No newline at end of file diff --git a/lib/numerics/slope/Slope.Tests.cs b/lib/numerics/slope/Slope.Tests.cs index 124b2033..1a28f779 100644 --- a/lib/numerics/slope/Slope.Tests.cs +++ b/lib/numerics/slope/Slope.Tests.cs @@ -94,7 +94,7 @@ public class SlopeTests // Output must be same length as source Assert.Throws(() => - Slope.Calculate(source.AsSpan(), wrongSizeOutput.AsSpan())); + Slope.Batch(source.AsSpan(), wrongSizeOutput.AsSpan())); } [Fact] @@ -107,7 +107,7 @@ public class SlopeTests // 1. Batch Mode (static span) var tValues = series.Values.ToArray(); var batchOutput = new double[tValues.Length]; - Slope.Calculate(tValues, batchOutput); + Slope.Batch(tValues, batchOutput); double expected = batchOutput[^1]; // 2. Streaming Mode @@ -119,7 +119,7 @@ public class SlopeTests double streamingResult = streamingInd.Last.Value; // 3. TSeries Batch Mode - var batchSeriesResult = Slope.Calculate(series); + var batchSeriesResult = Slope.Batch(series); double tseriesResult = batchSeriesResult.Last.Value; Assert.Equal(expected, streamingResult, precision: 9); @@ -194,7 +194,7 @@ public class SlopeTests // Batch var batchResults = new double[count]; - Slope.Calculate(data, batchResults); + Slope.Batch(data, batchResults); // Compare for (int i = 0; i < count; i++) diff --git a/lib/numerics/slope/Slope.Validation.Tests.cs b/lib/numerics/slope/Slope.Validation.Tests.cs index ddc97b97..9afdf86c 100644 --- a/lib/numerics/slope/Slope.Validation.Tests.cs +++ b/lib/numerics/slope/Slope.Validation.Tests.cs @@ -104,7 +104,7 @@ public class SlopeValidationTests double[] expected = [0, 2, 2, 2, 2, 2]; double[] output = new double[data.Length]; - Slope.Calculate(data, output); + Slope.Batch(data, output); for (int i = 0; i < data.Length; i++) { diff --git a/lib/numerics/slope/Slope.cs b/lib/numerics/slope/Slope.cs index 701b432e..0b61abde 100644 --- a/lib/numerics/slope/Slope.cs +++ b/lib/numerics/slope/Slope.cs @@ -114,7 +114,7 @@ public sealed class Slope : AbstractBase var tSpan = CollectionsMarshal.AsSpan(t); var vSpan = CollectionsMarshal.AsSpan(v); - Calculate(sourceValues, vSpan); + Batch(sourceValues, vSpan); sourceTimes.CopyTo(tSpan); // Prime state with last value @@ -145,7 +145,7 @@ public sealed class Slope : AbstractBase } } - public static TSeries Calculate(TSeries source) + public static TSeries Batch(TSeries source) { var slope = new Slope(); return slope.Update(source); @@ -155,7 +155,7 @@ public sealed class Slope : AbstractBase /// Calculates first derivative (slope) for a span. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void Calculate(ReadOnlySpan source, Span output) + public static void Batch(ReadOnlySpan source, Span output) { if (source.Length != output.Length) { @@ -290,4 +290,11 @@ public sealed class Slope : AbstractBase output[i] = curr - prev; } } -} + + public static (TSeries Results, Slope Indicator) Calculate(TSeries source) + { + var indicator = new Slope(); + TSeries results = indicator.Update(source); + return (results, indicator); + } +} \ No newline at end of file diff --git a/lib/numerics/sqrttrans/Sqrttrans.Tests.cs b/lib/numerics/sqrttrans/Sqrttrans.Tests.cs index eaa9d768..30dc5042 100644 --- a/lib/numerics/sqrttrans/Sqrttrans.Tests.cs +++ b/lib/numerics/sqrttrans/Sqrttrans.Tests.cs @@ -201,7 +201,7 @@ public class SqrttransTests } // Batch - var batch = Sqrttrans.Calculate(source); + var batch = Sqrttrans.Batch(source); // Compare all values for (int i = 0; i < source.Count; i++) @@ -219,12 +219,12 @@ public class SqrttransTests var source = bars.Close; // TSeries batch - var batchResult = Sqrttrans.Calculate(source); + var batchResult = Sqrttrans.Batch(source); // Span calculation var values = source.Values.ToArray(); var output = new double[count]; - Sqrttrans.Calculate(values, output); + Sqrttrans.Batch(values, output); for (int i = 0; i < source.Count; i++) { @@ -238,14 +238,14 @@ public class SqrttransTests Assert.Throws(() => { Span output = stackalloc double[10]; - Sqrttrans.Calculate(ReadOnlySpan.Empty, output); + Sqrttrans.Batch(ReadOnlySpan.Empty, output); }); Assert.Throws(() => { ReadOnlySpan source = stackalloc double[10]; Span output = stackalloc double[5]; - Sqrttrans.Calculate(source, output); + Sqrttrans.Batch(source, output); }); } diff --git a/lib/numerics/sqrttrans/Sqrttrans.Validation.Tests.cs b/lib/numerics/sqrttrans/Sqrttrans.Validation.Tests.cs index 34b29c63..e8c15aff 100644 --- a/lib/numerics/sqrttrans/Sqrttrans.Validation.Tests.cs +++ b/lib/numerics/sqrttrans/Sqrttrans.Validation.Tests.cs @@ -17,7 +17,7 @@ public class SqrttransValidationTests var bars = gbm.Fetch(count, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1)); var source = bars.Close; - var result = Sqrttrans.Calculate(source); + var result = Sqrttrans.Batch(source); for (int i = 0; i < source.Count; i++) { @@ -54,7 +54,7 @@ public class SqrttransValidationTests var values = source.Values.ToArray(); var output = new double[count]; - Sqrttrans.Calculate(values, output); + Sqrttrans.Batch(values, output); for (int i = 0; i < count; i++) { @@ -132,7 +132,7 @@ public class SqrttransValidationTests squared.Add(new TValue(tv.Time, tv.Value * tv.Value)); } - var sqrtResult = Sqrttrans.Calculate(squared); + var sqrtResult = Sqrttrans.Batch(squared); for (int i = 0; i < source.Count; i++) { diff --git a/lib/numerics/sqrttrans/Sqrttrans.cs b/lib/numerics/sqrttrans/Sqrttrans.cs index 0f721985..41d0e55d 100644 --- a/lib/numerics/sqrttrans/Sqrttrans.cs +++ b/lib/numerics/sqrttrans/Sqrttrans.cs @@ -99,7 +99,7 @@ public sealed class Sqrttrans : AbstractBase } } - public static TSeries Calculate(TSeries source) + public static TSeries Batch(TSeries source) { var indicator = new Sqrttrans(); return indicator.Update(source); @@ -108,7 +108,7 @@ public sealed class Sqrttrans : AbstractBase /// /// Calculates square root over a span of values. /// - public static void Calculate(ReadOnlySpan source, Span output) + public static void Batch(ReadOnlySpan source, Span output) { if (source.Length == 0) { @@ -137,10 +137,17 @@ public sealed class Sqrttrans : AbstractBase } } + public static (TSeries Results, Sqrttrans Indicator) Calculate(TSeries source) + { + var indicator = new Sqrttrans(); + TSeries results = indicator.Update(source); + return (results, indicator); + } + public override void Reset() { _state = default; _p_state = default; Last = default; } -} +} \ No newline at end of file diff --git a/lib/numerics/standardize/Standardize.Tests.cs b/lib/numerics/standardize/Standardize.Tests.cs index a23162bb..33a816a1 100644 --- a/lib/numerics/standardize/Standardize.Tests.cs +++ b/lib/numerics/standardize/Standardize.Tests.cs @@ -258,7 +258,7 @@ public class StandardizeTests } // Static calculation - var staticResult = Standardize.Calculate(tseries, 14); + var staticResult = Standardize.Batch(tseries, 14); // Streaming calculation var streamStandardize = new Standardize(14); @@ -283,7 +283,7 @@ public class StandardizeTests double[] output = new double[values.Length]; // Span calculation - Standardize.Calculate(values, output, 14); + Standardize.Batch(values, output, 14); // Streaming calculation var standardize = new Standardize(14); @@ -300,9 +300,9 @@ public class StandardizeTests double[] source = [1, 2, 3, 4, 5]; double[] output = new double[5]; - Assert.Throws(() => Standardize.Calculate([], output)); - Assert.Throws(() => Standardize.Calculate(source, new double[3])); - Assert.Throws(() => Standardize.Calculate(source, output, 1)); + Assert.Throws(() => Standardize.Batch([], output)); + Assert.Throws(() => Standardize.Batch(source, new double[3])); + Assert.Throws(() => Standardize.Batch(source, output, 1)); } [Fact] diff --git a/lib/numerics/standardize/Standardize.Validation.Tests.cs b/lib/numerics/standardize/Standardize.Validation.Tests.cs index c006ee55..f9aecd86 100644 --- a/lib/numerics/standardize/Standardize.Validation.Tests.cs +++ b/lib/numerics/standardize/Standardize.Validation.Tests.cs @@ -243,7 +243,7 @@ public class StandardizeValidationTests // Batch double[] batchResults = new double[values.Length]; - Standardize.Calculate(values, batchResults, 14); + Standardize.Batch(values, batchResults, 14); // Compare all values for (int i = 0; i < values.Length; i++) @@ -273,12 +273,12 @@ public class StandardizeValidationTests tseries.Add(new TValue(bar.Time, bar.Close), true); } - var results2 = Standardize.Calculate(tseries, period); + var results2 = Standardize.Batch(tseries, period); // Mode 3: Static span Calculate double[] values = series.Select(b => b.Close).ToArray(); double[] results3 = new double[values.Length]; - Standardize.Calculate(values, results3, period); + Standardize.Batch(values, results3, period); // Mode 4: Event-based chaining var source = new TSeries(); diff --git a/lib/numerics/standardize/Standardize.cs b/lib/numerics/standardize/Standardize.cs index 1194fc52..a00f1e25 100644 --- a/lib/numerics/standardize/Standardize.cs +++ b/lib/numerics/standardize/Standardize.cs @@ -175,7 +175,7 @@ public sealed class Standardize : AbstractBase } } - public static TSeries Calculate(TSeries source, int period = 20) + public static TSeries Batch(TSeries source, int period = 20) { var indicator = new Standardize(period); return indicator.Update(source); @@ -184,7 +184,7 @@ public sealed class Standardize : AbstractBase /// /// Calculates Z-score normalization over a span of values. /// - public static void Calculate(ReadOnlySpan source, Span output, int period = 20) + public static void Batch(ReadOnlySpan source, Span output, int period = 20) { if (source.Length == 0) { @@ -264,6 +264,13 @@ public sealed class Standardize : AbstractBase } } + public static (TSeries Results, Standardize Indicator) Calculate(TSeries source, int period = 20) + { + var indicator = new Standardize(period); + TSeries results = indicator.Update(source); + return (results, indicator); + } + public override void Reset() { _buffer.Clear(); @@ -271,4 +278,4 @@ public sealed class Standardize : AbstractBase _p_state = _state; Last = default; } -} +} \ No newline at end of file diff --git a/lib/oscillators/_index.md b/lib/oscillators/_index.md index 78b9b8c2..87753345 100644 --- a/lib/oscillators/_index.md +++ b/lib/oscillators/_index.md @@ -6,22 +6,22 @@ Oscillators fluctuate above and below a centerline or within bounded ranges. Use | Indicator | Full Name | Description | | :--- | :--- | :--- | -| [AC](ac/Ac.md) | Acceleration Oscillator | Second derivative of AO. Measures acceleration of market driving force. | +| AC | Acceleration Oscillator | Second derivative of AO. Measures acceleration of market driving force. | | [AO](ao/Ao.md) | Awesome Oscillator | 5-period SMA minus 34-period SMA of bar midpoint. Bill Williams creation. | | [APO](apo/Apo.md) | Absolute Price Oscillator | Raw currency difference between fast and slow EMAs. Unbounded. | -| [BBB](bbb/Bbb.md) | Bollinger %B | Position within Bollinger Bands. 0=lower band, 1=upper band. | -| [BBS](bbs/Bbs.md) | Bollinger Band Squeeze | BB width < KC width indicates consolidation. Breakout imminent. | -| [CFO](cfo/Cfo.md) | Chande Forecast Oscillator | Percentage difference between price and linear regression forecast. | -| [DPO](dpo/Dpo.md) | Detrended Price Oscillator | Removes trend via displaced SMA. Reveals cycles. | -| [FISHER](fisher/Fisher.md) | Fisher Transform | Converts prices to Gaussian distribution. Sharp reversals. | -| [INERTIA](inertia/Inertia.md) | Inertia | Trend strength from distance to linear regression line. | -| [KDJ](kdj/Kdj.md) | KDJ Indicator | Enhanced Stochastic. J = 3K - 2D provides leading signal. | -| [PGO](pgo/Pgo.md) | Pretty Good Oscillator | Distance from SMA normalized by ATR. Units: ATR multiples. | -| [SMI](smi/Smi.md) | Stochastic Momentum Index | Distance from range midpoint. More sensitive than classic Stochastic. | -| [STOCH](stoch/Stoch.md) | Stochastic Oscillator | Close position within N-period high-low range. Classic overbought/oversold. | -| [STOCHF](stochf/Stochf.md) | Stochastic Fast | Unsmoothed Stochastic. Faster but noisier. | -| [STOCHRSI](stochrsi/Stochrsi.md) | Stochastic RSI | Stochastic applied to RSI. More sensitive than either alone. | -| [TRIX](trix/Trix.md) | Triple Exponential Average | ROC of triple EMA. Filters noise through three smoothings. | +| BBB | Bollinger %B | Position within Bollinger Bands. 0=lower band, 1=upper band. | +| BBS | Bollinger Band Squeeze | BB width < KC width indicates consolidation. Breakout imminent. | +| CFO | Chande Forecast Oscillator | Percentage difference between price and linear regression forecast. | +| DPO | Detrended Price Oscillator | Removes trend via displaced SMA. Reveals cycles. | +| FISHER | Fisher Transform | Converts prices to Gaussian distribution. Sharp reversals. | +| INERTIA | Inertia | Trend strength from distance to linear regression line. | +| KDJ | KDJ Indicator | Enhanced Stochastic. J = 3K - 2D provides leading signal. | +| PGO | Pretty Good Oscillator | Distance from SMA normalized by ATR. Units: ATR multiples. | +| SMI | Stochastic Momentum Index | Distance from range midpoint. More sensitive than classic Stochastic. | +| STOCH | Stochastic Oscillator | Close position within N-period high-low range. Classic overbought/oversold. | +| STOCHF | Stochastic Fast | Unsmoothed Stochastic. Faster but noisier. | +| STOCHRSI | Stochastic RSI | Stochastic applied to RSI. More sensitive than either alone. | +| TRIX | Triple Exponential Average | ROC of triple EMA. Filters noise through three smoothings. | | [TTM_WAVE](ttm_wave/TtmWave.md) | TTM Wave | Fibonacci-period MACD composite (Waves A/B/C). John Carter. | | [ULTOSC](ultosc/Ultosc.md) | Ultimate Oscillator | Multi-timeframe oscillator. Combines 7, 14, 28 period buying pressure. | -| [WILLR](willr/Willr.md) | Williams %R | Inverse Stochastic. -100 to 0 range. Overbought/oversold. | +| WILLR | Williams %R | Inverse Stochastic. -100 to 0 range. Overbought/oversold. | diff --git a/lib/oscillators/ao/Ao.cs b/lib/oscillators/ao/Ao.cs index 5a04f88f..ac468b04 100644 --- a/lib/oscillators/ao/Ao.cs +++ b/lib/oscillators/ao/Ao.cs @@ -179,7 +179,7 @@ public sealed class Ao : ITValuePublisher int len = source.Count; var v = new double[len]; - Calculate(source.High.Values, source.Low.Values, v, _fastPeriod, _slowPeriod); + Batch(source.High.Values, source.Low.Values, v, _fastPeriod, _slowPeriod); // Bulk copy timestamps using CollectionsMarshal var tList = new List(len); @@ -212,7 +212,7 @@ public sealed class Ao : ITValuePublisher /// Slow SMA period (default 34) /// Output AO values [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void Calculate(ReadOnlySpan high, ReadOnlySpan low, Span destination, int fastPeriod = 5, int slowPeriod = 34) + public static void Batch(ReadOnlySpan high, ReadOnlySpan low, Span destination, int fastPeriod = 5, int slowPeriod = 34) { if (high.Length != low.Length || high.Length != destination.Length) { @@ -267,7 +267,7 @@ public sealed class Ao : ITValuePublisher int len = source.Count; var v = new double[len]; - Calculate(source.High.Values, source.Low.Values, v, fastPeriod, slowPeriod); + Batch(source.High.Values, source.Low.Values, v, fastPeriod, slowPeriod); // Bulk copy timestamps using CollectionsMarshal var tList = new List(len); @@ -283,4 +283,11 @@ public sealed class Ao : ITValuePublisher return new TSeries(tList, vList); } + + public static (TSeries Results, Ao Indicator) Calculate(TBarSeries source, int fastPeriod = 5, int slowPeriod = 34) + { + var indicator = new Ao(fastPeriod, slowPeriod); + TSeries results = indicator.Update(source); + return (results, indicator); + } } diff --git a/lib/oscillators/apo/Apo.Validation.Tests.cs b/lib/oscillators/apo/Apo.Validation.Tests.cs index 37f71484..73836718 100644 --- a/lib/oscillators/apo/Apo.Validation.Tests.cs +++ b/lib/oscillators/apo/Apo.Validation.Tests.cs @@ -64,7 +64,7 @@ public sealed class ApoValidationTests : IDisposable // 3. Span Mode double[] spanOutput = new double[input.Length]; - Apo.Calculate(input.AsSpan(), spanOutput.AsSpan(), fastPeriod, slowPeriod); + Apo.Batch(input.AsSpan(), spanOutput.AsSpan(), fastPeriod, slowPeriod); ValidationHelper.VerifyData(spanOutput, output, outRange, lookback: slowPeriod - 1); } @@ -102,7 +102,7 @@ public sealed class ApoValidationTests : IDisposable // 3. Span Mode double[] spanOutput = new double[input.Length]; - Apo.Calculate(input.AsSpan(), spanOutput.AsSpan(), fastPeriod, slowPeriod); + Apo.Batch(input.AsSpan(), spanOutput.AsSpan(), fastPeriod, slowPeriod); ValidationHelper.VerifyData(spanOutput, output, lookback: 1); } @@ -143,7 +143,7 @@ public sealed class ApoValidationTests : IDisposable // 3. Span Mode double[] input = _testData.Data.Values.ToArray(); double[] spanOutput = new double[input.Length]; - Apo.Calculate(input.AsSpan(), spanOutput.AsSpan(), fastPeriod, slowPeriod); + Apo.Batch(input.AsSpan(), spanOutput.AsSpan(), fastPeriod, slowPeriod); ValidationHelper.VerifyData(spanOutput, output, lookback: 0, tolerance: ValidationHelper.OoplesTolerance); } } diff --git a/lib/oscillators/apo/Apo.cs b/lib/oscillators/apo/Apo.cs index 28cd74bc..cee521fe 100644 --- a/lib/oscillators/apo/Apo.cs +++ b/lib/oscillators/apo/Apo.cs @@ -182,7 +182,7 @@ public sealed class Apo : ITValuePublisher, IDisposable /// Fast EMA period (default 12) /// Slow EMA period (default 26) [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void Calculate(ReadOnlySpan source, Span output, int fastPeriod = 12, int slowPeriod = 26) + public static void Batch(ReadOnlySpan source, Span output, int fastPeriod = 12, int slowPeriod = 26) { if (source.Length != output.Length) { @@ -198,6 +198,13 @@ public sealed class Apo : ITValuePublisher, IDisposable SimdExtensions.Subtract(fastEma, slowEma, output); } + public static (TSeries Results, Apo Indicator) Calculate(TSeries source, int fastPeriod = 12, int slowPeriod = 26) + { + var indicator = new Apo(fastPeriod, slowPeriod); + TSeries results = indicator.Update(source); + return (results, indicator); + } + /// /// Disposes resources and unsubscribes from the source publisher. /// @@ -215,4 +222,4 @@ public sealed class Apo : ITValuePublisher, IDisposable _source = null; } } -} +} \ No newline at end of file diff --git a/lib/oscillators/ultosc/Ultosc.Validation.Tests.cs b/lib/oscillators/ultosc/Ultosc.Validation.Tests.cs index 3e148101..f9751539 100644 --- a/lib/oscillators/ultosc/Ultosc.Validation.Tests.cs +++ b/lib/oscillators/ultosc/Ultosc.Validation.Tests.cs @@ -290,7 +290,7 @@ public sealed class UltoscValidationTests : IDisposable double[] spanOutput = new double[hData.Length]; // Calculate using span method - Ultosc.Calculate(hData, lData, cData, spanOutput, p1, p2, p3); + Ultosc.Batch(hData, lData, cData, spanOutput, p1, p2, p3); // Calculate using TBarSeries batch var ultosc = new Ultosc(p1, p2, p3); diff --git a/lib/oscillators/ultosc/Ultosc.cs b/lib/oscillators/ultosc/Ultosc.cs index 83a298e3..4903b1df 100644 --- a/lib/oscillators/ultosc/Ultosc.cs +++ b/lib/oscillators/ultosc/Ultosc.cs @@ -46,6 +46,7 @@ public sealed class Ultosc : AbstractBase private int _p_index; private readonly TBarSeries? _source; private readonly TBarPublishedHandler? _handler; + private bool _disposed; // Weights: 4:2:1 private const double Weight1 = 4.0; @@ -118,9 +119,13 @@ public sealed class Ultosc : AbstractBase protected override void Dispose(bool disposing) { - if (disposing && _source != null && _handler != null) + if (!_disposed) { - _source.Pub -= _handler; + if (disposing && _source != null && _handler != null) + { + _source.Pub -= _handler; + } + _disposed = true; } base.Dispose(disposing); } @@ -241,7 +246,7 @@ public sealed class Ultosc : AbstractBase var vSpan = CollectionsMarshal.AsSpan(v); // Calculate using span method - Calculate(source.High.Values, source.Low.Values, source.Close.Values, + Batch(source.High.Values, source.Low.Values, source.Close.Values, vSpan, _period1, _period2, _period3); source.Times.CopyTo(tSpan); @@ -290,7 +295,7 @@ public sealed class Ultosc : AbstractBase } [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void Calculate( + public static void Batch( ReadOnlySpan high, ReadOnlySpan low, ReadOnlySpan close, @@ -412,6 +417,14 @@ public sealed class Ultosc : AbstractBase } } + public static (TSeries Results, Ultosc Indicator) Calculate(TBarSeries source, int period1 = 7, int period2 = 14, int period3 = 28) + { + var indicator = new Ultosc(period1, period2, period3); + TSeries results = indicator.Update(source); + return (results, indicator); + } + + public override void Reset() { _bp1.Clear(); diff --git a/lib/reversals/_index.md b/lib/reversals/_index.md index 31de4b88..5af82fb5 100644 --- a/lib/reversals/_index.md +++ b/lib/reversals/_index.md @@ -6,13 +6,13 @@ Reversal indicators identify potential turning points where price may change dir | Indicator | Full Name | Description | | :--- | :--- | :--- | -| [FRACTALS](fractals/Fractals.md) | Williams Fractals | Five-bar pattern identifying local peaks/troughs; marks support/resistance levels. | -| [PIVOT](pivot/Pivot.md) | Pivot Points (Classic) | Standard floor trader pivots with 7 levels (PP, R1-R3, S1-S3). | -| [PIVOTCAM](pivotcam/Pivotcam.md) | Camarilla Pivot Points | Mean-reversion pivots with 9 levels; R3/S3 are key reversal zones. | -| [PIVOTDEM](pivotdem/Pivotdem.md) | DeMark Pivot Points | Minimalist trend-following pivots with only 3 levels and conditional logic. | -| [PIVOTEXT](pivotext/Pivotext.md) | Extended Traditional Pivots | Extended pivots with 11 levels (R1-R5, S1-S5) for volatile markets. | -| [PIVOTFIB](pivotfib/Pivotfib.md) | Fibonacci Pivot Points | Fibonacci-ratio based pivots; Golden Ratio (61.8%) at R2/S2. | -| [PIVOTWOOD](pivotwood/Pivotwood.md) | Woodie's Pivot Points | Weighted close pivots (2× close weight) for intraday trading. | -| [PSAR](psar/Psar.md) | Parabolic Stop And Reverse | Trailing stop that accelerates with trend; SAR dots mark entry/exit signals. | -| [SWINGS](swings/Swings.md) | Swing High/Low Detection | Identifies significant price reversals and swing points using configurable lookback. | +| FRACTALS | Williams Fractals | Five-bar pattern identifying local peaks/troughs; marks support/resistance levels. | +| PIVOT | Pivot Points (Classic) | Standard floor trader pivots with 7 levels (PP, R1-R3, S1-S3). | +| PIVOTCAM | Camarilla Pivot Points | Mean-reversion pivots with 9 levels; R3/S3 are key reversal zones. | +| PIVOTDEM | DeMark Pivot Points | Minimalist trend-following pivots with only 3 levels and conditional logic. | +| PIVOTEXT | Extended Traditional Pivots | Extended pivots with 11 levels (R1-R5, S1-S5) for volatile markets. | +| PIVOTFIB | Fibonacci Pivot Points | Fibonacci-ratio based pivots; Golden Ratio (61.8%) at R2/S2. | +| PIVOTWOOD | Woodie's Pivot Points | Weighted close pivots (2× close weight) for intraday trading. | +| PSAR | Parabolic Stop And Reverse | Trailing stop that accelerates with trend; SAR dots mark entry/exit signals. | +| SWINGS | Swing High/Low Detection | Identifies significant price reversals and swing points using configurable lookback. | | [TTM_SCALPER](ttm_scalper/TtmScalper.md) | TTM Scalper Alert | 3-bar pivot high/low detection for scalping entries. John Carter. | diff --git a/lib/statistics/_index.md b/lib/statistics/_index.md index 7eb3b5a6..3bff44a4 100644 --- a/lib/statistics/_index.md +++ b/lib/statistics/_index.md @@ -13,26 +13,26 @@ Statistical tools applied to price and returns. These indicators quantify relati | [COINTEGRATION](cointegration/Cointegration.md) | Cointegration | Tests if series share long-term equilibrium. Pairs trading foundation. | | [CORRELATION](correlation/Correlation.md) | Correlation | Linear relationship between two variables. Range: -1 to +1. | | [COVARIANCE](covariance/Covariance.md) | Covariance | Joint variability of two random variables. Building block for β. | -| [ENTROPY](entropy/Entropy.md) | Shannon Entropy | Measures uncertainty/randomness. Higher entropy = less predictable. | -| [GEOMEAN](geomean/Geomean.md) | Geometric Mean | nth root of product. Use for growth rates and ratios. | -| [GRANGER](granger/Granger.md) | Granger Causality | Tests if one series helps predict another. Not true causality. | -| [HARMEAN](harmean/Harmean.md) | Harmonic Mean | Reciprocal of arithmetic mean of reciprocals. For rates/ratios. | -| [HURST](hurst/Hurst.md) | Hurst Exponent | Long-term memory. H>0.5: trending. H<0.5: mean-reverting. | -| [IQR](iqr/Iqr.md) | Interquartile Range | P75 - P25. Robust dispersion measure. | -| [JB](jb/Jb.md) | Jarque-Bera Test | Normality test using skewness and kurtosis. | -| [KENDALL](kendall/Kendall.md) | Kendall Rank Correlation | Ordinal association. Robust to outliers. | -| [KURTOSIS](kurtosis/Kurtosis.md) | Kurtosis | Tail heaviness. High kurtosis = fat tails = more extreme events. | +| ENTROPY | Shannon Entropy | Measures uncertainty/randomness. Higher entropy = less predictable. | +| GEOMEAN | Geometric Mean | nth root of product. Use for growth rates and ratios. | +| GRANGER | Granger Causality | Tests if one series helps predict another. Not true causality. | +| HARMEAN | Harmonic Mean | Reciprocal of arithmetic mean of reciprocals. For rates/ratios. | +| HURST | Hurst Exponent | Long-term memory. H>0.5: trending. H<0.5: mean-reverting. | +| IQR | Interquartile Range | P75 - P25. Robust dispersion measure. | +| JB | Jarque-Bera Test | Normality test using skewness and kurtosis. | +| KENDALL | Kendall Rank Correlation | Ordinal association. Robust to outliers. | +| KURTOSIS | Kurtosis | Tail heaviness. High kurtosis = fat tails = more extreme events. | | [LINREG](linreg/LinReg.md) | Linear Regression | Least squares fit. Outputs slope, intercept, R². | | [MEDIAN](median/Median.md) | Median | Middle value in sorted window. Robust to outliers. | -| [MODE](mode/Mode.md) | Mode | Most frequent value. Use for categorical or discrete data. | +| MODE | Mode | Most frequent value. Use for categorical or discrete data. | | [PACF](pacf/Pacf.md) | Partial Autocorrelation Function | Direct correlation at lag k after removing intermediate effects. For AR model identification. | -| [PERCENTILE](percentile/Percentile.md) | Percentile | Value below which given percentage of observations fall. | -| [QUANTILE](quantile/Quantile.md) | Quantile | Divides distribution into equal probability intervals. | +| PERCENTILE | Percentile | Value below which given percentage of observations fall. | +| QUANTILE | Quantile | Divides distribution into equal probability intervals. | | [SKEW](skew/Skew.md) | Skewness | Distribution asymmetry. Positive: right tail. Negative: left tail. | -| [SPEARMAN](spearman/Spearman.md) | Spearman Rank Correlation | Pearson on ranks. Measures monotonic relationship. | +| SPEARMAN | Spearman Rank Correlation | Pearson on ranks. Measures monotonic relationship. | | [STDDEV](stddev/StdDev.md) | Standard Deviation | Square root of variance. Same units as data. | | [SUM](sum/Sum.md) | Rolling Sum | Kahan-Babuška summation. Numerically stable. | -| [THEIL](theil/Theil.md) | Theil Index | Inequality measure. Decomposable into within/between group. | +| THEIL | Theil Index | Inequality measure. Decomposable into within/between group. | | [VARIANCE](variance/Variance.md) | Variance | Average squared deviation from mean. Units are squared. | -| [ZSCORE](zscore/Zscore.md) | Z-Score | Standard deviations from mean. Normalizes different scales. | -| [ZTEST](ztest/Ztest.md) | Z-Test | Hypothesis test comparing sample mean to population mean. | +| ZSCORE | Z-Score | Standard deviations from mean. Normalizes different scales. | +| ZTEST | Z-Test | Hypothesis test comparing sample mean to population mean. | diff --git a/lib/statistics/acf/Acf.Tests.cs b/lib/statistics/acf/Acf.Tests.cs index 747635a9..19d8b3cb 100644 --- a/lib/statistics/acf/Acf.Tests.cs +++ b/lib/statistics/acf/Acf.Tests.cs @@ -416,7 +416,7 @@ public class AcfTests var batchResult = batchIndicator.Update(tSeries); // Mode 3: Static Calculate - var staticResult = Acf.Calculate(tSeries, period, lag); + var staticResult = Acf.Batch(tSeries, period, lag); // Mode 4: Span-based Batch double[] sourceArray = new double[dataLen]; diff --git a/lib/statistics/acf/Acf.Validation.Tests.cs b/lib/statistics/acf/Acf.Validation.Tests.cs index 112de94c..9a3872e9 100644 --- a/lib/statistics/acf/Acf.Validation.Tests.cs +++ b/lib/statistics/acf/Acf.Validation.Tests.cs @@ -159,7 +159,7 @@ public class AcfValidationTests tSeries.Add(new TValue(bar.Time, bar.Close)); } - var batch = Acf.Calculate(tSeries, period, lag); + var batch = Acf.Batch(tSeries, period, lag); // Compare last values Assert.Equal(batch[^1].Value, streaming.Last.Value, Tolerance); @@ -182,7 +182,7 @@ public class AcfValidationTests tSeries.Add(new TValue(bar.Time, bar.Close)); } - var tSeriesResult = Acf.Calculate(tSeries, period, lag); + var tSeriesResult = Acf.Batch(tSeries, period, lag); // Span approach double[] source = new double[dataLen]; diff --git a/lib/statistics/acf/Acf.cs b/lib/statistics/acf/Acf.cs index 7ef53ac0..43990f6e 100644 --- a/lib/statistics/acf/Acf.cs +++ b/lib/statistics/acf/Acf.cs @@ -296,7 +296,7 @@ public sealed class Acf : AbstractBase /// /// Calculates ACF for a time series. /// - public static TSeries Calculate(TSeries source, int period, int lag = 1) + public static TSeries Batch(TSeries source, int period, int lag = 1) { var acf = new Acf(period, lag); return acf.Update(source); @@ -332,6 +332,13 @@ public sealed class Acf : AbstractBase CalculateScalarCore(source, output, period, lag); } + public static (TSeries Results, Acf Indicator) Calculate(TSeries source, int period, int lag = 1) + { + var indicator = new Acf(period, lag); + TSeries results = indicator.Update(source); + return (results, indicator); + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] private static void CalculateScalarCore(ReadOnlySpan source, Span output, int period, int lag) { diff --git a/lib/statistics/cointegration/Cointegration.Tests.cs b/lib/statistics/cointegration/Cointegration.Tests.cs index 1f057ea8..70885faf 100644 --- a/lib/statistics/cointegration/Cointegration.Tests.cs +++ b/lib/statistics/cointegration/Cointegration.Tests.cs @@ -401,7 +401,7 @@ public class CointegrationTests seriesB.Add(barB.Time, barB.Close); } - var result = Cointegration.Calculate(seriesA, seriesB, DefaultPeriod); + var result = Cointegration.Batch(seriesA, seriesB, DefaultPeriod); Assert.Equal(seriesA.Count, result.Count); } @@ -423,7 +423,7 @@ public class CointegrationTests } // Batch calculation - var batchResult = Cointegration.Calculate(seriesA, seriesB, DefaultPeriod); + var batchResult = Cointegration.Batch(seriesA, seriesB, DefaultPeriod); // Streaming calculation var streamingIndicator = new Cointegration(DefaultPeriod); @@ -464,7 +464,7 @@ public class CointegrationTests seriesB.Add(bar.Time, bar.Close); } - var ex = Assert.Throws(() => Cointegration.Calculate(seriesA, seriesB, DefaultPeriod)); + var ex = Assert.Throws(() => Cointegration.Batch(seriesA, seriesB, DefaultPeriod)); Assert.Equal("seriesB", ex.ParamName); } @@ -485,7 +485,7 @@ public class CointegrationTests } // Span calculation - Cointegration.Calculate(seriesA, seriesB, output, DefaultPeriod); + Cointegration.Batch(seriesA, seriesB, output, DefaultPeriod); // Streaming calculation var streamingIndicator = new Cointegration(DefaultPeriod); @@ -515,7 +515,7 @@ public class CointegrationTests var seriesB = new double[30]; var output = new double[50]; - var ex = Assert.Throws(() => Cointegration.Calculate(seriesA, seriesB, output, DefaultPeriod)); + var ex = Assert.Throws(() => Cointegration.Batch(seriesA, seriesB, output, DefaultPeriod)); Assert.Equal("seriesB", ex.ParamName); } @@ -526,7 +526,7 @@ public class CointegrationTests var seriesB = new double[50]; var output = new double[30]; - var ex = Assert.Throws(() => Cointegration.Calculate(seriesA, seriesB, output, DefaultPeriod)); + var ex = Assert.Throws(() => Cointegration.Batch(seriesA, seriesB, output, DefaultPeriod)); Assert.Equal("output", ex.ParamName); } @@ -537,7 +537,7 @@ public class CointegrationTests var seriesB = new double[50]; var output = new double[50]; - var ex = Assert.Throws(() => Cointegration.Calculate(seriesA, seriesB, output, 1)); + var ex = Assert.Throws(() => Cointegration.Batch(seriesA, seriesB, output, 1)); Assert.Equal("period", ex.ParamName); } diff --git a/lib/statistics/cointegration/Cointegration.Validation.Tests.cs b/lib/statistics/cointegration/Cointegration.Validation.Tests.cs index faab34bb..c87286fa 100644 --- a/lib/statistics/cointegration/Cointegration.Validation.Tests.cs +++ b/lib/statistics/cointegration/Cointegration.Validation.Tests.cs @@ -125,7 +125,7 @@ public class CointegrationValidationTests } // Batch calculation - var batchResult = Cointegration.Calculate(seriesA, seriesB, 20); + var batchResult = Cointegration.Batch(seriesA, seriesB, 20); // Streaming calculation var streamingIndicator = new Cointegration(20); @@ -160,7 +160,7 @@ public class CointegrationValidationTests } // Span calculation - Cointegration.Calculate(seriesA, seriesB, output, 20); + Cointegration.Batch(seriesA, seriesB, output, 20); // Streaming calculation var streamingIndicator = new Cointegration(20); diff --git a/lib/statistics/cointegration/Cointegration.cs b/lib/statistics/cointegration/Cointegration.cs index 19f5dd96..d71e53bc 100644 --- a/lib/statistics/cointegration/Cointegration.cs +++ b/lib/statistics/cointegration/Cointegration.cs @@ -127,7 +127,7 @@ public sealed class Cointegration : AbstractBase /// Not supported for bi-input indicator. Use Calculate(seriesA, seriesB, period) instead. public override TSeries Update(TSeries source) { - throw new NotSupportedException("Cointegration requires two inputs. Use Calculate(seriesA, seriesB, period)."); + throw new NotSupportedException("Cointegration requires two inputs. Use Batch(seriesA, seriesB, period)."); } [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -472,7 +472,7 @@ public sealed class Cointegration : AbstractBase /// /// Calculates cointegration for two time series. /// - public static TSeries Calculate(TSeries seriesA, TSeries seriesB, int period = 20) + public static TSeries Batch(TSeries seriesA, TSeries seriesB, int period = 20) { if (seriesA.Count != seriesB.Count) { @@ -499,7 +499,7 @@ public sealed class Cointegration : AbstractBase /// /// Static batch calculation for span-based processing. /// - public static void Calculate( + public static void Batch( ReadOnlySpan seriesA, ReadOnlySpan seriesB, Span output, @@ -528,4 +528,12 @@ public sealed class Cointegration : AbstractBase output[i] = result.Value; } } -} \ No newline at end of file + + public static (TSeries Results, Cointegration Indicator) Calculate(TSeries seriesA, TSeries seriesB, int period = 20) + { + var indicator = new Cointegration(period); + TSeries results = Batch(seriesA, seriesB, period); + return (results, indicator); + } + +} diff --git a/lib/statistics/correlation/Correlation.Tests.cs b/lib/statistics/correlation/Correlation.Tests.cs index 415c9fb9..f2a2d686 100644 --- a/lib/statistics/correlation/Correlation.Tests.cs +++ b/lib/statistics/correlation/Correlation.Tests.cs @@ -251,7 +251,7 @@ public class CorrelationTests seriesY.Add(new TValue(DateTime.UtcNow.AddMinutes(i), 200.0 + (i * 2))); } - var result = Correlation.Calculate(seriesX, seriesY, 5); + var result = Correlation.Batch(seriesX, seriesY, 5); Assert.Equal(20, result.Count); } @@ -271,7 +271,7 @@ public class CorrelationTests seriesY.Add(new TValue(DateTime.UtcNow.AddMinutes(i), 200.0 + i)); } - Assert.Throws(() => Correlation.Calculate(seriesX, seriesY, 5)); + Assert.Throws(() => Correlation.Batch(seriesX, seriesY, 5)); } [Fact] @@ -287,7 +287,7 @@ public class CorrelationTests seriesY[i] = 200.0 + (i * 2); } - Correlation.Calculate(seriesX, seriesY, output, 5); + Correlation.Batch(seriesX, seriesY, output, 5); // First value should be NaN (not enough data) Assert.True(double.IsNaN(output[0])); @@ -303,7 +303,7 @@ public class CorrelationTests double[] seriesY = new double[15]; double[] output = new double[10]; - Assert.Throws(() => Correlation.Calculate(seriesX, seriesY, output, 5)); + Assert.Throws(() => Correlation.Batch(seriesX, seriesY, output, 5)); } [Fact] @@ -313,7 +313,7 @@ public class CorrelationTests double[] seriesY = new double[20]; double[] output = new double[10]; - Assert.Throws(() => Correlation.Calculate(seriesX, seriesY, output, 5)); + Assert.Throws(() => Correlation.Batch(seriesX, seriesY, output, 5)); } [Fact] @@ -323,7 +323,7 @@ public class CorrelationTests double[] seriesY = new double[20]; double[] output = new double[20]; - Assert.Throws(() => Correlation.Calculate(seriesX, seriesY, output, 1)); + Assert.Throws(() => Correlation.Batch(seriesX, seriesY, output, 1)); } [Fact] @@ -374,7 +374,7 @@ public class CorrelationTests // Batch calculation double[] batchResults = new double[length]; - Correlation.Calculate(seriesX, seriesY, batchResults, period); + Correlation.Batch(seriesX, seriesY, batchResults, period); // Compare last 50 values (after warmup) for (int i = length - 50; i < length; i++) diff --git a/lib/statistics/correlation/Correlation.Validation.Tests.cs b/lib/statistics/correlation/Correlation.Validation.Tests.cs index e985afee..dffc3d6e 100644 --- a/lib/statistics/correlation/Correlation.Validation.Tests.cs +++ b/lib/statistics/correlation/Correlation.Validation.Tests.cs @@ -190,7 +190,7 @@ public class CorrelationValidationTests } // Batch calculation - var batchResult = Correlation.Calculate(seriesX, seriesY, 20); + var batchResult = Correlation.Batch(seriesX, seriesY, 20); // Streaming calculation var streamingIndicator = new Correlation(20); @@ -227,7 +227,7 @@ public class CorrelationValidationTests } // Span calculation - Correlation.Calculate(seriesX, seriesY, output, 20); + Correlation.Batch(seriesX, seriesY, output, 20); // Streaming calculation var streamingIndicator = new Correlation(20); diff --git a/lib/statistics/correlation/Correlation.cs b/lib/statistics/correlation/Correlation.cs index 8ad0529d..f7c5a4b4 100644 --- a/lib/statistics/correlation/Correlation.cs +++ b/lib/statistics/correlation/Correlation.cs @@ -113,7 +113,7 @@ public sealed class Correlation : AbstractBase /// Not supported for bi-input indicator. Use Calculate(seriesX, seriesY, period) instead. public override TSeries Update(TSeries source) { - throw new NotSupportedException("Correlation requires two inputs. Use Calculate(seriesX, seriesY, period)."); + throw new NotSupportedException("Correlation requires two inputs. Use Batch(seriesX, seriesY, period)."); } [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -286,7 +286,7 @@ public sealed class Correlation : AbstractBase /// /// Calculates correlation for two time series. /// - public static TSeries Calculate(TSeries seriesX, TSeries seriesY, int period = 20) + public static TSeries Batch(TSeries seriesX, TSeries seriesY, int period = 20) { if (seriesX.Count != seriesY.Count) { @@ -313,7 +313,7 @@ public sealed class Correlation : AbstractBase /// /// Static batch calculation for span-based processing. /// - public static void Calculate( + public static void Batch( ReadOnlySpan seriesX, ReadOnlySpan seriesY, Span output, @@ -342,4 +342,12 @@ public sealed class Correlation : AbstractBase output[i] = result.Value; } } -} \ No newline at end of file + + public static (TSeries Results, Correlation Indicator) Calculate(TSeries seriesX, TSeries seriesY, int period = 20) + { + var indicator = new Correlation(period); + TSeries results = Batch(seriesX, seriesY, period); + return (results, indicator); + } + +} diff --git a/lib/statistics/covariance/Covariance.Simd.Tests.cs b/lib/statistics/covariance/Covariance.Simd.Tests.cs index 55da1d5b..29f1ea18 100644 --- a/lib/statistics/covariance/Covariance.Simd.Tests.cs +++ b/lib/statistics/covariance/Covariance.Simd.Tests.cs @@ -26,7 +26,7 @@ public class CovarianceSimdTests // Act // This will use SIMD if available and length >= 256 - var simdResult = Covariance.Calculate(sourceX, sourceY, period); + var simdResult = Covariance.Batch(sourceX, sourceY, period); // Calculate expected using scalar loop (simulating by using small chunks or manual calc, // but easier to just use the streaming update which is scalar) @@ -67,7 +67,7 @@ public class CovarianceSimdTests // The implementation checks for ContainsNonFinite() before using SIMD. // If NaN is present, it should fall back to Scalar. // We want to verify that the result is correct regardless of the path taken. - var result = Covariance.Calculate(sourceX, sourceY, period); + var result = Covariance.Batch(sourceX, sourceY, period); // Assert // Verify around the NaN values @@ -119,7 +119,7 @@ public class CovarianceSimdTests sourceY.Add(dataY); // Act - var result = Covariance.Calculate(sourceX, sourceY, period); + var result = Covariance.Batch(sourceX, sourceY, period); // Assert // For y=2x, Cov(X,Y) = 2*Var(X) diff --git a/lib/statistics/covariance/Covariance.cs b/lib/statistics/covariance/Covariance.cs index c243a44c..90e5fbc9 100644 --- a/lib/statistics/covariance/Covariance.cs +++ b/lib/statistics/covariance/Covariance.cs @@ -179,7 +179,7 @@ public sealed class Covariance : AbstractBase _sumXY = sumXY; } - public static TSeries Calculate(TSeries sourceX, TSeries sourceY, int period, bool isPopulation = false) + public static TSeries Batch(TSeries sourceX, TSeries sourceY, int period, bool isPopulation = false) { if (sourceX.Count != sourceY.Count) { @@ -231,6 +231,13 @@ public sealed class Covariance : AbstractBase CalculateScalarCore(sourceX, sourceY, output, period, isPopulation); } + public static (TSeries Results, Covariance Indicator) Calculate(TSeries sourceX, TSeries sourceY, int period, bool isPopulation = false) + { + var indicator = new Covariance(period, isPopulation); + TSeries results = Batch(sourceX, sourceY, period, isPopulation); + return (results, indicator); + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] private static void CalculateScalarCore(ReadOnlySpan sourceX, ReadOnlySpan sourceY, Span output, int period, bool isPopulation) { @@ -512,4 +519,4 @@ public sealed class Covariance : AbstractBase Unsafe.Add(ref outRef, i) = numerator * invDenom; } } -} +} \ No newline at end of file diff --git a/lib/statistics/linreg/LinReg.Tests.cs b/lib/statistics/linreg/LinReg.Tests.cs index 71535bf3..6104849a 100644 --- a/lib/statistics/linreg/LinReg.Tests.cs +++ b/lib/statistics/linreg/LinReg.Tests.cs @@ -120,11 +120,11 @@ public class LinRegTests // Period must be > 0 Assert.Throws(() => - LinReg.Calculate(source.AsSpan(), output.AsSpan(), 0)); + LinReg.Batch(source.AsSpan(), output.AsSpan(), 0)); // Output must be same length as source Assert.Throws(() => - LinReg.Calculate(source.AsSpan(), wrongSizeOutput.AsSpan(), 3)); + LinReg.Batch(source.AsSpan(), wrongSizeOutput.AsSpan(), 3)); } [Fact] @@ -145,7 +145,7 @@ public class LinRegTests var tseriesResult = LinReg.Batch(series, period); double[] output = new double[100]; - LinReg.Calculate(source.AsSpan(), output.AsSpan(), period); + LinReg.Batch(source.AsSpan(), output.AsSpan(), period); for (int i = 0; i < 100; i++) { @@ -213,7 +213,7 @@ public class LinRegTests var tValues = series.Values.ToArray(); var spanInput = new ReadOnlySpan(tValues); var spanOutput = new double[tValues.Length]; - LinReg.Calculate(spanInput, spanOutput, period); + LinReg.Batch(spanInput, spanOutput, period); double spanResult = spanOutput[^1]; // 3. Streaming Mode diff --git a/lib/statistics/linreg/LinReg.cs b/lib/statistics/linreg/LinReg.cs index 3467c7a0..2d19c4ba 100644 --- a/lib/statistics/linreg/LinReg.cs +++ b/lib/statistics/linreg/LinReg.cs @@ -279,7 +279,7 @@ public sealed class LinReg : AbstractBase var vSpan = CollectionsMarshal.AsSpan(v); double initialLastValid = _state.LastValidValue; - Calculate(source.Values, vSpan, _period, _offset, initialLastValid); + Batch(source.Values, vSpan, _period, _offset, initialLastValid); source.Times.CopyTo(tSpan); // Restore state @@ -334,7 +334,7 @@ public sealed class LinReg : AbstractBase } [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void Calculate(ReadOnlySpan source, Span output, int period, int offset = 0, double initialLastValid = 0) + public static void Batch(ReadOnlySpan source, Span output, int period, int offset = 0, double initialLastValid = 0) { if (source.Length != output.Length) { @@ -458,6 +458,13 @@ public sealed class LinReg : AbstractBase } } + public static (TSeries Results, LinReg Indicator) Calculate(TSeries source, int period, int offset = 0) + { + var indicator = new LinReg(period, offset); + TSeries results = indicator.Update(source); + return (results, indicator); + } + public override void Reset() { _buffer.Clear(); @@ -469,4 +476,4 @@ public sealed class LinReg : AbstractBase Intercept = 0; RSquared = 0; } -} +} \ No newline at end of file diff --git a/lib/statistics/median/Median.cs b/lib/statistics/median/Median.cs index a2dc527b..ce354c32 100644 --- a/lib/statistics/median/Median.cs +++ b/lib/statistics/median/Median.cs @@ -21,14 +21,14 @@ namespace QuanTAlib; /// This is significantly faster than O(N log N) full sort for each update. /// [SkipLocalsInit] -public sealed class Median : AbstractBase, IDisposable +public sealed class Median : AbstractBase { private readonly int _period; private readonly RingBuffer _buffer; private readonly double[] _sortedBuffer; private readonly double[] _p_sortedBuffer; private readonly TValuePublishedHandler _handler; - private ITValuePublisher? _source; + private readonly ITValuePublisher? _source; private bool _disposed; /// @@ -312,6 +312,13 @@ public sealed class Median : AbstractBase, IDisposable } } + public static (TSeries Results, Median Indicator) Calculate(TSeries source, int period) + { + var indicator = new Median(period); + TSeries results = indicator.Update(source); + return (results, indicator); + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] private static int BinarySearchSpan(Span span, int length, double value) { @@ -352,19 +359,16 @@ public sealed class Median : AbstractBase, IDisposable /// /// Disposes the indicator and unsubscribes from the source. /// - public new void Dispose() + protected override void Dispose(bool disposing) { - if (_disposed) + if (!_disposed) { - return; + if (disposing && _source != null) + { + _source.Pub -= _handler; + } + _disposed = true; } - - if (_source != null) - { - _source.Pub -= _handler; - _source = null; - } - - _disposed = true; + base.Dispose(disposing); } -} +} \ No newline at end of file diff --git a/lib/statistics/pacf/Pacf.Tests.cs b/lib/statistics/pacf/Pacf.Tests.cs index 4b3b8406..ff05e20f 100644 --- a/lib/statistics/pacf/Pacf.Tests.cs +++ b/lib/statistics/pacf/Pacf.Tests.cs @@ -414,7 +414,7 @@ public class PacfTests var batchResult = batchIndicator.Update(tSeries); // Mode 3: Static Calculate - var staticResult = Pacf.Calculate(tSeries, period, lag); + var staticResult = Pacf.Batch(tSeries, period, lag); // Mode 4: Span-based Batch double[] sourceArray = new double[dataLen]; diff --git a/lib/statistics/pacf/Pacf.Validation.Tests.cs b/lib/statistics/pacf/Pacf.Validation.Tests.cs index fdaf9444..80811844 100644 --- a/lib/statistics/pacf/Pacf.Validation.Tests.cs +++ b/lib/statistics/pacf/Pacf.Validation.Tests.cs @@ -201,7 +201,7 @@ public class PacfValidationTests } // Batch - var batchResult = Pacf.Calculate(tSeries, period, lag); + var batchResult = Pacf.Batch(tSeries, period, lag); // Compare last values Assert.Equal(batchResult[^1].Value, streaming.Last.Value, Epsilon); @@ -228,7 +228,7 @@ public class PacfValidationTests { tSeries.Add(new TValue(bar.Time, bar.Close)); } - var tSeriesResult = Pacf.Calculate(tSeries, period, lag); + var tSeriesResult = Pacf.Batch(tSeries, period, lag); // Compare last 50 values for (int i = source.Length - 50; i < source.Length; i++) diff --git a/lib/statistics/pacf/Pacf.cs b/lib/statistics/pacf/Pacf.cs index bb398b6d..69586f44 100644 --- a/lib/statistics/pacf/Pacf.cs +++ b/lib/statistics/pacf/Pacf.cs @@ -303,7 +303,7 @@ public sealed class Pacf : AbstractBase /// /// Calculates PACF for a time series. /// - public static TSeries Calculate(TSeries source, int period, int lag = 1) + public static TSeries Batch(TSeries source, int period, int lag = 1) { var pacf = new Pacf(period, lag); return pacf.Update(source); @@ -339,6 +339,13 @@ public sealed class Pacf : AbstractBase CalculateScalarCore(source, output, period, lag); } + public static (TSeries Results, Pacf Indicator) Calculate(TSeries source, int period, int lag = 1) + { + var indicator = new Pacf(period, lag); + TSeries results = indicator.Update(source); + return (results, indicator); + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] private static void CalculateScalarCore(ReadOnlySpan source, Span output, int period, int lag) { diff --git a/lib/statistics/skew/Skew.Tests.cs b/lib/statistics/skew/Skew.Tests.cs index 79df4a00..e377c25f 100644 --- a/lib/statistics/skew/Skew.Tests.cs +++ b/lib/statistics/skew/Skew.Tests.cs @@ -131,7 +131,7 @@ public class SkewTests var series = new TSeries(times, values); // 1. Batch Mode (static method) - var batchSeries = Skew.Calculate(series, period); + var batchSeries = Skew.Batch(series, period); double expected = batchSeries.Last.Value; // 2. Span Mode (static method with spans) @@ -192,7 +192,7 @@ public class SkewTests var series = new TSeries(times, values); - var tseriesResult = Skew.Calculate(series, 10); + var tseriesResult = Skew.Batch(series, 10); Skew.Batch(source.AsSpan(), output.AsSpan(), 10); for (int i = 0; i < count; i++) @@ -313,7 +313,7 @@ public class SkewTests // Batch var series = new TSeries(new System.Collections.Generic.List(new long[data.Length]), new System.Collections.Generic.List(data)); - var batchResult = Skew.Calculate(series, period); + var batchResult = Skew.Batch(series, period); for (int i = 0; i < data.Length; i++) { @@ -391,7 +391,7 @@ public class SkewTests var series = new TSeries(new System.Collections.Generic.List(new long[count]), new System.Collections.Generic.List(data)); // Batch calculation - var batchResult = Skew.Calculate(series, 10); + var batchResult = Skew.Batch(series, 10); // Verify last value against streaming var skew = new Skew(10); diff --git a/lib/statistics/skew/Skew.cs b/lib/statistics/skew/Skew.cs index fe2c19c2..14904f4f 100644 --- a/lib/statistics/skew/Skew.cs +++ b/lib/statistics/skew/Skew.cs @@ -222,7 +222,7 @@ public sealed class Skew : AbstractBase } } - public static TSeries Calculate(TSeries source, int period, bool isPopulation = false) + public static TSeries Batch(TSeries source, int period, bool isPopulation = false) { var skew = new Skew(period, isPopulation); return skew.Update(source); @@ -260,6 +260,13 @@ public sealed class Skew : AbstractBase CalculateScalarCore(source, output, period, isPopulation); } + public static (TSeries Results, Skew Indicator) Calculate(TSeries source, int period, bool isPopulation = false) + { + var indicator = new Skew(period, isPopulation); + TSeries results = indicator.Update(source); + return (results, indicator); + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] private static void CalculateScalarCore(ReadOnlySpan source, Span output, int period, bool isPopulation) { @@ -547,4 +554,4 @@ public sealed class Skew : AbstractBase Unsafe.Add(ref outRef, i) = CalculateSkewFromSums(sum, sumSq, sumCu, n, isPopulation); } } -} +} \ No newline at end of file diff --git a/lib/statistics/stddev/StdDev.Tests.cs b/lib/statistics/stddev/StdDev.Tests.cs index 0737c9e5..7dab9f2e 100644 --- a/lib/statistics/stddev/StdDev.Tests.cs +++ b/lib/statistics/stddev/StdDev.Tests.cs @@ -135,7 +135,7 @@ public class StdDevTests double streamingResult = streamingInd.Last.Value; // 3. TSeries Batch Mode - var batchSeriesResult = StdDev.Calculate(series, period); + var batchSeriesResult = StdDev.Batch(series, period); double tseriesResult = batchSeriesResult.Last.Value; Assert.Equal(expected, streamingResult, precision: 6); diff --git a/lib/statistics/stddev/StdDev.cs b/lib/statistics/stddev/StdDev.cs index 7dd34db1..78aacc26 100644 --- a/lib/statistics/stddev/StdDev.cs +++ b/lib/statistics/stddev/StdDev.cs @@ -108,7 +108,7 @@ public sealed class StdDev : AbstractBase } } - public static TSeries Calculate(TSeries source, int period, bool isPopulation = false) + public static TSeries Batch(TSeries source, int period, bool isPopulation = false) { var stdDev = new StdDev(period, isPopulation); return stdDev.Update(source); @@ -127,6 +127,13 @@ public sealed class StdDev : AbstractBase SqrtSpan(output); } + public static (TSeries Results, StdDev Indicator) Calculate(TSeries source, int period, bool isPopulation = false) + { + var indicator = new StdDev(period, isPopulation); + TSeries results = indicator.Update(source); + return (results, indicator); + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] private static void SqrtSpan(Span data) { @@ -192,4 +199,4 @@ public sealed class StdDev : AbstractBase data[i] = (val > 0) ? Math.Sqrt(val) : 0.0; } } -} +} \ No newline at end of file diff --git a/lib/statistics/variance/Variance.Tests.cs b/lib/statistics/variance/Variance.Tests.cs index 6806f7e8..c6c409ec 100644 --- a/lib/statistics/variance/Variance.Tests.cs +++ b/lib/statistics/variance/Variance.Tests.cs @@ -104,7 +104,7 @@ public class VarianceTests var series = new TSeries(times, values); // 1. Batch Mode (static method) - var batchSeries = Variance.Calculate(series, period); + var batchSeries = Variance.Batch(series, period); double expected = batchSeries.Last.Value; // 2. Span Mode (static method with spans) @@ -165,7 +165,7 @@ public class VarianceTests var series = new TSeries(times, values); - var tseriesResult = Variance.Calculate(series, 10); + var tseriesResult = Variance.Batch(series, 10); Variance.Batch(source.AsSpan(), output.AsSpan(), 10); for (int i = 0; i < count; i++) @@ -359,7 +359,7 @@ public class VarianceTests var series = new TSeries(new System.Collections.Generic.List(new long[count]), new System.Collections.Generic.List(data)); // Batch calculation - var batchResult = Variance.Calculate(series, 10); + var batchResult = Variance.Batch(series, 10); Assert.True(double.IsFinite(batchResult.Last.Value)); Assert.True(batchResult.Last.Value >= 0); @@ -471,7 +471,7 @@ public class VarianceTests source.Add(DateTime.UtcNow.Ticks + 1, 20); source.Add(DateTime.UtcNow.Ticks + 2, 30); - var result = Variance.Calculate(source, 3); // Sample variance by default + var result = Variance.Batch(source, 3); // Sample variance by default Assert.Equal(3, result.Count); Assert.Equal(100.0, result.Last.Value, precision: 6); // Sample variance: 200/2 = 100 @@ -485,7 +485,7 @@ public class VarianceTests source.Add(DateTime.UtcNow.Ticks + 1, 20); source.Add(DateTime.UtcNow.Ticks + 2, 30); - var result = Variance.Calculate(source, 3, isPopulation: true); + var result = Variance.Batch(source, 3, isPopulation: true); Assert.Equal(3, result.Count); Assert.Equal(66.666666, result.Last.Value, precision: 5); // Population variance: 200/3 ≈ 66.67 diff --git a/lib/statistics/variance/Variance.cs b/lib/statistics/variance/Variance.cs index 7fa8b372..eaa17357 100644 --- a/lib/statistics/variance/Variance.cs +++ b/lib/statistics/variance/Variance.cs @@ -168,7 +168,7 @@ public sealed class Variance : AbstractBase } } - public static TSeries Calculate(TSeries source, int period, bool isPopulation = false) + public static TSeries Batch(TSeries source, int period, bool isPopulation = false) { var variance = new Variance(period, isPopulation); return variance.Update(source); @@ -229,6 +229,13 @@ public sealed class Variance : AbstractBase CalculateScalarCore(source, output, period, isPopulation); } + public static (TSeries Results, Variance Indicator) Calculate(TSeries source, int period, bool isPopulation = false) + { + var indicator = new Variance(period, isPopulation); + TSeries results = indicator.Update(source); + return (results, indicator); + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] private static void CalculateScalarCore(ReadOnlySpan source, Span output, int period, bool isPopulation) { @@ -694,4 +701,4 @@ public sealed class Variance : AbstractBase Unsafe.Add(ref outRef, i) = numerator * invDenom; } } -} +} \ No newline at end of file diff --git a/lib/trends_FIR/alma/Alma.Tests.cs b/lib/trends_FIR/alma/Alma.Tests.cs index cfc74506..63902075 100644 --- a/lib/trends_FIR/alma/Alma.Tests.cs +++ b/lib/trends_FIR/alma/Alma.Tests.cs @@ -115,7 +115,7 @@ public class AlmaTests double[] input = series.Values.ToArray(); double[] output = new double[input.Length]; - Alma.Calculate(input.AsSpan(), output.AsSpan(), 10); + Alma.Batch(input.AsSpan(), output.AsSpan(), 10); for (int i = 0; i < input.Length; i++) { @@ -284,7 +284,7 @@ public class AlmaTests var tValues = series.Values.ToArray(); var spanInput = new ReadOnlySpan(tValues); var spanOutput = new double[tValues.Length]; - Alma.Calculate(spanInput, spanOutput, period); + Alma.Batch(spanInput, spanOutput, period); double spanResult = spanOutput[^1]; // 3. Streaming Mode @@ -317,12 +317,12 @@ public class AlmaTests double[] output = new double[5]; double[] wrongSizeOutput = new double[3]; - Assert.Throws(() => Alma.Calculate(source.AsSpan(), output.AsSpan(), 0)); - Assert.Throws(() => Alma.Calculate(source.AsSpan(), output.AsSpan(), 3, sigma: 0)); - Assert.Throws(() => Alma.Calculate(source.AsSpan(), output.AsSpan(), 3, sigma: -1)); - Assert.Throws(() => Alma.Calculate(source.AsSpan(), output.AsSpan(), 3, offset: -0.1)); - Assert.Throws(() => Alma.Calculate(source.AsSpan(), output.AsSpan(), 3, offset: 1.1)); - Assert.Throws(() => Alma.Calculate(source.AsSpan(), wrongSizeOutput.AsSpan(), 3)); + Assert.Throws(() => Alma.Batch(source.AsSpan(), output.AsSpan(), 0)); + Assert.Throws(() => Alma.Batch(source.AsSpan(), output.AsSpan(), 3, sigma: 0)); + Assert.Throws(() => Alma.Batch(source.AsSpan(), output.AsSpan(), 3, sigma: -1)); + Assert.Throws(() => Alma.Batch(source.AsSpan(), output.AsSpan(), 3, offset: -0.1)); + Assert.Throws(() => Alma.Batch(source.AsSpan(), output.AsSpan(), 3, offset: 1.1)); + Assert.Throws(() => Alma.Batch(source.AsSpan(), wrongSizeOutput.AsSpan(), 3)); } [Fact] @@ -331,7 +331,7 @@ public class AlmaTests double[] source = [100, 110, double.NaN, 120, 130]; double[] output = new double[5]; - Alma.Calculate(source.AsSpan(), output.AsSpan(), 3); + Alma.Batch(source.AsSpan(), output.AsSpan(), 3); foreach (var val in output) { diff --git a/lib/trends_FIR/alma/Alma.Validation.Tests.cs b/lib/trends_FIR/alma/Alma.Validation.Tests.cs index 336371dd..5549b504 100644 --- a/lib/trends_FIR/alma/Alma.Validation.Tests.cs +++ b/lib/trends_FIR/alma/Alma.Validation.Tests.cs @@ -102,7 +102,7 @@ public sealed class AlmaValidationTests : IDisposable { // Calculate QuanTAlib ALMA (Span API) double[] qOutput = new double[sourceData.Length]; - global::QuanTAlib.Alma.Calculate(sourceData, qOutput.AsSpan(), period, offset, sigma); + global::QuanTAlib.Alma.Batch(sourceData, qOutput.AsSpan(), period, offset, sigma); // Calculate Skender ALMA var sResult = _testData.SkenderQuotes.GetAlma(period, offset, sigma).ToList(); diff --git a/lib/trends_FIR/alma/Alma.cs b/lib/trends_FIR/alma/Alma.cs index 47163b23..8ea14141 100644 --- a/lib/trends_FIR/alma/Alma.cs +++ b/lib/trends_FIR/alma/Alma.cs @@ -26,6 +26,7 @@ public sealed class Alma : AbstractBase private readonly ITValuePublisher? _source; private readonly TValuePublishedHandler? _pubHandler; private bool _isNew = true; + private bool _disposed; [StructLayout(LayoutKind.Auto)] private record struct State(double LastValidValue, bool IsInitialized); @@ -83,9 +84,13 @@ public sealed class Alma : AbstractBase protected override void Dispose(bool disposing) { - if (disposing && _source != null && _pubHandler != null) + if (!_disposed) { - _source.Pub -= _pubHandler; + if (disposing && _source != null && _pubHandler != null) + { + _source.Pub -= _pubHandler; + } + _disposed = true; } base.Dispose(disposing); } @@ -181,7 +186,7 @@ public sealed class Alma : AbstractBase var tSpan = CollectionsMarshal.AsSpan(t); var vSpan = CollectionsMarshal.AsSpan(v); - Calculate(source.Values, vSpan, _period, _offset, _sigma); + Batch(source.Values, vSpan, _period, _offset, _sigma); source.Times.CopyTo(tSpan); // Restore state @@ -305,7 +310,7 @@ public sealed class Alma : AbstractBase } [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void Calculate(ReadOnlySpan source, Span output, int period, double offset = 0.85, double sigma = 6.0) + public static void Batch(ReadOnlySpan source, Span output, int period, double offset = 0.85, double sigma = 6.0) { if (period <= 0) { @@ -433,6 +438,13 @@ public sealed class Alma : AbstractBase } } + public static (TSeries Results, Alma Indicator) Calculate(TSeries source, int period, double offset = 0.85, double sigma = 6.0) + { + var indicator = new Alma(period, offset, sigma); + TSeries results = indicator.Update(source); + return (results, indicator); + } + public override void Reset() { _buffer.Clear(); @@ -440,4 +452,4 @@ public sealed class Alma : AbstractBase _p_state = _state; Last = default; } -} +} \ No newline at end of file diff --git a/lib/trends_FIR/blma/Blma.Tests.cs b/lib/trends_FIR/blma/Blma.Tests.cs index 2a351821..13f057fe 100644 --- a/lib/trends_FIR/blma/Blma.Tests.cs +++ b/lib/trends_FIR/blma/Blma.Tests.cs @@ -57,7 +57,7 @@ public class BlmaTests var tValues = series.Values.ToArray(); var spanInput = new ReadOnlySpan(tValues); var spanOutput = new double[tValues.Length]; - Blma.Calculate(spanInput, spanOutput, period); + Blma.Batch(spanInput, spanOutput, period); double spanResult = spanOutput[^1]; // 3. Streaming Mode diff --git a/lib/trends_FIR/blma/Blma.cs b/lib/trends_FIR/blma/Blma.cs index 2c87541d..180e643b 100644 --- a/lib/trends_FIR/blma/Blma.cs +++ b/lib/trends_FIR/blma/Blma.cs @@ -151,7 +151,7 @@ public sealed class Blma : AbstractBase try { - Calculate(source.Values, output, _period); + BatchCore(source.Values, output, _period); for (int i = 0; i < len; i++) { @@ -243,17 +243,11 @@ public sealed class Blma : AbstractBase /// /// Calculates BLMA values for a TSeries and returns both results and a primed indicator. /// - public static (TSeries Results, Blma Indicator) Calculate(TSeries source, int period) - { - var indicator = new Blma(period); - var results = indicator.Update(source); - return (results, indicator); - } /// - /// Calculates BLMA values using spans (high-performance batch API). + /// Core batch calculation of BLMA values using spans (high-performance batch API). /// - public static void Calculate(ReadOnlySpan source, Span destination, int period) + private static void BatchCore(ReadOnlySpan source, Span destination, int period) { if (period < 1) { @@ -377,6 +371,12 @@ public sealed class Blma : AbstractBase /// public static void Batch(ReadOnlySpan source, Span destination, int period) { - Calculate(source, destination, period); + BatchCore(source, destination, period); } -} + public static (TSeries Results, Blma Indicator) Calculate(TSeries source, int period) + { + var indicator = new Blma(period); + var results = indicator.Update(source); + return (results, indicator); + } +} \ No newline at end of file diff --git a/lib/trends_FIR/bwma/Bwma.Tests.cs b/lib/trends_FIR/bwma/Bwma.Tests.cs index 9f805fca..47187a8f 100644 --- a/lib/trends_FIR/bwma/Bwma.Tests.cs +++ b/lib/trends_FIR/bwma/Bwma.Tests.cs @@ -152,7 +152,7 @@ public class BwmaTests } var spanResults = new double[series.Count]; - Bwma.Calculate(series.Values, spanResults, 10); + Bwma.Batch(series.Values, spanResults, 10); for (int i = 0; i < spanResults.Length; i++) { @@ -175,7 +175,7 @@ public class BwmaTests } var spanResults = new double[series.Count]; - Bwma.Calculate(series.Values, spanResults, 10, 2); + Bwma.Batch(series.Values, spanResults, 10, 2); for (int i = 0; i < spanResults.Length; i++) { diff --git a/lib/trends_FIR/bwma/Bwma.Validation.Tests.cs b/lib/trends_FIR/bwma/Bwma.Validation.Tests.cs index 13ab185b..5cd48bd6 100644 --- a/lib/trends_FIR/bwma/Bwma.Validation.Tests.cs +++ b/lib/trends_FIR/bwma/Bwma.Validation.Tests.cs @@ -60,7 +60,7 @@ public sealed class BwmaValidationTests : IDisposable // 3. Span API ReadOnlySpan sourceData = _testData.RawData.Span; double[] spanOutput = new double[sourceData.Length]; - Bwma.Calculate(sourceData, spanOutput.AsSpan(), period, order); + Bwma.Batch(sourceData, spanOutput.AsSpan(), period, order); // Verify streaming vs batch Assert.Equal(streamingResults.Count, batchResults.Count); @@ -257,7 +257,7 @@ public sealed class BwmaValidationTests : IDisposable // Span double[] spanOutput = new double[dataWithNaN.Count]; - Bwma.Calculate(dataWithNaN.Values, spanOutput.AsSpan(), period); + Bwma.Batch(dataWithNaN.Values, spanOutput.AsSpan(), period); // Verify all produce same results for (int i = 0; i < streamingResults.Count; i++) diff --git a/lib/trends_FIR/bwma/Bwma.cs b/lib/trends_FIR/bwma/Bwma.cs index 1a3e1b1c..b31d7981 100644 --- a/lib/trends_FIR/bwma/Bwma.cs +++ b/lib/trends_FIR/bwma/Bwma.cs @@ -26,6 +26,7 @@ public sealed class Bwma : AbstractBase private readonly ITValuePublisher? _source; private readonly TValuePublishedHandler? _pubHandler; private bool _isNew = true; + private bool _disposed; [StructLayout(LayoutKind.Auto)] private record struct State @@ -81,9 +82,13 @@ public sealed class Bwma : AbstractBase protected override void Dispose(bool disposing) { - if (disposing && _source != null && _pubHandler != null) + if (!_disposed) { - _source.Pub -= _pubHandler; + if (disposing && _source != null && _pubHandler != null) + { + _source.Pub -= _pubHandler; + } + _disposed = true; } base.Dispose(disposing); } @@ -199,7 +204,7 @@ public sealed class Bwma : AbstractBase var tSpan = CollectionsMarshal.AsSpan(t); var vSpan = CollectionsMarshal.AsSpan(v); - Calculate(source.Values, vSpan, _period, _order); + Batch(source.Values, vSpan, _period, _order); source.Times.CopyTo(tSpan); _buffer.Clear(); @@ -338,7 +343,7 @@ public sealed class Bwma : AbstractBase } [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void Calculate(ReadOnlySpan source, Span output, int period, int order = 0) + public static void Batch(ReadOnlySpan source, Span output, int period, int order = 0) { if (period <= 0) { @@ -477,6 +482,13 @@ public sealed class Bwma : AbstractBase } } + public static (TSeries Results, Bwma Indicator) Calculate(TSeries source, int period, int order = 0) + { + var indicator = new Bwma(period, order); + TSeries results = indicator.Update(source); + return (results, indicator); + } + public override void Reset() { _buffer.Clear(); @@ -484,4 +496,4 @@ public sealed class Bwma : AbstractBase _p_state = _state; Last = default; } -} +} \ No newline at end of file diff --git a/lib/trends_FIR/conv/Conv.cs b/lib/trends_FIR/conv/Conv.cs index bd74cbc9..a4b9257f 100644 --- a/lib/trends_FIR/conv/Conv.cs +++ b/lib/trends_FIR/conv/Conv.cs @@ -22,6 +22,7 @@ public sealed class Conv : AbstractBase private readonly ITValuePublisher? _source; private readonly TValuePublishedHandler? _subHandler; private bool _isNew = true; + private bool _disposed; private record struct State(double LastValidValue); private State _state; @@ -56,9 +57,13 @@ public sealed class Conv : AbstractBase protected override void Dispose(bool disposing) { - if (_source != null && _subHandler != null) + if (!_disposed) { - _source.Pub -= _subHandler; + if (disposing && _source != null && _subHandler != null) + { + _source.Pub -= _subHandler; + } + _disposed = true; } base.Dispose(disposing); } @@ -276,6 +281,13 @@ public sealed class Conv : AbstractBase } } + public static (TSeries Results, Conv Indicator) Calculate(TSeries source, double[] kernel) + { + var indicator = new Conv(kernel); + TSeries results = indicator.Update(source); + return (results, indicator); + } + public override void Reset() { _buffer.Clear(); @@ -283,4 +295,4 @@ public sealed class Conv : AbstractBase _p_state.LastValidValue = double.NaN; Last = default; } -} +} \ No newline at end of file diff --git a/lib/trends_FIR/dwma/Dwma.Tests.cs b/lib/trends_FIR/dwma/Dwma.Tests.cs index 88252330..f9bc9255 100644 --- a/lib/trends_FIR/dwma/Dwma.Tests.cs +++ b/lib/trends_FIR/dwma/Dwma.Tests.cs @@ -150,8 +150,8 @@ public class DwmaTests double[] output = new double[5]; double[] wrongSizeOutput = new double[3]; - Assert.Throws(() => Dwma.Calculate(source.AsSpan(), output.AsSpan(), 0)); - Assert.Throws(() => Dwma.Calculate(source.AsSpan(), wrongSizeOutput.AsSpan(), 3)); + Assert.Throws(() => Dwma.Batch(source.AsSpan(), output.AsSpan(), 0)); + Assert.Throws(() => Dwma.Batch(source.AsSpan(), wrongSizeOutput.AsSpan(), 3)); } [Fact] @@ -160,7 +160,7 @@ public class DwmaTests double[] source = [100, 110, double.NaN, 120, 130]; double[] output = new double[5]; - Dwma.Calculate(source.AsSpan(), output.AsSpan(), 3); + Dwma.Batch(source.AsSpan(), output.AsSpan(), 3); foreach (var val in output) { @@ -185,7 +185,7 @@ public class DwmaTests var tValues = series.Values.ToArray(); var spanInput = new ReadOnlySpan(tValues); var spanOutput = new double[tValues.Length]; - Dwma.Calculate(spanInput, spanOutput, period); + Dwma.Batch(spanInput, spanOutput, period); double spanResult = spanOutput[^1]; // 3. Streaming Mode diff --git a/lib/trends_FIR/dwma/Dwma.cs b/lib/trends_FIR/dwma/Dwma.cs index a8108c2c..0916bd77 100644 --- a/lib/trends_FIR/dwma/Dwma.cs +++ b/lib/trends_FIR/dwma/Dwma.cs @@ -22,6 +22,7 @@ public sealed class Dwma : AbstractBase private readonly Wma _wma2; private readonly ITValuePublisher? _source; private readonly TValuePublishedHandler? _handler; + private bool _disposed; private int _sampleCount; public override bool IsHot => _sampleCount >= WarmupPeriod; @@ -53,9 +54,13 @@ public sealed class Dwma : AbstractBase protected override void Dispose(bool disposing) { - if (disposing && _source != null && _handler != null) + if (!_disposed) { - _source.Pub -= _handler; + if (disposing && _source != null && _handler != null) + { + _source.Pub -= _handler; + } + _disposed = true; } base.Dispose(disposing); } @@ -91,7 +96,7 @@ public sealed class Dwma : AbstractBase var vSpan = CollectionsMarshal.AsSpan(v); source.Times.CopyTo(tSpan); - Calculate(source.Values, vSpan, _period); + Batch(source.Values, vSpan, _period); Reset(); @@ -129,7 +134,7 @@ public sealed class Dwma : AbstractBase } [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void Calculate(ReadOnlySpan source, Span output, int period) + public static void Batch(ReadOnlySpan source, Span output, int period) { if (period <= 0) { @@ -166,6 +171,13 @@ public sealed class Dwma : AbstractBase } } + public static (TSeries Results, Dwma Indicator) Calculate(TSeries source, int period) + { + var indicator = new Dwma(period); + TSeries results = indicator.Update(source); + return (results, indicator); + } + public override void Reset() { _wma1.Reset(); @@ -173,4 +185,4 @@ public sealed class Dwma : AbstractBase _sampleCount = 0; Last = default; } -} +} \ No newline at end of file diff --git a/lib/trends_FIR/gwma/Gwma.Tests.cs b/lib/trends_FIR/gwma/Gwma.Tests.cs index 972603c5..8b6a34e9 100644 --- a/lib/trends_FIR/gwma/Gwma.Tests.cs +++ b/lib/trends_FIR/gwma/Gwma.Tests.cs @@ -114,7 +114,7 @@ public class GwmaTests double[] input = series.Values.ToArray(); double[] output = new double[input.Length]; - Gwma.Calculate(input.AsSpan(), output.AsSpan(), 10); + Gwma.Batch(input.AsSpan(), output.AsSpan(), 10); for (int i = 0; i < input.Length; i++) { @@ -283,7 +283,7 @@ public class GwmaTests var tValues = series.Values.ToArray(); var spanInput = new ReadOnlySpan(tValues); var spanOutput = new double[tValues.Length]; - Gwma.Calculate(spanInput, spanOutput, period); + Gwma.Batch(spanInput, spanOutput, period); double spanResult = spanOutput[^1]; // 3. Streaming Mode @@ -316,11 +316,11 @@ public class GwmaTests double[] output = new double[5]; double[] wrongSizeOutput = new double[3]; - Assert.Throws(() => Gwma.Calculate(source.AsSpan(), output.AsSpan(), 0)); - Assert.Throws(() => Gwma.Calculate(source.AsSpan(), output.AsSpan(), 3, sigma: 0)); - Assert.Throws(() => Gwma.Calculate(source.AsSpan(), output.AsSpan(), 3, sigma: -1)); - Assert.Throws(() => Gwma.Calculate(source.AsSpan(), output.AsSpan(), 3, sigma: 1.1)); - Assert.Throws(() => Gwma.Calculate(source.AsSpan(), wrongSizeOutput.AsSpan(), 3)); + Assert.Throws(() => Gwma.Batch(source.AsSpan(), output.AsSpan(), 0)); + Assert.Throws(() => Gwma.Batch(source.AsSpan(), output.AsSpan(), 3, sigma: 0)); + Assert.Throws(() => Gwma.Batch(source.AsSpan(), output.AsSpan(), 3, sigma: -1)); + Assert.Throws(() => Gwma.Batch(source.AsSpan(), output.AsSpan(), 3, sigma: 1.1)); + Assert.Throws(() => Gwma.Batch(source.AsSpan(), wrongSizeOutput.AsSpan(), 3)); } [Fact] @@ -329,7 +329,7 @@ public class GwmaTests double[] source = [100, 110, double.NaN, 120, 130]; double[] output = new double[5]; - Gwma.Calculate(source.AsSpan(), output.AsSpan(), 3); + Gwma.Batch(source.AsSpan(), output.AsSpan(), 3); foreach (var val in output) { diff --git a/lib/trends_FIR/gwma/Gwma.Validation.Tests.cs b/lib/trends_FIR/gwma/Gwma.Validation.Tests.cs index f1b44912..27379e2c 100644 --- a/lib/trends_FIR/gwma/Gwma.Validation.Tests.cs +++ b/lib/trends_FIR/gwma/Gwma.Validation.Tests.cs @@ -83,7 +83,7 @@ public sealed class GwmaValidationTests : IDisposable { // Calculate QuanTAlib GWMA (Span API) double[] spanOutput = new double[sourceData.Length]; - Gwma.Calculate(sourceData, spanOutput.AsSpan(), period, sigma); + Gwma.Batch(sourceData, spanOutput.AsSpan(), period, sigma); // Calculate QuanTAlib GWMA (batch TSeries) var gwmaBatch = new Gwma(period, sigma); diff --git a/lib/trends_FIR/gwma/Gwma.cs b/lib/trends_FIR/gwma/Gwma.cs index f52332c2..732e8989 100644 --- a/lib/trends_FIR/gwma/Gwma.cs +++ b/lib/trends_FIR/gwma/Gwma.cs @@ -25,6 +25,7 @@ public sealed class Gwma : AbstractBase private readonly ITValuePublisher? _source; private readonly TValuePublishedHandler? _pubHandler; private bool _isNew = true; + private bool _disposed; [StructLayout(LayoutKind.Auto)] private record struct State @@ -84,9 +85,13 @@ public sealed class Gwma : AbstractBase protected override void Dispose(bool disposing) { - if (disposing && _source != null && _pubHandler != null) + if (!_disposed) { - _source.Pub -= _pubHandler; + if (disposing && _source != null && _pubHandler != null) + { + _source.Pub -= _pubHandler; + } + _disposed = true; } base.Dispose(disposing); } @@ -178,7 +183,7 @@ public sealed class Gwma : AbstractBase var tSpan = CollectionsMarshal.AsSpan(t); var vSpan = CollectionsMarshal.AsSpan(v); - Calculate(source.Values, vSpan, _period, _sigma); + Batch(source.Values, vSpan, _period, _sigma); source.Times.CopyTo(tSpan); // Restore internal state to match the streaming path: @@ -288,7 +293,7 @@ public sealed class Gwma : AbstractBase } [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void Calculate(ReadOnlySpan source, Span output, int period, double sigma = 0.4) + public static void Batch(ReadOnlySpan source, Span output, int period, double sigma = 0.4) { if (period <= 0) { @@ -438,6 +443,13 @@ public sealed class Gwma : AbstractBase } } + public static (TSeries Results, Gwma Indicator) Calculate(TSeries source, int period, double sigma = 0.4) + { + var indicator = new Gwma(period, sigma); + TSeries results = indicator.Update(source); + return (results, indicator); + } + public override void Reset() { _buffer.Clear(); @@ -445,4 +457,4 @@ public sealed class Gwma : AbstractBase _p_state = _state; Last = default; } -} +} \ No newline at end of file diff --git a/lib/trends_FIR/hamma/Hamma.Tests.cs b/lib/trends_FIR/hamma/Hamma.Tests.cs index 3bbc870b..fe02e46d 100644 --- a/lib/trends_FIR/hamma/Hamma.Tests.cs +++ b/lib/trends_FIR/hamma/Hamma.Tests.cs @@ -108,7 +108,7 @@ public class HammaTests double[] input = series.Values.ToArray(); double[] output = new double[input.Length]; - Hamma.Calculate(input.AsSpan(), output.AsSpan(), 10); + Hamma.Batch(input.AsSpan(), output.AsSpan(), 10); for (int i = 0; i < input.Length; i++) { @@ -277,7 +277,7 @@ public class HammaTests var tValues = series.Values.ToArray(); var spanInput = new ReadOnlySpan(tValues); var spanOutput = new double[tValues.Length]; - Hamma.Calculate(spanInput, spanOutput, period); + Hamma.Batch(spanInput, spanOutput, period); double spanResult = spanOutput[^1]; // 3. Streaming Mode @@ -310,8 +310,8 @@ public class HammaTests double[] output = new double[5]; double[] wrongSizeOutput = new double[3]; - Assert.Throws(() => Hamma.Calculate(source.AsSpan(), output.AsSpan(), 0)); - Assert.Throws(() => Hamma.Calculate(source.AsSpan(), wrongSizeOutput.AsSpan(), 3)); + Assert.Throws(() => Hamma.Batch(source.AsSpan(), output.AsSpan(), 0)); + Assert.Throws(() => Hamma.Batch(source.AsSpan(), wrongSizeOutput.AsSpan(), 3)); } [Fact] @@ -320,7 +320,7 @@ public class HammaTests double[] source = [100, 110, double.NaN, 120, 130]; double[] output = new double[5]; - Hamma.Calculate(source.AsSpan(), output.AsSpan(), 3); + Hamma.Batch(source.AsSpan(), output.AsSpan(), 3); foreach (var val in output) { diff --git a/lib/trends_FIR/hamma/Hamma.Validation.Tests.cs b/lib/trends_FIR/hamma/Hamma.Validation.Tests.cs index e478175a..abf1d0de 100644 --- a/lib/trends_FIR/hamma/Hamma.Validation.Tests.cs +++ b/lib/trends_FIR/hamma/Hamma.Validation.Tests.cs @@ -75,7 +75,7 @@ public sealed class HammaValidationTests : IDisposable { // Calculate QuanTAlib HAMMA (Span API) double[] spanOutput = new double[sourceData.Length]; - Hamma.Calculate(sourceData, spanOutput.AsSpan(), period); + Hamma.Batch(sourceData, spanOutput.AsSpan(), period); // Calculate QuanTAlib HAMMA (batch TSeries) var hammaBatch = new Hamma(period); diff --git a/lib/trends_FIR/hamma/Hamma.cs b/lib/trends_FIR/hamma/Hamma.cs index 01f4f4a9..dae49415 100644 --- a/lib/trends_FIR/hamma/Hamma.cs +++ b/lib/trends_FIR/hamma/Hamma.cs @@ -24,6 +24,7 @@ public sealed class Hamma : AbstractBase private readonly ITValuePublisher? _source; private readonly TValuePublishedHandler? _pubHandler; private bool _isNew = true; + private bool _disposed; [StructLayout(LayoutKind.Auto)] private record struct State @@ -78,9 +79,13 @@ public sealed class Hamma : AbstractBase protected override void Dispose(bool disposing) { - if (disposing && _source != null && _pubHandler != null) + if (!_disposed) { - _source.Pub -= _pubHandler; + if (disposing && _source != null && _pubHandler != null) + { + _source.Pub -= _pubHandler; + } + _disposed = true; } base.Dispose(disposing); } @@ -183,7 +188,7 @@ public sealed class Hamma : AbstractBase var tSpan = CollectionsMarshal.AsSpan(t); var vSpan = CollectionsMarshal.AsSpan(v); - Calculate(source.Values, vSpan, _period); + Batch(source.Values, vSpan, _period); source.Times.CopyTo(tSpan); // Restore state @@ -271,7 +276,7 @@ public sealed class Hamma : AbstractBase /// Lookback period for the Hamming window (default: 10) /// Thrown when output length doesn't match source length. [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void Calculate(ReadOnlySpan source, Span output, int period = 10) + public static void Batch(ReadOnlySpan source, Span output, int period = 10) { if (period <= 0) { @@ -389,6 +394,13 @@ public sealed class Hamma : AbstractBase } } + public static (TSeries Results, Hamma Indicator) Calculate(TSeries source, int period = 10) + { + var indicator = new Hamma(period); + TSeries results = indicator.Update(source); + return (results, indicator); + } + public override void Reset() { _buffer.Clear(); @@ -397,4 +409,4 @@ public sealed class Hamma : AbstractBase _p_state = _state; Last = default; } -} +} \ No newline at end of file diff --git a/lib/trends_FIR/hanma/Hanma.Tests.cs b/lib/trends_FIR/hanma/Hanma.Tests.cs index a5cd1d3d..062ed0f8 100644 --- a/lib/trends_FIR/hanma/Hanma.Tests.cs +++ b/lib/trends_FIR/hanma/Hanma.Tests.cs @@ -111,7 +111,7 @@ public class HanmaTests double[] input = series.Values.ToArray(); double[] output = new double[input.Length]; - Hanma.Calculate(input.AsSpan(), output.AsSpan(), 10); + Hanma.Batch(input.AsSpan(), output.AsSpan(), 10); for (int i = 0; i < input.Length; i++) { @@ -292,7 +292,7 @@ public class HanmaTests var tValues = series.Values.ToArray(); var spanInput = new ReadOnlySpan(tValues); var spanOutput = new double[tValues.Length]; - Hanma.Calculate(spanInput, spanOutput, period); + Hanma.Batch(spanInput, spanOutput, period); double spanResult = spanOutput[^1]; // 3. Streaming Mode @@ -325,8 +325,8 @@ public class HanmaTests double[] output = new double[5]; double[] wrongSizeOutput = new double[3]; - Assert.Throws(() => Hanma.Calculate(source.AsSpan(), output.AsSpan(), 0)); - Assert.Throws(() => Hanma.Calculate(source.AsSpan(), wrongSizeOutput.AsSpan(), 3)); + Assert.Throws(() => Hanma.Batch(source.AsSpan(), output.AsSpan(), 0)); + Assert.Throws(() => Hanma.Batch(source.AsSpan(), wrongSizeOutput.AsSpan(), 3)); } [Fact] @@ -335,7 +335,7 @@ public class HanmaTests double[] source = [100, 110, double.NaN, 120, 130]; double[] output = new double[5]; - Hanma.Calculate(source.AsSpan(), output.AsSpan(), 3); + Hanma.Batch(source.AsSpan(), output.AsSpan(), 3); foreach (var val in output) { diff --git a/lib/trends_FIR/hanma/Hanma.Validation.Tests.cs b/lib/trends_FIR/hanma/Hanma.Validation.Tests.cs index ecb505f2..2ba33374 100644 --- a/lib/trends_FIR/hanma/Hanma.Validation.Tests.cs +++ b/lib/trends_FIR/hanma/Hanma.Validation.Tests.cs @@ -104,7 +104,7 @@ public class HanmaValidationTests // Span double[] input = series.Values.ToArray(); double[] spanOutput = new double[input.Length]; - Hanma.Calculate(input.AsSpan(), spanOutput.AsSpan(), period); + Hanma.Batch(input.AsSpan(), spanOutput.AsSpan(), period); // All should match for (int i = 0; i < series.Count; i++) diff --git a/lib/trends_FIR/hanma/Hanma.cs b/lib/trends_FIR/hanma/Hanma.cs index 626c18f7..eab38ed5 100644 --- a/lib/trends_FIR/hanma/Hanma.cs +++ b/lib/trends_FIR/hanma/Hanma.cs @@ -24,6 +24,7 @@ public sealed class Hanma : AbstractBase private readonly ITValuePublisher? _source; private readonly TValuePublishedHandler? _pubHandler; private bool _isNew = true; + private bool _disposed; [StructLayout(LayoutKind.Auto)] private record struct State(double LastValidValue, bool IsInitialized); @@ -56,7 +57,7 @@ public sealed class Hanma : AbstractBase } /// - /// + /// Creates HANMA chained to a source publisher for event-based updates. /// /// Data source for event-based updates /// Lookback period for the Hanning window (default: 10) @@ -73,9 +74,13 @@ public sealed class Hanma : AbstractBase protected override void Dispose(bool disposing) { - if (disposing && _source != null && _pubHandler != null) + if (!_disposed) { - _source.Pub -= _pubHandler; + if (disposing && _source != null && _pubHandler != null) + { + _source.Pub -= _pubHandler; + } + _disposed = true; } base.Dispose(disposing); } @@ -177,7 +182,7 @@ public sealed class Hanma : AbstractBase var tSpan = CollectionsMarshal.AsSpan(t); var vSpan = CollectionsMarshal.AsSpan(v); - Calculate(source.Values, vSpan, _period); + Batch(source.Values, vSpan, _period); source.Times.CopyTo(tSpan); // Restore state @@ -277,7 +282,7 @@ public sealed class Hanma : AbstractBase /// Lookback period for the Hanning window (default: 10) /// Thrown when output length doesn't match source length. [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void Calculate(ReadOnlySpan source, Span output, int period = 10) + public static void Batch(ReadOnlySpan source, Span output, int period = 10) { if (period <= 0) { @@ -425,6 +430,13 @@ public sealed class Hanma : AbstractBase } } + public static (TSeries Results, Hanma Indicator) Calculate(TSeries source, int period = 10) + { + var indicator = new Hanma(period); + TSeries results = indicator.Update(source); + return (results, indicator); + } + public override void Reset() { _buffer.Clear(); @@ -432,4 +444,4 @@ public sealed class Hanma : AbstractBase _p_state = _state; Last = default; } -} +} \ No newline at end of file diff --git a/lib/trends_FIR/hma/Hma.Tests.cs b/lib/trends_FIR/hma/Hma.Tests.cs index 6ae445dc..0db76898 100644 --- a/lib/trends_FIR/hma/Hma.Tests.cs +++ b/lib/trends_FIR/hma/Hma.Tests.cs @@ -114,7 +114,7 @@ public class HmaTests double[] input = series.Values.ToArray(); double[] output = new double[input.Length]; - Hma.Calculate(input.AsSpan(), output.AsSpan(), 14); + Hma.Batch(input.AsSpan(), output.AsSpan(), 14); for (int i = 0; i < input.Length; i++) { @@ -219,8 +219,8 @@ public class HmaTests double[] output = new double[5]; double[] wrongSizeOutput = new double[3]; - Assert.Throws(() => Hma.Calculate(source.AsSpan(), output.AsSpan(), 0)); - Assert.Throws(() => Hma.Calculate(source.AsSpan(), wrongSizeOutput.AsSpan(), 3)); + Assert.Throws(() => Hma.Batch(source.AsSpan(), output.AsSpan(), 0)); + Assert.Throws(() => Hma.Batch(source.AsSpan(), wrongSizeOutput.AsSpan(), 3)); } [Fact] @@ -229,7 +229,7 @@ public class HmaTests double[] source = [100, 110, double.NaN, 120, 130]; double[] output = new double[5]; - Hma.Calculate(source.AsSpan(), output.AsSpan(), 3); + Hma.Batch(source.AsSpan(), output.AsSpan(), 3); foreach (var val in output) { @@ -254,7 +254,7 @@ public class HmaTests var tValues = series.Values.ToArray(); var spanInput = new ReadOnlySpan(tValues); var spanOutput = new double[tValues.Length]; - Hma.Calculate(spanInput, spanOutput, period); + Hma.Batch(spanInput, spanOutput, period); double spanResult = spanOutput[^1]; // 3. Streaming Mode diff --git a/lib/trends_FIR/hma/Hma.Validation.Tests.cs b/lib/trends_FIR/hma/Hma.Validation.Tests.cs index 0e40d356..084f91bb 100644 --- a/lib/trends_FIR/hma/Hma.Validation.Tests.cs +++ b/lib/trends_FIR/hma/Hma.Validation.Tests.cs @@ -140,7 +140,7 @@ public sealed class HmaValidationTests : IDisposable { // Calculate QuanTAlib HMA (Span API) double[] qOutput = new double[sourceData.Length]; - global::QuanTAlib.Hma.Calculate(sourceData.AsSpan(), qOutput.AsSpan(), period); + global::QuanTAlib.Hma.Batch(sourceData.AsSpan(), qOutput.AsSpan(), period); // Calculate Skender HMA var sResult = _testData.SkenderQuotes.GetHma(period).ToList(); diff --git a/lib/trends_FIR/hma/Hma.cs b/lib/trends_FIR/hma/Hma.cs index e45b5174..3636e5cb 100644 --- a/lib/trends_FIR/hma/Hma.cs +++ b/lib/trends_FIR/hma/Hma.cs @@ -94,7 +94,7 @@ public sealed class Hma : AbstractBase var tSpan = CollectionsMarshal.AsSpan(t); var vSpan = CollectionsMarshal.AsSpan(v); - Calculate(source.Values, vSpan, _period); + Batch(source.Values, vSpan, _period); source.Times.CopyTo(tSpan); // Restore state for streaming @@ -144,14 +144,14 @@ public sealed class Hma : AbstractBase CollectionsMarshal.SetCount(t, len); CollectionsMarshal.SetCount(v, len); - Calculate(source.Values, CollectionsMarshal.AsSpan(v), period); + Batch(source.Values, CollectionsMarshal.AsSpan(v), period); source.Times.CopyTo(CollectionsMarshal.AsSpan(t)); return new TSeries(t, v); } [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void Calculate(ReadOnlySpan source, Span output, int period) + public static void Batch(ReadOnlySpan source, Span output, int period) { if (source.Length != output.Length) { @@ -204,6 +204,13 @@ public sealed class Hma : AbstractBase } } + public static (TSeries Results, Hma Indicator) Calculate(TSeries source, int period) + { + var indicator = new Hma(period); + TSeries results = indicator.Update(source); + return (results, indicator); + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] private static void CalculateIntermediate(ReadOnlySpan halfWma, ReadOnlySpan fullWma, Span output) { @@ -262,4 +269,4 @@ public sealed class Hma : AbstractBase _sampleCount = 0; Last = default; } -} +} \ No newline at end of file diff --git a/lib/trends_FIR/hwma/Hwma.Tests.cs b/lib/trends_FIR/hwma/Hwma.Tests.cs index 52c1ada0..7829f51b 100644 --- a/lib/trends_FIR/hwma/Hwma.Tests.cs +++ b/lib/trends_FIR/hwma/Hwma.Tests.cs @@ -122,7 +122,7 @@ public class HwmaTests double[] input = series.Values.ToArray(); double[] output = new double[input.Length]; - Hwma.Calculate(input.AsSpan(), output.AsSpan(), 10); + Hwma.Batch(input.AsSpan(), output.AsSpan(), 10); for (int i = 0; i < input.Length; i++) { @@ -292,7 +292,7 @@ public class HwmaTests var tValues = series.Values.ToArray(); var spanInput = new ReadOnlySpan(tValues); var spanOutput = new double[tValues.Length]; - Hwma.Calculate(spanInput, spanOutput, period); + Hwma.Batch(spanInput, spanOutput, period); double spanResult = spanOutput[^1]; // 3. Streaming Mode @@ -325,8 +325,8 @@ public class HwmaTests double[] output = new double[5]; double[] wrongSizeOutput = new double[3]; - Assert.Throws(() => Hwma.Calculate(source.AsSpan(), output.AsSpan(), 0)); - Assert.Throws(() => Hwma.Calculate(source.AsSpan(), wrongSizeOutput.AsSpan(), 3)); + Assert.Throws(() => Hwma.Batch(source.AsSpan(), output.AsSpan(), 0)); + Assert.Throws(() => Hwma.Batch(source.AsSpan(), wrongSizeOutput.AsSpan(), 3)); } [Fact] @@ -335,7 +335,7 @@ public class HwmaTests double[] source = [100, 110, double.NaN, 120, 130]; double[] output = new double[5]; - Hwma.Calculate(source.AsSpan(), output.AsSpan(), 3); + Hwma.Batch(source.AsSpan(), output.AsSpan(), 3); foreach (var val in output) { diff --git a/lib/trends_FIR/hwma/Hwma.Validation.Tests.cs b/lib/trends_FIR/hwma/Hwma.Validation.Tests.cs index 97aa7dd6..e8484001 100644 --- a/lib/trends_FIR/hwma/Hwma.Validation.Tests.cs +++ b/lib/trends_FIR/hwma/Hwma.Validation.Tests.cs @@ -99,7 +99,7 @@ public class HwmaValidationTests // Span double[] input = series.Values.ToArray(); double[] spanOutput = new double[input.Length]; - Hwma.Calculate(input.AsSpan(), spanOutput.AsSpan(), period); + Hwma.Batch(input.AsSpan(), spanOutput.AsSpan(), period); // All should match for (int i = 0; i < series.Count; i++) diff --git a/lib/trends_FIR/hwma/Hwma.cs b/lib/trends_FIR/hwma/Hwma.cs index efb5b4f6..b2707c7c 100644 --- a/lib/trends_FIR/hwma/Hwma.cs +++ b/lib/trends_FIR/hwma/Hwma.cs @@ -26,6 +26,7 @@ public sealed class Hwma : AbstractBase private readonly ITValuePublisher? _source; private readonly TValuePublishedHandler? _pubHandler; private bool _isNew = true; + private bool _disposed; [StructLayout(LayoutKind.Auto)] private record struct State( @@ -118,9 +119,13 @@ public sealed class Hwma : AbstractBase protected override void Dispose(bool disposing) { - if (disposing && _source != null && _pubHandler != null) + if (!_disposed) { - _source.Pub -= _pubHandler; + if (disposing && _source != null && _pubHandler != null) + { + _source.Pub -= _pubHandler; + } + _disposed = true; } base.Dispose(disposing); } @@ -263,7 +268,7 @@ public sealed class Hwma : AbstractBase /// Period for smoothing factors (default: 10) /// Thrown when output length doesn't match source length. [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void Calculate(ReadOnlySpan source, Span output, int period = 10) + public static void Batch(ReadOnlySpan source, Span output, int period = 10) { if (period <= 0) { @@ -342,10 +347,17 @@ public sealed class Hwma : AbstractBase } } + public static (TSeries Results, Hwma Indicator) Calculate(TSeries source, int period = 10) + { + var indicator = new Hwma(period); + TSeries results = indicator.Update(source); + return (results, indicator); + } + public override void Reset() { _state = new State(double.NaN, 0, 0, double.NaN, IsInitialized: false); _p_state = _state; Last = default; } -} +} \ No newline at end of file diff --git a/lib/trends_FIR/lsma/Lsma.Tests.cs b/lib/trends_FIR/lsma/Lsma.Tests.cs index 5a78f20d..d32b1bb6 100644 --- a/lib/trends_FIR/lsma/Lsma.Tests.cs +++ b/lib/trends_FIR/lsma/Lsma.Tests.cs @@ -161,7 +161,7 @@ public class LsmaTests values[i] = bar.Close; } - Lsma.Calculate(values, output, period); + Lsma.Batch(values, output, period); var lsma = new Lsma(period); for (int i = 0; i < count; i++) diff --git a/lib/trends_FIR/lsma/Lsma.Validation.Tests.cs b/lib/trends_FIR/lsma/Lsma.Validation.Tests.cs index a443ff59..8580d5c6 100644 --- a/lib/trends_FIR/lsma/Lsma.Validation.Tests.cs +++ b/lib/trends_FIR/lsma/Lsma.Validation.Tests.cs @@ -67,7 +67,7 @@ public class LsmaValidationTests { // Calculate QuanTAlib LSMA (Span API) double[] qOutput = new double[_testData.RawData.Length]; - global::QuanTAlib.Lsma.Calculate(_testData.RawData.Span, qOutput.AsSpan(), period); + global::QuanTAlib.Lsma.Batch(_testData.RawData.Span, qOutput.AsSpan(), period); // Calculate Skender EPMA var sResult = _testData.SkenderQuotes.GetEpma(period).ToList(); diff --git a/lib/trends_FIR/lsma/Lsma.cs b/lib/trends_FIR/lsma/Lsma.cs index 5ef5c5b0..1e46c574 100644 --- a/lib/trends_FIR/lsma/Lsma.cs +++ b/lib/trends_FIR/lsma/Lsma.cs @@ -221,7 +221,7 @@ public sealed class Lsma : AbstractBase var vSpan = CollectionsMarshal.AsSpan(v); double initialLastValid = _state.LastValidValue; - Calculate(source.Values, vSpan, _period, _offset, initialLastValid); + Batch(source.Values, vSpan, _period, _offset, initialLastValid); source.Times.CopyTo(tSpan); // Restore state @@ -282,7 +282,7 @@ public sealed class Lsma : AbstractBase /// Zero-allocation method for maximum performance. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void Calculate(ReadOnlySpan source, Span output, int period, int offset = 0, double initialLastValid = double.NaN) + public static void Batch(ReadOnlySpan source, Span output, int period, int offset = 0, double initialLastValid = double.NaN) { if (source.Length != output.Length) { @@ -395,6 +395,13 @@ public sealed class Lsma : AbstractBase } } + public static (TSeries Results, Lsma Indicator) Calculate(TSeries source, int period, int offset = 0) + { + var indicator = new Lsma(period, offset); + TSeries results = indicator.Update(source); + return (results, indicator); + } + /// /// Resets the LSMA state. /// @@ -422,4 +429,4 @@ public sealed class Lsma : AbstractBase } base.Dispose(disposing); } -} +} \ No newline at end of file diff --git a/lib/trends_FIR/pwma/Pwma.Tests.cs b/lib/trends_FIR/pwma/Pwma.Tests.cs index 0bddb2d5..154ac4d8 100644 --- a/lib/trends_FIR/pwma/Pwma.Tests.cs +++ b/lib/trends_FIR/pwma/Pwma.Tests.cs @@ -300,11 +300,11 @@ public class PwmaTests double[] wrongSizeOutput = new double[3]; // Period must be > 0 - Assert.Throws(() => Pwma.Calculate(source.AsSpan(), output.AsSpan(), 0)); - Assert.Throws(() => Pwma.Calculate(source.AsSpan(), output.AsSpan(), -1)); + Assert.Throws(() => Pwma.Batch(source.AsSpan(), output.AsSpan(), 0)); + Assert.Throws(() => Pwma.Batch(source.AsSpan(), output.AsSpan(), -1)); // Output must be same length as source - Assert.Throws(() => Pwma.Calculate(source.AsSpan(), wrongSizeOutput.AsSpan(), 3)); + Assert.Throws(() => Pwma.Batch(source.AsSpan(), wrongSizeOutput.AsSpan(), 3)); } [Fact] @@ -326,7 +326,7 @@ public class PwmaTests var tseriesResult = Pwma.Batch(series, 10); // Calculate with Span API - Pwma.Calculate(source.AsSpan(), output.AsSpan(), 10); + Pwma.Batch(source.AsSpan(), output.AsSpan(), 10); // Compare results for (int i = 0; i < 100; i++) @@ -341,7 +341,7 @@ public class PwmaTests double[] source = [10, 20, 30]; double[] output = new double[3]; - Pwma.Calculate(source.AsSpan(), output.AsSpan(), 3); + Pwma.Batch(source.AsSpan(), output.AsSpan(), 3); // PWMA(3) warmup: // i=0: 10 (1^2*10 / 1^2) = 10 @@ -369,7 +369,7 @@ public class PwmaTests var tValues = series.Values.ToArray(); var spanInput = new ReadOnlySpan(tValues); var spanOutput = new double[tValues.Length]; - Pwma.Calculate(spanInput, spanOutput, period); + Pwma.Batch(spanInput, spanOutput, period); double spanResult = spanOutput[^1]; // 3. Streaming Mode diff --git a/lib/trends_FIR/pwma/Pwma.cs b/lib/trends_FIR/pwma/Pwma.cs index 08a7d30a..a6c67f9f 100644 --- a/lib/trends_FIR/pwma/Pwma.cs +++ b/lib/trends_FIR/pwma/Pwma.cs @@ -175,7 +175,7 @@ public sealed class Pwma : AbstractBase var tSpan = CollectionsMarshal.AsSpan(t); var vSpan = CollectionsMarshal.AsSpan(v); - Calculate(source.Values, vSpan, _period); + Batch(source.Values, vSpan, _period); source.Times.CopyTo(tSpan); // Restore state @@ -234,7 +234,7 @@ public sealed class Pwma : AbstractBase } [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void Calculate(ReadOnlySpan source, Span output, int period) + public static void Batch(ReadOnlySpan source, Span output, int period) { if (source.Length != output.Length) { @@ -255,6 +255,13 @@ public sealed class Pwma : AbstractBase CalculateScalarCore(source, output, period); } + public static (TSeries Results, Pwma Indicator) Calculate(TSeries source, int period) + { + var indicator = new Pwma(period); + TSeries results = indicator.Update(source); + return (results, indicator); + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] private static void CalculateScalarCore(ReadOnlySpan source, Span output, int period) { @@ -356,4 +363,4 @@ public sealed class Pwma : AbstractBase _p_state = default; Last = default; } -} +} \ No newline at end of file diff --git a/lib/trends_FIR/sgma/Sgma.Tests.cs b/lib/trends_FIR/sgma/Sgma.Tests.cs index 07d016e4..1d5842e6 100644 --- a/lib/trends_FIR/sgma/Sgma.Tests.cs +++ b/lib/trends_FIR/sgma/Sgma.Tests.cs @@ -109,7 +109,7 @@ public class SgmaTests double[] input = series.Values.ToArray(); double[] output = new double[input.Length]; - Sgma.Calculate(input.AsSpan(), output.AsSpan(), 9); + Sgma.Batch(input.AsSpan(), output.AsSpan(), 9); for (int i = 0; i < input.Length; i++) { @@ -268,7 +268,7 @@ public class SgmaTests var tValues = series.Values.ToArray(); var spanInput = new ReadOnlySpan(tValues); var spanOutput = new double[tValues.Length]; - Sgma.Calculate(spanInput, spanOutput, period); + Sgma.Batch(spanInput, spanOutput, period); double spanResult = spanOutput[^1]; var streamingInd = new Sgma(period); @@ -298,9 +298,9 @@ public class SgmaTests double[] output = new double[5]; double[] wrongSizeOutput = new double[3]; - Assert.Throws(() => Sgma.Calculate(source.AsSpan(), output.AsSpan(), 2)); - Assert.Throws(() => Sgma.Calculate(source.AsSpan(), output.AsSpan(), 5, 5)); - Assert.Throws(() => Sgma.Calculate(source.AsSpan(), wrongSizeOutput.AsSpan(), 3)); + Assert.Throws(() => Sgma.Batch(source.AsSpan(), output.AsSpan(), 2)); + Assert.Throws(() => Sgma.Batch(source.AsSpan(), output.AsSpan(), 5, 5)); + Assert.Throws(() => Sgma.Batch(source.AsSpan(), wrongSizeOutput.AsSpan(), 3)); } [Fact] @@ -309,7 +309,7 @@ public class SgmaTests double[] source = [100, 110, double.NaN, 120, 130]; double[] output = new double[5]; - Sgma.Calculate(source.AsSpan(), output.AsSpan(), 3); + Sgma.Batch(source.AsSpan(), output.AsSpan(), 3); foreach (var val in output) { diff --git a/lib/trends_FIR/sgma/Sgma.Validation.Tests.cs b/lib/trends_FIR/sgma/Sgma.Validation.Tests.cs index 6569845c..03eae961 100644 --- a/lib/trends_FIR/sgma/Sgma.Validation.Tests.cs +++ b/lib/trends_FIR/sgma/Sgma.Validation.Tests.cs @@ -65,7 +65,7 @@ public class SgmaValidationTests double[] sgmaOutput = new double[input.Length]; double[] smaOutput = new double[input.Length]; - Sgma.Calculate(input.AsSpan(), sgmaOutput.AsSpan(), 9, 0); + Sgma.Batch(input.AsSpan(), sgmaOutput.AsSpan(), 9, 0); Sma.Batch(input.AsSpan(), smaOutput.AsSpan(), 9); for (int i = 0; i < input.Length; i++) @@ -209,7 +209,7 @@ public class SgmaValidationTests // Span double[] input = series.Values.ToArray(); double[] output = new double[input.Length]; - Sgma.Calculate(input.AsSpan(), output.AsSpan(), 9, degree); + Sgma.Batch(input.AsSpan(), output.AsSpan(), 9, degree); Assert.Equal(expected, output[^1], Tolerance); // Streaming @@ -238,7 +238,7 @@ public class SgmaValidationTests double[] sourceWithNaN = [100, 110, 120, double.NaN, 140, 150, 160, 170, 180]; double[] output = new double[sourceWithNaN.Length]; - Sgma.Calculate(sourceWithNaN.AsSpan(), output.AsSpan(), 5, 2); + Sgma.Batch(sourceWithNaN.AsSpan(), output.AsSpan(), 5, 2); // All outputs should be finite foreach (var val in output) diff --git a/lib/trends_FIR/sgma/Sgma.cs b/lib/trends_FIR/sgma/Sgma.cs index 3faedfc4..17654988 100644 --- a/lib/trends_FIR/sgma/Sgma.cs +++ b/lib/trends_FIR/sgma/Sgma.cs @@ -25,6 +25,7 @@ public sealed class Sgma : AbstractBase private readonly ITValuePublisher? _source; private readonly TValuePublishedHandler? _pubHandler; private bool _isNew = true; + private bool _disposed; private double _lastValidValue = double.NaN; private double _p_lastValidValue = double.NaN; @@ -163,9 +164,13 @@ public sealed class Sgma : AbstractBase protected override void Dispose(bool disposing) { - if (disposing && _source != null && _pubHandler != null) + if (!_disposed) { - _source.Pub -= _pubHandler; + if (disposing && _source != null && _pubHandler != null) + { + _source.Pub -= _pubHandler; + } + _disposed = true; } base.Dispose(disposing); } @@ -277,7 +282,7 @@ public sealed class Sgma : AbstractBase var tSpan = CollectionsMarshal.AsSpan(t); var vSpan = CollectionsMarshal.AsSpan(v); - Calculate(source.Values, vSpan, _period, _degree); + Batch(source.Values, vSpan, _period, _degree); source.Times.CopyTo(tSpan); // Restore state by replaying last period bars @@ -317,7 +322,7 @@ public sealed class Sgma : AbstractBase /// Polynomial degree (default: 2) /// Thrown when output length doesn't match source length. [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void Calculate(ReadOnlySpan source, Span output, int period = 9, int degree = 2) + public static void Batch(ReadOnlySpan source, Span output, int period = 9, int degree = 2) { if (period < 3) { @@ -422,6 +427,13 @@ public sealed class Sgma : AbstractBase } } + public static (TSeries Results, Sgma Indicator) Calculate(TSeries source, int period = 9, int degree = 2) + { + var indicator = new Sgma(period, degree); + TSeries results = indicator.Update(source); + return (results, indicator); + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] private static double CalculateWeightedSumFull(RingBuffer buffer, double[] weights, double invWeightSum, double fallbackValue) { @@ -557,4 +569,4 @@ public sealed class Sgma : AbstractBase _p_lastValidValue = double.NaN; Last = default; } -} +} \ No newline at end of file diff --git a/lib/trends_FIR/trima/Trima.cs b/lib/trends_FIR/trima/Trima.cs index 095d79ed..dbfa5c69 100644 --- a/lib/trends_FIR/trima/Trima.cs +++ b/lib/trends_FIR/trima/Trima.cs @@ -165,4 +165,11 @@ public sealed class Trima : AbstractBase ArrayPool.Shared.Return(tempArray); } } -} + + public static (TSeries Results, Trima Indicator) Calculate(TSeries source, int period) + { + var indicator = new Trima(period); + TSeries results = indicator.Update(source); + return (results, indicator); + } +} \ No newline at end of file diff --git a/lib/trends_FIR/wma/Wma.cs b/lib/trends_FIR/wma/Wma.cs index 09b34682..c08d4c36 100644 --- a/lib/trends_FIR/wma/Wma.cs +++ b/lib/trends_FIR/wma/Wma.cs @@ -24,6 +24,7 @@ public sealed class Wma : AbstractBase private readonly RingBuffer _buffer; private readonly ITValuePublisher? _source; private readonly TValuePublishedHandler? _handler; + private bool _disposed; [StructLayout(LayoutKind.Auto)] private record struct State(double Sum, double WSum, double LastInput, double LastValidValue, int TickCount, bool HasSeenValidData); @@ -68,9 +69,13 @@ public sealed class Wma : AbstractBase protected override void Dispose(bool disposing) { - if (_source != null && _handler != null) + if (!_disposed) { - _source.Pub -= _handler; + if (disposing && _source != null && _handler != null) + { + _source.Pub -= _handler; + } + _disposed = true; } base.Dispose(disposing); } @@ -298,6 +303,13 @@ public sealed class Wma : AbstractBase CalculateScalarCore(source, output, period); } + public static (TSeries Results, Wma Indicator) Calculate(TSeries source, int period) + { + var indicator = new Wma(period); + TSeries results = indicator.Update(source); + return (results, indicator); + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] private static void CalculateScalarCore(ReadOnlySpan source, Span output, int period) { @@ -856,4 +868,4 @@ public sealed class Wma : AbstractBase Unsafe.Add(ref outRef, idx) = wsum * invDivisor; } } -} +} \ No newline at end of file diff --git a/lib/trends_IIR/dema/Dema.Tests.cs b/lib/trends_IIR/dema/Dema.Tests.cs index 183b1280..ca0f859e 100644 --- a/lib/trends_IIR/dema/Dema.Tests.cs +++ b/lib/trends_IIR/dema/Dema.Tests.cs @@ -44,7 +44,7 @@ public class DemaTests } // Act - var demaSeries = Dema.Calculate(source, period); + var demaSeries = Dema.Batch(source, period); var demaObj = new Dema(period); // Assert @@ -71,7 +71,7 @@ public class DemaTests } // Act - Dema.Calculate(source, output, period); + Dema.Batch(source, output, period); var demaObj = new Dema(period); // Assert @@ -130,7 +130,7 @@ public class DemaTests } // Act - var demaSeries = Dema.Calculate(source, alpha); + var demaSeries = Dema.Batch(source, alpha); var demaObj = new Dema(alpha); // Assert @@ -157,7 +157,7 @@ public class DemaTests } // Act - Dema.Calculate(source, output, alpha); + Dema.Batch(source, output, alpha); var demaObj = new Dema(alpha); // Assert @@ -250,8 +250,8 @@ public class DemaTests double[] output = new double[5]; double[] wrongSizeOutput = new double[3]; - Assert.Throws(() => Dema.Calculate(source.AsSpan(), output.AsSpan(), 0)); - Assert.Throws(() => Dema.Calculate(source.AsSpan(), wrongSizeOutput.AsSpan(), 3)); + Assert.Throws(() => Dema.Batch(source.AsSpan(), output.AsSpan(), 0)); + Assert.Throws(() => Dema.Batch(source.AsSpan(), wrongSizeOutput.AsSpan(), 3)); } [Fact] @@ -260,7 +260,7 @@ public class DemaTests double[] source = [100, 110, double.NaN, 120, 130]; double[] output = new double[5]; - Dema.Calculate(source.AsSpan(), output.AsSpan(), 3); + Dema.Batch(source.AsSpan(), output.AsSpan(), 3); foreach (var val in output) { @@ -278,14 +278,14 @@ public class DemaTests var series = bars.Close; // 1. Batch Mode - var batchSeries = Dema.Calculate(series, period); + var batchSeries = Dema.Batch(series, period); double expected = batchSeries.Last.Value; // 2. Span Mode var tValues = series.Values.ToArray(); var spanInput = new ReadOnlySpan(tValues); var spanOutput = new double[tValues.Length]; - Dema.Calculate(spanInput, spanOutput, period); + Dema.Batch(spanInput, spanOutput, period); double spanResult = spanOutput[^1]; // 3. Streaming Mode @@ -317,7 +317,7 @@ public class DemaTests double[] source = { double.NaN, double.NaN, 10.0, 11.0, 12.0 }; double[] output = new double[source.Length]; - Dema.Calculate(source, output, 3); + Dema.Batch(source, output, 3); // We expect the first two outputs to be NaN because the input was NaN Assert.True(double.IsNaN(output[0]), $"Output[0] should be NaN, but was {output[0]}"); diff --git a/lib/trends_IIR/dema/Dema.Validation.Tests.cs b/lib/trends_IIR/dema/Dema.Validation.Tests.cs index 361c3197..cf339950 100644 --- a/lib/trends_IIR/dema/Dema.Validation.Tests.cs +++ b/lib/trends_IIR/dema/Dema.Validation.Tests.cs @@ -140,7 +140,7 @@ public sealed class DemaValidationTests : IDisposable { // Calculate QuanTAlib DEMA (Span API) double[] qOutput = new double[sourceData.Length]; - global::QuanTAlib.Dema.Calculate(sourceData.AsSpan(), qOutput.AsSpan(), period); + global::QuanTAlib.Dema.Batch(sourceData.AsSpan(), qOutput.AsSpan(), period); // Calculate TA-Lib DEMA var retCode = TALib.Functions.Dema(sourceData, 0..^0, talibOutput, out var outRange, period); diff --git a/lib/trends_IIR/dema/Dema.cs b/lib/trends_IIR/dema/Dema.cs index a5512a71..ddba7430 100644 --- a/lib/trends_IIR/dema/Dema.cs +++ b/lib/trends_IIR/dema/Dema.cs @@ -230,19 +230,19 @@ public sealed class Dema : AbstractBase return result; } - public static TSeries Calculate(TSeries source, int period) + public static TSeries Batch(TSeries source, int period) { var dema = new Dema(period); return dema.Update(source); } - public static TSeries Calculate(TSeries source, double alpha) + public static TSeries Batch(TSeries source, double alpha) { var dema = new Dema(alpha); return dema.Update(source); } - public static void Calculate(ReadOnlySpan source, Span output, int period) + public static void Batch(ReadOnlySpan source, Span output, int period) { if (period <= 0) { @@ -250,10 +250,10 @@ public sealed class Dema : AbstractBase } double alpha = 2.0 / (period + 1); - Calculate(source, output, alpha); + Batch(source, output, alpha); } - public static void Calculate(ReadOnlySpan source, Span output, double alpha) + public static void Batch(ReadOnlySpan source, Span output, double alpha) { if (source.Length != output.Length) { @@ -348,6 +348,13 @@ public sealed class Dema : AbstractBase } } + public static (TSeries Results, Dema Indicator) Calculate(TSeries source, int period) + { + var indicator = new Dema(period); + TSeries results = indicator.Update(source); + return (results, indicator); + } + public override void Reset() { _state1 = EmaState.New(); @@ -369,4 +376,4 @@ public sealed class Dema : AbstractBase } private void Handle(object? sender, in TValueEventArgs e) => Update(e.Value, e.IsNew); -} +} \ No newline at end of file diff --git a/lib/trends_IIR/dsma/Dsma.Tests.cs b/lib/trends_IIR/dsma/Dsma.Tests.cs index fb90f15a..48daa605 100644 --- a/lib/trends_IIR/dsma/Dsma.Tests.cs +++ b/lib/trends_IIR/dsma/Dsma.Tests.cs @@ -352,7 +352,7 @@ public class DsmaTests // Act - Span var spanOutput = new double[values.Length]; - Dsma.Calculate(values, spanOutput, period, scale); + Dsma.Batch(values, spanOutput, period, scale); // Assert for (int i = 0; i < values.Length; i++) @@ -386,7 +386,7 @@ public class DsmaTests // Act - Span var spanOutput = new double[values.Length]; - Dsma.Calculate(values, spanOutput, period, scale); + Dsma.Batch(values, spanOutput, period, scale); // Assert for (int i = 0; i < values.Length; i++) @@ -439,7 +439,7 @@ public class DsmaTests // Act & Assert var ex = Assert.Throws(() => - Dsma.Calculate(source, shortOutput, period: 10)); + Dsma.Batch(source, shortOutput, period: 10)); Assert.Equal("output", ex.ParamName); } @@ -457,7 +457,7 @@ public class DsmaTests var output = new double[50]; // Act - Dsma.Calculate(values, output, period: 10, scaleFactor: 0.5); + Dsma.Batch(values, output, period: 10, scaleFactor: 0.5); // Assert Assert.All(output, val => Assert.True(double.IsFinite(val))); @@ -477,7 +477,7 @@ public class DsmaTests var output = new double[100]; // Act - Dsma.Calculate(values, output, period: 10, scaleFactor: 0.5); + Dsma.Batch(values, output, period: 10, scaleFactor: 0.5); // Assert Assert.All(output.Take(50), val => Assert.True(double.IsFinite(val))); @@ -493,7 +493,7 @@ public class DsmaTests var output = new double[20]; // Act - Dsma.Calculate(values, output, period: 5, scaleFactor: 0.5); + Dsma.Batch(values, output, period: 5, scaleFactor: 0.5); // Assert Assert.All(output, val => Assert.True(double.IsFinite(val))); diff --git a/lib/trends_IIR/dsma/Dsma.cs b/lib/trends_IIR/dsma/Dsma.cs index 02abe25a..ff4c319a 100644 --- a/lib/trends_IIR/dsma/Dsma.cs +++ b/lib/trends_IIR/dsma/Dsma.cs @@ -316,7 +316,7 @@ public sealed class Dsma : AbstractBase /// Lookback period (≥2) /// Scaling factor (0.01-0.9) /// If output span is shorter than source - public static void Calculate(ReadOnlySpan source, + public static void Batch(ReadOnlySpan source, Span output, int period, double scaleFactor = 0.5) @@ -332,4 +332,11 @@ public sealed class Dsma : AbstractBase output[i] = dsma.Step(source[i], isNew: true); } } -} + + public static (TSeries Results, Dsma Indicator) Calculate(TSeries source, int period, double scaleFactor = 0.5) + { + var indicator = new Dsma(period, scaleFactor); + TSeries results = indicator.Update(source); + return (results, indicator); + } +} \ No newline at end of file diff --git a/lib/trends_IIR/ema/Ema.cs b/lib/trends_IIR/ema/Ema.cs index a167d6b9..00918b22 100644 --- a/lib/trends_IIR/ema/Ema.cs +++ b/lib/trends_IIR/ema/Ema.cs @@ -487,12 +487,6 @@ public sealed class Ema : AbstractBase /// Historical time series /// EMA Period /// A tuple containing the full calculation results and the hot indicator instance - public static (TSeries Results, Ema Indicator) Calculate(TSeries source, int period) - { - var ema = new Ema(period); - TSeries results = ema.Update(source); - return (results, ema); - } /// /// Calculates EMA for the entire series using a new instance. @@ -580,6 +574,12 @@ public sealed class Ema : AbstractBase CalculateCore(source, output, alpha, ref state, ref lastValid); } + public static (TSeries Results, Ema Indicator) Calculate(TSeries source, int period) + { + var ema = new Ema(period); + TSeries results = ema.Update(source); + return (results, ema); + } /// /// Resets the EMA state. @@ -592,4 +592,4 @@ public sealed class Ema : AbstractBase _p_lastValidValue = 0; Last = default; } -} +} \ No newline at end of file diff --git a/lib/trends_IIR/frama/Frama.Tests.cs b/lib/trends_IIR/frama/Frama.Tests.cs index 0e0e4f3b..c4ff4c75 100644 --- a/lib/trends_IIR/frama/Frama.Tests.cs +++ b/lib/trends_IIR/frama/Frama.Tests.cs @@ -101,7 +101,7 @@ public class FramaTests var series = BuildSeries(60, seed: 21); double[] output = new double[series.Count]; - Frama.Calculate(series.High.Values, series.Low.Values, period, output); + Frama.Batch(series.High.Values, series.Low.Values, period, output); TSeries batch = FramaBatch(series, period); for (int i = 0; i < series.Count; i++) diff --git a/lib/trends_IIR/frama/Frama.Validation.Tests.cs b/lib/trends_IIR/frama/Frama.Validation.Tests.cs index 138d4c7b..e95d2289 100644 --- a/lib/trends_IIR/frama/Frama.Validation.Tests.cs +++ b/lib/trends_IIR/frama/Frama.Validation.Tests.cs @@ -46,7 +46,7 @@ public class FramaValidationTests double[] reference = new double[series.Count]; ReferenceFrama(series.High.Values, series.Low.Values, period, reference); - Frama.Calculate(series.High.Values, series.Low.Values, period, output); + Frama.Batch(series.High.Values, series.Low.Values, period, output); for (int i = 0; i < series.Count; i++) { diff --git a/lib/trends_IIR/frama/Frama.cs b/lib/trends_IIR/frama/Frama.cs index cdc71a66..bddcb1fe 100644 --- a/lib/trends_IIR/frama/Frama.cs +++ b/lib/trends_IIR/frama/Frama.cs @@ -193,7 +193,7 @@ public sealed class Frama : ITValuePublisher, IDisposable int len = source.Count; var v = new double[len]; - Calculate(source.High.Values, source.Low.Values, _periodEven, v); + Batch(source.High.Values, source.Low.Values, _periodEven, v); var tList = new List(len); var times = source.Open.Times; @@ -242,7 +242,7 @@ public sealed class Frama : ITValuePublisher, IDisposable [MethodImpl(MethodImplOptions.AggressiveInlining)] private void Handle(object? sender, in TValueEventArgs args) => Update(args.Value, args.IsNew); - public static void Calculate(ReadOnlySpan high, ReadOnlySpan low, int period, Span output) + public static void Batch(ReadOnlySpan high, ReadOnlySpan low, int period, Span output) { if (high.Length != low.Length || high.Length != output.Length) { @@ -260,7 +260,7 @@ public sealed class Frama : ITValuePublisher, IDisposable } [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void Calculate(ReadOnlySpan source, Span output, int period) + public static void Batch(ReadOnlySpan source, Span output, int period) { if (source.Length != output.Length) { @@ -286,7 +286,7 @@ public sealed class Frama : ITValuePublisher, IDisposable int len = source.Count; var v = new double[len]; - Calculate(source.High.Values, source.Low.Values, period, v); + Batch(source.High.Values, source.Low.Values, period, v); var tList = new List(len); var times = source.Open.Times; @@ -298,6 +298,13 @@ public sealed class Frama : ITValuePublisher, IDisposable return new TSeries(tList, [.. v]); } + public static (TSeries Results, Frama Indicator) Calculate(TBarSeries source, int period) + { + var indicator = new Frama(period); + TSeries results = indicator.Update(source); + return (results, indicator); + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] private static double GetMax(RingBuffer buffer, int length, int startOffset = -1) { diff --git a/lib/trends_IIR/hema/Hema.Tests.cs b/lib/trends_IIR/hema/Hema.Tests.cs index ccfdb616..1c6c16ae 100644 --- a/lib/trends_IIR/hema/Hema.Tests.cs +++ b/lib/trends_IIR/hema/Hema.Tests.cs @@ -91,7 +91,7 @@ public class HemaTests int period = 12; TSeries series = BuildSeries(120, seed: 11); - TSeries batch = Hema.Calculate(series, period); + TSeries batch = Hema.Batch(series, period); var hema = new Hema(period); var streamValues = new List(series.Count); @@ -114,8 +114,8 @@ public class HemaTests double[] values = series.Values.ToArray(); var output = new double[values.Length]; - Hema.Calculate(values, output, period); - TSeries batch = Hema.Calculate(series, period); + Hema.Batch(values, output, period); + TSeries batch = Hema.Batch(series, period); for (int i = 0; i < values.Length; i++) { @@ -153,7 +153,7 @@ public class HemaTests double[] source = [1, 2, 3, 4, 5]; double[] output = new double[3]; - var ex = Assert.Throws(() => Hema.Calculate(source, output, 10)); + var ex = Assert.Throws(() => Hema.Batch(source, output, 10)); Assert.Equal("output", ex.ParamName); } diff --git a/lib/trends_IIR/hema/Hema.Validation.Tests.cs b/lib/trends_IIR/hema/Hema.Validation.Tests.cs index 9cdd43a9..0d82508b 100644 --- a/lib/trends_IIR/hema/Hema.Validation.Tests.cs +++ b/lib/trends_IIR/hema/Hema.Validation.Tests.cs @@ -29,7 +29,7 @@ public class HemaValidationTests double[] reference = new double[series.Count]; ReferenceHema(series.Values, reference, period); - TSeries batch = Hema.Calculate(series, period); + TSeries batch = Hema.Batch(series, period); for (int i = 0; i < series.Count; i++) { @@ -47,7 +47,7 @@ public class HemaValidationTests var reference = new double[values.Length]; ReferenceHema(values, reference, period); - Hema.Calculate(values, output, period); + Hema.Batch(values, output, period); for (int i = 0; i < values.Length; i++) { diff --git a/lib/trends_IIR/hema/Hema.cs b/lib/trends_IIR/hema/Hema.cs index 3c69f164..c7af2012 100644 --- a/lib/trends_IIR/hema/Hema.cs +++ b/lib/trends_IIR/hema/Hema.cs @@ -276,13 +276,13 @@ public sealed class Hema : AbstractBase return fastResult; } - public static TSeries Calculate(TSeries source, int period) + public static TSeries Batch(TSeries source, int period) { var hema = new Hema(period); return hema.Update(source); } - public static void Calculate(ReadOnlySpan source, Span output, int period) + public static void Batch(ReadOnlySpan source, Span output, int period) { if (source.Length != output.Length) { @@ -406,6 +406,13 @@ public sealed class Hema : AbstractBase } } + public static (TSeries Results, Hema Indicator) Calculate(TSeries source, int period) + { + var indicator = new Hema(period); + TSeries results = indicator.Update(source); + return (results, indicator); + } + public override void Reset() { _state = State.New(); @@ -473,4 +480,4 @@ public sealed class Hema : AbstractBase return (int)Math.Ceiling(steps); } -} +} \ No newline at end of file diff --git a/lib/trends_IIR/htit/Htit.Tests.cs b/lib/trends_IIR/htit/Htit.Tests.cs index baaf17f2..32e54ff7 100644 --- a/lib/trends_IIR/htit/Htit.Tests.cs +++ b/lib/trends_IIR/htit/Htit.Tests.cs @@ -57,7 +57,7 @@ public class HtitTests var spanInput = data.Values.ToArray(); var spanOutput = new double[spanInput.Length]; - Htit.Calculate(spanInput, spanOutput); + Htit.Batch(spanInput, spanOutput); for (int i = 0; i < resultSeries.Count; i++) { @@ -134,7 +134,7 @@ public class HtitTests double[] source = [1, 2, 3, 4, 5]; double[] wrongSizeOutput = new double[3]; - Assert.Throws(() => Htit.Calculate(source.AsSpan(), wrongSizeOutput.AsSpan())); + Assert.Throws(() => Htit.Batch(source.AsSpan(), wrongSizeOutput.AsSpan())); } [Fact] @@ -143,7 +143,7 @@ public class HtitTests double[] source = [100, 110, double.NaN, 120, 130]; double[] output = new double[5]; - Htit.Calculate(source.AsSpan(), output.AsSpan()); + Htit.Batch(source.AsSpan(), output.AsSpan()); foreach (var val in output) { @@ -167,7 +167,7 @@ public class HtitTests var tValues = series.Values.ToArray(); var spanInput = new ReadOnlySpan(tValues); var spanOutput = new double[tValues.Length]; - Htit.Calculate(spanInput, spanOutput); + Htit.Batch(spanInput, spanOutput); double spanResult = spanOutput[^1]; // 3. Streaming Mode diff --git a/lib/trends_IIR/htit/Htit.cs b/lib/trends_IIR/htit/Htit.cs index 43433f5b..ff941540 100644 --- a/lib/trends_IIR/htit/Htit.cs +++ b/lib/trends_IIR/htit/Htit.cs @@ -308,7 +308,7 @@ public sealed class Htit : AbstractBase } [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void Calculate(ReadOnlySpan source, Span output) + public static void Batch(ReadOnlySpan source, Span output) { if (source.Length != output.Length) { @@ -512,4 +512,11 @@ public sealed class Htit : AbstractBase } } } -} + + public static (TSeries Results, Htit Indicator) Calculate(TSeries source) + { + var indicator = new Htit(); + TSeries results = indicator.Update(source); + return (results, indicator); + } +} \ No newline at end of file diff --git a/lib/trends_IIR/jma/Jma.Tests.cs b/lib/trends_IIR/jma/Jma.Tests.cs index 88f66b70..ba4f2d3e 100644 --- a/lib/trends_IIR/jma/Jma.Tests.cs +++ b/lib/trends_IIR/jma/Jma.Tests.cs @@ -32,11 +32,11 @@ public class JmaTests double[] wrongSizeOutput = new double[3]; // Period must be > 0 - Assert.Throws(() => Jma.Calculate(source.AsSpan(), output.AsSpan(), 0, 0, 1.0)); - Assert.Throws(() => Jma.Calculate(source.AsSpan(), output.AsSpan(), -1, 0, 1.0)); + Assert.Throws(() => Jma.Batch(source.AsSpan(), output.AsSpan(), 0, 0, 1.0)); + Assert.Throws(() => Jma.Batch(source.AsSpan(), output.AsSpan(), -1, 0, 1.0)); // Output must be same length as source - Assert.Throws(() => Jma.Calculate(source.AsSpan(), wrongSizeOutput.AsSpan(), 3, 0, 1.0)); + Assert.Throws(() => Jma.Batch(source.AsSpan(), wrongSizeOutput.AsSpan(), 3, 0, 1.0)); } [Fact] @@ -163,7 +163,7 @@ public class JmaTests var tseriesResult = Jma.Batch(series, 10); // Calculate with Span API - Jma.Calculate(source.AsSpan(), output.AsSpan(), 10); + Jma.Batch(source.AsSpan(), output.AsSpan(), 10); // Compare results for (int i = 0; i < 100; i++) @@ -189,7 +189,7 @@ public class JmaTests var tValues = series.Values.ToArray(); var spanInput = new ReadOnlySpan(tValues); var spanOutput = new double[tValues.Length]; - Jma.Calculate(spanInput, spanOutput, period); + Jma.Batch(spanInput, spanOutput, period); double spanResult = spanOutput[^1]; // 3. Streaming Mode @@ -261,7 +261,7 @@ public class JmaTests double[] source = [100, 110, double.NaN, 120, 130]; double[] output = new double[5]; - Jma.Calculate(source.AsSpan(), output.AsSpan(), 3); + Jma.Batch(source.AsSpan(), output.AsSpan(), 3); foreach (var val in output) { diff --git a/lib/trends_IIR/jma/Jma.cs b/lib/trends_IIR/jma/Jma.cs index 20ec63df..64f1ace3 100644 --- a/lib/trends_IIR/jma/Jma.cs +++ b/lib/trends_IIR/jma/Jma.cs @@ -36,6 +36,7 @@ public sealed class Jma : AbstractBase private readonly RingBuffer _volBuffer; private readonly TValuePublishedHandler _handler; private readonly ITValuePublisher? _source; + private bool _disposed; // Streaming state (current + previous snapshot for isNew=false) private State _state; @@ -203,11 +204,11 @@ public sealed class Jma : AbstractBase private double CalculateJma(double value) { // 1. Local deviation: |price - {UpperBand, LowerBand}| - double diffA = value - _state.UpperBand; - double diffB = value - _state.LowerBand; - double absA = Math.Abs(diffA); - double absB = Math.Abs(diffB); - double absValue = absA > absB ? absA : absB; + double uBand = value - _state.UpperBand; + double lBand = value - _state.LowerBand; + double absUBand = Math.Abs(uBand); + double absLBand = Math.Abs(lBand); + double absValue = absUBand > absLBand ? absUBand : absLBand; double deviation = absValue + 1e-10; // 2. 10-bar SMA of local deviation -> "volatility" @@ -338,9 +339,13 @@ public sealed class Jma : AbstractBase protected override void Dispose(bool disposing) { - if (disposing && _source != null) + if (!_disposed) { - _source.Pub -= _handler; + if (disposing && _source != null) + { + _source.Pub -= _handler; + } + _disposed = true; } base.Dispose(disposing); } @@ -362,7 +367,7 @@ public sealed class Jma : AbstractBase /// /// Static helper compatible with your existing signature. /// - public static void Calculate(ReadOnlySpan source, + public static void Batch(ReadOnlySpan source, Span output, int period, int phase = 0, @@ -385,6 +390,13 @@ public sealed class Jma : AbstractBase } } + public static (TSeries Results, Jma Indicator) Calculate(TSeries source, int period, int phase = 0, double power = 0.45) + { + var indicator = new Jma(period, phase, power); + TSeries results = indicator.Update(source); + return (results, indicator); + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] private double CalculateTrimmedMean(double fallback) { @@ -431,4 +443,4 @@ public sealed class Jma : AbstractBase int len = end - start + 1; return sorted.Slice(start, len).SumSIMD() / len; } -} +} \ No newline at end of file diff --git a/lib/trends_IIR/kama/Kama.Tests.cs b/lib/trends_IIR/kama/Kama.Tests.cs index 47c0c06e..4714721f 100644 --- a/lib/trends_IIR/kama/Kama.Tests.cs +++ b/lib/trends_IIR/kama/Kama.Tests.cs @@ -86,7 +86,7 @@ public class KamaTests var instanceResults = new Kama(10).Update(series); var staticResults = new double[series.Count]; - Kama.Calculate(series.Values.ToArray().AsSpan(), staticResults.AsSpan(), 10); + Kama.Batch(series.Values.ToArray().AsSpan(), staticResults.AsSpan(), 10); for (int i = 0; i < instanceResults.Count; i++) { @@ -214,8 +214,8 @@ public class KamaTests double[] output = new double[5]; double[] wrongSizeOutput = new double[3]; - Assert.Throws(() => Kama.Calculate(source.AsSpan(), output.AsSpan(), 0)); - Assert.Throws(() => Kama.Calculate(source.AsSpan(), wrongSizeOutput.AsSpan(), 3)); + Assert.Throws(() => Kama.Batch(source.AsSpan(), output.AsSpan(), 0)); + Assert.Throws(() => Kama.Batch(source.AsSpan(), wrongSizeOutput.AsSpan(), 3)); } [Fact] @@ -224,7 +224,7 @@ public class KamaTests double[] source = [100, 110, double.NaN, 120, 130]; double[] output = new double[5]; - Kama.Calculate(source.AsSpan(), output.AsSpan(), 3); + Kama.Batch(source.AsSpan(), output.AsSpan(), 3); foreach (var val in output) { @@ -249,7 +249,7 @@ public class KamaTests var tValues = series.Values.ToArray(); var spanInput = new ReadOnlySpan(tValues); var spanOutput = new double[tValues.Length]; - Kama.Calculate(spanInput, spanOutput, period); + Kama.Batch(spanInput, spanOutput, period); double spanResult = spanOutput[^1]; // 3. Streaming Mode diff --git a/lib/trends_IIR/kama/Kama.Validation.Tests.cs b/lib/trends_IIR/kama/Kama.Validation.Tests.cs index 95fc1301..374069e6 100644 --- a/lib/trends_IIR/kama/Kama.Validation.Tests.cs +++ b/lib/trends_IIR/kama/Kama.Validation.Tests.cs @@ -97,7 +97,7 @@ public sealed class KamaValidationTests : IDisposable { // Calculate QuanTAlib KAMA (Span API) double[] qOutput = new double[_testData.RawData.Length]; - global::QuanTAlib.Kama.Calculate(_testData.RawData.Span, qOutput.AsSpan(), period, fastPeriod, slowPeriod); + global::QuanTAlib.Kama.Batch(_testData.RawData.Span, qOutput.AsSpan(), period, fastPeriod, slowPeriod); // Calculate Skender KAMA var sResult = _testData.SkenderQuotes.GetKama(period, fastPeriod, slowPeriod).ToList(); diff --git a/lib/trends_IIR/kama/Kama.cs b/lib/trends_IIR/kama/Kama.cs index 86aaac0b..9cedd968 100644 --- a/lib/trends_IIR/kama/Kama.cs +++ b/lib/trends_IIR/kama/Kama.cs @@ -239,7 +239,7 @@ public sealed class Kama : AbstractBase return kama.Update(source); } - public static void Calculate(ReadOnlySpan source, Span output, int period, int fastPeriod = 2, int slowPeriod = 30) + public static void Batch(ReadOnlySpan source, Span output, int period, int fastPeriod = 2, int slowPeriod = 30) { if (period <= 0) { @@ -363,6 +363,13 @@ public sealed class Kama : AbstractBase } } + public static (TSeries Results, Kama Indicator) Calculate(TSeries source, int period, int fastPeriod = 2, int slowPeriod = 30) + { + var indicator = new Kama(period, fastPeriod, slowPeriod); + TSeries results = indicator.Update(source); + return (results, indicator); + } + public override void Reset() { _buffer.Clear(); @@ -372,4 +379,4 @@ public sealed class Kama : AbstractBase _p_state = _state; Last = default; } -} +} \ No newline at end of file diff --git a/lib/trends_IIR/mama/Mama.Tests.cs b/lib/trends_IIR/mama/Mama.Tests.cs index 9a2672d0..3976d914 100644 --- a/lib/trends_IIR/mama/Mama.Tests.cs +++ b/lib/trends_IIR/mama/Mama.Tests.cs @@ -81,7 +81,7 @@ public class MamaTests var famaOutput = new double[100]; // This should complete without hanging - Mama.Calculate(data, output, famaOutput: famaOutput); + Mama.Batch(data, output, famaOutput: famaOutput); // Verify all outputs are finite (no NaN or Infinity propagation) for (int i = 0; i < 100; i++) @@ -252,7 +252,7 @@ public class MamaTests } var output = new double[count]; - Mama.Calculate(data, output); + Mama.Batch(data, output); var mama = new Mama(); for (int i = 0; i < count; i++) @@ -267,7 +267,7 @@ public class MamaTests { var data = new double[10]; var output = new double[5]; - Assert.Throws(() => Mama.Calculate(data, output)); + Assert.Throws(() => Mama.Batch(data, output)); } [Fact] @@ -278,39 +278,39 @@ public class MamaTests // fastLimit <= 0 var ex1 = Assert.Throws(() => - Mama.Calculate(data, output, fastLimit: 0.0)); + Mama.Batch(data, output, fastLimit: 0.0)); Assert.Equal("fastLimit", ex1.ParamName); var ex2 = Assert.Throws(() => - Mama.Calculate(data, output, fastLimit: -0.1)); + Mama.Batch(data, output, fastLimit: -0.1)); Assert.Equal("fastLimit", ex2.ParamName); // slowLimit <= 0 var ex3 = Assert.Throws(() => - Mama.Calculate(data, output, slowLimit: 0.0)); + Mama.Batch(data, output, slowLimit: 0.0)); Assert.Equal("slowLimit", ex3.ParamName); var ex4 = Assert.Throws(() => - Mama.Calculate(data, output, slowLimit: -0.1)); + Mama.Batch(data, output, slowLimit: -0.1)); Assert.Equal("slowLimit", ex4.ParamName); // fastLimit > 1 var ex5 = Assert.Throws(() => - Mama.Calculate(data, output, fastLimit: 1.1)); + Mama.Batch(data, output, fastLimit: 1.1)); Assert.Equal("fastLimit", ex5.ParamName); // slowLimit > 1 var ex6 = Assert.Throws(() => - Mama.Calculate(data, output, slowLimit: 1.1)); + Mama.Batch(data, output, slowLimit: 1.1)); Assert.Equal("slowLimit", ex6.ParamName); // fastLimit <= slowLimit var ex7 = Assert.Throws(() => - Mama.Calculate(data, output, fastLimit: 0.05, slowLimit: 0.5)); + Mama.Batch(data, output, fastLimit: 0.05, slowLimit: 0.5)); Assert.Equal("fastLimit", ex7.ParamName); var ex8 = Assert.Throws(() => - Mama.Calculate(data, output, fastLimit: 0.5, slowLimit: 0.5)); + Mama.Batch(data, output, fastLimit: 0.5, slowLimit: 0.5)); Assert.Equal("fastLimit", ex8.ParamName); } @@ -356,7 +356,7 @@ public class MamaTests var mamaOutput = new double[count]; var famaOutput = new double[count]; - Mama.Calculate(data, mamaOutput, famaOutput: famaOutput); + Mama.Batch(data, mamaOutput, famaOutput: famaOutput); var mama = new Mama(); for (int i = 0; i < count; i++) @@ -382,10 +382,10 @@ public class MamaTests var output2 = new double[count]; // Call without famaOutput parameter (backwards compatibility) - Mama.Calculate(data, output1); + Mama.Batch(data, output1); // Call with empty famaOutput span - Mama.Calculate(data, output2, famaOutput: Span.Empty); + Mama.Batch(data, output2, famaOutput: Span.Empty); // Both should produce identical MAMA results for (int i = 0; i < count; i++) @@ -402,7 +402,7 @@ public class MamaTests var famaOutput = new double[5]; var ex = Assert.Throws(() => - Mama.Calculate(data, mamaOutput, famaOutput: famaOutput)); + Mama.Batch(data, mamaOutput, famaOutput: famaOutput)); Assert.Equal("famaOutput", ex.ParamName); } @@ -421,7 +421,7 @@ public class MamaTests // Get values from span calculation var mamaOutput = new double[count]; var famaOutput = new double[count]; - Mama.Calculate(data, mamaOutput, famaOutput: famaOutput); + Mama.Batch(data, mamaOutput, famaOutput: famaOutput); // Get values from instance method var mama = new Mama(); @@ -459,7 +459,7 @@ public class MamaTests // 2. Span Mode (static method with FAMA) var spanMama = new double[count]; var spanFama = new double[count]; - Mama.Calculate(data, spanMama, famaOutput: spanFama); + Mama.Batch(data, spanMama, famaOutput: spanFama); // 3. Verify MAMA matches for (int i = 0; i < count; i++) diff --git a/lib/trends_IIR/mama/Mama.cs b/lib/trends_IIR/mama/Mama.cs index c5c8b2af..ef7eaeb1 100644 --- a/lib/trends_IIR/mama/Mama.cs +++ b/lib/trends_IIR/mama/Mama.cs @@ -316,7 +316,7 @@ public sealed class Mama : AbstractBase return mama.Update(source); } - public static void Calculate(ReadOnlySpan source, Span output, double fastLimit = 0.5, double slowLimit = 0.05, Span famaOutput = default) + public static void Batch(ReadOnlySpan source, Span output, double fastLimit = 0.5, double slowLimit = 0.05, Span famaOutput = default) { if (fastLimit <= 0) { @@ -540,4 +540,11 @@ public sealed class Mama : AbstractBase } } } -} + + public static (TSeries Results, Mama Indicator) Calculate(TSeries source, double fastLimit = 0.5, double slowLimit = 0.05) + { + var indicator = new Mama(fastLimit, slowLimit); + TSeries results = indicator.Update(source); + return (results, indicator); + } +} \ No newline at end of file diff --git a/lib/trends_IIR/mgdi/Mgdi.Tests.cs b/lib/trends_IIR/mgdi/Mgdi.Tests.cs index ec1e88c8..fe8c35e5 100644 --- a/lib/trends_IIR/mgdi/Mgdi.Tests.cs +++ b/lib/trends_IIR/mgdi/Mgdi.Tests.cs @@ -48,10 +48,10 @@ public class MgdiTests var source = new double[10]; var output = new double[10]; - Assert.Throws(() => Mgdi.Calculate(source, output, 14, double.NaN)); - Assert.Throws(() => Mgdi.Calculate(source, output, 14, double.PositiveInfinity)); - Assert.Throws(() => Mgdi.Calculate(source, output, 14, double.NegativeInfinity)); - Assert.Throws(() => Mgdi.Calculate(source, output, 14, 0)); - Assert.Throws(() => Mgdi.Calculate(source, output, 14, -1)); + Assert.Throws(() => Mgdi.Batch(source, output, 14, double.NaN)); + Assert.Throws(() => Mgdi.Batch(source, output, 14, double.PositiveInfinity)); + Assert.Throws(() => Mgdi.Batch(source, output, 14, double.NegativeInfinity)); + Assert.Throws(() => Mgdi.Batch(source, output, 14, 0)); + Assert.Throws(() => Mgdi.Batch(source, output, 14, -1)); } } diff --git a/lib/trends_IIR/mgdi/Mgdi.cs b/lib/trends_IIR/mgdi/Mgdi.cs index 1c3a149a..f9560041 100644 --- a/lib/trends_IIR/mgdi/Mgdi.cs +++ b/lib/trends_IIR/mgdi/Mgdi.cs @@ -134,7 +134,7 @@ public sealed class Mgdi : AbstractBase var tSpan = CollectionsMarshal.AsSpan(t); var vSpan = CollectionsMarshal.AsSpan(v); - Calculate(source.Values, vSpan, _period, _k); + Batch(source.Values, vSpan, _period, _k); source.Times.CopyTo(tSpan); // Restore state @@ -164,7 +164,7 @@ public sealed class Mgdi : AbstractBase } [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void Calculate(ReadOnlySpan source, Span output, int period = 14, double k = 0.6) + public static void Batch(ReadOnlySpan source, Span output, int period = 14, double k = 0.6) { ArgumentOutOfRangeException.ThrowIfLessThan(period, 1); if (double.IsNaN(k) || double.IsInfinity(k) || k <= 0) @@ -229,6 +229,13 @@ public sealed class Mgdi : AbstractBase } } + public static (TSeries Results, Mgdi Indicator) Calculate(TSeries source, int period = 14, double k = 0.6) + { + var indicator = new Mgdi(period, k); + TSeries results = indicator.Update(source); + return (results, indicator); + } + public override void Reset() { Init(); diff --git a/lib/trends_IIR/mma/Mma.Tests.cs b/lib/trends_IIR/mma/Mma.Tests.cs index e4b3fb4b..3671333d 100644 --- a/lib/trends_IIR/mma/Mma.Tests.cs +++ b/lib/trends_IIR/mma/Mma.Tests.cs @@ -91,7 +91,7 @@ public class MmaTests int period = 12; TSeries series = BuildSeries(120, seed: 11); - TSeries batch = Mma.Calculate(series, period); + TSeries batch = Mma.Batch(series, period); var mma = new Mma(period); var streamValues = new List(series.Count); @@ -114,8 +114,8 @@ public class MmaTests double[] values = series.Values.ToArray(); var output = new double[values.Length]; - Mma.Calculate(values, output, period); - TSeries batch = Mma.Calculate(series, period); + Mma.Batch(values, output, period); + TSeries batch = Mma.Batch(series, period); for (int i = 0; i < values.Length; i++) { @@ -153,7 +153,7 @@ public class MmaTests double[] source = [1, 2, 3, 4, 5]; double[] output = new double[3]; - var ex = Assert.Throws(() => Mma.Calculate(source, output, 10)); + var ex = Assert.Throws(() => Mma.Batch(source, output, 10)); Assert.Equal("output", ex.ParamName); } diff --git a/lib/trends_IIR/mma/Mma.Validation.Tests.cs b/lib/trends_IIR/mma/Mma.Validation.Tests.cs index 4419f844..5c220ffa 100644 --- a/lib/trends_IIR/mma/Mma.Validation.Tests.cs +++ b/lib/trends_IIR/mma/Mma.Validation.Tests.cs @@ -29,7 +29,7 @@ public class MmaValidationTests double[] reference = new double[series.Count]; ReferenceMma(series.Values, reference, period); - TSeries batch = Mma.Calculate(series, period); + TSeries batch = Mma.Batch(series, period); for (int i = 0; i < series.Count; i++) { @@ -47,7 +47,7 @@ public class MmaValidationTests var reference = new double[values.Length]; ReferenceMma(values, reference, period); - Mma.Calculate(values, output, period); + Mma.Batch(values, output, period); for (int i = 0; i < values.Length; i++) { diff --git a/lib/trends_IIR/mma/Mma.cs b/lib/trends_IIR/mma/Mma.cs index afb8c9ee..23e2192d 100644 --- a/lib/trends_IIR/mma/Mma.cs +++ b/lib/trends_IIR/mma/Mma.cs @@ -177,13 +177,13 @@ public sealed class Mma : AbstractBase } } - public static TSeries Calculate(TSeries source, int period) + public static TSeries Batch(TSeries source, int period) { var mma = new Mma(period); return mma.Update(source); } - public static void Calculate(ReadOnlySpan source, Span output, int period) + public static void Batch(ReadOnlySpan source, Span output, int period) { if (source.Length != output.Length) { @@ -252,6 +252,13 @@ public sealed class Mma : AbstractBase } } + public static (TSeries Results, Mma Indicator) Calculate(TSeries source, int period) + { + var indicator = new Mma(period); + TSeries results = indicator.Update(source); + return (results, indicator); + } + public override void Reset() { _state = State.New(); @@ -349,4 +356,4 @@ public sealed class Mma : AbstractBase return weightedSum; } -} +} \ No newline at end of file diff --git a/lib/trends_IIR/qema/Qema.cs b/lib/trends_IIR/qema/Qema.cs index 1c4048ee..f49edb7a 100644 --- a/lib/trends_IIR/qema/Qema.cs +++ b/lib/trends_IIR/qema/Qema.cs @@ -561,6 +561,13 @@ public sealed class Qema : AbstractBase } } + public static (TSeries Results, Qema Indicator) Calculate(TSeries source, int period) + { + var indicator = new Qema(period); + TSeries results = indicator.Update(source); + return (results, indicator); + } + /// /// Resets the QEMA state. /// @@ -578,4 +585,4 @@ public sealed class Qema : AbstractBase _p_lastValidValue = 0; Last = default; } -} +} \ No newline at end of file diff --git a/lib/trends_IIR/rema/Rema.cs b/lib/trends_IIR/rema/Rema.cs index 841852d7..1b867ee5 100644 --- a/lib/trends_IIR/rema/Rema.cs +++ b/lib/trends_IIR/rema/Rema.cs @@ -383,12 +383,6 @@ public sealed class Rema : AbstractBase /// /// Runs a high-performance batch calculation and returns a hot REMA instance. /// - public static (TSeries Results, Rema Indicator) Calculate(TSeries source, int period, double lambda = 0.5) - { - var rema = new Rema(period, lambda); - TSeries results = rema.Update(source); - return (results, rema); - } /// /// Calculates REMA for the entire series using a new instance. @@ -450,6 +444,12 @@ public sealed class Rema : AbstractBase CalculateCore(source, output, alpha, lambda, ref state, ref lastValid); } + public static (TSeries Results, Rema Indicator) Calculate(TSeries source, int period, double lambda = 0.5) + { + var rema = new Rema(period, lambda); + TSeries results = rema.Update(source); + return (results, rema); + } /// public override void Reset() @@ -460,4 +460,4 @@ public sealed class Rema : AbstractBase _p_lastValidValue = 0; Last = default; } -} +} \ No newline at end of file diff --git a/lib/trends_IIR/rgma/Rgma.cs b/lib/trends_IIR/rgma/Rgma.cs index acf32618..1efcf74a 100644 --- a/lib/trends_IIR/rgma/Rgma.cs +++ b/lib/trends_IIR/rgma/Rgma.cs @@ -379,12 +379,6 @@ public sealed class Rgma : AbstractBase /// /// Runs a high-performance batch calculation and returns a hot RGMA instance. /// - public static (TSeries Results, Rgma Indicator) Calculate(TSeries source, int period, int passes = 3) - { - var rgma = new Rgma(period, passes); - TSeries results = rgma.Update(source); - return (results, rgma); - } /// /// Calculates RGMA for the entire series using a new instance. @@ -463,6 +457,12 @@ public sealed class Rgma : AbstractBase } } } + public static (TSeries Results, Rgma Indicator) Calculate(TSeries source, int period, int passes = 3) + { + var rgma = new Rgma(period, passes); + TSeries results = rgma.Update(source); + return (results, rgma); + } /// public override void Reset() @@ -490,4 +490,4 @@ public sealed class Rgma : AbstractBase } base.Dispose(disposing); } -} +} \ No newline at end of file diff --git a/lib/trends_IIR/t3/T3.cs b/lib/trends_IIR/t3/T3.cs index f9232f9c..cd92dc22 100644 --- a/lib/trends_IIR/t3/T3.cs +++ b/lib/trends_IIR/t3/T3.cs @@ -368,6 +368,13 @@ public sealed class T3 : AbstractBase CalculateCore(source, output, p, ref state, ref lastValidValue); } + public static (TSeries Results, T3 Indicator) Calculate(TSeries source, int period, double vfactor = 0.7) + { + var indicator = new T3(period, vfactor); + TSeries results = indicator.Update(source); + return (results, indicator); + } + /// /// Resets the T3 state. /// @@ -390,4 +397,4 @@ public sealed class T3 : AbstractBase } base.Dispose(disposing); } -} +} \ No newline at end of file diff --git a/lib/trends_IIR/tema/Tema.cs b/lib/trends_IIR/tema/Tema.cs index 1366da1f..0c079ed6 100644 --- a/lib/trends_IIR/tema/Tema.cs +++ b/lib/trends_IIR/tema/Tema.cs @@ -470,6 +470,13 @@ public sealed class Tema : AbstractBase } } + public static (TSeries Results, Tema Indicator) Calculate(TSeries source, int period) + { + var indicator = new Tema(period); + TSeries results = indicator.Update(source); + return (results, indicator); + } + public override void Reset() { _state1 = EmaState.New(); @@ -482,4 +489,4 @@ public sealed class Tema : AbstractBase _p_lastValidValue = 0; Last = default; } -} +} \ No newline at end of file diff --git a/lib/trends_IIR/vama/Vama.cs b/lib/trends_IIR/vama/Vama.cs index 88b4c06d..92f704d8 100644 --- a/lib/trends_IIR/vama/Vama.cs +++ b/lib/trends_IIR/vama/Vama.cs @@ -377,6 +377,14 @@ public sealed class Vama : AbstractBase return vama.Update(source); } + public static (TSeries Results, Vama Indicator) Calculate(TBarSeries source, int baseLength = 20, int shortAtrPeriod = 10, int longAtrPeriod = 50, int minLength = 5, int maxLength = 100) + { + var indicator = new Vama(baseLength, shortAtrPeriod, longAtrPeriod, minLength, maxLength); + TSeries results = indicator.Update(source); + return (results, indicator); + } + + /// /// Resets the VAMA state. /// diff --git a/lib/trends_IIR/vidya/Vidya.cs b/lib/trends_IIR/vidya/Vidya.cs index 2a77a0c0..4b82e292 100644 --- a/lib/trends_IIR/vidya/Vidya.cs +++ b/lib/trends_IIR/vidya/Vidya.cs @@ -24,6 +24,7 @@ public sealed class Vidya : AbstractBase private readonly ITValuePublisher? _source; private readonly TValuePublishedHandler? _pubHandler; private bool _isNew = true; + private bool _disposed; [StructLayout(LayoutKind.Auto)] private record struct State( @@ -59,9 +60,13 @@ public sealed class Vidya : AbstractBase protected override void Dispose(bool disposing) { - if (disposing && _source != null && _pubHandler != null) + if (!_disposed) { - _source.Pub -= _pubHandler; + if (disposing && _source != null && _pubHandler != null) + { + _source.Pub -= _pubHandler; + } + _disposed = true; } base.Dispose(disposing); } @@ -388,4 +393,11 @@ public sealed class Vidya : AbstractBase System.Buffers.ArrayPool.Shared.Return(downs); } } -} + + public static (TSeries Results, Vidya Indicator) Calculate(TSeries source, int period) + { + var indicator = new Vidya(period); + TSeries results = indicator.Update(source); + return (results, indicator); + } +} \ No newline at end of file diff --git a/lib/trends_IIR/yzvama/Yzvama.cs b/lib/trends_IIR/yzvama/Yzvama.cs index a1e5a4c9..01cea0f4 100644 --- a/lib/trends_IIR/yzvama/Yzvama.cs +++ b/lib/trends_IIR/yzvama/Yzvama.cs @@ -545,6 +545,14 @@ public sealed class Yzvama : AbstractBase return yzvama.Update(source); } + public static (TSeries Results, Yzvama Indicator) Calculate(TBarSeries source, int yzvShortPeriod = 3, int yzvLongPeriod = 50, int percentileLookback = 100, int minLength = 5, int maxLength = 100) + { + var indicator = new Yzvama(yzvShortPeriod, yzvLongPeriod, percentileLookback, minLength, maxLength); + TSeries results = indicator.Update(source); + return (results, indicator); + } + + /// /// Resets the YZVAMA state. /// diff --git a/lib/trends_IIR/zldema/Zldema.Tests.cs b/lib/trends_IIR/zldema/Zldema.Tests.cs index b9c5acc6..100206c5 100644 --- a/lib/trends_IIR/zldema/Zldema.Tests.cs +++ b/lib/trends_IIR/zldema/Zldema.Tests.cs @@ -91,7 +91,7 @@ public class ZldemaTests int period = 12; TSeries series = BuildSeries(120, seed: 11); - TSeries batch = Zldema.Calculate(series, period); + TSeries batch = Zldema.Batch(series, period); var zldema = new Zldema(period); var streamValues = new List(series.Count); @@ -114,8 +114,8 @@ public class ZldemaTests double[] values = series.Values.ToArray(); var output = new double[values.Length]; - Zldema.Calculate(values, output, period); - TSeries batch = Zldema.Calculate(series, period); + Zldema.Batch(values, output, period); + TSeries batch = Zldema.Batch(series, period); for (int i = 0; i < values.Length; i++) { @@ -153,7 +153,7 @@ public class ZldemaTests double[] source = [1, 2, 3, 4, 5]; double[] output = new double[3]; - var ex = Assert.Throws(() => Zldema.Calculate(source, output, 10)); + var ex = Assert.Throws(() => Zldema.Batch(source, output, 10)); Assert.Equal("output", ex.ParamName); } diff --git a/lib/trends_IIR/zldema/Zldema.Validation.Tests.cs b/lib/trends_IIR/zldema/Zldema.Validation.Tests.cs index 16dc907b..0daec811 100644 --- a/lib/trends_IIR/zldema/Zldema.Validation.Tests.cs +++ b/lib/trends_IIR/zldema/Zldema.Validation.Tests.cs @@ -29,7 +29,7 @@ public class ZldemaValidationTests double[] reference = new double[series.Count]; ReferenceZldema(series.Values, reference, period); - TSeries batch = Zldema.Calculate(series, period); + TSeries batch = Zldema.Batch(series, period); for (int i = 0; i < series.Count; i++) { @@ -47,7 +47,7 @@ public class ZldemaValidationTests var reference = new double[values.Length]; ReferenceZldema(values, reference, period); - Zldema.Calculate(values, output, period); + Zldema.Batch(values, output, period); for (int i = 0; i < values.Length; i++) { diff --git a/lib/trends_IIR/zldema/Zldema.cs b/lib/trends_IIR/zldema/Zldema.cs index 71e13936..e24cc954 100644 --- a/lib/trends_IIR/zldema/Zldema.cs +++ b/lib/trends_IIR/zldema/Zldema.cs @@ -201,13 +201,13 @@ public sealed class Zldema : AbstractBase } } - public static TSeries Calculate(TSeries source, int period) + public static TSeries Batch(TSeries source, int period) { var zldema = new Zldema(period); return zldema.Update(source); } - public static void Calculate(ReadOnlySpan source, Span output, int period) + public static void Batch(ReadOnlySpan source, Span output, int period) { if (source.Length != output.Length) { @@ -222,10 +222,10 @@ public sealed class Zldema : AbstractBase } double alpha = 2.0 / (period + 1); - Calculate(source, output, alpha, period); + BatchCore(source, output, alpha, period); } - public static void Calculate(ReadOnlySpan source, Span output, double alpha) + public static void Batch(ReadOnlySpan source, Span output, double alpha) { if (source.Length != output.Length) { @@ -243,7 +243,14 @@ public sealed class Zldema : AbstractBase } double period = (2.0 / alpha) - 1.0; - Calculate(source, output, alpha, period); + BatchCore(source, output, alpha, period); + } + + public static (TSeries Results, Zldema Indicator) Calculate(TSeries source, int period) + { + var indicator = new Zldema(period); + TSeries results = indicator.Update(source); + return (results, indicator); } [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -344,7 +351,7 @@ public sealed class Zldema : AbstractBase [MethodImpl(MethodImplOptions.AggressiveInlining)] private void Handle(object? sender, in TValueEventArgs e) => Update(e.Value, e.IsNew); - private static void Calculate(ReadOnlySpan source, Span output, double alpha, double period) + private static void BatchCore(ReadOnlySpan source, Span output, double alpha, double period) { int lag = ComputeLag(period); int bufferSize = lag + 1; diff --git a/lib/trends_IIR/zlema/Zlema.Tests.cs b/lib/trends_IIR/zlema/Zlema.Tests.cs index 7d987609..69e8f420 100644 --- a/lib/trends_IIR/zlema/Zlema.Tests.cs +++ b/lib/trends_IIR/zlema/Zlema.Tests.cs @@ -91,7 +91,7 @@ public class ZlemaTests int period = 12; TSeries series = BuildSeries(120, seed: 11); - TSeries batch = Zlema.Calculate(series, period); + TSeries batch = Zlema.Batch(series, period); var zlema = new Zlema(period); var streamValues = new List(series.Count); @@ -114,8 +114,8 @@ public class ZlemaTests double[] values = series.Values.ToArray(); var output = new double[values.Length]; - Zlema.Calculate(values, output, period); - TSeries batch = Zlema.Calculate(series, period); + Zlema.Batch(values, output, period); + TSeries batch = Zlema.Batch(series, period); for (int i = 0; i < values.Length; i++) { @@ -153,7 +153,7 @@ public class ZlemaTests double[] source = [1, 2, 3, 4, 5]; double[] output = new double[3]; - var ex = Assert.Throws(() => Zlema.Calculate(source, output, 10)); + var ex = Assert.Throws(() => Zlema.Batch(source, output, 10)); Assert.Equal("output", ex.ParamName); } diff --git a/lib/trends_IIR/zlema/Zlema.Validation.Tests.cs b/lib/trends_IIR/zlema/Zlema.Validation.Tests.cs index 8e90806a..0531ba0a 100644 --- a/lib/trends_IIR/zlema/Zlema.Validation.Tests.cs +++ b/lib/trends_IIR/zlema/Zlema.Validation.Tests.cs @@ -29,7 +29,7 @@ public class ZlemaValidationTests double[] reference = new double[series.Count]; ReferenceZlema(series.Values, reference, period); - TSeries batch = Zlema.Calculate(series, period); + TSeries batch = Zlema.Batch(series, period); for (int i = 0; i < series.Count; i++) { @@ -47,7 +47,7 @@ public class ZlemaValidationTests var reference = new double[values.Length]; ReferenceZlema(values, reference, period); - Zlema.Calculate(values, output, period); + Zlema.Batch(values, output, period); for (int i = 0; i < values.Length; i++) { diff --git a/lib/trends_IIR/zlema/Zlema.cs b/lib/trends_IIR/zlema/Zlema.cs index 631fae6c..6057138f 100644 --- a/lib/trends_IIR/zlema/Zlema.cs +++ b/lib/trends_IIR/zlema/Zlema.cs @@ -197,13 +197,13 @@ public sealed class Zlema : AbstractBase } } - public static TSeries Calculate(TSeries source, int period) + public static TSeries Batch(TSeries source, int period) { var zlema = new Zlema(period); return zlema.Update(source); } - public static void Calculate(ReadOnlySpan source, Span output, int period) + public static void Batch(ReadOnlySpan source, Span output, int period) { if (source.Length != output.Length) { @@ -218,10 +218,10 @@ public sealed class Zlema : AbstractBase } double alpha = 2.0 / (period + 1); - Calculate(source, output, alpha, period); + BatchCore(source, output, alpha, period); } - public static void Calculate(ReadOnlySpan source, Span output, double alpha) + public static void Batch(ReadOnlySpan source, Span output, double alpha) { if (source.Length != output.Length) { @@ -239,7 +239,14 @@ public sealed class Zlema : AbstractBase } double period = (2.0 / alpha) - 1.0; - Calculate(source, output, alpha, period); + BatchCore(source, output, alpha, period); + } + + public static (TSeries Results, Zlema Indicator) Calculate(TSeries source, int period) + { + var indicator = new Zlema(period); + TSeries results = indicator.Update(source); + return (results, indicator); } [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -334,7 +341,7 @@ public sealed class Zlema : AbstractBase [MethodImpl(MethodImplOptions.AggressiveInlining)] private void Handle(object? sender, in TValueEventArgs e) => Update(e.Value, e.IsNew); - private static void Calculate(ReadOnlySpan source, Span output, double alpha, double period) + private static void BatchCore(ReadOnlySpan source, Span output, double alpha, double period) { int lag = ComputeLag(period); int bufferSize = lag + 1; @@ -402,4 +409,4 @@ public sealed class Zlema : AbstractBase } } } -} +} \ No newline at end of file diff --git a/lib/trends_IIR/zltema/Zltema.Tests.cs b/lib/trends_IIR/zltema/Zltema.Tests.cs index 51b6141a..99bd4acd 100644 --- a/lib/trends_IIR/zltema/Zltema.Tests.cs +++ b/lib/trends_IIR/zltema/Zltema.Tests.cs @@ -91,7 +91,7 @@ public class ZltemaTests int period = 12; TSeries series = BuildSeries(120, seed: 11); - TSeries batch = Zltema.Calculate(series, period); + TSeries batch = Zltema.Batch(series, period); var zltema = new Zltema(period); var streamValues = new List(series.Count); @@ -114,8 +114,8 @@ public class ZltemaTests double[] values = series.Values.ToArray(); var output = new double[values.Length]; - Zltema.Calculate(values, output, period); - TSeries batch = Zltema.Calculate(series, period); + Zltema.Batch(values, output, period); + TSeries batch = Zltema.Batch(series, period); for (int i = 0; i < values.Length; i++) { @@ -153,7 +153,7 @@ public class ZltemaTests double[] source = [1, 2, 3, 4, 5]; double[] output = new double[3]; - var ex = Assert.Throws(() => Zltema.Calculate(source, output, 10)); + var ex = Assert.Throws(() => Zltema.Batch(source, output, 10)); Assert.Equal("output", ex.ParamName); } diff --git a/lib/trends_IIR/zltema/Zltema.Validation.Tests.cs b/lib/trends_IIR/zltema/Zltema.Validation.Tests.cs index 263bda0a..84f52e04 100644 --- a/lib/trends_IIR/zltema/Zltema.Validation.Tests.cs +++ b/lib/trends_IIR/zltema/Zltema.Validation.Tests.cs @@ -29,7 +29,7 @@ public class ZltemaValidationTests double[] reference = new double[series.Count]; ReferenceZltema(series.Values, reference, period); - TSeries batch = Zltema.Calculate(series, period); + TSeries batch = Zltema.Batch(series, period); for (int i = 0; i < series.Count; i++) { @@ -47,7 +47,7 @@ public class ZltemaValidationTests var reference = new double[values.Length]; ReferenceZltema(values, reference, period); - Zltema.Calculate(values, output, period); + Zltema.Batch(values, output, period); for (int i = 0; i < values.Length; i++) { diff --git a/lib/trends_IIR/zltema/Zltema.cs b/lib/trends_IIR/zltema/Zltema.cs index b4ec8527..8f582b9b 100644 --- a/lib/trends_IIR/zltema/Zltema.cs +++ b/lib/trends_IIR/zltema/Zltema.cs @@ -201,13 +201,13 @@ public sealed class Zltema : AbstractBase } } - public static TSeries Calculate(TSeries source, int period) + public static TSeries Batch(TSeries source, int period) { var zltema = new Zltema(period); return zltema.Update(source); } - public static void Calculate(ReadOnlySpan source, Span output, int period) + public static void Batch(ReadOnlySpan source, Span output, int period) { if (source.Length != output.Length) { @@ -222,10 +222,10 @@ public sealed class Zltema : AbstractBase } double alpha = 2.0 / (period + 1); - Calculate(source, output, alpha, period); + BatchCore(source, output, alpha, period); } - public static void Calculate(ReadOnlySpan source, Span output, double alpha) + public static void Batch(ReadOnlySpan source, Span output, double alpha) { if (source.Length != output.Length) { @@ -243,7 +243,14 @@ public sealed class Zltema : AbstractBase } double period = (2.0 / alpha) - 1.0; - Calculate(source, output, alpha, period); + BatchCore(source, output, alpha, period); + } + + public static (TSeries Results, Zltema Indicator) Calculate(TSeries source, int period) + { + var indicator = new Zltema(period); + TSeries results = indicator.Update(source); + return (results, indicator); } [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -351,7 +358,7 @@ public sealed class Zltema : AbstractBase [MethodImpl(MethodImplOptions.AggressiveInlining)] private void Handle(object? sender, in TValueEventArgs e) => Update(e.Value, e.IsNew); - private static void Calculate(ReadOnlySpan source, Span output, double alpha, double period) + private static void BatchCore(ReadOnlySpan source, Span output, double alpha, double period) { int lag = ComputeLag(period); int bufferSize = lag + 1; diff --git a/lib/volatility/adr/Adr.cs b/lib/volatility/adr/Adr.cs index 57faeb03..a174593e 100644 --- a/lib/volatility/adr/Adr.cs +++ b/lib/volatility/adr/Adr.cs @@ -209,6 +209,13 @@ public sealed class Adr : AbstractBase var adr = new Adr(period, method); return adr.Update(source); } + + public static (TSeries Results, Adr Indicator) Calculate(TBarSeries source, int period, AdrMethod method = AdrMethod.Sma) + { + var indicator = new Adr(period, method); + TSeries results = indicator.Update(source); + return (results, indicator); + } } /// diff --git a/lib/volatility/atr/Atr.cs b/lib/volatility/atr/Atr.cs index e6918ae2..adb78656 100644 --- a/lib/volatility/atr/Atr.cs +++ b/lib/volatility/atr/Atr.cs @@ -240,4 +240,11 @@ public sealed class Atr : AbstractBase var atr = new Atr(period); return atr.Update(source); } + + public static (TSeries Results, Atr Indicator) Calculate(TBarSeries source, int period) + { + var indicator = new Atr(period); + TSeries results = indicator.Update(source); + return (results, indicator); + } } diff --git a/lib/volatility/atrn/Atrn.cs b/lib/volatility/atrn/Atrn.cs index 5d33a061..95f5cd3b 100644 --- a/lib/volatility/atrn/Atrn.cs +++ b/lib/volatility/atrn/Atrn.cs @@ -289,6 +289,13 @@ public sealed class Atrn : AbstractBase return atrn.Update(source); } + public static (TSeries Results, Atrn Indicator) Calculate(TBarSeries source, int period) + { + var indicator = new Atrn(period); + TSeries results = indicator.Update(source); + return (results, indicator); + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] private double GetMax() { diff --git a/lib/volatility/atrp/Atrp.cs b/lib/volatility/atrp/Atrp.cs index fb5da205..b83daeed 100644 --- a/lib/volatility/atrp/Atrp.cs +++ b/lib/volatility/atrp/Atrp.cs @@ -270,4 +270,11 @@ public sealed class Atrp : AbstractBase var atrp = new Atrp(period); return atrp.Update(source); } + + public static (TSeries Results, Atrp Indicator) Calculate(TBarSeries source, int period) + { + var indicator = new Atrp(period); + TSeries results = indicator.Update(source); + return (results, indicator); + } } diff --git a/lib/volatility/bbw/Bbw.Tests.cs b/lib/volatility/bbw/Bbw.Tests.cs index 920f7784..a3e5e5e2 100644 --- a/lib/volatility/bbw/Bbw.Tests.cs +++ b/lib/volatility/bbw/Bbw.Tests.cs @@ -276,7 +276,7 @@ public class BbwTests { ts.Add(new TValue(times[i], close[i])); } - var batchResult = Bbw.Calculate(ts, 20); + var batchResult = Bbw.Batch(ts, 20); Assert.Equal(iterativeResult, batchResult[batchResult.Count - 1].Value, 1e-8); } @@ -302,7 +302,7 @@ public class BbwTests { ts.Add(new TValue(times[i], close[i])); } - var bbwBatch = Bbw.Calculate(ts, 20); + var bbwBatch = Bbw.Batch(ts, 20); var smaResult = smaBatch.Update(bbwBatch); Assert.Equal(sma.Last.Value, smaResult[smaResult.Count - 1].Value, 1e-8); @@ -321,7 +321,7 @@ public class BbwTests ts.Add(new TValue(times[i], close[i])); } - var result = Bbw.Calculate(ts, 20, 2.0); + var result = Bbw.Batch(ts, 20, 2.0); Assert.Equal(100, result.Count); Assert.True(double.IsFinite(result[result.Count - 1].Value)); @@ -336,10 +336,10 @@ public class BbwTests ts.Add(new TValue(DateTime.UtcNow.AddMinutes(i), 100 + i)); } - Assert.Throws(() => Bbw.Calculate(ts, 0)); - Assert.Throws(() => Bbw.Calculate(ts, -1)); - Assert.Throws(() => Bbw.Calculate(ts, 5, 0)); - Assert.Throws(() => Bbw.Calculate(ts, 5, -1)); + Assert.Throws(() => Bbw.Batch(ts, 0)); + Assert.Throws(() => Bbw.Batch(ts, -1)); + Assert.Throws(() => Bbw.Batch(ts, 5, 0)); + Assert.Throws(() => Bbw.Batch(ts, 5, -1)); } [Fact] diff --git a/lib/volatility/bbw/Bbw.cs b/lib/volatility/bbw/Bbw.cs index 03d39426..4d56ec30 100644 --- a/lib/volatility/bbw/Bbw.cs +++ b/lib/volatility/bbw/Bbw.cs @@ -219,7 +219,7 @@ public sealed class Bbw : AbstractBase /// /// Calculates BBW for entire series. /// - public static TSeries Calculate(TSeries source, int period, double multiplier = 2.0) + public static TSeries Batch(TSeries source, int period, double multiplier = 2.0) { int len = source.Count; var t = new List(len); @@ -308,4 +308,11 @@ public sealed class Bbw : AbstractBase output[i] = mean > 0 ? (mult2 * stddev) / mean : 0.0; } } + + public static (TSeries Results, Bbw Indicator) Calculate(TSeries source, int period, double multiplier = 2.0) + { + var indicator = new Bbw(period, multiplier); + TSeries results = indicator.Update(source); + return (results, indicator); + } } \ No newline at end of file diff --git a/lib/volatility/bbwn/Bbwn.Tests.cs b/lib/volatility/bbwn/Bbwn.Tests.cs index 8fcb2e99..3e52bb75 100644 --- a/lib/volatility/bbwn/Bbwn.Tests.cs +++ b/lib/volatility/bbwn/Bbwn.Tests.cs @@ -212,7 +212,7 @@ public class BbwnTests ts.Add(new TValue(bars.Times[i], bars.CloseValues[i])); } - var result = Bbwn.Calculate(ts, 10, 2.0, 50); + var result = Bbwn.Batch(ts, 10, 2.0, 50); Assert.Equal(ts.Count, result.Count); @@ -332,7 +332,7 @@ public class BbwnTests { ts.Add(new TValue(times[i], close[i])); } - var batchResult = Bbwn.Calculate(ts, 10, 2.0, 30); + var batchResult = Bbwn.Batch(ts, 10, 2.0, 30); Assert.Equal(iterativeResult, batchResult[batchResult.Count - 1].Value, 1e-8); } @@ -350,7 +350,7 @@ public class BbwnTests ts.Add(new TValue(times[i], close[i])); } - var result = Bbwn.Calculate(ts, 20, 2.0, 50); + var result = Bbwn.Batch(ts, 20, 2.0, 50); Assert.Equal(100, result.Count); Assert.True(double.IsFinite(result[result.Count - 1].Value)); @@ -367,12 +367,12 @@ public class BbwnTests ts.Add(new TValue(DateTime.UtcNow.AddMinutes(i), 100 + i)); } - Assert.Throws(() => Bbwn.Calculate(ts, 0)); - Assert.Throws(() => Bbwn.Calculate(ts, -1)); - Assert.Throws(() => Bbwn.Calculate(ts, 5, 0)); - Assert.Throws(() => Bbwn.Calculate(ts, 5, -1)); - Assert.Throws(() => Bbwn.Calculate(ts, 5, 2.0, 0)); - Assert.Throws(() => Bbwn.Calculate(ts, 5, 2.0, -1)); + Assert.Throws(() => Bbwn.Batch(ts, 0)); + Assert.Throws(() => Bbwn.Batch(ts, -1)); + Assert.Throws(() => Bbwn.Batch(ts, 5, 0)); + Assert.Throws(() => Bbwn.Batch(ts, 5, -1)); + Assert.Throws(() => Bbwn.Batch(ts, 5, 2.0, 0)); + Assert.Throws(() => Bbwn.Batch(ts, 5, 2.0, -1)); } [Fact] diff --git a/lib/volatility/bbwn/Bbwn.Validation.Tests.cs b/lib/volatility/bbwn/Bbwn.Validation.Tests.cs index 4767781f..045a70cc 100644 --- a/lib/volatility/bbwn/Bbwn.Validation.Tests.cs +++ b/lib/volatility/bbwn/Bbwn.Validation.Tests.cs @@ -141,7 +141,7 @@ public class BbwnValidationTests { source.Add(new TValue(bars.Times[i], bars.CloseValues[i])); } - var result = Bbwn.Calculate(source, period: 10, multiplier: 2.0, lookback: 20); + var result = Bbwn.Batch(source, period: 10, multiplier: 2.0, lookback: 20); Assert.Equal(source.Count, result.Count); diff --git a/lib/volatility/bbwn/Bbwn.cs b/lib/volatility/bbwn/Bbwn.cs index 736254c7..7609d60b 100644 --- a/lib/volatility/bbwn/Bbwn.cs +++ b/lib/volatility/bbwn/Bbwn.cs @@ -273,7 +273,7 @@ public sealed class Bbwn : AbstractBase /// /// Calculates BBWN for entire series. /// - public static TSeries Calculate(TSeries source, int period, double multiplier = 2.0, int lookback = 252) + public static TSeries Batch(TSeries source, int period, double multiplier = 2.0, int lookback = 252) { int len = source.Count; var t = new List(len); @@ -382,4 +382,11 @@ public sealed class Bbwn : AbstractBase output[i] = Math.Max(0.0, Math.Min(1.0, bbwn)); } } + + public static (TSeries Results, Bbwn Indicator) Calculate(TSeries source, int period, double multiplier = 2.0, int lookback = 252) + { + var indicator = new Bbwn(period, multiplier, lookback); + TSeries results = indicator.Update(source); + return (results, indicator); + } } \ No newline at end of file diff --git a/lib/volatility/bbwp/Bbwp.Tests.cs b/lib/volatility/bbwp/Bbwp.Tests.cs index 12385242..1b097e81 100644 --- a/lib/volatility/bbwp/Bbwp.Tests.cs +++ b/lib/volatility/bbwp/Bbwp.Tests.cs @@ -211,7 +211,7 @@ public class BbwpTests ts.Add(new TValue(bars.Times[i], bars.CloseValues[i])); } - var result = Bbwp.Calculate(ts, 10, 2.0, 50); + var result = Bbwp.Batch(ts, 10, 2.0, 50); Assert.Equal(ts.Count, result.Count); @@ -331,7 +331,7 @@ public class BbwpTests { ts.Add(new TValue(times[i], close[i])); } - var batchResult = Bbwp.Calculate(ts, 10, 2.0, 30); + var batchResult = Bbwp.Batch(ts, 10, 2.0, 30); Assert.Equal(iterativeResult, batchResult[batchResult.Count - 1].Value, 1e-8); } @@ -349,7 +349,7 @@ public class BbwpTests ts.Add(new TValue(times[i], close[i])); } - var result = Bbwp.Calculate(ts, 20, 2.0, 50); + var result = Bbwp.Batch(ts, 20, 2.0, 50); Assert.Equal(100, result.Count); Assert.True(double.IsFinite(result[result.Count - 1].Value)); @@ -366,12 +366,12 @@ public class BbwpTests ts.Add(new TValue(DateTime.UtcNow.AddMinutes(i), 100 + i)); } - Assert.Throws(() => Bbwp.Calculate(ts, 0)); - Assert.Throws(() => Bbwp.Calculate(ts, -1)); - Assert.Throws(() => Bbwp.Calculate(ts, 5, 0)); - Assert.Throws(() => Bbwp.Calculate(ts, 5, -1)); - Assert.Throws(() => Bbwp.Calculate(ts, 5, 2.0, 0)); - Assert.Throws(() => Bbwp.Calculate(ts, 5, 2.0, -1)); + Assert.Throws(() => Bbwp.Batch(ts, 0)); + Assert.Throws(() => Bbwp.Batch(ts, -1)); + Assert.Throws(() => Bbwp.Batch(ts, 5, 0)); + Assert.Throws(() => Bbwp.Batch(ts, 5, -1)); + Assert.Throws(() => Bbwp.Batch(ts, 5, 2.0, 0)); + Assert.Throws(() => Bbwp.Batch(ts, 5, 2.0, -1)); } [Fact] diff --git a/lib/volatility/bbwp/Bbwp.Validation.Tests.cs b/lib/volatility/bbwp/Bbwp.Validation.Tests.cs index 012b557b..05859511 100644 --- a/lib/volatility/bbwp/Bbwp.Validation.Tests.cs +++ b/lib/volatility/bbwp/Bbwp.Validation.Tests.cs @@ -50,7 +50,7 @@ public class BbwpValidationTests { ts.Add(new TValue(times[i], close[i])); } - var batchResults = Bbwp.Calculate(ts, 10, 2.0, 50); + var batchResults = Bbwp.Batch(ts, 10, 2.0, 50); // Compare results (should be identical) for (int i = 0; i < bars.Count; i++) diff --git a/lib/volatility/bbwp/Bbwp.cs b/lib/volatility/bbwp/Bbwp.cs index f9c3615c..a5088607 100644 --- a/lib/volatility/bbwp/Bbwp.cs +++ b/lib/volatility/bbwp/Bbwp.cs @@ -266,7 +266,7 @@ public sealed class Bbwp : AbstractBase /// /// Calculates BBWP for entire series. /// - public static TSeries Calculate(TSeries source, int period, double multiplier = 2.0, int lookback = 252) + public static TSeries Batch(TSeries source, int period, double multiplier = 2.0, int lookback = 252) { if (period <= 0) { @@ -396,4 +396,11 @@ public sealed class Bbwp : AbstractBase output[i] = Math.Max(0.0, Math.Min(1.0, bbwp)); } } + + public static (TSeries Results, Bbwp Indicator) Calculate(TSeries source, int period, double multiplier = 2.0, int lookback = 252) + { + var indicator = new Bbwp(period, multiplier, lookback); + TSeries results = indicator.Update(source); + return (results, indicator); + } } \ No newline at end of file diff --git a/lib/volatility/ccv/Ccv.Tests.cs b/lib/volatility/ccv/Ccv.Tests.cs index cb971bc5..460f11de 100644 --- a/lib/volatility/ccv/Ccv.Tests.cs +++ b/lib/volatility/ccv/Ccv.Tests.cs @@ -268,7 +268,7 @@ public class CcvTests { ts.Add(new TValue(times[i], close[i])); } - var batchResult = Ccv.Calculate(ts, 20); + var batchResult = Ccv.Batch(ts, 20); Assert.Equal(iterativeResult, batchResult[batchResult.Count - 1].Value, 1e-8); } @@ -286,7 +286,7 @@ public class CcvTests ts.Add(new TValue(times[i], close[i])); } - var result = Ccv.Calculate(ts, 20); + var result = Ccv.Batch(ts, 20); Assert.Equal(100, result.Count); Assert.True(double.IsFinite(result[result.Count - 1].Value)); @@ -301,10 +301,10 @@ public class CcvTests ts.Add(new TValue(DateTime.UtcNow.AddMinutes(i), 100 + i)); } - Assert.Throws(() => Ccv.Calculate(ts, 0)); - Assert.Throws(() => Ccv.Calculate(ts, -1)); - Assert.Throws(() => Ccv.Calculate(ts, 5, 0)); - Assert.Throws(() => Ccv.Calculate(ts, 5, 4)); + Assert.Throws(() => Ccv.Batch(ts, 0)); + Assert.Throws(() => Ccv.Batch(ts, -1)); + Assert.Throws(() => Ccv.Batch(ts, 5, 0)); + Assert.Throws(() => Ccv.Batch(ts, 5, 4)); } [Fact] diff --git a/lib/volatility/ccv/Ccv.cs b/lib/volatility/ccv/Ccv.cs index 9864c106..493a648a 100644 --- a/lib/volatility/ccv/Ccv.cs +++ b/lib/volatility/ccv/Ccv.cs @@ -299,7 +299,7 @@ public sealed class Ccv : AbstractBase /// /// Calculates CCV for entire series. /// - public static TSeries Calculate(TSeries source, int period, int method = 1) + public static TSeries Batch(TSeries source, int period, int method = 1) { if (period <= 0) { @@ -448,4 +448,11 @@ public sealed class Ccv : AbstractBase output[i] = double.IsFinite(result) ? result : 0.0; } } + + public static (TSeries Results, Ccv Indicator) Calculate(TSeries source, int period, int method = 1) + { + var indicator = new Ccv(period, method); + TSeries results = indicator.Update(source); + return (results, indicator); + } } \ No newline at end of file diff --git a/lib/volatility/cv/Cv.Tests.cs b/lib/volatility/cv/Cv.Tests.cs index 19e166cf..70314d18 100644 --- a/lib/volatility/cv/Cv.Tests.cs +++ b/lib/volatility/cv/Cv.Tests.cs @@ -277,7 +277,7 @@ public class CvTests { ts.Add(new TValue(times[i], close[i])); } - var batchResult = Cv.Calculate(ts, 20); + var batchResult = Cv.Batch(ts, 20); Assert.Equal(iterativeResult, batchResult[batchResult.Count - 1].Value, 1e-8); } @@ -295,7 +295,7 @@ public class CvTests ts.Add(new TValue(times[i], close[i])); } - var result = Cv.Calculate(ts, 20); + var result = Cv.Batch(ts, 20); Assert.Equal(100, result.Count); Assert.True(double.IsFinite(result[result.Count - 1].Value)); @@ -310,10 +310,10 @@ public class CvTests ts.Add(new TValue(DateTime.UtcNow.AddMinutes(i), 100 + i)); } - Assert.Throws(() => Cv.Calculate(ts, 0)); - Assert.Throws(() => Cv.Calculate(ts, -1)); - Assert.Throws(() => Cv.Calculate(ts, 5, 0.0)); // alpha = 0 - Assert.Throws(() => Cv.Calculate(ts, 5, 0.5, 0.6)); // alpha + beta >= 1 + Assert.Throws(() => Cv.Batch(ts, 0)); + Assert.Throws(() => Cv.Batch(ts, -1)); + Assert.Throws(() => Cv.Batch(ts, 5, 0.0)); // alpha = 0 + Assert.Throws(() => Cv.Batch(ts, 5, 0.5, 0.6)); // alpha + beta >= 1 } [Fact] diff --git a/lib/volatility/cv/Cv.Validation.Tests.cs b/lib/volatility/cv/Cv.Validation.Tests.cs index fbca6b79..d10daef8 100644 --- a/lib/volatility/cv/Cv.Validation.Tests.cs +++ b/lib/volatility/cv/Cv.Validation.Tests.cs @@ -229,13 +229,13 @@ public class CvValidationTests streamingCv.Update(new TValue(times[i], close[i])); } - // Batch calculation using Calculate (TSeries -> TSeries) + // Batch calculation using Batch(TSeries -> TSeries) var source = new TSeries(); for (int i = 0; i < bars.Count; i++) { source.Add(times[i], close[i]); } - var batchResult = Cv.Calculate(source, 20, 0.2, 0.7); + var batchResult = Cv.Batch(source, 20, 0.2, 0.7); // Compare last values Assert.Equal(batchResult.Last.Value, streamingCv.Last.Value, 8); @@ -266,7 +266,7 @@ public class CvValidationTests } // TSeries batch using Calculate - var batch = Cv.Calculate(source, 20, 0.2, 0.7); + var batch = Cv.Batch(source, 20, 0.2, 0.7); // Compare Assert.Equal(batch.Last.Value, streaming.Last.Value, 10); diff --git a/lib/volatility/cv/Cv.cs b/lib/volatility/cv/Cv.cs index f75b584b..82a02bb7 100644 --- a/lib/volatility/cv/Cv.cs +++ b/lib/volatility/cv/Cv.cs @@ -286,7 +286,7 @@ public sealed class Cv : AbstractBase /// /// Calculates CV for entire series. /// - public static TSeries Calculate(TSeries source, int period = 20, double alpha = 0.2, double beta = 0.7) + public static TSeries Batch(TSeries source, int period = 20, double alpha = 0.2, double beta = 0.7) { if (period <= 0) { @@ -430,4 +430,11 @@ public sealed class Cv : AbstractBase output[i] = double.IsFinite(result) ? result : 0.0; } } + + public static (TSeries Results, Cv Indicator) Calculate(TSeries source, int period = 20, double alpha = 0.2, double beta = 0.7) + { + var indicator = new Cv(period, alpha, beta); + TSeries results = indicator.Update(source); + return (results, indicator); + } } \ No newline at end of file diff --git a/lib/volatility/cvi/Cvi.Tests.cs b/lib/volatility/cvi/Cvi.Tests.cs index dc82a795..a04d5020 100644 --- a/lib/volatility/cvi/Cvi.Tests.cs +++ b/lib/volatility/cvi/Cvi.Tests.cs @@ -311,7 +311,7 @@ public class CviTests var iterativeResult = cvi.Last.Value; // Batch via static method - var batchResult = Cvi.Calculate(bars, 10, 10); + var batchResult = Cvi.Batch(bars, 10, 10); Assert.Equal(iterativeResult, batchResult[batchResult.Count - 1].Value, 1e-8); } @@ -321,7 +321,7 @@ public class CviTests { var bars = GenerateTestData(100); - var result = Cvi.Calculate(bars, 14, 10); + var result = Cvi.Batch(bars, 14, 10); Assert.Equal(100, result.Count); Assert.True(double.IsFinite(result[result.Count - 1].Value)); @@ -336,10 +336,10 @@ public class CviTests ts.Add(new TValue(DateTime.UtcNow.AddMinutes(i), 100 + i)); } - Assert.Throws(() => Cvi.Calculate(ts, 0, 10)); - Assert.Throws(() => Cvi.Calculate(ts, -1, 10)); - Assert.Throws(() => Cvi.Calculate(ts, 10, 0)); - Assert.Throws(() => Cvi.Calculate(ts, 10, -1)); + Assert.Throws(() => Cvi.Batch(ts, 0, 10)); + Assert.Throws(() => Cvi.Batch(ts, -1, 10)); + Assert.Throws(() => Cvi.Batch(ts, 10, 0)); + Assert.Throws(() => Cvi.Batch(ts, 10, -1)); } [Fact] diff --git a/lib/volatility/cvi/Cvi.Validation.Tests.cs b/lib/volatility/cvi/Cvi.Validation.Tests.cs index cdb69c3b..89f1d6d7 100644 --- a/lib/volatility/cvi/Cvi.Validation.Tests.cs +++ b/lib/volatility/cvi/Cvi.Validation.Tests.cs @@ -196,7 +196,7 @@ public class CviValidationTests } // Batch calculation - var batchResult = Cvi.Calculate(bars, 10, 10); + var batchResult = Cvi.Batch(bars, 10, 10); // Compare last values Assert.Equal(batchResult.Last.Value, streamingCvi.Last.Value, 8); diff --git a/lib/volatility/cvi/Cvi.cs b/lib/volatility/cvi/Cvi.cs index 82c30666..55d619b7 100644 --- a/lib/volatility/cvi/Cvi.cs +++ b/lib/volatility/cvi/Cvi.cs @@ -276,7 +276,7 @@ public sealed class Cvi : AbstractBase /// /// Calculates CVI for entire TBarSeries. /// - public static TSeries Calculate(TBarSeries source, int rocLength = 10, int smoothLength = 10) + public static TSeries Batch(TBarSeries source, int rocLength = 10, int smoothLength = 10) { var cvi = new Cvi(rocLength, smoothLength); return cvi.Update(source); @@ -285,7 +285,7 @@ public sealed class Cvi : AbstractBase /// /// Calculates CVI for entire series (assumes values are pre-calculated ranges). /// - public static TSeries Calculate(TSeries source, int rocLength = 10, int smoothLength = 10) + public static TSeries Batch(TSeries source, int rocLength = 10, int smoothLength = 10) { if (rocLength <= 0) { @@ -381,4 +381,11 @@ public sealed class Cvi : AbstractBase output[i] = double.IsFinite(result) ? result : 0.0; } } + + public static (TSeries Results, Cvi Indicator) Calculate(TBarSeries source, int rocLength = 10, int smoothLength = 10) + { + var indicator = new Cvi(rocLength, smoothLength); + TSeries results = indicator.Update(source); + return (results, indicator); + } } \ No newline at end of file diff --git a/lib/volatility/ewma/Ewma.Tests.cs b/lib/volatility/ewma/Ewma.Tests.cs index 2516b332..c6f7eee0 100644 --- a/lib/volatility/ewma/Ewma.Tests.cs +++ b/lib/volatility/ewma/Ewma.Tests.cs @@ -274,7 +274,7 @@ public class EwmaTests { ts.Add(new TValue(times[i], close[i])); } - var batchResult = Ewma.Calculate(ts, 20); + var batchResult = Ewma.Batch(ts, 20); Assert.Equal(iterativeResult, batchResult[batchResult.Count - 1].Value, 1e-8); } @@ -292,7 +292,7 @@ public class EwmaTests ts.Add(new TValue(times[i], close[i])); } - var result = Ewma.Calculate(ts, 20); + var result = Ewma.Batch(ts, 20); Assert.Equal(100, result.Count); Assert.True(double.IsFinite(result[result.Count - 1].Value)); @@ -307,10 +307,10 @@ public class EwmaTests ts.Add(new TValue(DateTime.UtcNow.AddMinutes(i), 100 + i)); } - Assert.Throws(() => Ewma.Calculate(ts, 0)); - Assert.Throws(() => Ewma.Calculate(ts, -1)); - Assert.Throws(() => Ewma.Calculate(ts, 5, true, 0)); - Assert.Throws(() => Ewma.Calculate(ts, 5, true, -1)); + Assert.Throws(() => Ewma.Batch(ts, 0)); + Assert.Throws(() => Ewma.Batch(ts, -1)); + Assert.Throws(() => Ewma.Batch(ts, 5, true, 0)); + Assert.Throws(() => Ewma.Batch(ts, 5, true, -1)); } [Fact] diff --git a/lib/volatility/ewma/Ewma.Validation.Tests.cs b/lib/volatility/ewma/Ewma.Validation.Tests.cs index 8333c6ed..0890b71d 100644 --- a/lib/volatility/ewma/Ewma.Validation.Tests.cs +++ b/lib/volatility/ewma/Ewma.Validation.Tests.cs @@ -160,7 +160,7 @@ public class EwmaValidationTests } // Batch - var batchResult = Ewma.Calculate(ts, DefaultPeriod, DefaultAnnualize, DefaultAnnualPeriods); + var batchResult = Ewma.Batch(ts, DefaultPeriod, DefaultAnnualize, DefaultAnnualPeriods); Assert.Equal(ewmaStream.Last.Value, batchResult[batchResult.Count - 1].Value, StreamingTolerance); } @@ -226,7 +226,7 @@ public class EwmaValidationTests double tseriesValue = tseriesResult[tseriesResult.Count - 1].Value; // Mode 3: Static Calculate - var batchResult = Ewma.Calculate(ts, DefaultPeriod, DefaultAnnualize, DefaultAnnualPeriods); + var batchResult = Ewma.Batch(ts, DefaultPeriod, DefaultAnnualize, DefaultAnnualPeriods); double batchValue = batchResult[batchResult.Count - 1].Value; // Mode 4: Span Batch diff --git a/lib/volatility/ewma/Ewma.cs b/lib/volatility/ewma/Ewma.cs index b7597697..63001a7e 100644 --- a/lib/volatility/ewma/Ewma.cs +++ b/lib/volatility/ewma/Ewma.cs @@ -238,7 +238,7 @@ public sealed class Ewma : AbstractBase /// /// Calculates EWMA Volatility for entire series. /// - public static TSeries Calculate(TSeries source, int period = 20, bool annualize = true, int annualPeriods = 252) + public static TSeries Batch(TSeries source, int period = 20, bool annualize = true, int annualPeriods = 252) { if (period <= 0) { @@ -350,4 +350,11 @@ public sealed class Ewma : AbstractBase output[i] = double.IsFinite(result) ? result : 0.0; } } + + public static (TSeries Results, Ewma Indicator) Calculate(TSeries source, int period = 20, bool annualize = true, int annualPeriods = 252) + { + var indicator = new Ewma(period, annualize, annualPeriods); + TSeries results = indicator.Update(source); + return (results, indicator); + } } \ No newline at end of file diff --git a/lib/volatility/gkv/Gkv.Tests.cs b/lib/volatility/gkv/Gkv.Tests.cs index bf0333fd..e37f2bfc 100644 --- a/lib/volatility/gkv/Gkv.Tests.cs +++ b/lib/volatility/gkv/Gkv.Tests.cs @@ -420,7 +420,7 @@ public class GkvTests const int dataCount = 50; var barSeries = GenerateTestData(dataCount); - var result = Gkv.Calculate(barSeries, period: 10); + var result = Gkv.Batch(barSeries, period: 10); Assert.Equal(dataCount, result.Count); } @@ -614,7 +614,7 @@ public class GkvTests { var bars = GenerateTestData(100); - var result = Gkv.Calculate(bars, period: 14); + var result = Gkv.Batch(bars, period: 14); Assert.Equal(100, result.Count); Assert.True(double.IsFinite(result[result.Count - 1].Value)); @@ -625,9 +625,9 @@ public class GkvTests { var bars = GenerateTestData(10); - Assert.Throws(() => Gkv.Calculate(bars, period: 0)); - Assert.Throws(() => Gkv.Calculate(bars, period: -1)); - Assert.Throws(() => Gkv.Calculate(bars, period: 10, annualize: true, annualPeriods: 0)); + Assert.Throws(() => Gkv.Batch(bars, period: 0)); + Assert.Throws(() => Gkv.Batch(bars, period: -1)); + Assert.Throws(() => Gkv.Batch(bars, period: 10, annualize: true, annualPeriods: 0)); } [Fact] diff --git a/lib/volatility/gkv/Gkv.Validation.Tests.cs b/lib/volatility/gkv/Gkv.Validation.Tests.cs index 35fc5363..e6052c17 100644 --- a/lib/volatility/gkv/Gkv.Validation.Tests.cs +++ b/lib/volatility/gkv/Gkv.Validation.Tests.cs @@ -191,7 +191,7 @@ public class GkvValidationTests } // Batch calculation - var batchResult = Gkv.Calculate(bars, 14); + var batchResult = Gkv.Batch(bars, 14); // Compare last values Assert.Equal(batchResult.Last.Value, streamingGkv.Last.Value, 8); diff --git a/lib/volatility/gkv/Gkv.cs b/lib/volatility/gkv/Gkv.cs index 26357eca..c545c4b0 100644 --- a/lib/volatility/gkv/Gkv.cs +++ b/lib/volatility/gkv/Gkv.cs @@ -350,7 +350,7 @@ public sealed class Gkv : AbstractBase /// Whether to annualize. /// Periods per year. /// A TSeries containing the volatility values. - public static TSeries Calculate(TBarSeries source, int period = 20, bool annualize = true, int annualPeriods = 252) + public static TSeries Batch(TBarSeries source, int period = 20, bool annualize = true, int annualPeriods = 252) { var gkv = new Gkv(period, annualize, annualPeriods); return gkv.Update(source); @@ -359,7 +359,7 @@ public sealed class Gkv : AbstractBase /// /// Calculates GKV for a TSeries (treats values as pre-computed GK estimators). /// - public static TSeries Calculate(TSeries source, int period = 20, bool annualize = true, int annualPeriods = 252) + public static TSeries Batch(TSeries source, int period = 20, bool annualize = true, int annualPeriods = 252) { if (period <= 0) { @@ -496,6 +496,14 @@ public sealed class Gkv : AbstractBase } } + public static (TSeries Results, Gkv Indicator) Calculate(TBarSeries source, int period = 20, bool annualize = true, int annualPeriods = 252) + { + var indicator = new Gkv(period, annualize, annualPeriods); + TSeries results = indicator.Update(source); + return (results, indicator); + } + + /// /// Batch calculation from pre-computed GK estimators. /// @@ -570,4 +578,4 @@ public sealed class Gkv : AbstractBase output[i] = volatility; } } -} \ No newline at end of file +} diff --git a/lib/volatility/hlv/Hlv.Tests.cs b/lib/volatility/hlv/Hlv.Tests.cs index a283de29..04e6e739 100644 --- a/lib/volatility/hlv/Hlv.Tests.cs +++ b/lib/volatility/hlv/Hlv.Tests.cs @@ -415,7 +415,7 @@ public class HlvTests const int dataCount = 50; var barSeries = GenerateTestData(dataCount); - var result = Hlv.Calculate(barSeries, period: 10); + var result = Hlv.Batch(barSeries, period: 10); Assert.Equal(dataCount, result.Count); } @@ -599,7 +599,7 @@ public class HlvTests { var bars = GenerateTestData(100); - var result = Hlv.Calculate(bars, period: 14); + var result = Hlv.Batch(bars, period: 14); Assert.Equal(100, result.Count); Assert.True(double.IsFinite(result[result.Count - 1].Value)); @@ -610,9 +610,9 @@ public class HlvTests { var bars = GenerateTestData(10); - Assert.Throws(() => Hlv.Calculate(bars, period: 0)); - Assert.Throws(() => Hlv.Calculate(bars, period: -1)); - Assert.Throws(() => Hlv.Calculate(bars, period: 10, annualize: true, annualPeriods: 0)); + Assert.Throws(() => Hlv.Batch(bars, period: 0)); + Assert.Throws(() => Hlv.Batch(bars, period: -1)); + Assert.Throws(() => Hlv.Batch(bars, period: 10, annualize: true, annualPeriods: 0)); } [Fact] diff --git a/lib/volatility/hlv/Hlv.Validation.Tests.cs b/lib/volatility/hlv/Hlv.Validation.Tests.cs index 09709833..99e33853 100644 --- a/lib/volatility/hlv/Hlv.Validation.Tests.cs +++ b/lib/volatility/hlv/Hlv.Validation.Tests.cs @@ -192,7 +192,7 @@ public class HlvValidationTests } // Batch calculation - var batchResult = Hlv.Calculate(bars, 14); + var batchResult = Hlv.Batch(bars, 14); // Compare last values Assert.Equal(batchResult.Last.Value, streamingHlv.Last.Value, 8); diff --git a/lib/volatility/hlv/Hlv.cs b/lib/volatility/hlv/Hlv.cs index 591cf79b..6e8b6cf5 100644 --- a/lib/volatility/hlv/Hlv.cs +++ b/lib/volatility/hlv/Hlv.cs @@ -335,7 +335,7 @@ public sealed class Hlv : AbstractBase /// Whether to annualize. /// Periods per year. /// A TSeries containing the volatility values. - public static TSeries Calculate(TBarSeries source, int period = 20, bool annualize = true, int annualPeriods = 252) + public static TSeries Batch(TBarSeries source, int period = 20, bool annualize = true, int annualPeriods = 252) { var hlv = new Hlv(period, annualize, annualPeriods); return hlv.Update(source); @@ -344,7 +344,7 @@ public sealed class Hlv : AbstractBase /// /// Calculates HLV for a TSeries (treats values as pre-computed Parkinson estimators). /// - public static TSeries Calculate(TSeries source, int period = 20, bool annualize = true, int annualPeriods = 252) + public static TSeries Batch(TSeries source, int period = 20, bool annualize = true, int annualPeriods = 252) { if (period <= 0) { @@ -474,6 +474,14 @@ public sealed class Hlv : AbstractBase } } + public static (TSeries Results, Hlv Indicator) Calculate(TBarSeries source, int period = 20, bool annualize = true, int annualPeriods = 252) + { + var indicator = new Hlv(period, annualize, annualPeriods); + TSeries results = indicator.Update(source); + return (results, indicator); + } + + /// /// Batch calculation from pre-computed Parkinson estimators. /// @@ -548,4 +556,4 @@ public sealed class Hlv : AbstractBase output[i] = volatility; } } -} \ No newline at end of file +} diff --git a/lib/volatility/hv/Hv.Tests.cs b/lib/volatility/hv/Hv.Tests.cs index 1387c66c..73cc6fbb 100644 --- a/lib/volatility/hv/Hv.Tests.cs +++ b/lib/volatility/hv/Hv.Tests.cs @@ -465,7 +465,7 @@ public class HvTests const int dataCount = 50; var priceSeries = GeneratePriceSeries(dataCount); - var result = Hv.Calculate(priceSeries, period: 10); + var result = Hv.Batch(priceSeries, period: 10); Assert.Equal(dataCount, result.Count); } @@ -657,7 +657,7 @@ public class HvTests { var prices = GeneratePriceSeries(100); - var result = Hv.Calculate(prices, period: 14); + var result = Hv.Batch(prices, period: 14); Assert.Equal(100, result.Count); Assert.True(double.IsFinite(result[result.Count - 1].Value)); @@ -668,7 +668,7 @@ public class HvTests { var bars = GenerateTestData(100); - var result = Hv.Calculate(bars, period: 14); + var result = Hv.Batch(bars, period: 14); Assert.Equal(100, result.Count); Assert.True(double.IsFinite(result[result.Count - 1].Value)); @@ -679,10 +679,10 @@ public class HvTests { var prices = GeneratePriceSeries(10); - Assert.Throws(() => Hv.Calculate(prices, period: 1)); - Assert.Throws(() => Hv.Calculate(prices, period: 0)); - Assert.Throws(() => Hv.Calculate(prices, period: -1)); - Assert.Throws(() => Hv.Calculate(prices, period: 10, annualize: true, annualPeriods: 0)); + Assert.Throws(() => Hv.Batch(prices, period: 1)); + Assert.Throws(() => Hv.Batch(prices, period: 0)); + Assert.Throws(() => Hv.Batch(prices, period: -1)); + Assert.Throws(() => Hv.Batch(prices, period: 10, annualize: true, annualPeriods: 0)); } [Fact] diff --git a/lib/volatility/hv/Hv.Validation.Tests.cs b/lib/volatility/hv/Hv.Validation.Tests.cs index f0c0ed0d..78d8666b 100644 --- a/lib/volatility/hv/Hv.Validation.Tests.cs +++ b/lib/volatility/hv/Hv.Validation.Tests.cs @@ -192,7 +192,7 @@ public class HvValidationTests } // Batch calculation - var batchResult = Hv.Calculate(prices, 14); + var batchResult = Hv.Batch(prices, 14); // Compare last values Assert.Equal(batchResult.Last.Value, streamingHv.Last.Value, 8); diff --git a/lib/volatility/hv/Hv.cs b/lib/volatility/hv/Hv.cs index 64ee011e..a413196d 100644 --- a/lib/volatility/hv/Hv.cs +++ b/lib/volatility/hv/Hv.cs @@ -335,7 +335,7 @@ public sealed class Hv : AbstractBase /// Whether to annualize. /// Periods per year. /// A TSeries containing the volatility values. - public static TSeries Calculate(TSeries source, int period = 20, bool annualize = true, int annualPeriods = 252) + public static TSeries Batch(TSeries source, int period = 20, bool annualize = true, int annualPeriods = 252) { if (period < 2) { @@ -364,7 +364,7 @@ public sealed class Hv : AbstractBase /// /// Calculates HV for a bar series (static). /// - public static TSeries Calculate(TBarSeries source, int period = 20, bool annualize = true, int annualPeriods = 252) + public static TSeries Batch(TBarSeries source, int period = 20, bool annualize = true, int annualPeriods = 252) { var hv = new Hv(period, annualize, annualPeriods); return hv.Update(source); @@ -492,4 +492,12 @@ public sealed class Hv : AbstractBase output[i] = volatility; } } -} \ No newline at end of file + + public static (TSeries Results, Hv Indicator) Calculate(TSeries source, int period = 20, bool annualize = true, int annualPeriods = 252) + { + var indicator = new Hv(period, annualize, annualPeriods); + TSeries results = indicator.Update(source); + return (results, indicator); + } + +} diff --git a/lib/volatility/jvolty/Jvolty.Tests.cs b/lib/volatility/jvolty/Jvolty.Tests.cs index e31ee61c..8c2af310 100644 --- a/lib/volatility/jvolty/Jvolty.Tests.cs +++ b/lib/volatility/jvolty/Jvolty.Tests.cs @@ -397,7 +397,7 @@ public class JvoltyTests // Span calculation var output = new double[series.Count]; - Jvolty.Calculate(series.Values, output, 10); + Jvolty.Batch(series.Values, output, 10); // Compare last value (after warmup) Assert.Equal(streamingLast, output[series.Count - 1], 1e-6); @@ -435,7 +435,7 @@ public class JvoltyTests var source = new double[10]; var output = new double[5]; // Wrong size - var ex = Assert.Throws(() => Jvolty.Calculate(source, output, 10)); + var ex = Assert.Throws(() => Jvolty.Batch(source, output, 10)); Assert.Equal("output", ex.ParamName); } @@ -445,7 +445,7 @@ public class JvoltyTests var source = Array.Empty(); var output = Array.Empty(); - var exception = Record.Exception(() => Jvolty.Calculate(source, output, 10)); + var exception = Record.Exception(() => Jvolty.Batch(source, output, 10)); Assert.Null(exception); } @@ -455,7 +455,7 @@ public class JvoltyTests var source = new double[10]; var output = new double[10]; - Assert.Throws(() => Jvolty.Calculate(source, output, 0)); + Assert.Throws(() => Jvolty.Batch(source, output, 0)); } // ============== Edge Cases ============== diff --git a/lib/volatility/jvolty/Jvolty.cs b/lib/volatility/jvolty/Jvolty.cs index fb7a8550..9617a978 100644 --- a/lib/volatility/jvolty/Jvolty.cs +++ b/lib/volatility/jvolty/Jvolty.cs @@ -37,6 +37,7 @@ public sealed class Jvolty : AbstractBase private readonly RingBuffer _volBuffer; private readonly TValuePublishedHandler _handler; private readonly ITValuePublisher? _source; + private bool _disposed; // Streaming state (current + previous snapshot for isNew=false) private State _s; @@ -293,9 +294,13 @@ public sealed class Jvolty : AbstractBase protected override void Dispose(bool disposing) { - if (disposing && _source != null) + if (!_disposed) { - _source.Pub -= _handler; + if (disposing && _source != null) + { + _source.Pub -= _handler; + } + _disposed = true; } base.Dispose(disposing); } @@ -320,7 +325,7 @@ public sealed class Jvolty : AbstractBase /// /// Static helper for span-based calculation. /// - public static void Calculate(ReadOnlySpan source, + public static void Batch(ReadOnlySpan source, Span output, int period) { @@ -341,6 +346,13 @@ public sealed class Jvolty : AbstractBase } } + public static (TSeries Results, Jvolty Indicator) Calculate(TSeries source, int period) + { + var indicator = new Jvolty(period); + TSeries results = indicator.Update(source); + return (results, indicator); + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] private double CalculateTrimmedMean(double fallback) { diff --git a/lib/volatility/jvoltyn/Jvoltyn.Tests.cs b/lib/volatility/jvoltyn/Jvoltyn.Tests.cs index 02ac7898..4c312632 100644 --- a/lib/volatility/jvoltyn/Jvoltyn.Tests.cs +++ b/lib/volatility/jvoltyn/Jvoltyn.Tests.cs @@ -424,7 +424,7 @@ public class JvoltynTests // Span calculation var output = new double[series.Count]; - Jvoltyn.Calculate(series.Values, output, 10); + Jvoltyn.Batch(series.Values, output, 10); // Compare last value (after warmup) Assert.Equal(streamingLast, output[series.Count - 1], 1e-6); @@ -503,7 +503,7 @@ public class JvoltynTests var source = new double[10]; var output = new double[5]; // Wrong size - var ex = Assert.Throws(() => Jvoltyn.Calculate(source, output, 10)); + var ex = Assert.Throws(() => Jvoltyn.Batch(source, output, 10)); Assert.Equal("output", ex.ParamName); } @@ -513,7 +513,7 @@ public class JvoltynTests var source = Array.Empty(); var output = Array.Empty(); - var exception = Record.Exception(() => Jvoltyn.Calculate(source, output, 10)); + var exception = Record.Exception(() => Jvoltyn.Batch(source, output, 10)); Assert.Null(exception); } @@ -523,7 +523,7 @@ public class JvoltynTests var source = new double[10]; var output = new double[10]; - Assert.Throws(() => Jvoltyn.Calculate(source, output, 0)); + Assert.Throws(() => Jvoltyn.Batch(source, output, 0)); } // ============== Edge Cases ============== diff --git a/lib/volatility/jvoltyn/Jvoltyn.cs b/lib/volatility/jvoltyn/Jvoltyn.cs index 2eef98f9..ab81c0e5 100644 --- a/lib/volatility/jvoltyn/Jvoltyn.cs +++ b/lib/volatility/jvoltyn/Jvoltyn.cs @@ -38,6 +38,7 @@ public sealed class Jvoltyn : AbstractBase private readonly RingBuffer _volBuffer; private readonly TValuePublishedHandler _handler; private readonly ITValuePublisher? _source; + private bool _disposed; // Streaming state (current + previous snapshot for isNew=false) private State _s; @@ -305,9 +306,13 @@ public sealed class Jvoltyn : AbstractBase protected override void Dispose(bool disposing) { - if (disposing && _source != null) + if (!_disposed) { - _source.Pub -= _handler; + if (disposing && _source != null) + { + _source.Pub -= _handler; + } + _disposed = true; } base.Dispose(disposing); } @@ -332,7 +337,7 @@ public sealed class Jvoltyn : AbstractBase /// /// Static helper for span-based calculation. /// - public static void Calculate(ReadOnlySpan source, + public static void Batch(ReadOnlySpan source, Span output, int period) { @@ -353,6 +358,13 @@ public sealed class Jvoltyn : AbstractBase } } + public static (TSeries Results, Jvoltyn Indicator) Calculate(TSeries source, int period) + { + var indicator = new Jvoltyn(period); + TSeries results = indicator.Update(source); + return (results, indicator); + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] private double CalculateTrimmedMean(double fallback) { diff --git a/lib/volatility/massi/Massi.Tests.cs b/lib/volatility/massi/Massi.Tests.cs index 491436e6..2be0ae73 100644 --- a/lib/volatility/massi/Massi.Tests.cs +++ b/lib/volatility/massi/Massi.Tests.cs @@ -439,7 +439,7 @@ public class MassiTests // Span calculation var output = new double[series.Count]; - Massi.Calculate(series.Values, output, 9, 25); + Massi.Batch(series.Values, output, 9, 25); // Compare last value Assert.Equal(streamingLast, output[series.Count - 1], 1e-6); @@ -530,7 +530,7 @@ public class MassiTests var source = new double[10]; var output = new double[5]; // Wrong size - var ex = Assert.Throws(() => Massi.Calculate(source, output, 9, 25)); + var ex = Assert.Throws(() => Massi.Batch(source, output, 9, 25)); Assert.Equal("output", ex.ParamName); } @@ -540,7 +540,7 @@ public class MassiTests var source = Array.Empty(); var output = Array.Empty(); - var exception = Record.Exception(() => Massi.Calculate(source, output, 9, 25)); + var exception = Record.Exception(() => Massi.Batch(source, output, 9, 25)); Assert.Null(exception); } @@ -550,8 +550,8 @@ public class MassiTests var source = new double[10]; var output = new double[10]; - Assert.Throws(() => Massi.Calculate(source, output, 0, 25)); - Assert.Throws(() => Massi.Calculate(source, output, 9, 0)); + Assert.Throws(() => Massi.Batch(source, output, 0, 25)); + Assert.Throws(() => Massi.Batch(source, output, 9, 0)); } // ============== Edge Cases ============== diff --git a/lib/volatility/massi/Massi.cs b/lib/volatility/massi/Massi.cs index f4f1d313..cbf96e2e 100644 --- a/lib/volatility/massi/Massi.cs +++ b/lib/volatility/massi/Massi.cs @@ -30,6 +30,7 @@ public sealed class Massi : AbstractBase private readonly RingBuffer _sumBuffer; private readonly TValuePublishedHandler _handler; private readonly ITValuePublisher? _source; + private bool _disposed; private State _s; private State _ps; @@ -324,9 +325,13 @@ public sealed class Massi : AbstractBase protected override void Dispose(bool disposing) { - if (disposing && _source != null) + if (!_disposed) { - _source.Pub -= _handler; + if (disposing && _source != null) + { + _source.Pub -= _handler; + } + _disposed = true; } base.Dispose(disposing); } @@ -363,7 +368,7 @@ public sealed class Massi : AbstractBase /// /// Static helper for span-based calculation (assumes input is H-L range). /// - public static void Calculate(ReadOnlySpan source, Span output, + public static void Batch(ReadOnlySpan source, Span output, int emaLength = 9, int sumLength = 25) { if (output.Length != source.Length) @@ -382,4 +387,11 @@ public sealed class Massi : AbstractBase output[i] = massi.CalculateMassiStep(source[i]); } } + + public static (TSeries Results, Massi Indicator) Calculate(TBarSeries source, int emaLength = 9, int sumLength = 25) + { + var indicator = new Massi(emaLength, sumLength); + TSeries results = indicator.Update(source); + return (results, indicator); + } } \ No newline at end of file diff --git a/lib/volatility/natr/Natr.cs b/lib/volatility/natr/Natr.cs index 097531fa..947ce476 100644 --- a/lib/volatility/natr/Natr.cs +++ b/lib/volatility/natr/Natr.cs @@ -275,4 +275,11 @@ public sealed class Natr : AbstractBase var natr = new Natr(period); return natr.Update(source); } -} \ No newline at end of file + + public static (TSeries Results, Natr Indicator) Calculate(TBarSeries source, int period = 14) + { + var indicator = new Natr(period); + TSeries results = indicator.Update(source); + return (results, indicator); + } +} diff --git a/lib/volatility/rsv/Rsv.Tests.cs b/lib/volatility/rsv/Rsv.Tests.cs index 4306e495..80575f0f 100644 --- a/lib/volatility/rsv/Rsv.Tests.cs +++ b/lib/volatility/rsv/Rsv.Tests.cs @@ -419,7 +419,7 @@ public class RsvTests const int dataCount = 50; var barSeries = GenerateTestData(dataCount); - var result = Rsv.Calculate(barSeries, period: 10); + var result = Rsv.Batch(barSeries, period: 10); Assert.Equal(dataCount, result.Count); } @@ -685,7 +685,7 @@ public class RsvTests { var bars = GenerateTestData(100); - var result = Rsv.Calculate(bars, period: 14); + var result = Rsv.Batch(bars, period: 14); Assert.Equal(100, result.Count); Assert.True(double.IsFinite(result[result.Count - 1].Value)); @@ -696,9 +696,9 @@ public class RsvTests { var bars = GenerateTestData(10); - Assert.Throws(() => Rsv.Calculate(bars, period: 0)); - Assert.Throws(() => Rsv.Calculate(bars, period: -1)); - Assert.Throws(() => Rsv.Calculate(bars, period: 10, annualize: true, annualPeriods: 0)); + Assert.Throws(() => Rsv.Batch(bars, period: 0)); + Assert.Throws(() => Rsv.Batch(bars, period: -1)); + Assert.Throws(() => Rsv.Batch(bars, period: 10, annualize: true, annualPeriods: 0)); } [Fact] diff --git a/lib/volatility/rsv/Rsv.Validation.Tests.cs b/lib/volatility/rsv/Rsv.Validation.Tests.cs index 29c93a85..9d172021 100644 --- a/lib/volatility/rsv/Rsv.Validation.Tests.cs +++ b/lib/volatility/rsv/Rsv.Validation.Tests.cs @@ -184,7 +184,7 @@ public class RsvValidationTests } // Batch calculation - var batchResult = Rsv.Calculate(bars, 14); + var batchResult = Rsv.Batch(bars, 14); // Compare last values Assert.Equal(batchResult.Last.Value, streamingRsv.Last.Value, 8); diff --git a/lib/volatility/rsv/Rsv.cs b/lib/volatility/rsv/Rsv.cs index f28d1189..68325554 100644 --- a/lib/volatility/rsv/Rsv.cs +++ b/lib/volatility/rsv/Rsv.cs @@ -49,6 +49,7 @@ public sealed class Rsv : AbstractBase // Event source for disposal private readonly ITValuePublisher? _source; + private bool _disposed; [StructLayout(LayoutKind.Auto)] private record struct State( @@ -363,9 +364,13 @@ public sealed class Rsv : AbstractBase /// True if disposing managed resources. protected override void Dispose(bool disposing) { - if (disposing && _source is not null) + if (!_disposed) { - _source.Pub -= Handle; + if (disposing && _source is not null) + { + _source.Pub -= Handle; + } + _disposed = true; } base.Dispose(disposing); } @@ -378,7 +383,7 @@ public sealed class Rsv : AbstractBase /// Whether to annualize. /// Periods per year. /// A TSeries containing the volatility values. - public static TSeries Calculate(TBarSeries source, int period = 20, bool annualize = true, int annualPeriods = 252) + public static TSeries Batch(TBarSeries source, int period = 20, bool annualize = true, int annualPeriods = 252) { var rsv = new Rsv(period, annualize, annualPeriods); return rsv.Update(source); @@ -387,7 +392,7 @@ public sealed class Rsv : AbstractBase /// /// Calculates RSV for a TSeries (treats values as pre-computed RS variances). /// - public static TSeries Calculate(TSeries source, int period = 20, bool annualize = true, int annualPeriods = 252) + public static TSeries Batch(TSeries source, int period = 20, bool annualize = true, int annualPeriods = 252) { if (period <= 0) { @@ -523,6 +528,14 @@ public sealed class Rsv : AbstractBase } } + public static (TSeries Results, Rsv Indicator) Calculate(TBarSeries source, int period = 20, bool annualize = true, int annualPeriods = 252) + { + var indicator = new Rsv(period, annualize, annualPeriods); + TSeries results = indicator.Update(source); + return (results, indicator); + } + + /// /// Batch calculation from pre-computed RS variances. /// @@ -596,4 +609,4 @@ public sealed class Rsv : AbstractBase output[i] = volatility; } } -} \ No newline at end of file +} diff --git a/lib/volatility/rv/Rv.Tests.cs b/lib/volatility/rv/Rv.Tests.cs index e6aa6efb..db56a38c 100644 --- a/lib/volatility/rv/Rv.Tests.cs +++ b/lib/volatility/rv/Rv.Tests.cs @@ -439,7 +439,7 @@ public class RvTests const int dataCount = 50; var priceSeries = GeneratePriceSeries(dataCount); - var result = Rv.Calculate(priceSeries, period: 5, smoothingPeriod: 10); + var result = Rv.Batch(priceSeries, period: 5, smoothingPeriod: 10); Assert.Equal(dataCount, result.Count); } @@ -617,7 +617,7 @@ public class RvTests { var prices = GeneratePriceSeries(100); - var result = Rv.Calculate(prices, period: 5, smoothingPeriod: 14); + var result = Rv.Batch(prices, period: 5, smoothingPeriod: 14); Assert.Equal(100, result.Count); Assert.True(double.IsFinite(result[result.Count - 1].Value)); @@ -628,7 +628,7 @@ public class RvTests { var bars = GenerateTestData(100); - var result = Rv.Calculate(bars, period: 5, smoothingPeriod: 14); + var result = Rv.Batch(bars, period: 5, smoothingPeriod: 14); Assert.Equal(100, result.Count); Assert.True(double.IsFinite(result[result.Count - 1].Value)); @@ -639,10 +639,10 @@ public class RvTests { var prices = GeneratePriceSeries(10); - Assert.Throws(() => Rv.Calculate(prices, period: 0)); - Assert.Throws(() => Rv.Calculate(prices, period: -1)); - Assert.Throws(() => Rv.Calculate(prices, period: 5, smoothingPeriod: 0)); - Assert.Throws(() => Rv.Calculate(prices, period: 5, smoothingPeriod: 10, annualize: true, annualPeriods: 0)); + Assert.Throws(() => Rv.Batch(prices, period: 0)); + Assert.Throws(() => Rv.Batch(prices, period: -1)); + Assert.Throws(() => Rv.Batch(prices, period: 5, smoothingPeriod: 0)); + Assert.Throws(() => Rv.Batch(prices, period: 5, smoothingPeriod: 10, annualize: true, annualPeriods: 0)); } [Fact] diff --git a/lib/volatility/rv/Rv.Validation.Tests.cs b/lib/volatility/rv/Rv.Validation.Tests.cs index 98988e91..6a3fdd9f 100644 --- a/lib/volatility/rv/Rv.Validation.Tests.cs +++ b/lib/volatility/rv/Rv.Validation.Tests.cs @@ -178,7 +178,7 @@ public class RvValidationTests } // Batch calculation - var batchResult = Rv.Calculate(prices, 5, 10); + var batchResult = Rv.Batch(prices, 5, 10); Assert.Equal(batchResult.Last.Value, streamingRv.Last.Value, 8); } diff --git a/lib/volatility/rv/Rv.cs b/lib/volatility/rv/Rv.cs index a9ab1ff3..930a205d 100644 --- a/lib/volatility/rv/Rv.cs +++ b/lib/volatility/rv/Rv.cs @@ -349,7 +349,7 @@ public sealed class Rv : AbstractBase /// Whether to annualize. /// Periods per year. /// A TSeries containing the volatility values. - public static TSeries Calculate(TSeries source, int period = 5, int smoothingPeriod = 20, bool annualize = true, int annualPeriods = 252) + public static TSeries Batch(TSeries source, int period = 5, int smoothingPeriod = 20, bool annualize = true, int annualPeriods = 252) { if (period < 1) { @@ -382,7 +382,7 @@ public sealed class Rv : AbstractBase /// /// Calculates RV for a bar series (static). /// - public static TSeries Calculate(TBarSeries source, int period = 5, int smoothingPeriod = 20, bool annualize = true, int annualPeriods = 252) + public static TSeries Batch(TBarSeries source, int period = 5, int smoothingPeriod = 20, bool annualize = true, int annualPeriods = 252) { var rv = new Rv(period, smoothingPeriod, annualize, annualPeriods); return rv.Update(source); @@ -524,4 +524,12 @@ public sealed class Rv : AbstractBase output[i] = result; } } -} \ No newline at end of file + + public static (TSeries Results, Rv Indicator) Calculate(TSeries source, int period = 5, int smoothingPeriod = 20, bool annualize = true, int annualPeriods = 252) + { + var indicator = new Rv(period, smoothingPeriod, annualize, annualPeriods); + TSeries results = indicator.Update(source); + return (results, indicator); + } + +} diff --git a/lib/volatility/rvi/Rvi.Tests.cs b/lib/volatility/rvi/Rvi.Tests.cs index f7f53c70..fb69762b 100644 --- a/lib/volatility/rvi/Rvi.Tests.cs +++ b/lib/volatility/rvi/Rvi.Tests.cs @@ -423,7 +423,7 @@ public class RviTests // Mode 2: Batch via TSeries var tSeries = new TSeries(new List(times), new List(prices)); - var batchResult = Rvi.Calculate(tSeries, stdevLength: 10, rmaLength: 14); + var batchResult = Rvi.Batch(tSeries, stdevLength: 10, rmaLength: 14); // Mode 3: Span-based double[] spanOutput = new double[dataLen]; @@ -636,7 +636,7 @@ public class RviTests source.Add(new TValue(DateTime.UtcNow.AddSeconds(i), 100.0 + i * 0.5)); } - var result = Rvi.Calculate(source, stdevLength: 10, rmaLength: 14); + var result = Rvi.Batch(source, stdevLength: 10, rmaLength: 14); Assert.Equal(50, result.Count); // Allow small floating-point tolerance beyond [0,100] @@ -654,7 +654,7 @@ public class RviTests source.Add(new TBar(time, price - 1, price + 1, price - 2, price, 1000)); } - var result = Rvi.Calculate(source, stdevLength: 10, rmaLength: 14); + var result = Rvi.Batch(source, stdevLength: 10, rmaLength: 14); Assert.Equal(50, result.Count); } diff --git a/lib/volatility/rvi/Rvi.Validation.Tests.cs b/lib/volatility/rvi/Rvi.Validation.Tests.cs index c944a65c..389e2df5 100644 --- a/lib/volatility/rvi/Rvi.Validation.Tests.cs +++ b/lib/volatility/rvi/Rvi.Validation.Tests.cs @@ -204,7 +204,7 @@ public class RviValidationTests } // Batch calculation - var batchResult = Rvi.Calculate(prices, 10, 14); + var batchResult = Rvi.Batch(prices, 10, 14); // Compare last values Assert.Equal(batchResult.Last.Value, streamingRvi.Last.Value, 8); diff --git a/lib/volatility/rvi/Rvi.cs b/lib/volatility/rvi/Rvi.cs index 87c9263a..e00758f9 100644 --- a/lib/volatility/rvi/Rvi.cs +++ b/lib/volatility/rvi/Rvi.cs @@ -370,7 +370,7 @@ public sealed class Rvi : AbstractBase /// The lookback period for standard deviation. /// The lookback period for RMA smoothing. /// A TSeries containing the RVI values. - public static TSeries Calculate(TSeries source, int stdevLength = 10, int rmaLength = 14) + public static TSeries Batch(TSeries source, int stdevLength = 10, int rmaLength = 14) { if (stdevLength < 2) { @@ -399,7 +399,7 @@ public sealed class Rvi : AbstractBase /// /// Calculates RVI for a bar series (static). /// - public static TSeries Calculate(TBarSeries source, int stdevLength = 10, int rmaLength = 14) + public static TSeries Batch(TBarSeries source, int stdevLength = 10, int rmaLength = 14) { var rvi = new Rvi(stdevLength, rmaLength); return rvi.Update(source); @@ -563,4 +563,12 @@ public sealed class Rvi : AbstractBase output[i] = rviValue; } } -} \ No newline at end of file + + public static (TSeries Results, Rvi Indicator) Calculate(TSeries source, int stdevLength = 10, int rmaLength = 14) + { + var indicator = new Rvi(stdevLength, rmaLength); + TSeries results = indicator.Update(source); + return (results, indicator); + } + +} diff --git a/lib/volatility/tr/Tr.Tests.cs b/lib/volatility/tr/Tr.Tests.cs index ee457ffe..7a584ddc 100644 --- a/lib/volatility/tr/Tr.Tests.cs +++ b/lib/volatility/tr/Tr.Tests.cs @@ -343,7 +343,7 @@ public class TrTests } // Mode 2: Batch via TBarSeries - var batchResult = Tr.Calculate(bars); + var batchResult = Tr.Batch(bars); // Mode 3: Span-based double[] highs = new double[dataLen]; @@ -523,7 +523,7 @@ public class TrTests { var bars = GenerateBars(50); - var result = Tr.Calculate(bars); + var result = Tr.Batch(bars); Assert.Equal(50, result.Count); Assert.All(result.Values.ToArray(), v => Assert.True(v >= 0)); diff --git a/lib/volatility/tr/Tr.Validation.Tests.cs b/lib/volatility/tr/Tr.Validation.Tests.cs index 4820b014..60974efd 100644 --- a/lib/volatility/tr/Tr.Validation.Tests.cs +++ b/lib/volatility/tr/Tr.Validation.Tests.cs @@ -210,7 +210,7 @@ public class TrValidationTests } // Batch via TBarSeries - var batchResult = Tr.Calculate(bars); + var batchResult = Tr.Batch(bars); Assert.Equal(streamingTr.Last.Value, batchResult.Last.Value, 10); } diff --git a/lib/volatility/tr/Tr.cs b/lib/volatility/tr/Tr.cs index 591bb42b..f3a3dea9 100644 --- a/lib/volatility/tr/Tr.cs +++ b/lib/volatility/tr/Tr.cs @@ -281,7 +281,7 @@ public sealed class Tr : AbstractBase /// /// The source bar series. /// A TSeries containing the True Range values. - public static TSeries Calculate(TBarSeries source) + public static TSeries Batch(TBarSeries source) { var tr = new Tr(); return tr.Update(source); @@ -412,4 +412,12 @@ public sealed class Tr : AbstractBase Batch(highs, lows, closes, output); } -} \ No newline at end of file + + public static (TSeries Results, Tr Indicator) Calculate(TBarSeries source) + { + var indicator = new Tr(); + TSeries results = indicator.Update(source); + return (results, indicator); + } + +} diff --git a/lib/volatility/ui/Ui.Tests.cs b/lib/volatility/ui/Ui.Tests.cs index 5eb0da23..cd67f703 100644 --- a/lib/volatility/ui/Ui.Tests.cs +++ b/lib/volatility/ui/Ui.Tests.cs @@ -405,7 +405,7 @@ public class UiTests } // Mode 2: Batch via TSeries - var batchResult = Ui.Calculate(data, DefaultPeriod); + var batchResult = Ui.Batch(data, DefaultPeriod); // Mode 3: Span-based double[] spanOutput = new double[dataLen]; @@ -575,7 +575,7 @@ public class UiTests { var data = GenerateData(50); - var result = Ui.Calculate(data, DefaultPeriod); + var result = Ui.Batch(data, DefaultPeriod); Assert.Equal(50, result.Count); Assert.All(result.Values.ToArray(), v => Assert.True(v >= 0)); diff --git a/lib/volatility/ui/Ui.Validation.Tests.cs b/lib/volatility/ui/Ui.Validation.Tests.cs index 47624458..5b22f168 100644 --- a/lib/volatility/ui/Ui.Validation.Tests.cs +++ b/lib/volatility/ui/Ui.Validation.Tests.cs @@ -220,7 +220,7 @@ public class UiValidationTests } // Batch via TSeries - var batchResult = Ui.Calculate(data, DefaultPeriod); + var batchResult = Ui.Batch(data, DefaultPeriod); Assert.Equal(streamingUi.Last.Value, batchResult.Last.Value, 10); } diff --git a/lib/volatility/ui/Ui.cs b/lib/volatility/ui/Ui.cs index a51022f1..a3573bb3 100644 --- a/lib/volatility/ui/Ui.cs +++ b/lib/volatility/ui/Ui.cs @@ -263,7 +263,7 @@ public sealed class Ui : AbstractBase /// The source series. /// The lookback period. /// A TSeries containing the Ulcer Index values. - public static TSeries Calculate(TSeries source, int period = 14) + public static TSeries Batch(TSeries source, int period = 14) { var ui = new Ui(period); return ui.Update(source); @@ -398,4 +398,12 @@ public sealed class Ui : AbstractBase } } } -} \ No newline at end of file + + public static (TSeries Results, Ui Indicator) Calculate(TSeries source, int period = 14) + { + var indicator = new Ui(period); + TSeries results = indicator.Update(source); + return (results, indicator); + } + +} diff --git a/lib/volatility/vov/Vov.Tests.cs b/lib/volatility/vov/Vov.Tests.cs index 57959aac..b6ff0bdc 100644 --- a/lib/volatility/vov/Vov.Tests.cs +++ b/lib/volatility/vov/Vov.Tests.cs @@ -399,7 +399,7 @@ public class VovTests { var data = GenerateData(100); - var result = Vov.Calculate(data, volatilityPeriod: 10, vovPeriod: 5); + var result = Vov.Batch(data, volatilityPeriod: 10, vovPeriod: 5); Assert.Equal(data.Count, result.Count); for (int i = 0; i < result.Count; i++) @@ -482,7 +482,7 @@ public class VovTests } // Mode 2: TSeries batch - var batchResult = Vov.Calculate(data, volPeriod, vovPeriod); + var batchResult = Vov.Batch(data, volPeriod, vovPeriod); // Mode 3: Span batch double[] spanOutput = new double[dataLen]; diff --git a/lib/volatility/vov/Vov.Validation.Tests.cs b/lib/volatility/vov/Vov.Validation.Tests.cs index a40fb2b5..29d867a8 100644 --- a/lib/volatility/vov/Vov.Validation.Tests.cs +++ b/lib/volatility/vov/Vov.Validation.Tests.cs @@ -187,7 +187,7 @@ public class VovValidationTests } // Batch via TSeries - var batchResult = Vov.Calculate(data, DefaultVolatilityPeriod, DefaultVovPeriod); + var batchResult = Vov.Batch(data, DefaultVolatilityPeriod, DefaultVovPeriod); Assert.Equal(streamingVov.Last.Value, batchResult.Last.Value, 10); } diff --git a/lib/volatility/vov/Vov.cs b/lib/volatility/vov/Vov.cs index 730b86d6..91c6e6f3 100644 --- a/lib/volatility/vov/Vov.cs +++ b/lib/volatility/vov/Vov.cs @@ -308,7 +308,7 @@ public sealed class Vov : AbstractBase /// The volatility period. /// The VOV period. /// A TSeries containing the VOV values. - public static TSeries Calculate(TSeries source, int volatilityPeriod = 20, int vovPeriod = 10) + public static TSeries Batch(TSeries source, int volatilityPeriod = 20, int vovPeriod = 10) { var vov = new Vov(volatilityPeriod, vovPeriod); return vov.Update(source); @@ -462,4 +462,12 @@ public sealed class Vov : AbstractBase } } } -} \ No newline at end of file + + public static (TSeries Results, Vov Indicator) Calculate(TSeries source, int volatilityPeriod = 20, int vovPeriod = 10) + { + var indicator = new Vov(volatilityPeriod, vovPeriod); + TSeries results = indicator.Update(source); + return (results, indicator); + } + +} diff --git a/lib/volatility/vr/Vr.Tests.cs b/lib/volatility/vr/Vr.Tests.cs index 6bb0eab3..15ce5745 100644 --- a/lib/volatility/vr/Vr.Tests.cs +++ b/lib/volatility/vr/Vr.Tests.cs @@ -377,7 +377,7 @@ public class VrTests { var bars = GenerateBarData(100); - var result = Vr.Calculate(bars, period: 10); + var result = Vr.Batch(bars, period: 10); Assert.Equal(bars.Count, result.Count); for (int i = 0; i < result.Count; i++) @@ -472,7 +472,7 @@ public class VrTests } // Mode 2: TBarSeries batch - var batchResult = Vr.Calculate(bars, period); + var batchResult = Vr.Batch(bars, period); // Mode 3: Span batch double[] spanOutput = new double[bars.Count]; diff --git a/lib/volatility/vr/Vr.Validation.Tests.cs b/lib/volatility/vr/Vr.Validation.Tests.cs index 254625ee..f4e100b0 100644 --- a/lib/volatility/vr/Vr.Validation.Tests.cs +++ b/lib/volatility/vr/Vr.Validation.Tests.cs @@ -167,7 +167,7 @@ public class VrValidationTests int period = 14; // Using static Calculate - var calculateResult = Vr.Calculate(bars, period); + var calculateResult = Vr.Batch(bars, period); // Using Batch double[] batchOutput = new double[bars.Count]; diff --git a/lib/volatility/vr/Vr.cs b/lib/volatility/vr/Vr.cs index 12c7d3b5..76596d71 100644 --- a/lib/volatility/vr/Vr.cs +++ b/lib/volatility/vr/Vr.cs @@ -269,7 +269,7 @@ public sealed class Vr : AbstractBase /// /// Calculates VR for a TBarSeries (static). /// - public static TSeries Calculate(TBarSeries source, int period = 14) + public static TSeries Batch(TBarSeries source, int period = 14) { var vr = new Vr(period); return vr.Update(source); @@ -411,4 +411,12 @@ public sealed class Vr : AbstractBase output[i] = vr; } } -} \ No newline at end of file + + public static (TSeries Results, Vr Indicator) Calculate(TBarSeries source, int period = 14) + { + var indicator = new Vr(period); + TSeries results = indicator.Update(source); + return (results, indicator); + } + +} diff --git a/lib/volatility/yzv/Yzv.Tests.cs b/lib/volatility/yzv/Yzv.Tests.cs index e3f5faad..5061aa9b 100644 --- a/lib/volatility/yzv/Yzv.Tests.cs +++ b/lib/volatility/yzv/Yzv.Tests.cs @@ -374,7 +374,7 @@ public class YzvTests { var bars = GenerateBarData(100); - var result = Yzv.Calculate(bars, period: 10); + var result = Yzv.Batch(bars, period: 10); Assert.Equal(bars.Count, result.Count); for (int i = 0; i < result.Count; i++) @@ -471,7 +471,7 @@ public class YzvTests } // Mode 2: TBarSeries batch - var batchResult = Yzv.Calculate(bars, period); + var batchResult = Yzv.Batch(bars, period); // Mode 3: Span batch double[] spanOutput = new double[bars.Count]; diff --git a/lib/volatility/yzv/Yzv.Validation.Tests.cs b/lib/volatility/yzv/Yzv.Validation.Tests.cs index f56609c8..6849e3c0 100644 --- a/lib/volatility/yzv/Yzv.Validation.Tests.cs +++ b/lib/volatility/yzv/Yzv.Validation.Tests.cs @@ -167,7 +167,7 @@ public class YzvValidationTests int period = 14; // Using static Calculate - var calculateResult = Yzv.Calculate(bars, period); + var calculateResult = Yzv.Batch(bars, period); // Using Batch double[] batchOutput = new double[bars.Count]; diff --git a/lib/volatility/yzv/Yzv.cs b/lib/volatility/yzv/Yzv.cs index 2e1c4fb1..3cd2be6f 100644 --- a/lib/volatility/yzv/Yzv.cs +++ b/lib/volatility/yzv/Yzv.cs @@ -280,7 +280,7 @@ public sealed class Yzv : AbstractBase /// /// Calculates YZV for a TBarSeries (static). /// - public static TSeries Calculate(TBarSeries source, int period = 20) + public static TSeries Batch(TBarSeries source, int period = 20) { var yzv = new Yzv(period); return yzv.Update(source); @@ -441,4 +441,12 @@ public sealed class Yzv : AbstractBase output[i] = yzv; } } -} \ No newline at end of file + + public static (TSeries Results, Yzv Indicator) Calculate(TBarSeries source, int period = 20) + { + var indicator = new Yzv(period); + TSeries results = indicator.Update(source); + return (results, indicator); + } + +} diff --git a/lib/volume/adl/Adl.Tests.cs b/lib/volume/adl/Adl.Tests.cs index c5b20650..6c72f7ca 100644 --- a/lib/volume/adl/Adl.Tests.cs +++ b/lib/volume/adl/Adl.Tests.cs @@ -134,7 +134,7 @@ public class AdlTests bars.Add(new TBar(time.AddMinutes(1), 10, 12, 8, 12, 200)); bars.Add(new TBar(time.AddMinutes(2), 12, 12, 8, 8, 100)); - var result = Adl.Calculate(bars); + var result = Adl.Batch(bars); Assert.Equal(3, result.Count); Assert.Equal(0, result[0].Value); @@ -151,7 +151,7 @@ public class AdlTests double[] volume = { 100, 200, 100 }; double[] output = new double[3]; - Adl.Calculate(high, low, close, volume, output); + Adl.Batch(high, low, close, volume, output); Assert.Equal(0, output[0]); Assert.Equal(200, output[1]); @@ -168,14 +168,14 @@ public class AdlTests double[] output = new double[2]; Assert.Throws(() => - Adl.Calculate(high, low, close, volume, output)); + Adl.Batch(high, low, close, volume, output)); } [Fact] public void Adl_Calculate_EmptySeries_ReturnsEmpty() { var bars = new TBarSeries(); - var result = Adl.Calculate(bars); + var result = Adl.Batch(bars); Assert.Empty(result); } @@ -199,7 +199,7 @@ public class AdlTests volume[i] = 10; } - Adl.Calculate(high, low, close, volume, output); + Adl.Batch(high, low, close, volume, output); for (int i = 0; i < count; i++) { diff --git a/lib/volume/adl/Adl.cs b/lib/volume/adl/Adl.cs index 477c78aa..65f26b22 100644 --- a/lib/volume/adl/Adl.cs +++ b/lib/volume/adl/Adl.cs @@ -121,7 +121,7 @@ public sealed class Adl : ITValuePublisher return new TSeries(t, v); } - public static TSeries Calculate(TBarSeries source) + public static TSeries Batch(TBarSeries source) { if (source.Count == 0) { @@ -131,13 +131,13 @@ public sealed class Adl : ITValuePublisher var t = source.Open.Times.ToArray(); // Times are same for all series var v = new double[source.Count]; - Calculate(source.High.Values, source.Low.Values, source.Close.Values, source.Volume.Values, v); + Batch(source.High.Values, source.Low.Values, source.Close.Values, source.Volume.Values, v); return new TSeries(t, v); } [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void Calculate(ReadOnlySpan high, ReadOnlySpan low, ReadOnlySpan close, ReadOnlySpan volume, Span output) + public static void Batch(ReadOnlySpan high, ReadOnlySpan low, ReadOnlySpan close, ReadOnlySpan volume, Span output) { if (high.Length != low.Length || high.Length != close.Length || high.Length != volume.Length || high.Length != output.Length) { @@ -195,4 +195,11 @@ public sealed class Adl : ITValuePublisher output[i] = sum; } } -} + + public static (TSeries Results, Adl Indicator) Calculate(TBarSeries source) + { + var indicator = new Adl(); + TSeries results = indicator.Update(source); + return (results, indicator); + } +} \ No newline at end of file diff --git a/lib/volume/adosc/Adosc.Tests.cs b/lib/volume/adosc/Adosc.Tests.cs index 688b88ea..6b273e56 100644 --- a/lib/volume/adosc/Adosc.Tests.cs +++ b/lib/volume/adosc/Adosc.Tests.cs @@ -92,7 +92,7 @@ public class AdoscTests } var spanOutput = new double[_bars.Count]; - Adosc.Calculate(_bars.High.Values, _bars.Low.Values, _bars.Close.Values, _bars.Volume.Values, spanOutput, 3, 10); + Adosc.Batch(_bars.High.Values, _bars.Low.Values, _bars.Close.Values, _bars.Volume.Values, spanOutput, 3, 10); for (int i = 0; i < _bars.Count; i++) { @@ -176,7 +176,7 @@ public class AdoscTests double[] volume = [1000, 1100, 1200, 1300, 1400]; double[] output = new double[5]; - Adosc.Calculate(high, low, close, volume, output, 3, 5); + Adosc.Batch(high, low, close, volume, output, 3, 5); // Verify output is finite for (int i = 0; i < output.Length; i++) @@ -191,7 +191,7 @@ public class AdoscTests var batchResult = Adosc.Batch(_bars, 3, 10); var spanOutput = new double[_bars.Count]; - Adosc.Calculate(_bars.High.Values, _bars.Low.Values, _bars.Close.Values, _bars.Volume.Values, spanOutput, 3, 10); + Adosc.Batch(_bars.High.Values, _bars.Low.Values, _bars.Close.Values, _bars.Volume.Values, spanOutput, 3, 10); for (int i = 0; i < _bars.Count; i++) { diff --git a/lib/volume/adosc/Adosc.Validation.Tests.cs b/lib/volume/adosc/Adosc.Validation.Tests.cs index 5c3072d5..936dca03 100644 --- a/lib/volume/adosc/Adosc.Validation.Tests.cs +++ b/lib/volume/adosc/Adosc.Validation.Tests.cs @@ -66,7 +66,7 @@ public sealed class AdoscValidationTests : IDisposable // 3. Span Mode double[] spanOutput = new double[close.Length]; - Adosc.Calculate(high, low, close, volume, spanOutput, fastPeriod, slowPeriod); + Adosc.Batch(high, low, close, volume, spanOutput, fastPeriod, slowPeriod); ValidationHelper.VerifyData(spanOutput, output, outRange, lookback: slowPeriod - 1, tolerance: ValidationHelper.TalibTolerance); } @@ -105,7 +105,7 @@ public sealed class AdoscValidationTests : IDisposable // 3. Span Mode double[] spanOutput = new double[close.Length]; - Adosc.Calculate(high, low, close, volume, spanOutput, fastPeriod, slowPeriod); + Adosc.Batch(high, low, close, volume, spanOutput, fastPeriod, slowPeriod); ValidationHelper.VerifyData(spanOutput, output, lookback: start, tolerance: ValidationHelper.TulipTolerance); } @@ -137,7 +137,7 @@ public sealed class AdoscValidationTests : IDisposable double[] close = _testData.Bars.Close.Values.ToArray(); double[] volume = _testData.Bars.Volume.Values.ToArray(); double[] spanOutput = new double[close.Length]; - Adosc.Calculate(high, low, close, volume, spanOutput, fastPeriod, slowPeriod); + Adosc.Batch(high, low, close, volume, spanOutput, fastPeriod, slowPeriod); ValidationHelper.VerifyData(spanOutput, skenderResults, (x) => x.Oscillator, tolerance: ValidationHelper.SkenderTolerance); } @@ -181,7 +181,7 @@ public sealed class AdoscValidationTests : IDisposable double[] close = _testData.Bars.Close.Values.ToArray(); double[] volume = _testData.Bars.Volume.Values.ToArray(); double[] spanOutput = new double[close.Length]; - Adosc.Calculate(high, low, close, volume, spanOutput, fastPeriod, slowPeriod); + Adosc.Batch(high, low, close, volume, spanOutput, fastPeriod, slowPeriod); ValidationHelper.VerifyData(spanOutput, output, lookback: 0, tolerance: ValidationHelper.OoplesTolerance); } } diff --git a/lib/volume/adosc/Adosc.cs b/lib/volume/adosc/Adosc.cs index b9c95555..d53b400e 100644 --- a/lib/volume/adosc/Adosc.cs +++ b/lib/volume/adosc/Adosc.cs @@ -136,6 +136,9 @@ public sealed class Adosc : ITValuePublisher return new TSeries(t, v); } + // EMA compensator threshold (same as in Ema.cs) + private const double COMPENSATOR_THRESHOLD = 1e-10; + /// /// Calculates ADOSC for the entire series using a new instance. /// @@ -149,9 +152,6 @@ public sealed class Adosc : ITValuePublisher return adosc.Update(source); } - // EMA compensator threshold (same as in Ema.cs) - private const double COMPENSATOR_THRESHOLD = 1e-10; - /// /// Calculates ADOSC for the entire span using a single-pass algorithm. /// Zero allocation for maximum performance. @@ -165,7 +165,7 @@ public sealed class Adosc : ITValuePublisher /// Fast EMA period (default 3) /// Slow EMA period (default 10) [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void Calculate(ReadOnlySpan high, ReadOnlySpan low, ReadOnlySpan close, ReadOnlySpan volume, Span output, int fastPeriod = 3, int slowPeriod = 10) + public static void Batch(ReadOnlySpan high, ReadOnlySpan low, ReadOnlySpan close, ReadOnlySpan volume, Span output, int fastPeriod = 3, int slowPeriod = 10) { if (high.Length != low.Length || high.Length != close.Length || high.Length != volume.Length || high.Length != output.Length) @@ -276,4 +276,11 @@ public sealed class Adosc : ITValuePublisher output[i] = fastValue - slowValue; } } -} + + public static (TSeries Results, Adosc Indicator) Calculate(TBarSeries source, int fastPeriod = 3, int slowPeriod = 10) + { + var indicator = new Adosc(fastPeriod, slowPeriod); + TSeries results = indicator.Update(source); + return (results, indicator); + } +} \ No newline at end of file diff --git a/lib/volume/aobv/Aobv.Tests.cs b/lib/volume/aobv/Aobv.Tests.cs index 7d2dee43..5b45949a 100644 --- a/lib/volume/aobv/Aobv.Tests.cs +++ b/lib/volume/aobv/Aobv.Tests.cs @@ -232,7 +232,7 @@ public class AobvTests double[] outputFast = new double[5]; double[] outputSlow = new double[5]; - Aobv.Calculate(close, volume, outputFast, outputSlow); + Aobv.Batch(close, volume, outputFast, outputSlow); for (int i = 0; i < 5; i++) { @@ -250,7 +250,7 @@ public class AobvTests double[] outputSlow = new double[2]; Assert.Throws(() => - Aobv.Calculate(close, volume, outputFast, outputSlow)); + Aobv.Batch(close, volume, outputFast, outputSlow)); } [Fact] @@ -262,7 +262,7 @@ public class AobvTests double[] outputSlow = new double[2]; Assert.Throws(() => - Aobv.Calculate(close, volume, outputFast, outputSlow)); + Aobv.Batch(close, volume, outputFast, outputSlow)); } [Fact] diff --git a/lib/volume/aobv/Aobv.Validation.Tests.cs b/lib/volume/aobv/Aobv.Validation.Tests.cs index 08d32774..aba3e976 100644 --- a/lib/volume/aobv/Aobv.Validation.Tests.cs +++ b/lib/volume/aobv/Aobv.Validation.Tests.cs @@ -86,7 +86,7 @@ public class AobvValidationTests var spanFast = new double[close.Length]; var spanSlow = new double[close.Length]; - Aobv.Calculate(close, volume, spanFast, spanSlow); + Aobv.Batch(close, volume, spanFast, spanSlow); ValidationHelper.VerifyData(streamingFast.ToArray(), spanFast, 0, 100, 1e-12); } diff --git a/lib/volume/aobv/Aobv.cs b/lib/volume/aobv/Aobv.cs index 1ed9a138..715dabb0 100644 --- a/lib/volume/aobv/Aobv.cs +++ b/lib/volume/aobv/Aobv.cs @@ -267,7 +267,7 @@ public sealed class Aobv : ITValuePublisher var vFast = new double[source.Count]; var vSlow = new double[source.Count]; - Calculate(source.Close.Values, source.Volume.Values, vFast, vSlow); + Batch(source.Close.Values, source.Volume.Values, vFast, vSlow); return (new TSeries(t, vFast), new TSeries(t, vSlow)); } @@ -284,7 +284,7 @@ public sealed class Aobv : ITValuePublisher /// value seen. If no valid value has been seen yet, 0 is used as a neutral fallback. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void Calculate(ReadOnlySpan close, ReadOnlySpan volume, + public static void Batch(ReadOnlySpan close, ReadOnlySpan volume, Span outputFast, Span outputSlow) { if (close.Length != volume.Length) diff --git a/lib/volume/cmf/Cmf.Tests.cs b/lib/volume/cmf/Cmf.Tests.cs index e8e674d3..ab211820 100644 --- a/lib/volume/cmf/Cmf.Tests.cs +++ b/lib/volume/cmf/Cmf.Tests.cs @@ -225,7 +225,7 @@ public class CmfTests bars.Add(new TBar(time.AddMinutes(1), 10, 12, 8, 12, 200)); bars.Add(new TBar(time.AddMinutes(2), 12, 12, 8, 8, 100)); - var result = Cmf.Calculate(bars, 3); + var result = Cmf.Batch(bars, 3); Assert.Equal(3, result.Count); } @@ -239,7 +239,7 @@ public class CmfTests double[] volume = { 100, 200, 100 }; double[] output = new double[3]; - Cmf.Calculate(high, low, close, volume, output, 3); + Cmf.Batch(high, low, close, volume, output, 3); // Bar 0: MFV=0, Vol=100 -> CMF=0/100=0 Assert.Equal(0, output[0]); @@ -259,7 +259,7 @@ public class CmfTests double[] output = new double[2]; Assert.Throws(() => - Cmf.Calculate(high, low, close, volume, output, 3)); + Cmf.Batch(high, low, close, volume, output, 3)); } [Fact] @@ -272,14 +272,14 @@ public class CmfTests double[] output = new double[1]; Assert.Throws(() => - Cmf.Calculate(high, low, close, volume, output, 0)); + Cmf.Batch(high, low, close, volume, output, 0)); } [Fact] public void Cmf_Calculate_EmptySeries_ReturnsEmpty() { var bars = new TBarSeries(); - var result = Cmf.Calculate(bars); + var result = Cmf.Batch(bars); Assert.Empty(result); } @@ -302,7 +302,7 @@ public class CmfTests volume[i] = 10; } - Cmf.Calculate(high, low, close, volume, output, 20); + Cmf.Batch(high, low, close, volume, output, 20); // All bars have MFM=1, so CMF should be 1.0 once we have enough data for (int i = 19; i < count; i++) @@ -331,7 +331,7 @@ public class CmfTests } // Batch - var batchResult = Cmf.Calculate(bars, 20); + var batchResult = Cmf.Batch(bars, 20); // Compare last 80 values (after warmup) for (int i = 20; i < 100; i++) diff --git a/lib/volume/cmf/Cmf.Validation.Tests.cs b/lib/volume/cmf/Cmf.Validation.Tests.cs index 2d9f006c..1b699006 100644 --- a/lib/volume/cmf/Cmf.Validation.Tests.cs +++ b/lib/volume/cmf/Cmf.Validation.Tests.cs @@ -91,7 +91,7 @@ public class CmfValidationTests } // Batch - var batchResult = Cmf.Calculate(_data.Bars, DefaultPeriod); + var batchResult = Cmf.Batch(_data.Bars, DefaultPeriod); var batchValues = batchResult.Values.ToArray(); ValidationHelper.VerifyData(streamingValues.ToArray(), batchValues, 0, 100, 1e-12); @@ -115,7 +115,7 @@ public class CmfValidationTests var volume = _data.Bars.Volume.Values.ToArray(); var spanValues = new double[high.Length]; - Cmf.Calculate(high, low, close, volume, spanValues, DefaultPeriod); + Cmf.Batch(high, low, close, volume, spanValues, DefaultPeriod); ValidationHelper.VerifyData(streamingValues.ToArray(), spanValues, 0, 100, 1e-12); } diff --git a/lib/volume/cmf/Cmf.cs b/lib/volume/cmf/Cmf.cs index a7493aaa..209628bb 100644 --- a/lib/volume/cmf/Cmf.cs +++ b/lib/volume/cmf/Cmf.cs @@ -174,7 +174,7 @@ public sealed class Cmf : ITValuePublisher return new TSeries(t, v); } - public static TSeries Calculate(TBarSeries source, int period = 20) + public static TSeries Batch(TBarSeries source, int period = 20) { if (source.Count == 0) { @@ -184,13 +184,13 @@ public sealed class Cmf : ITValuePublisher var t = source.Open.Times.ToArray(); var v = new double[source.Count]; - Calculate(source.High.Values, source.Low.Values, source.Close.Values, source.Volume.Values, v, period); + Batch(source.High.Values, source.Low.Values, source.Close.Values, source.Volume.Values, v, period); return new TSeries(t, v); } [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void Calculate(ReadOnlySpan high, ReadOnlySpan low, ReadOnlySpan close, ReadOnlySpan volume, Span output, int period = 20) + public static void Batch(ReadOnlySpan high, ReadOnlySpan low, ReadOnlySpan close, ReadOnlySpan volume, Span output, int period = 20) { if (high.Length != low.Length) { @@ -282,4 +282,11 @@ public sealed class Cmf : ITValuePublisher output[i] = sumVol > double.Epsilon ? sumMfv / sumVol : 0; } } -} + + public static (TSeries Results, Cmf Indicator) Calculate(TBarSeries source, int period = 20) + { + var indicator = new Cmf(period); + TSeries results = indicator.Update(source); + return (results, indicator); + } +} \ No newline at end of file diff --git a/lib/volume/efi/Efi.Tests.cs b/lib/volume/efi/Efi.Tests.cs index 4539864b..419ec282 100644 --- a/lib/volume/efi/Efi.Tests.cs +++ b/lib/volume/efi/Efi.Tests.cs @@ -225,7 +225,7 @@ public class EfiTests bars.Add(new TBar(time.AddMinutes(1), 10, 12, 8, 12, 200)); bars.Add(new TBar(time.AddMinutes(2), 12, 12, 8, 8, 100)); - var result = Efi.Calculate(bars, 3); + var result = Efi.Batch(bars, 3); Assert.Equal(3, result.Count); } @@ -240,7 +240,7 @@ public class EfiTests double[] volume = { 100, 200, 100 }; double[] output = new double[3]; - Efi.Calculate(close, volume, output, 3); + Efi.Batch(close, volume, output, 3); // Bar 0: raw force = 0, result = 0 Assert.Equal(0, output[0]); @@ -258,7 +258,7 @@ public class EfiTests double[] output = new double[2]; Assert.Throws(() => - Efi.Calculate(close, volume, output, 3)); + Efi.Batch(close, volume, output, 3)); } [Fact] @@ -269,14 +269,14 @@ public class EfiTests double[] output = new double[1]; Assert.Throws(() => - Efi.Calculate(close, volume, output, 0)); + Efi.Batch(close, volume, output, 0)); } [Fact] public void Efi_Calculate_EmptySeries_ReturnsEmpty() { var bars = new TBarSeries(); - var result = Efi.Calculate(bars); + var result = Efi.Batch(bars); Assert.Empty(result); } @@ -300,7 +300,7 @@ public class EfiTests } // Batch - var batchResult = Efi.Calculate(bars, 13); + var batchResult = Efi.Batch(bars, 13); // Compare all values for (int i = 0; i < 100; i++) diff --git a/lib/volume/efi/Efi.Validation.Tests.cs b/lib/volume/efi/Efi.Validation.Tests.cs index 66276102..e5be2c87 100644 --- a/lib/volume/efi/Efi.Validation.Tests.cs +++ b/lib/volume/efi/Efi.Validation.Tests.cs @@ -57,7 +57,7 @@ public class EfiValidationTests } // Batch - var batchResult = Efi.Calculate(_data.Bars, DefaultPeriod); + var batchResult = Efi.Batch(_data.Bars, DefaultPeriod); var batchValues = batchResult.Values.ToArray(); ValidationHelper.VerifyData(streamingValues.ToArray(), batchValues, 0, 100, 1e-12); @@ -79,7 +79,7 @@ public class EfiValidationTests var volume = _data.Bars.Volume.Values.ToArray(); var spanValues = new double[close.Length]; - Efi.Calculate(close, volume, spanValues, DefaultPeriod); + Efi.Batch(close, volume, spanValues, DefaultPeriod); ValidationHelper.VerifyData(streamingValues.ToArray(), spanValues, 0, 100, 1e-12); } diff --git a/lib/volume/efi/Efi.cs b/lib/volume/efi/Efi.cs index f846fdda..0c958dbd 100644 --- a/lib/volume/efi/Efi.cs +++ b/lib/volume/efi/Efi.cs @@ -225,7 +225,7 @@ public sealed class Efi : ITValuePublisher return new TSeries(t, v); } - public static TSeries Calculate(TBarSeries source, int period = 13) + public static TSeries Batch(TBarSeries source, int period = 13) { if (source.Count == 0) { @@ -235,13 +235,13 @@ public sealed class Efi : ITValuePublisher var t = source.Open.Times.ToArray(); var v = new double[source.Count]; - Calculate(source.Close.Values, source.Volume.Values, v, period); + Batch(source.Close.Values, source.Volume.Values, v, period); return new TSeries(t, v); } [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void Calculate(ReadOnlySpan close, ReadOnlySpan volume, Span output, int period = 13) + public static void Batch(ReadOnlySpan close, ReadOnlySpan volume, Span output, int period = 13) { if (close.Length != volume.Length) { @@ -298,4 +298,11 @@ public sealed class Efi : ITValuePublisher } } } + + public static (TSeries Results, Efi Indicator) Calculate(TBarSeries source, int period = 13) + { + var indicator = new Efi(period); + TSeries results = indicator.Update(source); + return (results, indicator); + } } \ No newline at end of file diff --git a/lib/volume/eom/Eom.Tests.cs b/lib/volume/eom/Eom.Tests.cs index a83f07cd..78de98ff 100644 --- a/lib/volume/eom/Eom.Tests.cs +++ b/lib/volume/eom/Eom.Tests.cs @@ -234,7 +234,7 @@ public class EomTests } // Batch - var batchResult = Eom.Calculate(bars); + var batchResult = Eom.Batch(bars); Assert.Equal(bars.Count, batchResult.Count); for (int i = 0; i < bars.Count; i++) @@ -268,7 +268,7 @@ public class EomTests var volume = bars.Volume.Values.ToArray(); var spanValues = new double[bars.Count]; - Eom.Calculate(high, low, volume, spanValues); + Eom.Batch(high, low, volume, spanValues); for (int i = 0; i < bars.Count; i++) { @@ -284,7 +284,7 @@ public class EomTests var volume = new double[100]; var output = new double[100]; - Assert.Throws(() => Eom.Calculate(high, low, volume, output)); + Assert.Throws(() => Eom.Batch(high, low, volume, output)); } [Fact] @@ -295,7 +295,7 @@ public class EomTests var volume = new double[100]; var output = new double[100]; - Assert.Throws(() => Eom.Calculate(high, low, volume, output, period: 0)); + Assert.Throws(() => Eom.Batch(high, low, volume, output, period: 0)); } [Fact] @@ -306,7 +306,7 @@ public class EomTests var volume = new double[100]; var output = new double[100]; - Assert.Throws(() => Eom.Calculate(high, low, volume, output, volumeScale: 0)); + Assert.Throws(() => Eom.Batch(high, low, volume, output, volumeScale: 0)); } [Fact] @@ -326,7 +326,7 @@ public class EomTests } // Should not throw - Eom.Calculate(high, low, volume, output); + Eom.Batch(high, low, volume, output); Assert.True(double.IsFinite(output[size - 1])); } diff --git a/lib/volume/eom/Eom.Validation.Tests.cs b/lib/volume/eom/Eom.Validation.Tests.cs index 7ed9dd50..c56865a3 100644 --- a/lib/volume/eom/Eom.Validation.Tests.cs +++ b/lib/volume/eom/Eom.Validation.Tests.cs @@ -51,7 +51,7 @@ public class EomValidationTests } // Batch - var batchResult = Eom.Calculate(_data.Bars, DefaultPeriod); + var batchResult = Eom.Batch(_data.Bars, DefaultPeriod); var batchValues = batchResult.Values.ToArray(); ValidationHelper.VerifyData(streamingValues.ToArray(), batchValues, 0, 100, 1e-9); @@ -74,7 +74,7 @@ public class EomValidationTests var volume = _data.Bars.Volume.Values.ToArray(); var spanValues = new double[high.Length]; - Eom.Calculate(high, low, volume, spanValues, DefaultPeriod); + Eom.Batch(high, low, volume, spanValues, DefaultPeriod); ValidationHelper.VerifyData(streamingValues.ToArray(), spanValues, 0, 100, 1e-9); } diff --git a/lib/volume/eom/Eom.cs b/lib/volume/eom/Eom.cs index f4dfbf37..0aa8e261 100644 --- a/lib/volume/eom/Eom.cs +++ b/lib/volume/eom/Eom.cs @@ -208,7 +208,7 @@ public sealed class Eom : ITValuePublisher /// The smoothing period /// The volume scaling factor /// A TSeries containing the EOM values - public static TSeries Calculate(TBarSeries bars, int period = 14, double volumeScale = 10000) + public static TSeries Batch(TBarSeries bars, int period = 14, double volumeScale = 10000) { if (bars.Count == 0) { @@ -218,7 +218,7 @@ public sealed class Eom : ITValuePublisher var t = bars.Open.Times.ToArray(); var v = new double[bars.Count]; - Calculate(bars.High.Values, bars.Low.Values, bars.Volume.Values, v, period, volumeScale); + Batch(bars.High.Values, bars.Low.Values, bars.Volume.Values, v, period, volumeScale); return new TSeries(t, v); } @@ -234,7 +234,7 @@ public sealed class Eom : ITValuePublisher /// The volume scaling factor /// Thrown when spans have different lengths [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void Calculate(ReadOnlySpan high, ReadOnlySpan low, + public static void Batch(ReadOnlySpan high, ReadOnlySpan low, ReadOnlySpan volume, Span output, int period = 14, double volumeScale = 10000) { if (high.Length != low.Length) @@ -332,4 +332,11 @@ public sealed class Eom : ITValuePublisher } } } + + public static (TSeries Results, Eom Indicator) Calculate(TBarSeries bars, int period = 14, double volumeScale = 10000) + { + var indicator = new Eom(period, volumeScale); + TSeries results = indicator.Update(bars); + return (results, indicator); + } } \ No newline at end of file diff --git a/lib/volume/iii/Iii.Tests.cs b/lib/volume/iii/Iii.Tests.cs index 41049b77..6379ae87 100644 --- a/lib/volume/iii/Iii.Tests.cs +++ b/lib/volume/iii/Iii.Tests.cs @@ -244,7 +244,7 @@ public class IiiTests } // Batch - var batchResult = Iii.Calculate(bars); + var batchResult = Iii.Batch(bars); Assert.Equal(bars.Count, batchResult.Count); for (int i = 0; i < bars.Count; i++) @@ -279,7 +279,7 @@ public class IiiTests var volume = bars.Volume.Values.ToArray(); var spanValues = new double[bars.Count]; - Iii.Calculate(high, low, close, volume, spanValues); + Iii.Batch(high, low, close, volume, spanValues); for (int i = 0; i < bars.Count; i++) { @@ -296,7 +296,7 @@ public class IiiTests var volume = new double[100]; var output = new double[100]; - Assert.Throws(() => Iii.Calculate(high, low, close, volume, output)); + Assert.Throws(() => Iii.Batch(high, low, close, volume, output)); } [Fact] @@ -308,7 +308,7 @@ public class IiiTests var volume = new double[100]; var output = new double[100]; - Assert.Throws(() => Iii.Calculate(high, low, close, volume, output, period: 0)); + Assert.Throws(() => Iii.Batch(high, low, close, volume, output, period: 0)); } [Fact] @@ -330,7 +330,7 @@ public class IiiTests } // Should not throw - Iii.Calculate(high, low, close, volume, output); + Iii.Batch(high, low, close, volume, output); Assert.True(double.IsFinite(output[size - 1])); } @@ -360,7 +360,7 @@ public class IiiTests var volume = bars.Volume.Values.ToArray(); var spanValues = new double[bars.Count]; - Iii.Calculate(high, low, close, volume, spanValues, period: 14, cumulative: true); + Iii.Batch(high, low, close, volume, spanValues, period: 14, cumulative: true); for (int i = 0; i < bars.Count; i++) { diff --git a/lib/volume/iii/Iii.Validation.Tests.cs b/lib/volume/iii/Iii.Validation.Tests.cs index 398a8f9c..4cb6ea56 100644 --- a/lib/volume/iii/Iii.Validation.Tests.cs +++ b/lib/volume/iii/Iii.Validation.Tests.cs @@ -36,7 +36,7 @@ public class IiiValidationTests } // Batch mode - var batchResults = Iii.Calculate(bars, DefaultPeriod); + var batchResults = Iii.Batch(bars, DefaultPeriod); // Compare results Assert.Equal(bars.Count, batchResults.Count); @@ -65,7 +65,7 @@ public class IiiValidationTests var close = bars.Close.Values.ToArray(); var volume = bars.Volume.Values.ToArray(); var spanResults = new double[bars.Count]; - Iii.Calculate(high, low, close, volume, spanResults, DefaultPeriod); + Iii.Batch(high, low, close, volume, spanResults, DefaultPeriod); // Compare results for (int i = 0; i < bars.Count; i++) @@ -88,7 +88,7 @@ public class IiiValidationTests } // Batch mode - var batchResults = Iii.Calculate(bars, DefaultPeriod, cumulative: true); + var batchResults = Iii.Batch(bars, DefaultPeriod, cumulative: true); // Compare results Assert.Equal(bars.Count, batchResults.Count); @@ -117,7 +117,7 @@ public class IiiValidationTests var close = bars.Close.Values.ToArray(); var volume = bars.Volume.Values.ToArray(); var spanResults = new double[bars.Count]; - Iii.Calculate(high, low, close, volume, spanResults, DefaultPeriod, cumulative: true); + Iii.Batch(high, low, close, volume, spanResults, DefaultPeriod, cumulative: true); // Compare results for (int i = 0; i < bars.Count; i++) @@ -140,7 +140,7 @@ public class IiiValidationTests } // Batch mode - var batchResults = Iii.Calculate(bars, DefaultPeriod); + var batchResults = Iii.Batch(bars, DefaultPeriod); // Span mode var high = bars.High.Values.ToArray(); @@ -148,7 +148,7 @@ public class IiiValidationTests var close = bars.Close.Values.ToArray(); var volume = bars.Volume.Values.ToArray(); var spanResults = new double[bars.Count]; - Iii.Calculate(high, low, close, volume, spanResults, DefaultPeriod); + Iii.Batch(high, low, close, volume, spanResults, DefaultPeriod); // All three should match for (int i = 0; i < bars.Count; i++) @@ -177,7 +177,7 @@ public class IiiValidationTests } // Batch mode - var batchResults = Iii.Calculate(bars, DefaultPeriod); + var batchResults = Iii.Batch(bars, DefaultPeriod); // Span mode var high = bars.High.Values.ToArray(); @@ -185,7 +185,7 @@ public class IiiValidationTests var close = bars.Close.Values.ToArray(); var volume = bars.Volume.Values.ToArray(); var spanResults = new double[bars.Count]; - Iii.Calculate(high, low, close, volume, spanResults, DefaultPeriod); + Iii.Batch(high, low, close, volume, spanResults, DefaultPeriod); // Focus on last 100 values (well past warmup) int startIdx = bars.Count - 100; @@ -285,7 +285,7 @@ public class IiiValidationTests } // Batch - var batchResults = Iii.Calculate(bars, DefaultPeriod); + var batchResults = Iii.Batch(bars, DefaultPeriod); // Should match for any seed for (int i = bars.Count - 50; i < bars.Count; i++) diff --git a/lib/volume/iii/Iii.cs b/lib/volume/iii/Iii.cs index 47c2129e..b3a4eb96 100644 --- a/lib/volume/iii/Iii.cs +++ b/lib/volume/iii/Iii.cs @@ -211,7 +211,7 @@ public sealed class Iii : ITValuePublisher /// The smoothing period /// Whether to use cumulative mode /// A TSeries containing the III values - public static TSeries Calculate(TBarSeries bars, int period = 14, bool cumulative = false) + public static TSeries Batch(TBarSeries bars, int period = 14, bool cumulative = false) { if (bars.Count == 0) { @@ -221,7 +221,7 @@ public sealed class Iii : ITValuePublisher var t = bars.Open.Times.ToArray(); var v = new double[bars.Count]; - Calculate(bars.High.Values, bars.Low.Values, bars.Close.Values, bars.Volume.Values, v, period, cumulative); + Batch(bars.High.Values, bars.Low.Values, bars.Close.Values, bars.Volume.Values, v, period, cumulative); return new TSeries(t, v); } @@ -238,7 +238,7 @@ public sealed class Iii : ITValuePublisher /// Whether to use cumulative mode /// Thrown when spans have different lengths [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void Calculate(ReadOnlySpan high, ReadOnlySpan low, + public static void Batch(ReadOnlySpan high, ReadOnlySpan low, ReadOnlySpan close, ReadOnlySpan volume, Span output, int period = 14, bool cumulative = false) { @@ -337,4 +337,11 @@ public sealed class Iii : ITValuePublisher } } } + + public static (TSeries Results, Iii Indicator) Calculate(TBarSeries bars, int period = 14, bool cumulative = false) + { + var indicator = new Iii(period, cumulative); + TSeries results = indicator.Update(bars); + return (results, indicator); + } } \ No newline at end of file diff --git a/lib/volume/kvo/Kvo.Tests.cs b/lib/volume/kvo/Kvo.Tests.cs index 74b6c409..5590121e 100644 --- a/lib/volume/kvo/Kvo.Tests.cs +++ b/lib/volume/kvo/Kvo.Tests.cs @@ -306,7 +306,7 @@ public class KvoTests } // Batch - var batchResult = Kvo.Calculate(bars); + var batchResult = Kvo.Batch(bars); Assert.Equal(bars.Count, batchResult.Count); for (int i = 0; i < bars.Count; i++) @@ -345,7 +345,7 @@ public class KvoTests var spanKvo = new double[bars.Count]; var spanSignal = new double[bars.Count]; - Kvo.Calculate(high, low, close, volume, spanKvo, spanSignal); + Kvo.Batch(high, low, close, volume, spanKvo, spanSignal); for (int i = 0; i < bars.Count; i++) { @@ -364,7 +364,7 @@ public class KvoTests var output = new double[100]; var signal = new double[100]; - Assert.Throws(() => Kvo.Calculate(high, low, close, volume, output, signal)); + Assert.Throws(() => Kvo.Batch(high, low, close, volume, output, signal)); } [Fact] @@ -377,7 +377,7 @@ public class KvoTests var output = new double[100]; var signal = new double[100]; - Assert.Throws(() => Kvo.Calculate(high, low, close, volume, output, signal, fastPeriod: 0)); + Assert.Throws(() => Kvo.Batch(high, low, close, volume, output, signal, fastPeriod: 0)); } [Fact] @@ -390,7 +390,7 @@ public class KvoTests var output = new double[100]; var signal = new double[100]; - Assert.Throws(() => Kvo.Calculate(high, low, close, volume, output, signal, slowPeriod: 0)); + Assert.Throws(() => Kvo.Batch(high, low, close, volume, output, signal, slowPeriod: 0)); } [Fact] @@ -403,7 +403,7 @@ public class KvoTests var output = new double[100]; var signal = new double[100]; - Assert.Throws(() => Kvo.Calculate(high, low, close, volume, output, signal, signalPeriod: 0)); + Assert.Throws(() => Kvo.Batch(high, low, close, volume, output, signal, signalPeriod: 0)); } [Fact] @@ -417,7 +417,7 @@ public class KvoTests var signal = Array.Empty(); // Should not throw - Kvo.Calculate(high, low, close, volume, output, signal); + Kvo.Batch(high, low, close, volume, output, signal); // Verify arrays remain empty (no out-of-bounds writes) Assert.Empty(output); diff --git a/lib/volume/kvo/Kvo.Validation.Tests.cs b/lib/volume/kvo/Kvo.Validation.Tests.cs index b5560220..e615ed50 100644 --- a/lib/volume/kvo/Kvo.Validation.Tests.cs +++ b/lib/volume/kvo/Kvo.Validation.Tests.cs @@ -75,7 +75,7 @@ public class KvoValidationTests } // Batch - var batchResult = Kvo.Calculate(_data.Bars, DefaultFastPeriod, DefaultSlowPeriod, DefaultSignalPeriod); + var batchResult = Kvo.Batch(_data.Bars, DefaultFastPeriod, DefaultSlowPeriod, DefaultSignalPeriod); var batchValues = batchResult.Values.ToArray(); ValidationHelper.VerifyData(streamingValues.ToArray(), batchValues, 0, 100, 1e-9); @@ -103,7 +103,7 @@ public class KvoValidationTests var spanKvo = new double[high.Length]; var spanSignal = new double[high.Length]; - Kvo.Calculate(high, low, close, volume, spanKvo, spanSignal, DefaultFastPeriod, DefaultSlowPeriod, DefaultSignalPeriod); + Kvo.Batch(high, low, close, volume, spanKvo, spanSignal, DefaultFastPeriod, DefaultSlowPeriod, DefaultSignalPeriod); ValidationHelper.VerifyData(streamingKvo.ToArray(), spanKvo, 0, 100, 1e-9); ValidationHelper.VerifyData(streamingSignal.ToArray(), spanSignal, 0, 100, 1e-9); diff --git a/lib/volume/kvo/Kvo.cs b/lib/volume/kvo/Kvo.cs index 41ea2a4f..bcf28a14 100644 --- a/lib/volume/kvo/Kvo.cs +++ b/lib/volume/kvo/Kvo.cs @@ -303,7 +303,7 @@ public sealed class Kvo : ITValuePublisher /// The slow EMA period /// The signal line EMA period /// A TSeries containing the KVO values - public static TSeries Calculate(TBarSeries bars, int fastPeriod = 34, int slowPeriod = 55, int signalPeriod = 13) + public static TSeries Batch(TBarSeries bars, int fastPeriod = 34, int slowPeriod = 55, int signalPeriod = 13) { if (bars.Count == 0) { @@ -314,7 +314,7 @@ public sealed class Kvo : ITValuePublisher var v = new double[bars.Count]; var signal = new double[bars.Count]; - Calculate(bars.High.Values, bars.Low.Values, bars.Close.Values, bars.Volume.Values, + Batch(bars.High.Values, bars.Low.Values, bars.Close.Values, bars.Volume.Values, v, signal, fastPeriod, slowPeriod, signalPeriod); return new TSeries(t, v); @@ -334,7 +334,7 @@ public sealed class Kvo : ITValuePublisher /// The signal line EMA period /// Thrown when spans have different lengths or parameters are invalid [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void Calculate(ReadOnlySpan high, ReadOnlySpan low, + public static void Batch(ReadOnlySpan high, ReadOnlySpan low, ReadOnlySpan close, ReadOnlySpan volume, Span output, Span signal, int fastPeriod = 34, int slowPeriod = 55, int signalPeriod = 13) @@ -478,4 +478,11 @@ public sealed class Kvo : ITValuePublisher prevHlc3 = hlc3; } } + + public static (TSeries Results, Kvo Indicator) Calculate(TBarSeries bars, int fastPeriod = 34, int slowPeriod = 55, int signalPeriod = 13) + { + var indicator = new Kvo(fastPeriod, slowPeriod, signalPeriod); + TSeries results = indicator.Update(bars); + return (results, indicator); + } } \ No newline at end of file diff --git a/lib/volume/mfi/Mfi.Tests.cs b/lib/volume/mfi/Mfi.Tests.cs index 9c82d192..04da523a 100644 --- a/lib/volume/mfi/Mfi.Tests.cs +++ b/lib/volume/mfi/Mfi.Tests.cs @@ -263,7 +263,7 @@ public class MfiTests } // Batch - var batchResult = Mfi.Calculate(bars); + var batchResult = Mfi.Batch(bars); Assert.Equal(bars.Count, batchResult.Count); for (int i = 0; i < bars.Count; i++) @@ -298,7 +298,7 @@ public class MfiTests var volume = bars.Volume.Values.ToArray(); var output = new double[bars.Count]; - Mfi.Calculate(high, low, close, volume, output); + Mfi.Batch(high, low, close, volume, output); for (int i = 0; i < bars.Count; i++) { @@ -315,7 +315,7 @@ public class MfiTests var volume = new double[100]; var output = new double[100]; - Assert.Throws(() => Mfi.Calculate(high, low, close, volume, output)); + Assert.Throws(() => Mfi.Batch(high, low, close, volume, output)); } [Fact] @@ -327,7 +327,7 @@ public class MfiTests var volume = new double[100]; var output = new double[100]; - Assert.Throws(() => Mfi.Calculate(high, low, close, volume, output, period: 0)); + Assert.Throws(() => Mfi.Batch(high, low, close, volume, output, period: 0)); } [Fact] @@ -339,7 +339,7 @@ public class MfiTests var volume = Array.Empty(); var output = Array.Empty(); - Mfi.Calculate(high, low, close, volume, output); + Mfi.Batch(high, low, close, volume, output); Assert.Empty(output); } diff --git a/lib/volume/mfi/Mfi.Validation.Tests.cs b/lib/volume/mfi/Mfi.Validation.Tests.cs index f3dd1f0f..8295f857 100644 --- a/lib/volume/mfi/Mfi.Validation.Tests.cs +++ b/lib/volume/mfi/Mfi.Validation.Tests.cs @@ -96,7 +96,7 @@ public class MfiValidationTests } // Batch - var batchResult = Mfi.Calculate(_data.Bars, DefaultPeriod); + var batchResult = Mfi.Batch(_data.Bars, DefaultPeriod); var batchValues = batchResult.Values.ToArray(); ValidationHelper.VerifyData(streamingValues.ToArray(), batchValues, 0, 100, 1e-9); @@ -120,7 +120,7 @@ public class MfiValidationTests var volume = _data.Bars.Volume.Values.ToArray(); var spanOutput = new double[high.Length]; - Mfi.Calculate(high, low, close, volume, spanOutput, DefaultPeriod); + Mfi.Batch(high, low, close, volume, spanOutput, DefaultPeriod); ValidationHelper.VerifyData(streamingValues.ToArray(), spanOutput, 0, 100, 1e-9); } diff --git a/lib/volume/mfi/Mfi.cs b/lib/volume/mfi/Mfi.cs index 4b215e97..a1769f7f 100644 --- a/lib/volume/mfi/Mfi.cs +++ b/lib/volume/mfi/Mfi.cs @@ -212,7 +212,7 @@ public sealed class Mfi : ITValuePublisher return new TSeries(t, v); } - public static TSeries Calculate(TBarSeries source, int period = 14) + public static TSeries Batch(TBarSeries source, int period = 14) { if (source.Count == 0) { @@ -222,13 +222,13 @@ public sealed class Mfi : ITValuePublisher var t = source.Open.Times.ToArray(); var v = new double[source.Count]; - Calculate(source.High.Values, source.Low.Values, source.Close.Values, source.Volume.Values, v, period); + Batch(source.High.Values, source.Low.Values, source.Close.Values, source.Volume.Values, v, period); return new TSeries(t, v); } [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void Calculate(ReadOnlySpan high, ReadOnlySpan low, ReadOnlySpan close, ReadOnlySpan volume, Span output, int period = 14) + public static void Batch(ReadOnlySpan high, ReadOnlySpan low, ReadOnlySpan close, ReadOnlySpan volume, Span output, int period = 14) { if (high.Length != low.Length) { @@ -326,4 +326,11 @@ public sealed class Mfi : ITValuePublisher } } } + + public static (TSeries Results, Mfi Indicator) Calculate(TBarSeries source, int period = 14) + { + var indicator = new Mfi(period); + TSeries results = indicator.Update(source); + return (results, indicator); + } } \ No newline at end of file diff --git a/lib/volume/nvi/Nvi.Tests.cs b/lib/volume/nvi/Nvi.Tests.cs index 0f0fdd97..0fc00bd4 100644 --- a/lib/volume/nvi/Nvi.Tests.cs +++ b/lib/volume/nvi/Nvi.Tests.cs @@ -286,7 +286,7 @@ public class NviTests } // Batch - var batchResult = Nvi.Calculate(bars); + var batchResult = Nvi.Batch(bars); Assert.Equal(bars.Count, batchResult.Count); for (int i = 0; i < bars.Count; i++) @@ -319,7 +319,7 @@ public class NviTests var volume = bars.Volume.Values.ToArray(); var output = new double[bars.Count]; - Nvi.Calculate(close, volume, output); + Nvi.Batch(close, volume, output); for (int i = 0; i < bars.Count; i++) { @@ -334,7 +334,7 @@ public class NviTests var volume = new double[99]; // Different length var output = new double[100]; - Assert.Throws(() => Nvi.Calculate(close, volume, output)); + Assert.Throws(() => Nvi.Batch(close, volume, output)); } [Fact] @@ -344,7 +344,7 @@ public class NviTests var volume = new double[100]; var output = new double[100]; - Assert.Throws(() => Nvi.Calculate(close, volume, output, startValue: 0)); + Assert.Throws(() => Nvi.Batch(close, volume, output, startValue: 0)); } [Fact] @@ -354,7 +354,7 @@ public class NviTests var volume = Array.Empty(); var output = Array.Empty(); - Nvi.Calculate(close, volume, output); + Nvi.Batch(close, volume, output); Assert.Empty(output); } diff --git a/lib/volume/nvi/Nvi.Validation.Tests.cs b/lib/volume/nvi/Nvi.Validation.Tests.cs index 8237aa47..7db9cbc5 100644 --- a/lib/volume/nvi/Nvi.Validation.Tests.cs +++ b/lib/volume/nvi/Nvi.Validation.Tests.cs @@ -62,7 +62,7 @@ public class NviValidationTests } // Batch - var batchResult = Nvi.Calculate(_data.Bars, DefaultStartValue); + var batchResult = Nvi.Batch(_data.Bars, DefaultStartValue); var batchValues = batchResult.Values.ToArray(); ValidationHelper.VerifyData(streamingValues.ToArray(), batchValues, 0, 100, 1e-9); @@ -84,7 +84,7 @@ public class NviValidationTests var volume = _data.Bars.Volume.Values.ToArray(); var spanOutput = new double[close.Length]; - Nvi.Calculate(close, volume, spanOutput, DefaultStartValue); + Nvi.Batch(close, volume, spanOutput, DefaultStartValue); ValidationHelper.VerifyData(streamingValues.ToArray(), spanOutput, 0, 100, 1e-9); } diff --git a/lib/volume/nvi/Nvi.cs b/lib/volume/nvi/Nvi.cs index ee00567e..ff8f8c64 100644 --- a/lib/volume/nvi/Nvi.cs +++ b/lib/volume/nvi/Nvi.cs @@ -181,7 +181,7 @@ public sealed class Nvi : ITValuePublisher return new TSeries(t, v); } - public static TSeries Calculate(TBarSeries source, double startValue = 100.0) + public static TSeries Batch(TBarSeries source, double startValue = 100.0) { if (source.Count == 0) { @@ -191,13 +191,13 @@ public sealed class Nvi : ITValuePublisher var t = source.Open.Times.ToArray(); var v = new double[source.Count]; - Calculate(source.Close.Values, source.Volume.Values, v, startValue); + Batch(source.Close.Values, source.Volume.Values, v, startValue); return new TSeries(t, v); } [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void Calculate(ReadOnlySpan close, ReadOnlySpan volume, Span output, double startValue = 100.0) + public static void Batch(ReadOnlySpan close, ReadOnlySpan volume, Span output, double startValue = 100.0) { if (close.Length != volume.Length) { @@ -273,4 +273,11 @@ public sealed class Nvi : ITValuePublisher prevVolume = currentVolume; } } + + public static (TSeries Results, Nvi Indicator) Calculate(TBarSeries source, double startValue = 100.0) + { + var indicator = new Nvi(startValue); + TSeries results = indicator.Update(source); + return (results, indicator); + } } \ No newline at end of file diff --git a/lib/volume/obv/Obv.Tests.cs b/lib/volume/obv/Obv.Tests.cs index de0b4475..8a37fee7 100644 --- a/lib/volume/obv/Obv.Tests.cs +++ b/lib/volume/obv/Obv.Tests.cs @@ -265,7 +265,7 @@ public class ObvTests } // Batch - var batchResult = Obv.Calculate(bars); + var batchResult = Obv.Batch(bars); Assert.Equal(bars.Count, batchResult.Count); for (int i = 0; i < bars.Count; i++) @@ -298,7 +298,7 @@ public class ObvTests var volume = bars.Volume.Values.ToArray(); var output = new double[bars.Count]; - Obv.Calculate(close, volume, output); + Obv.Batch(close, volume, output); for (int i = 0; i < bars.Count; i++) { @@ -313,7 +313,7 @@ public class ObvTests var volume = new double[99]; // Different length var output = new double[100]; - Assert.Throws(() => Obv.Calculate(close, volume, output)); + Assert.Throws(() => Obv.Batch(close, volume, output)); } [Fact] @@ -323,7 +323,7 @@ public class ObvTests var volume = Array.Empty(); var output = Array.Empty(); - Obv.Calculate(close, volume, output); + Obv.Batch(close, volume, output); Assert.Empty(output); } diff --git a/lib/volume/obv/Obv.Validation.Tests.cs b/lib/volume/obv/Obv.Validation.Tests.cs index 7a5803b4..ed50e572 100644 --- a/lib/volume/obv/Obv.Validation.Tests.cs +++ b/lib/volume/obv/Obv.Validation.Tests.cs @@ -133,7 +133,7 @@ public class ObvValidationTests } // Batch - var batchResult = Obv.Calculate(_data.Bars); + var batchResult = Obv.Batch(_data.Bars); var batchValues = batchResult.Values.ToArray(); ValidationHelper.VerifyData(streamingValues.ToArray(), batchValues, 0, 100, 1e-9); @@ -155,7 +155,7 @@ public class ObvValidationTests var volume = _data.Bars.Volume.Values.ToArray(); var spanOutput = new double[close.Length]; - Obv.Calculate(close, volume, spanOutput); + Obv.Batch(close, volume, spanOutput); ValidationHelper.VerifyData(streamingValues.ToArray(), spanOutput, 0, 100, 1e-9); } diff --git a/lib/volume/obv/Obv.cs b/lib/volume/obv/Obv.cs index e58d7e7e..738282cb 100644 --- a/lib/volume/obv/Obv.cs +++ b/lib/volume/obv/Obv.cs @@ -175,7 +175,7 @@ public sealed class Obv : ITValuePublisher return new TSeries(t, v); } - public static TSeries Calculate(TBarSeries source) + public static TSeries Batch(TBarSeries source) { if (source.Count == 0) { @@ -185,13 +185,13 @@ public sealed class Obv : ITValuePublisher var t = source.Open.Times.ToArray(); var v = new double[source.Count]; - Calculate(source.Close.Values, source.Volume.Values, v); + Batch(source.Close.Values, source.Volume.Values, v); return new TSeries(t, v); } [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void Calculate(ReadOnlySpan close, ReadOnlySpan volume, Span output) + public static void Batch(ReadOnlySpan close, ReadOnlySpan volume, Span output) { if (close.Length != volume.Length) { @@ -243,4 +243,11 @@ public sealed class Obv : ITValuePublisher } } } + + public static (TSeries Results, Obv Indicator) Calculate(TBarSeries source) + { + var indicator = new Obv(); + TSeries results = indicator.Update(source); + return (results, indicator); + } } \ No newline at end of file diff --git a/lib/volume/pvd/Pvd.Tests.cs b/lib/volume/pvd/Pvd.Tests.cs index e8dd8e89..39fdd933 100644 --- a/lib/volume/pvd/Pvd.Tests.cs +++ b/lib/volume/pvd/Pvd.Tests.cs @@ -341,7 +341,7 @@ public class PvdTests double[] volumes = [1000, 1100, 1200, 1300, 1400]; double[] output = new double[5]; - Pvd.Calculate(closes.AsSpan(), volumes.AsSpan(), output.AsSpan(), pricePeriod: 2, volumePeriod: 2, smoothingPeriod: 1); + Pvd.Batch(closes.AsSpan(), volumes.AsSpan(), output.AsSpan(), pricePeriod: 2, volumePeriod: 2, smoothingPeriod: 1); // Should handle NaN gracefully - result might be NaN or computed value Assert.True(output.Length == 5); @@ -373,10 +373,10 @@ public class PvdTests var pvdBatch = new Pvd(pricePeriod: period, volumePeriod: period, smoothingPeriod: 3); var batchResult = pvdBatch.Update(_bars); - // Mode 3: Static Calculate(TBarSeries) - var staticResult = Pvd.Calculate(_bars, pricePeriod: period, volumePeriod: period, smoothingPeriod: 3); + // Mode 3: Static Batch(TBarSeries) + var staticResult = Pvd.Batch(_bars, pricePeriod: period, volumePeriod: period, smoothingPeriod: 3); - // Mode 4: Static Calculate(Span) + // Mode 4: Static Batch(Span) double[] closes = new double[_bars.Count]; double[] volumes = new double[_bars.Count]; double[] spanOutput = new double[_bars.Count]; @@ -385,7 +385,7 @@ public class PvdTests closes[i] = _bars[i].Close; volumes[i] = _bars[i].Volume; } - Pvd.Calculate(closes.AsSpan(), volumes.AsSpan(), spanOutput.AsSpan(), pricePeriod: period, volumePeriod: period, smoothingPeriod: 3); + Pvd.Batch(closes.AsSpan(), volumes.AsSpan(), spanOutput.AsSpan(), pricePeriod: period, volumePeriod: period, smoothingPeriod: 3); // Compare last 100 values (after warmup) int compareStart = _bars.Count - 100; @@ -408,7 +408,7 @@ public class PvdTests double[] output = new double[5]; var ex = Assert.Throws(() => - Pvd.Calculate(closes.AsSpan(), volumes.AsSpan(), output.AsSpan())); + Pvd.Batch(closes.AsSpan(), volumes.AsSpan(), output.AsSpan())); Assert.Equal("volume", ex.ParamName); } @@ -420,7 +420,7 @@ public class PvdTests double[] output = new double[3]; // Too short var ex = Assert.Throws(() => - Pvd.Calculate(closes.AsSpan(), volumes.AsSpan(), output.AsSpan())); + Pvd.Batch(closes.AsSpan(), volumes.AsSpan(), output.AsSpan())); Assert.Equal("output", ex.ParamName); } @@ -432,7 +432,7 @@ public class PvdTests double[] output = new double[5]; var ex = Assert.Throws(() => - Pvd.Calculate(closes.AsSpan(), volumes.AsSpan(), output.AsSpan(), pricePeriod: 0)); + Pvd.Batch(closes.AsSpan(), volumes.AsSpan(), output.AsSpan(), pricePeriod: 0)); Assert.Equal("pricePeriod", ex.ParamName); } @@ -444,7 +444,7 @@ public class PvdTests double[] output = new double[5]; var ex = Assert.Throws(() => - Pvd.Calculate(closes.AsSpan(), volumes.AsSpan(), output.AsSpan(), volumePeriod: 0)); + Pvd.Batch(closes.AsSpan(), volumes.AsSpan(), output.AsSpan(), volumePeriod: 0)); Assert.Equal("volumePeriod", ex.ParamName); } @@ -456,7 +456,7 @@ public class PvdTests double[] output = new double[5]; var ex = Assert.Throws(() => - Pvd.Calculate(closes.AsSpan(), volumes.AsSpan(), output.AsSpan(), smoothingPeriod: 0)); + Pvd.Batch(closes.AsSpan(), volumes.AsSpan(), output.AsSpan(), smoothingPeriod: 0)); Assert.Equal("smoothingPeriod", ex.ParamName); } @@ -475,7 +475,7 @@ public class PvdTests } // Should not stack overflow - Pvd.Calculate(closes.AsSpan(), volumes.AsSpan(), output.AsSpan()); + Pvd.Batch(closes.AsSpan(), volumes.AsSpan(), output.AsSpan()); Assert.True(double.IsFinite(output[size - 1])); } diff --git a/lib/volume/pvd/Pvd.Validation.Tests.cs b/lib/volume/pvd/Pvd.Validation.Tests.cs index 695e98f0..ce4ec02d 100644 --- a/lib/volume/pvd/Pvd.Validation.Tests.cs +++ b/lib/volume/pvd/Pvd.Validation.Tests.cs @@ -41,7 +41,7 @@ public class PvdValidationTests } // Batch calculation (uses static Calculate which uses span internally) - var batchResults = Pvd.Calculate(_data, pricePeriod, volumePeriod, smoothingPeriod); + var batchResults = Pvd.Batch(_data, pricePeriod, volumePeriod, smoothingPeriod); // Compare after full warmup (streaming and span may differ during warmup due to smoothing initialization) Assert.Equal(_data.Count, batchResults.Count); @@ -70,10 +70,10 @@ public class PvdValidationTests // Span calculation double[] spanResults = new double[_data.Count]; - Pvd.Calculate(closes.AsSpan(), volumes.AsSpan(), spanResults.AsSpan(), pricePeriod, volumePeriod, smoothingPeriod); + Pvd.Batch(closes.AsSpan(), volumes.AsSpan(), spanResults.AsSpan(), pricePeriod, volumePeriod, smoothingPeriod); // Batch calculation - var batchResults = Pvd.Calculate(_data, pricePeriod, volumePeriod, smoothingPeriod); + var batchResults = Pvd.Batch(_data, pricePeriod, volumePeriod, smoothingPeriod); // Compare after warmup int startCompare = Math.Max(pricePeriod, volumePeriod) + smoothingPeriod; @@ -86,8 +86,8 @@ public class PvdValidationTests [Fact] public void Pvd_DifferentPeriods_ProduceDifferentResults() { - var pvd1 = Pvd.Calculate(_data, pricePeriod: 5, volumePeriod: 5, smoothingPeriod: 3); - var pvd2 = Pvd.Calculate(_data, pricePeriod: 20, volumePeriod: 20, smoothingPeriod: 3); + var pvd1 = Pvd.Batch(_data, pricePeriod: 5, volumePeriod: 5, smoothingPeriod: 3); + var pvd2 = Pvd.Batch(_data, pricePeriod: 20, volumePeriod: 20, smoothingPeriod: 3); // After warmup, values should differ int compareIdx = _data.Count - 1; @@ -98,10 +98,10 @@ public class PvdValidationTests public void Pvd_AsymmetricPeriods_Work() { // Price period longer than volume period - var pvd1 = Pvd.Calculate(_data, pricePeriod: 20, volumePeriod: 5, smoothingPeriod: 3); + var pvd1 = Pvd.Batch(_data, pricePeriod: 20, volumePeriod: 5, smoothingPeriod: 3); // Volume period longer than price period - var pvd2 = Pvd.Calculate(_data, pricePeriod: 5, volumePeriod: 20, smoothingPeriod: 3); + var pvd2 = Pvd.Batch(_data, pricePeriod: 5, volumePeriod: 20, smoothingPeriod: 3); // Results should differ int compareIdx = _data.Count - 1; @@ -128,7 +128,7 @@ public class PvdValidationTests // Price increasing, volume decreasing bars.Add(new TBar(time.AddMinutes(5), 110.0, 110.0, 110.0, 110.0, 800.0)); - var result = Pvd.Calculate(bars, pricePeriod: 2, volumePeriod: 2, smoothingPeriod: 1); + var result = Pvd.Batch(bars, pricePeriod: 2, volumePeriod: 2, smoothingPeriod: 1); // Last value should be positive (divergence detected) Assert.True(result[^1].Value > 0); @@ -150,7 +150,7 @@ public class PvdValidationTests // Price increasing, volume also increasing bars.Add(new TBar(time.AddMinutes(5), 110.0, 110.0, 110.0, 110.0, 1200.0)); - var result = Pvd.Calculate(bars, pricePeriod: 2, volumePeriod: 2, smoothingPeriod: 1); + var result = Pvd.Batch(bars, pricePeriod: 2, volumePeriod: 2, smoothingPeriod: 1); // Last value should be negative (price and volume moving same direction) Assert.True(result[^1].Value < 0); @@ -169,7 +169,7 @@ public class PvdValidationTests bars.Add(new TBar(time.AddMinutes(i), 100.0, 100.0, 100.0, 100.0, 1000.0)); } - var result = Pvd.Calculate(bars, pricePeriod: 3, volumePeriod: 3, smoothingPeriod: 2); + var result = Pvd.Batch(bars, pricePeriod: 3, volumePeriod: 3, smoothingPeriod: 2); // Should be zero (no momentum in either direction) Assert.Equal(0.0, result[^1].Value, precision: 10); @@ -198,7 +198,7 @@ public class PvdValidationTests // Magnitude = |0.9615| + |-11.111| = 12.073... // Divergence = 1 * -(-1) * 12.073 = 12.073... (positive: price up, volume down) - var result = Pvd.Calculate(bars, pricePeriod: 2, volumePeriod: 2, smoothingPeriod: 1); + var result = Pvd.Batch(bars, pricePeriod: 2, volumePeriod: 2, smoothingPeriod: 1); // Last value should be positive Assert.True(result[^1].Value > 0); @@ -211,8 +211,8 @@ public class PvdValidationTests [Fact] public void Pvd_SmoothingPeriod1_NoSmoothing() { - var result1 = Pvd.Calculate(_data, pricePeriod: 10, volumePeriod: 10, smoothingPeriod: 1); - var result3 = Pvd.Calculate(_data, pricePeriod: 10, volumePeriod: 10, smoothingPeriod: 3); + var result1 = Pvd.Batch(_data, pricePeriod: 10, volumePeriod: 10, smoothingPeriod: 1); + var result3 = Pvd.Batch(_data, pricePeriod: 10, volumePeriod: 10, smoothingPeriod: 3); // Smoothing should make values different (and generally smoother) bool foundDifference = false; @@ -230,8 +230,8 @@ public class PvdValidationTests [Fact] public void Pvd_HigherSmoothing_ReducesVolatility() { - var result1 = Pvd.Calculate(_data, pricePeriod: 10, volumePeriod: 10, smoothingPeriod: 1); - var result10 = Pvd.Calculate(_data, pricePeriod: 10, volumePeriod: 10, smoothingPeriod: 10); + var result1 = Pvd.Batch(_data, pricePeriod: 10, volumePeriod: 10, smoothingPeriod: 1); + var result10 = Pvd.Batch(_data, pricePeriod: 10, volumePeriod: 10, smoothingPeriod: 10); // Calculate variance of last 100 values double variance1 = CalculateVariance(result1.Skip(400).Select(x => x.Value).ToArray()); @@ -268,7 +268,7 @@ public class PvdValidationTests bars.Add(new TBar(time.AddMinutes(i), 100.0 + i, 101.0 + i, 99.0 + i, 100.5 + i, volume)); } - var result = Pvd.Calculate(bars, pricePeriod: 3, volumePeriod: 3, smoothingPeriod: 2); + var result = Pvd.Batch(bars, pricePeriod: 3, volumePeriod: 3, smoothingPeriod: 2); // Should complete without errors Assert.Equal(20, result.Count); @@ -281,7 +281,7 @@ public class PvdValidationTests var bars = new TBarSeries(); bars.Add(new TBar(DateTime.UtcNow, 100.0, 100.0, 100.0, 100.0, 1000.0)); - var result = Pvd.Calculate(bars, pricePeriod: 5, volumePeriod: 5, smoothingPeriod: 2); + var result = Pvd.Batch(bars, pricePeriod: 5, volumePeriod: 5, smoothingPeriod: 2); Assert.Single(result); Assert.Equal(0.0, result[0].Value); @@ -298,7 +298,7 @@ public class PvdValidationTests largeData.Add(gbm.Next()); } - var result = Pvd.Calculate(largeData, pricePeriod: 14, volumePeriod: 14, smoothingPeriod: 3); + var result = Pvd.Batch(largeData, pricePeriod: 14, volumePeriod: 14, smoothingPeriod: 3); Assert.Equal(10000, result.Count); Assert.True(double.IsFinite(result[^1].Value)); diff --git a/lib/volume/pvd/Pvd.cs b/lib/volume/pvd/Pvd.cs index d8507c5f..d339c2b2 100644 --- a/lib/volume/pvd/Pvd.cs +++ b/lib/volume/pvd/Pvd.cs @@ -250,7 +250,7 @@ public sealed class Pvd : ITValuePublisher return new TSeries(t, v); } - public static TSeries Calculate(TBarSeries source, int pricePeriod = 14, int volumePeriod = 14, int smoothingPeriod = 3) + public static TSeries Batch(TBarSeries source, int pricePeriod = 14, int volumePeriod = 14, int smoothingPeriod = 3) { if (source.Count == 0) { @@ -260,13 +260,13 @@ public sealed class Pvd : ITValuePublisher var t = source.Close.Times.ToArray(); var v = new double[source.Count]; - Calculate(source.Close.Values, source.Volume.Values, v, pricePeriod, volumePeriod, smoothingPeriod); + Batch(source.Close.Values, source.Volume.Values, v, pricePeriod, volumePeriod, smoothingPeriod); return new TSeries(t, v); } [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void Calculate(ReadOnlySpan close, ReadOnlySpan volume, Span output, + public static void Batch(ReadOnlySpan close, ReadOnlySpan volume, Span output, int pricePeriod = 14, int volumePeriod = 14, int smoothingPeriod = 3) { if (close.Length != volume.Length) @@ -379,4 +379,11 @@ public sealed class Pvd : ITValuePublisher } } } + + public static (TSeries Results, Pvd Indicator) Calculate(TBarSeries source, int pricePeriod = 14, int volumePeriod = 14, int smoothingPeriod = 3) + { + var indicator = new Pvd(pricePeriod, volumePeriod, smoothingPeriod); + TSeries results = indicator.Update(source); + return (results, indicator); + } } \ No newline at end of file diff --git a/lib/volume/pvi/Pvi.Tests.cs b/lib/volume/pvi/Pvi.Tests.cs index 5c6e7be0..fa04ecd2 100644 --- a/lib/volume/pvi/Pvi.Tests.cs +++ b/lib/volume/pvi/Pvi.Tests.cs @@ -286,7 +286,7 @@ public class PviTests } // Batch - var batchResult = Pvi.Calculate(bars); + var batchResult = Pvi.Batch(bars); Assert.Equal(bars.Count, batchResult.Count); for (int i = 0; i < bars.Count; i++) @@ -319,7 +319,7 @@ public class PviTests var volume = bars.Volume.Values.ToArray(); var output = new double[bars.Count]; - Pvi.Calculate(close, volume, output); + Pvi.Batch(close, volume, output); for (int i = 0; i < bars.Count; i++) { @@ -334,7 +334,7 @@ public class PviTests var volume = new double[99]; // Different length var output = new double[100]; - Assert.Throws(() => Pvi.Calculate(close, volume, output)); + Assert.Throws(() => Pvi.Batch(close, volume, output)); } [Fact] @@ -344,7 +344,7 @@ public class PviTests var volume = new double[100]; var output = new double[100]; - Assert.Throws(() => Pvi.Calculate(close, volume, output, startValue: 0)); + Assert.Throws(() => Pvi.Batch(close, volume, output, startValue: 0)); } [Fact] @@ -354,7 +354,7 @@ public class PviTests var volume = Array.Empty(); var output = Array.Empty(); - Pvi.Calculate(close, volume, output); + Pvi.Batch(close, volume, output); Assert.Empty(output); } diff --git a/lib/volume/pvi/Pvi.Validation.Tests.cs b/lib/volume/pvi/Pvi.Validation.Tests.cs index 66bebffb..9ff65df6 100644 --- a/lib/volume/pvi/Pvi.Validation.Tests.cs +++ b/lib/volume/pvi/Pvi.Validation.Tests.cs @@ -62,7 +62,7 @@ public class PviValidationTests } // Batch - var batchResult = Pvi.Calculate(_data.Bars, DefaultStartValue); + var batchResult = Pvi.Batch(_data.Bars, DefaultStartValue); var batchValues = batchResult.Values.ToArray(); ValidationHelper.VerifyData(streamingValues.ToArray(), batchValues, 0, 100, 1e-9); @@ -84,7 +84,7 @@ public class PviValidationTests var volume = _data.Bars.Volume.Values.ToArray(); var spanOutput = new double[close.Length]; - Pvi.Calculate(close, volume, spanOutput, DefaultStartValue); + Pvi.Batch(close, volume, spanOutput, DefaultStartValue); ValidationHelper.VerifyData(streamingValues.ToArray(), spanOutput, 0, 100, 1e-9); } diff --git a/lib/volume/pvi/Pvi.cs b/lib/volume/pvi/Pvi.cs index f8f8a2da..14622549 100644 --- a/lib/volume/pvi/Pvi.cs +++ b/lib/volume/pvi/Pvi.cs @@ -188,7 +188,7 @@ public sealed class Pvi : ITValuePublisher return new TSeries(t, v); } - public static TSeries Calculate(TBarSeries source, double startValue = 100.0) + public static TSeries Batch(TBarSeries source, double startValue = 100.0) { if (source.Count == 0) { @@ -198,13 +198,13 @@ public sealed class Pvi : ITValuePublisher var t = source.Close.Times.ToArray(); var v = new double[source.Count]; - Calculate(source.Close.Values, source.Volume.Values, v, startValue); + Batch(source.Close.Values, source.Volume.Values, v, startValue); return new TSeries(t, v); } [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void Calculate(ReadOnlySpan close, ReadOnlySpan volume, Span output, double startValue = 100.0) + public static void Batch(ReadOnlySpan close, ReadOnlySpan volume, Span output, double startValue = 100.0) { if (close.Length != volume.Length) { @@ -280,4 +280,11 @@ public sealed class Pvi : ITValuePublisher prevVolume = currentVolume; } } + + public static (TSeries Results, Pvi Indicator) Calculate(TBarSeries source, double startValue = 100.0) + { + var indicator = new Pvi(startValue); + TSeries results = indicator.Update(source); + return (results, indicator); + } } \ No newline at end of file diff --git a/lib/volume/pvo/Pvo.Tests.cs b/lib/volume/pvo/Pvo.Tests.cs index 655e4f35..cba74d1f 100644 --- a/lib/volume/pvo/Pvo.Tests.cs +++ b/lib/volume/pvo/Pvo.Tests.cs @@ -318,7 +318,7 @@ public class PvoTests } // Batch - var batchResult = Pvo.Calculate(bars); + var batchResult = Pvo.Batch(bars); Assert.Equal(bars.Count, batchResult.Count); for (int i = 0; i < bars.Count; i++) @@ -357,7 +357,7 @@ public class PvoTests var spanSignal = new double[bars.Count]; var spanHistogram = new double[bars.Count]; - Pvo.Calculate(volume, spanPvo, spanSignal, spanHistogram); + Pvo.Batch(volume, spanPvo, spanSignal, spanHistogram); for (int i = 0; i < bars.Count; i++) { @@ -375,7 +375,7 @@ public class PvoTests var signal = new double[100]; var histogram = new double[100]; - Assert.Throws(() => Pvo.Calculate(volume, output, signal, histogram)); + Assert.Throws(() => Pvo.Batch(volume, output, signal, histogram)); } [Fact] @@ -386,7 +386,7 @@ public class PvoTests var signal = new double[100]; var histogram = new double[100]; - Assert.Throws(() => Pvo.Calculate(volume, output, signal, histogram, fastPeriod: 0)); + Assert.Throws(() => Pvo.Batch(volume, output, signal, histogram, fastPeriod: 0)); } [Fact] @@ -397,7 +397,7 @@ public class PvoTests var signal = new double[100]; var histogram = new double[100]; - Assert.Throws(() => Pvo.Calculate(volume, output, signal, histogram, slowPeriod: 0)); + Assert.Throws(() => Pvo.Batch(volume, output, signal, histogram, slowPeriod: 0)); } [Fact] @@ -408,7 +408,7 @@ public class PvoTests var signal = new double[100]; var histogram = new double[100]; - Assert.Throws(() => Pvo.Calculate(volume, output, signal, histogram, signalPeriod: 0)); + Assert.Throws(() => Pvo.Batch(volume, output, signal, histogram, signalPeriod: 0)); } [Fact] @@ -419,7 +419,7 @@ public class PvoTests var signal = new double[100]; var histogram = new double[100]; - Assert.Throws(() => Pvo.Calculate(volume, output, signal, histogram, fastPeriod: 26, slowPeriod: 26)); + Assert.Throws(() => Pvo.Batch(volume, output, signal, histogram, fastPeriod: 26, slowPeriod: 26)); } [Fact] @@ -431,7 +431,7 @@ public class PvoTests var histogram = Array.Empty(); // Should not throw - Pvo.Calculate(volume, output, signal, histogram); + Pvo.Batch(volume, output, signal, histogram); Assert.Empty(output); } diff --git a/lib/volume/pvo/Pvo.Validation.Tests.cs b/lib/volume/pvo/Pvo.Validation.Tests.cs index 2ef579ee..9f699942 100644 --- a/lib/volume/pvo/Pvo.Validation.Tests.cs +++ b/lib/volume/pvo/Pvo.Validation.Tests.cs @@ -73,7 +73,7 @@ public class PvoValidationTests } // Batch - var batchResult = Pvo.Calculate(_data.Bars, DefaultFastPeriod, DefaultSlowPeriod, DefaultSignalPeriod); + var batchResult = Pvo.Batch(_data.Bars, DefaultFastPeriod, DefaultSlowPeriod, DefaultSignalPeriod); var batchValues = batchResult.Values.ToArray(); ValidationHelper.VerifyData(streamingValues.ToArray(), batchValues, 0, 100, 1e-9); @@ -101,7 +101,7 @@ public class PvoValidationTests var spanSignal = new double[volume.Length]; var spanHistogram = new double[volume.Length]; - Pvo.Calculate(volume, spanPvo, spanSignal, spanHistogram, DefaultFastPeriod, DefaultSlowPeriod, DefaultSignalPeriod); + Pvo.Batch(volume, spanPvo, spanSignal, spanHistogram, DefaultFastPeriod, DefaultSlowPeriod, DefaultSignalPeriod); ValidationHelper.VerifyData(streamingPvo.ToArray(), spanPvo, 0, 100, 1e-9); ValidationHelper.VerifyData(streamingSignal.ToArray(), spanSignal, 0, 100, 1e-9); @@ -212,7 +212,7 @@ public class PvoValidationTests } // Mode 3: Batch - var mode3Result = Pvo.Calculate(_data.Bars, DefaultFastPeriod, DefaultSlowPeriod, DefaultSignalPeriod); + var mode3Result = Pvo.Batch(_data.Bars, DefaultFastPeriod, DefaultSlowPeriod, DefaultSignalPeriod); var mode3Values = mode3Result.Values.ToArray(); // Mode 4: Span @@ -220,7 +220,7 @@ public class PvoValidationTests var mode4Values = new double[volume.Length]; var mode4Signal = new double[volume.Length]; var mode4Histogram = new double[volume.Length]; - Pvo.Calculate(volume, mode4Values, mode4Signal, mode4Histogram, DefaultFastPeriod, DefaultSlowPeriod, DefaultSignalPeriod); + Pvo.Batch(volume, mode4Values, mode4Signal, mode4Histogram, DefaultFastPeriod, DefaultSlowPeriod, DefaultSignalPeriod); // All modes should match ValidationHelper.VerifyData(mode1Values.ToArray(), mode2Values.ToArray(), 0, 100, 1e-9); diff --git a/lib/volume/pvo/Pvo.cs b/lib/volume/pvo/Pvo.cs index c18b37bf..dbbccb03 100644 --- a/lib/volume/pvo/Pvo.cs +++ b/lib/volume/pvo/Pvo.cs @@ -268,7 +268,7 @@ public sealed class Pvo : ITValuePublisher /// The slow EMA period /// The signal line EMA period /// A TSeries containing the PVO values - public static TSeries Calculate(TBarSeries bars, int fastPeriod = 12, int slowPeriod = 26, int signalPeriod = 9) + public static TSeries Batch(TBarSeries bars, int fastPeriod = 12, int slowPeriod = 26, int signalPeriod = 9) { if (bars.Count == 0) { @@ -280,7 +280,7 @@ public sealed class Pvo : ITValuePublisher var signal = new double[bars.Count]; var histogram = new double[bars.Count]; - Calculate(bars.Volume.Values, v, signal, histogram, fastPeriod, slowPeriod, signalPeriod); + Batch(bars.Volume.Values, v, signal, histogram, fastPeriod, slowPeriod, signalPeriod); return new TSeries(t, v); } @@ -297,7 +297,7 @@ public sealed class Pvo : ITValuePublisher /// The signal line EMA period /// Thrown when spans have different lengths or parameters are invalid [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void Calculate(ReadOnlySpan volume, Span output, Span signal, + public static void Batch(ReadOnlySpan volume, Span output, Span signal, Span histogram, int fastPeriod = 12, int slowPeriod = 26, int signalPeriod = 9) { if (volume.Length != output.Length) @@ -400,4 +400,11 @@ public sealed class Pvo : ITValuePublisher histogram[i] = pvoValue - signalValue; } } + + public static (TSeries Results, Pvo Indicator) Calculate(TBarSeries bars, int fastPeriod = 12, int slowPeriod = 26, int signalPeriod = 9) + { + var indicator = new Pvo(fastPeriod, slowPeriod, signalPeriod); + TSeries results = indicator.Update(bars); + return (results, indicator); + } } \ No newline at end of file diff --git a/lib/volume/pvr/Pvr.Tests.cs b/lib/volume/pvr/Pvr.Tests.cs index c396e708..8b964010 100644 --- a/lib/volume/pvr/Pvr.Tests.cs +++ b/lib/volume/pvr/Pvr.Tests.cs @@ -215,7 +215,7 @@ public class PvrTests } // Batch - var batchResult = Pvr.Calculate(bars); + var batchResult = Pvr.Batch(bars); Assert.Equal(bars.Count, batchResult.Count); for (int i = 0; i < bars.Count; i++) @@ -248,7 +248,7 @@ public class PvrTests var volume = bars.Volume.Values.ToArray(); var spanOutput = new double[bars.Count]; - Pvr.Calculate(price, volume, spanOutput); + Pvr.Batch(price, volume, spanOutput); for (int i = 0; i < bars.Count; i++) { @@ -263,7 +263,7 @@ public class PvrTests var volume = new double[100]; var output = new double[99]; // Different length - Assert.Throws(() => Pvr.Calculate(price, volume, output)); + Assert.Throws(() => Pvr.Batch(price, volume, output)); } [Fact] @@ -274,7 +274,7 @@ public class PvrTests var output = Array.Empty(); // Should not throw - Pvr.Calculate(price, volume, output); + Pvr.Batch(price, volume, output); Assert.Empty(output); } diff --git a/lib/volume/pvr/Pvr.Validation.Tests.cs b/lib/volume/pvr/Pvr.Validation.Tests.cs index 142bd72b..e298ec35 100644 --- a/lib/volume/pvr/Pvr.Validation.Tests.cs +++ b/lib/volume/pvr/Pvr.Validation.Tests.cs @@ -49,7 +49,7 @@ public class PvrValidationTests } // Batch - var batchResult = Pvr.Calculate(_data.Bars); + var batchResult = Pvr.Batch(_data.Bars); var batchValues = batchResult.Values.ToArray(); ValidationHelper.VerifyData(streamingValues.ToArray(), batchValues, 0, 100, 1e-9); @@ -71,7 +71,7 @@ public class PvrValidationTests var volume = _data.Bars.Volume.Values.ToArray(); var spanOutput = new double[price.Length]; - Pvr.Calculate(price, volume, spanOutput); + Pvr.Batch(price, volume, spanOutput); ValidationHelper.VerifyData(streamingValues.ToArray(), spanOutput, 0, 100, 1e-9); } @@ -122,14 +122,14 @@ public class PvrValidationTests } // Mode 3: Batch - var mode3Result = Pvr.Calculate(_data.Bars); + var mode3Result = Pvr.Batch(_data.Bars); var mode3Values = mode3Result.Values.ToArray(); // Mode 4: Span var price = _data.Bars.Close.Values.ToArray(); var volume = _data.Bars.Volume.Values.ToArray(); var mode4Values = new double[price.Length]; - Pvr.Calculate(price, volume, mode4Values); + Pvr.Batch(price, volume, mode4Values); // All modes should match ValidationHelper.VerifyData(mode1Values.ToArray(), mode2Values.ToArray(), 0, 100, 1e-9); diff --git a/lib/volume/pvr/Pvr.cs b/lib/volume/pvr/Pvr.cs index aac8a12a..4e916c36 100644 --- a/lib/volume/pvr/Pvr.cs +++ b/lib/volume/pvr/Pvr.cs @@ -172,7 +172,7 @@ public sealed class Pvr : ITValuePublisher /// /// Calculates PVR for a series of bars. /// - public static TSeries Calculate(TBarSeries bars) + public static TSeries Batch(TBarSeries bars) { if (bars.Count == 0) { @@ -182,7 +182,7 @@ public sealed class Pvr : ITValuePublisher var t = bars.Open.Times.ToArray(); var v = new double[bars.Count]; - Calculate(bars.Close.Values, bars.Volume.Values, v); + Batch(bars.Close.Values, bars.Volume.Values, v); return new TSeries(t, v); } @@ -191,7 +191,7 @@ public sealed class Pvr : ITValuePublisher /// Calculates PVR values using span-based processing. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void Calculate(ReadOnlySpan price, ReadOnlySpan volume, Span output) + public static void Batch(ReadOnlySpan price, ReadOnlySpan volume, Span output) { if (price.Length != output.Length) { @@ -247,4 +247,11 @@ public sealed class Pvr : ITValuePublisher prevVolume = currentVolume; } } + + public static (TSeries Results, Pvr Indicator) Calculate(TBarSeries bars) + { + var indicator = new Pvr(); + TSeries results = indicator.Update(bars); + return (results, indicator); + } } \ No newline at end of file diff --git a/lib/volume/pvt/Pvt.Tests.cs b/lib/volume/pvt/Pvt.Tests.cs index 6eaf16a8..036a5025 100644 --- a/lib/volume/pvt/Pvt.Tests.cs +++ b/lib/volume/pvt/Pvt.Tests.cs @@ -265,7 +265,7 @@ public class PvtTests streamingResults[i] = pvtStreaming.Update(bars[i]).Value; } - var batchResult = Pvt.Calculate(bars); + var batchResult = Pvt.Batch(bars); // Compare last 45 values (after warmup) for (int i = 5; i < bars.Count; i++) @@ -299,7 +299,7 @@ public class PvtTests } var spanResult = new double[bars.Count]; - Pvt.Calculate(close, volume, spanResult); + Pvt.Batch(close, volume, spanResult); // Compare values after first bar for (int i = 1; i < bars.Count; i++) @@ -343,7 +343,7 @@ public class PvtTests var volume = new double[8]; // Different length var output = new double[10]; - Assert.Throws(() => Pvt.Calculate(close, volume, output)); + Assert.Throws(() => Pvt.Batch(close, volume, output)); } [Fact] @@ -353,7 +353,7 @@ public class PvtTests var volume = new double[10]; var output = new double[8]; // Wrong length - Assert.Throws(() => Pvt.Calculate(close, volume, output)); + Assert.Throws(() => Pvt.Batch(close, volume, output)); } [Fact] @@ -363,7 +363,7 @@ public class PvtTests var volume = Array.Empty(); var output = Array.Empty(); - Pvt.Calculate(close, volume, output); // Should not throw + Pvt.Batch(close, volume, output); // Should not throw Assert.Empty(output); } @@ -374,7 +374,7 @@ public class PvtTests var volume = new double[] { 1000.0 }; var output = new double[1]; - Pvt.Calculate(close, volume, output); + Pvt.Batch(close, volume, output); Assert.Equal(0.0, output[0]); } diff --git a/lib/volume/pvt/Pvt.Validation.Tests.cs b/lib/volume/pvt/Pvt.Validation.Tests.cs index fda729c1..ac0fd228 100644 --- a/lib/volume/pvt/Pvt.Validation.Tests.cs +++ b/lib/volume/pvt/Pvt.Validation.Tests.cs @@ -59,7 +59,7 @@ public class PvtValidationTests } // Batch - var batchResult = Pvt.Calculate(_data.Bars); + var batchResult = Pvt.Batch(_data.Bars); var batchValues = batchResult.Values.ToArray(); ValidationHelper.VerifyData(streamingValues.ToArray(), batchValues, 0, 100, 1e-9); @@ -81,7 +81,7 @@ public class PvtValidationTests var volume = _data.Bars.Volume.Values.ToArray(); var spanOutput = new double[close.Length]; - Pvt.Calculate(close, volume, spanOutput); + Pvt.Batch(close, volume, spanOutput); ValidationHelper.VerifyData(streamingValues.ToArray(), spanOutput, 0, 100, 1e-9); } diff --git a/lib/volume/pvt/Pvt.cs b/lib/volume/pvt/Pvt.cs index f51f725a..1e9d09d0 100644 --- a/lib/volume/pvt/Pvt.cs +++ b/lib/volume/pvt/Pvt.cs @@ -230,7 +230,7 @@ public sealed class Pvt : ITValuePublisher return new TSeries(t, v); } - public static TSeries Calculate(TBarSeries source) + public static TSeries Batch(TBarSeries source) { if (source.Count == 0) { @@ -240,13 +240,13 @@ public sealed class Pvt : ITValuePublisher var t = source.Open.Times.ToArray(); var v = new double[source.Count]; - Calculate(source.Close.Values, source.Volume.Values, v); + Batch(source.Close.Values, source.Volume.Values, v); return new TSeries(t, v); } [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void Calculate(ReadOnlySpan close, ReadOnlySpan volume, Span output) + public static void Batch(ReadOnlySpan close, ReadOnlySpan volume, Span output) { if (close.Length != volume.Length) { @@ -293,4 +293,11 @@ public sealed class Pvt : ITValuePublisher } } } + + public static (TSeries Results, Pvt Indicator) Calculate(TBarSeries source) + { + var indicator = new Pvt(); + TSeries results = indicator.Update(source); + return (results, indicator); + } } \ No newline at end of file diff --git a/lib/volume/tvi/Tvi.Tests.cs b/lib/volume/tvi/Tvi.Tests.cs index 8f0b1bbc..a20f79ad 100644 --- a/lib/volume/tvi/Tvi.Tests.cs +++ b/lib/volume/tvi/Tvi.Tests.cs @@ -304,7 +304,7 @@ public class TviTests } // Batch - var batchResult = Tvi.Calculate(bars); + var batchResult = Tvi.Batch(bars); Assert.Equal(bars.Count, batchResult.Count); for (int i = 0; i < bars.Count; i++) @@ -337,7 +337,7 @@ public class TviTests var volume = bars.Volume.Values.ToArray(); var output = new double[bars.Count]; - Tvi.Calculate(price, volume, output); + Tvi.Batch(price, volume, output); for (int i = 0; i < bars.Count; i++) { @@ -352,7 +352,7 @@ public class TviTests var volume = new double[99]; // Different length var output = new double[100]; - Assert.Throws(() => Tvi.Calculate(price, volume, output)); + Assert.Throws(() => Tvi.Batch(price, volume, output)); } [Fact] @@ -362,8 +362,8 @@ public class TviTests var volume = new double[100]; var output = new double[100]; - Assert.Throws(() => Tvi.Calculate(price, volume, output, minTick: 0)); - Assert.Throws(() => Tvi.Calculate(price, volume, output, minTick: -1)); + Assert.Throws(() => Tvi.Batch(price, volume, output, minTick: 0)); + Assert.Throws(() => Tvi.Batch(price, volume, output, minTick: -1)); } [Fact] @@ -373,7 +373,7 @@ public class TviTests var volume = Array.Empty(); var output = Array.Empty(); - Tvi.Calculate(price, volume, output); + Tvi.Batch(price, volume, output); Assert.Empty(output); } diff --git a/lib/volume/tvi/Tvi.Validation.Tests.cs b/lib/volume/tvi/Tvi.Validation.Tests.cs index 02c54d59..4e1c0d18 100644 --- a/lib/volume/tvi/Tvi.Validation.Tests.cs +++ b/lib/volume/tvi/Tvi.Validation.Tests.cs @@ -26,7 +26,7 @@ public class TviValidationTests } // Batch - var batchResult = Tvi.Calculate(_data.Bars, minTick); + var batchResult = Tvi.Batch(_data.Bars, minTick); var batchValues = batchResult.Values.ToArray(); ValidationHelper.VerifyData(streamingValues.ToArray(), batchValues, 0, 100, 1e-9); @@ -50,7 +50,7 @@ public class TviValidationTests var volume = _data.Bars.Volume.Values.ToArray(); var spanOutput = new double[close.Length]; - Tvi.Calculate(close, volume, spanOutput, minTick); + Tvi.Batch(close, volume, spanOutput, minTick); ValidationHelper.VerifyData(streamingValues.ToArray(), spanOutput, 0, 100, 1e-9); } @@ -139,14 +139,14 @@ public class TviValidationTests } // Batch - var batchResult = Tvi.Calculate(_data.Bars, minTick); + var batchResult = Tvi.Batch(_data.Bars, minTick); var batchValues = batchResult.Values.ToArray(); // Span var close = _data.Bars.Close.Values.ToArray(); var volume = _data.Bars.Volume.Values.ToArray(); var spanOutput = new double[close.Length]; - Tvi.Calculate(close, volume, spanOutput, minTick); + Tvi.Batch(close, volume, spanOutput, minTick); // Verify all modes match ValidationHelper.VerifyData(streamingValues.ToArray(), batchValues, 0, 100, 1e-9); diff --git a/lib/volume/tvi/Tvi.cs b/lib/volume/tvi/Tvi.cs index b5d3567c..31d46f52 100644 --- a/lib/volume/tvi/Tvi.cs +++ b/lib/volume/tvi/Tvi.cs @@ -207,7 +207,7 @@ public sealed class Tvi : ITValuePublisher return new TSeries(t, v); } - public static TSeries Calculate(TBarSeries source, double minTick = 0.125) + public static TSeries Batch(TBarSeries source, double minTick = 0.125) { if (source.Count == 0) { @@ -217,13 +217,13 @@ public sealed class Tvi : ITValuePublisher var t = source.Open.Times.ToArray(); var v = new double[source.Count]; - Calculate(source.Close.Values, source.Volume.Values, v, minTick); + Batch(source.Close.Values, source.Volume.Values, v, minTick); return new TSeries(t, v); } [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void Calculate(ReadOnlySpan price, ReadOnlySpan volume, Span output, double minTick = 0.125) + public static void Batch(ReadOnlySpan price, ReadOnlySpan volume, Span output, double minTick = 0.125) { if (price.Length != volume.Length) { @@ -304,4 +304,11 @@ public sealed class Tvi : ITValuePublisher } } } + + public static (TSeries Results, Tvi Indicator) Calculate(TBarSeries source, double minTick = 0.125) + { + var indicator = new Tvi(minTick); + TSeries results = indicator.Update(source); + return (results, indicator); + } } \ No newline at end of file diff --git a/lib/volume/twap/Twap.Tests.cs b/lib/volume/twap/Twap.Tests.cs index 7360db72..825f93a7 100644 --- a/lib/volume/twap/Twap.Tests.cs +++ b/lib/volume/twap/Twap.Tests.cs @@ -241,7 +241,7 @@ public class TwapTests } // Batch - var batchResult = Twap.Calculate(bars, period: 10); + var batchResult = Twap.Batch(bars, period: 10); Assert.Equal(bars.Count, batchResult.Count); for (int i = 0; i < bars.Count; i++) @@ -272,7 +272,7 @@ public class TwapTests // Span var output = new double[prices.Length]; - Twap.Calculate(prices, output, period: 10); + Twap.Batch(prices, output, period: 10); for (int i = 0; i < prices.Length; i++) { @@ -286,7 +286,7 @@ public class TwapTests var price = new double[100]; var output = new double[99]; // Different length - Assert.Throws(() => Twap.Calculate(price, output)); + Assert.Throws(() => Twap.Batch(price, output)); } [Fact] @@ -295,7 +295,7 @@ public class TwapTests var price = new double[100]; var output = new double[100]; - Assert.Throws(() => Twap.Calculate(price, output, period: -1)); + Assert.Throws(() => Twap.Batch(price, output, period: -1)); } [Fact] @@ -304,7 +304,7 @@ public class TwapTests var price = Array.Empty(); var output = Array.Empty(); - Twap.Calculate(price, output); + Twap.Batch(price, output); Assert.Empty(output); } diff --git a/lib/volume/twap/Twap.Validation.Tests.cs b/lib/volume/twap/Twap.Validation.Tests.cs index a59154cf..838bcd95 100644 --- a/lib/volume/twap/Twap.Validation.Tests.cs +++ b/lib/volume/twap/Twap.Validation.Tests.cs @@ -26,7 +26,7 @@ public class TwapValidationTests } // Batch - var batchResult = Twap.Calculate(_data.Bars, period); + var batchResult = Twap.Batch(_data.Bars, period); var batchValues = batchResult.Values.ToArray(); ValidationHelper.VerifyData(streamingValues.ToArray(), batchValues, 0, 100, 1e-9); @@ -55,7 +55,7 @@ public class TwapValidationTests // Span var spanOutput = new double[typicalPrices.Length]; - Twap.Calculate(typicalPrices, spanOutput, period); + Twap.Batch(typicalPrices, spanOutput, period); ValidationHelper.VerifyData(streamingValues.ToArray(), spanOutput, 0, 100, 1e-9); } @@ -138,12 +138,12 @@ public class TwapValidationTests } // Batch - var batchResult = Twap.Calculate(_data.Bars, period); + var batchResult = Twap.Batch(_data.Bars, period); var batchValues = batchResult.Values.ToArray(); // Span var spanOutput = new double[typicalPrices.Length]; - Twap.Calculate(typicalPrices, spanOutput, period); + Twap.Batch(typicalPrices, spanOutput, period); // Verify all modes match ValidationHelper.VerifyData(streamingValues.ToArray(), batchValues, 0, 100, 1e-9); diff --git a/lib/volume/twap/Twap.cs b/lib/volume/twap/Twap.cs index 9f16d25b..547db6d1 100644 --- a/lib/volume/twap/Twap.cs +++ b/lib/volume/twap/Twap.cs @@ -197,7 +197,7 @@ public sealed class Twap : ITValuePublisher } var output = new double[source.Count]; - Calculate(prices, output, _period); + Batch(prices, output, _period); for (int i = 0; i < source.Count; i++) { @@ -224,7 +224,7 @@ public sealed class Twap : ITValuePublisher /// The bar series. /// The session period in bars (0 = never reset). /// The result series. - public static TSeries Calculate(TBarSeries source, int period = DefaultPeriod) + public static TSeries Batch(TBarSeries source, int period = DefaultPeriod) { var twap = new Twap(period); var result = new TSeries(source.Count); @@ -244,7 +244,7 @@ public sealed class Twap : ITValuePublisher /// The output TWAP span. /// The session period in bars (0 = never reset). Default is 0. /// Thrown when output length doesn't match price length or period is invalid. - public static void Calculate(ReadOnlySpan price, Span output, int period = DefaultPeriod) + public static void Batch(ReadOnlySpan price, Span output, int period = DefaultPeriod) { if (output.Length != price.Length) { @@ -288,4 +288,11 @@ public sealed class Twap : ITValuePublisher output[i] = sumPrices / count; } } + + public static (TSeries Results, Twap Indicator) Calculate(TBarSeries source, int period = DefaultPeriod) + { + var indicator = new Twap(period); + TSeries results = indicator.Update(source); + return (results, indicator); + } } \ No newline at end of file diff --git a/lib/volume/va/Va.Tests.cs b/lib/volume/va/Va.Tests.cs index da5a9c3d..f087626c 100644 --- a/lib/volume/va/Va.Tests.cs +++ b/lib/volume/va/Va.Tests.cs @@ -204,7 +204,7 @@ public class VaTests } // Batch - var batchResult = Va.Calculate(bars); + var batchResult = Va.Batch(bars); Assert.Equal(bars.Count, batchResult.Count); for (int i = 0; i < bars.Count; i++) @@ -243,7 +243,7 @@ public class VaTests // Span var output = new double[count]; - Va.Calculate(high, low, close, volume, output); + Va.Batch(high, low, close, volume, output); for (int i = 0; i < count; i++) { @@ -260,7 +260,7 @@ public class VaTests var volume = new double[99]; // Different length var output = new double[100]; - Assert.Throws(() => Va.Calculate(high, low, close, volume, output)); + Assert.Throws(() => Va.Batch(high, low, close, volume, output)); } [Fact] @@ -272,7 +272,7 @@ public class VaTests var volume = Array.Empty(); var output = Array.Empty(); - Va.Calculate(high, low, close, volume, output); + Va.Batch(high, low, close, volume, output); Assert.Empty(output); } diff --git a/lib/volume/va/Va.cs b/lib/volume/va/Va.cs index 9dcc562e..ea6db25c 100644 --- a/lib/volume/va/Va.cs +++ b/lib/volume/va/Va.cs @@ -184,7 +184,7 @@ public sealed class Va : ITValuePublisher /// /// The bar series. /// The result series. - public static TSeries Calculate(TBarSeries source) + public static TSeries Batch(TBarSeries source) { if (source.Count == 0) { @@ -194,7 +194,7 @@ public sealed class Va : ITValuePublisher var t = source.Open.Times.ToArray(); var v = new double[source.Count]; - Calculate(source.High.Values, source.Low.Values, source.Close.Values, source.Volume.Values, v); + Batch(source.High.Values, source.Low.Values, source.Close.Values, source.Volume.Values, v); return new TSeries(t, v); } @@ -209,7 +209,7 @@ public sealed class Va : ITValuePublisher /// The output VA span. /// Thrown when span lengths don't match. [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void Calculate(ReadOnlySpan high, ReadOnlySpan low, ReadOnlySpan close, ReadOnlySpan volume, Span output) + public static void Batch(ReadOnlySpan high, ReadOnlySpan low, ReadOnlySpan close, ReadOnlySpan volume, Span output) { if (high.Length != low.Length || high.Length != close.Length || high.Length != volume.Length) { @@ -266,4 +266,11 @@ public sealed class Va : ITValuePublisher output[i] = va; } } + + public static (TSeries Results, Va Indicator) Calculate(TBarSeries source) + { + var indicator = new Va(); + TSeries results = indicator.Update(source); + return (results, indicator); + } } \ No newline at end of file diff --git a/lib/volume/vf/Vf.Tests.cs b/lib/volume/vf/Vf.Tests.cs index cc009069..83d7c706 100644 --- a/lib/volume/vf/Vf.Tests.cs +++ b/lib/volume/vf/Vf.Tests.cs @@ -407,7 +407,7 @@ public class VfTests { var series = GenerateTestBarSeries(100); - var result = Vf.Calculate(series, DefaultPeriod); + var result = Vf.Batch(series, DefaultPeriod); Assert.Equal(100, result.Count); } @@ -417,7 +417,7 @@ public class VfTests { var series = new TBarSeries(); - var result = Vf.Calculate(series, DefaultPeriod); + var result = Vf.Batch(series, DefaultPeriod); Assert.Empty(result); } @@ -442,7 +442,7 @@ public class VfTests } // Span calculation - Vf.Calculate(close, volume, output, DefaultPeriod); + Vf.Batch(close, volume, output, DefaultPeriod); // Streaming calculation var vf = new Vf(DefaultPeriod); @@ -462,7 +462,7 @@ public class VfTests var volume = new double[50]; // Different length var output = new double[100]; - var ex = Assert.Throws(() => Vf.Calculate(close, volume, output, DefaultPeriod)); + var ex = Assert.Throws(() => Vf.Batch(close, volume, output, DefaultPeriod)); Assert.Equal("volume", ex.ParamName); } @@ -473,7 +473,7 @@ public class VfTests var volume = new double[100]; var output = new double[50]; // Different length - var ex = Assert.Throws(() => Vf.Calculate(close, volume, output, DefaultPeriod)); + var ex = Assert.Throws(() => Vf.Batch(close, volume, output, DefaultPeriod)); Assert.Equal("output", ex.ParamName); } @@ -484,7 +484,7 @@ public class VfTests var volume = new double[100]; var output = new double[100]; - var ex = Assert.Throws(() => Vf.Calculate(close, volume, output, period: 0)); + var ex = Assert.Throws(() => Vf.Batch(close, volume, output, period: 0)); Assert.Equal("period", ex.ParamName); } @@ -496,7 +496,7 @@ public class VfTests var output = Array.Empty(); // Should not throw - Vf.Calculate(close, volume, output, DefaultPeriod); + Vf.Batch(close, volume, output, DefaultPeriod); Assert.True(true); // Test passes if no exception } @@ -507,7 +507,7 @@ public class VfTests var volume = new double[] { 1000, 2000, 1500, 1800, 2200 }; var output = new double[5]; - Vf.Calculate(close, volume, output, period: 3); + Vf.Batch(close, volume, output, period: 3); Assert.Equal(0, output[0]); } @@ -554,11 +554,11 @@ public class VfTests var streamingResult = vf.Update(series); // Batch mode - var batchResult = Vf.Calculate(series, DefaultPeriod); + var batchResult = Vf.Batch(series, DefaultPeriod); // Span mode var spanOutput = new double[100]; - Vf.Calculate(close, volume, spanOutput, DefaultPeriod); + Vf.Batch(close, volume, spanOutput, DefaultPeriod); // Compare all modes (last 50 values to avoid warmup differences) for (int i = 50; i < 100; i++) diff --git a/lib/volume/vf/Vf.cs b/lib/volume/vf/Vf.cs index c1bedef6..8471a306 100644 --- a/lib/volume/vf/Vf.cs +++ b/lib/volume/vf/Vf.cs @@ -202,7 +202,7 @@ public sealed class Vf : ITValuePublisher /// The bar series. /// The smoothing period (default: 14). /// The result series. - public static TSeries Calculate(TBarSeries source, int period = 14) + public static TSeries Batch(TBarSeries source, int period = 14) { if (source.Count == 0) { @@ -212,7 +212,7 @@ public sealed class Vf : ITValuePublisher var t = source.Open.Times.ToArray(); var v = new double[source.Count]; - Calculate(source.Close.Values, source.Volume.Values, v, period); + Batch(source.Close.Values, source.Volume.Values, v, period); return new TSeries(t, v); } @@ -226,7 +226,7 @@ public sealed class Vf : ITValuePublisher /// The smoothing period (default: 14). /// Thrown when span lengths don't match or period is invalid. [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void Calculate(ReadOnlySpan close, ReadOnlySpan volume, Span output, int period = 14) + public static void Batch(ReadOnlySpan close, ReadOnlySpan volume, Span output, int period = 14) { if (period < 1) { @@ -307,4 +307,11 @@ public sealed class Vf : ITValuePublisher prevClose = c; } } + + public static (TSeries Results, Vf Indicator) Calculate(TBarSeries source, int period = 14) + { + var indicator = new Vf(period); + TSeries results = indicator.Update(source); + return (results, indicator); + } } \ No newline at end of file diff --git a/lib/volume/vo/Vo.Tests.cs b/lib/volume/vo/Vo.Tests.cs index fb710dce..3ba385a9 100644 --- a/lib/volume/vo/Vo.Tests.cs +++ b/lib/volume/vo/Vo.Tests.cs @@ -361,7 +361,7 @@ public class VoTests } // Batch - var batchResult = Vo.Calculate(_bars, shortPeriod: 5, longPeriod: 10, signalPeriod: 10); + var batchResult = Vo.Batch(_bars, shortPeriod: 5, longPeriod: 10, signalPeriod: 10); Assert.Equal(streamingResults.Count, batchResult.Count); for (int i = 0; i < streamingResults.Count; i++) @@ -386,7 +386,7 @@ public class VoTests // Span - pass arrays directly (implicit span conversion) var volume = _bars.Volume.Values.ToArray(); var output = new double[_bars.Count]; - Vo.Calculate(volume, output, shortPeriod: 5, longPeriod: 10); + Vo.Batch(volume, output, shortPeriod: 5, longPeriod: 10); for (int i = 0; i < streamingResults.Count; i++) { @@ -425,7 +425,7 @@ public class VoTests ArgumentException? caught = null; try { - Vo.Calculate(volume, output, shortPeriod: 5, longPeriod: 10); + Vo.Batch(volume, output, shortPeriod: 5, longPeriod: 10); } catch (ArgumentException ex) { @@ -445,7 +445,7 @@ public class VoTests ArgumentException? caught = null; try { - Vo.Calculate(volume, output, shortPeriod: 0, longPeriod: 10); + Vo.Batch(volume, output, shortPeriod: 0, longPeriod: 10); } catch (ArgumentException ex) { @@ -465,7 +465,7 @@ public class VoTests ArgumentException? caught = null; try { - Vo.Calculate(volume, output, shortPeriod: 5, longPeriod: 0); + Vo.Batch(volume, output, shortPeriod: 5, longPeriod: 0); } catch (ArgumentException ex) { @@ -485,7 +485,7 @@ public class VoTests ArgumentException? caught = null; try { - Vo.Calculate(volume, output, shortPeriod: 10, longPeriod: 5); + Vo.Batch(volume, output, shortPeriod: 10, longPeriod: 5); } catch (ArgumentException ex) { @@ -503,7 +503,7 @@ public class VoTests double[] outputArr = []; // Should not throw - Vo.Calculate(volumeArr, outputArr, shortPeriod: 5, longPeriod: 10); + Vo.Batch(volumeArr, outputArr, shortPeriod: 5, longPeriod: 10); Assert.Empty(outputArr); } @@ -519,7 +519,7 @@ public class VoTests volume[i] = i == 10 ? double.NaN : 500 + i; } - Vo.Calculate(volume, output, shortPeriod: 5, longPeriod: 10); + Vo.Batch(volume, output, shortPeriod: 5, longPeriod: 10); foreach (var val in output) { @@ -539,7 +539,7 @@ public class VoTests } // Should not throw stack overflow - Vo.Calculate(volume, output, shortPeriod: 50, longPeriod: 200); + Vo.Batch(volume, output, shortPeriod: 50, longPeriod: 200); Assert.True(double.IsFinite(output[^1])); } diff --git a/lib/volume/vo/Vo.cs b/lib/volume/vo/Vo.cs index e3e4ad72..d1e07309 100644 --- a/lib/volume/vo/Vo.cs +++ b/lib/volume/vo/Vo.cs @@ -279,7 +279,7 @@ public sealed class Vo : ITValuePublisher /// The long-term period (default: 10). /// The signal line period (default: 10). /// The result series. - public static TSeries Calculate(TBarSeries source, int shortPeriod = 5, int longPeriod = 10, int signalPeriod = 10) + public static TSeries Batch(TBarSeries source, int shortPeriod = 5, int longPeriod = 10, int signalPeriod = 10) { if (source.Count == 0) { @@ -289,7 +289,7 @@ public sealed class Vo : ITValuePublisher var t = source.Open.Times.ToArray(); var v = new double[source.Count]; - Calculate(source.Volume.Values, v, shortPeriod, longPeriod); + Batch(source.Volume.Values, v, shortPeriod, longPeriod); return new TSeries(t, v); } @@ -305,7 +305,7 @@ public sealed class Vo : ITValuePublisher /// The long-term period (default: 10). /// Thrown when parameters are invalid. [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void Calculate(ReadOnlySpan volume, Span output, int shortPeriod = 5, int longPeriod = 10) + public static void Batch(ReadOnlySpan volume, Span output, int shortPeriod = 5, int longPeriod = 10) { if (shortPeriod < 1) { @@ -421,4 +421,11 @@ public sealed class Vo : ITValuePublisher } } } + + public static (TSeries Results, Vo Indicator) Calculate(TBarSeries source, int shortPeriod = 5, int longPeriod = 10, int signalPeriod = 10) + { + var indicator = new Vo(shortPeriod, longPeriod, signalPeriod); + TSeries results = indicator.Update(source); + return (results, indicator); + } } \ No newline at end of file diff --git a/lib/volume/vroc/Vroc.Tests.cs b/lib/volume/vroc/Vroc.Tests.cs index 3b458cf8..b4412b41 100644 --- a/lib/volume/vroc/Vroc.Tests.cs +++ b/lib/volume/vroc/Vroc.Tests.cs @@ -440,7 +440,7 @@ public class VrocTests } // Batch - var batchResult = Vroc.Calculate(_bars, period: 12, usePercent: true); + var batchResult = Vroc.Batch(_bars, period: 12, usePercent: true); Assert.Equal(streamingResults.Count, batchResult.Count); for (int i = 0; i < streamingResults.Count; i++) @@ -465,7 +465,7 @@ public class VrocTests // Span - pass arrays directly (implicit span conversion) var volume = _bars.Volume.Values.ToArray(); var output = new double[_bars.Count]; - Vroc.Calculate(volume, output, period: 12, usePercent: true); + Vroc.Batch(volume, output, period: 12, usePercent: true); for (int i = 0; i < streamingResults.Count; i++) { @@ -507,7 +507,7 @@ public class VrocTests // Span - pass arrays directly (implicit span conversion) var volume = _bars.Volume.Values.ToArray(); var output = new double[_bars.Count]; - Vroc.Calculate(volume, output, period: 12, usePercent: false); + Vroc.Batch(volume, output, period: 12, usePercent: false); for (int i = 0; i < streamingResults.Count; i++) { @@ -528,7 +528,7 @@ public class VrocTests ArgumentException? caught = null; try { - Vroc.Calculate(volume, output, period: 12, usePercent: true); + Vroc.Batch(volume, output, period: 12, usePercent: true); } catch (ArgumentException ex) { @@ -548,7 +548,7 @@ public class VrocTests ArgumentException? caught = null; try { - Vroc.Calculate(volume, output, period: 0, usePercent: true); + Vroc.Batch(volume, output, period: 0, usePercent: true); } catch (ArgumentException ex) { @@ -566,7 +566,7 @@ public class VrocTests double[] outputArr = []; // Should not throw - Vroc.Calculate(volumeArr, outputArr, period: 12, usePercent: true); + Vroc.Batch(volumeArr, outputArr, period: 12, usePercent: true); Assert.Empty(outputArr); } @@ -582,7 +582,7 @@ public class VrocTests volume[i] = i == 10 ? double.NaN : 500 + i; } - Vroc.Calculate(volume, output, period: 5, usePercent: true); + Vroc.Batch(volume, output, period: 5, usePercent: true); foreach (var val in output) { @@ -602,7 +602,7 @@ public class VrocTests } // Should not throw stack overflow - Vroc.Calculate(volume, output, period: 100, usePercent: true); + Vroc.Batch(volume, output, period: 100, usePercent: true); Assert.True(double.IsFinite(output[^1])); } diff --git a/lib/volume/vroc/Vroc.cs b/lib/volume/vroc/Vroc.cs index 5cc8b314..969f92ce 100644 --- a/lib/volume/vroc/Vroc.cs +++ b/lib/volume/vroc/Vroc.cs @@ -218,7 +218,7 @@ public sealed class Vroc : ITValuePublisher /// The lookback period (default: 12). /// True for percentage mode, false for point change (default: true). /// The result series. - public static TSeries Calculate(TBarSeries source, int period = 12, bool usePercent = true) + public static TSeries Batch(TBarSeries source, int period = 12, bool usePercent = true) { if (source.Count == 0) { @@ -228,7 +228,7 @@ public sealed class Vroc : ITValuePublisher var t = source.Open.Times.ToArray(); var v = new double[source.Count]; - Calculate(source.Volume.Values, v, period, usePercent); + Batch(source.Volume.Values, v, period, usePercent); return new TSeries(t, v); } @@ -242,7 +242,7 @@ public sealed class Vroc : ITValuePublisher /// True for percentage mode, false for point change (default: true). /// Thrown when parameters are invalid. [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void Calculate(ReadOnlySpan volume, Span output, int period = 12, bool usePercent = true) + public static void Batch(ReadOnlySpan volume, Span output, int period = 12, bool usePercent = true) { if (period < 1) { @@ -295,4 +295,11 @@ public sealed class Vroc : ITValuePublisher } } } + + public static (TSeries Results, Vroc Indicator) Calculate(TBarSeries source, int period = 12, bool usePercent = true) + { + var indicator = new Vroc(period, usePercent); + TSeries results = indicator.Update(source); + return (results, indicator); + } } \ No newline at end of file diff --git a/lib/volume/vwad/Vwad.Tests.cs b/lib/volume/vwad/Vwad.Tests.cs index 4079d126..d05b1559 100644 --- a/lib/volume/vwad/Vwad.Tests.cs +++ b/lib/volume/vwad/Vwad.Tests.cs @@ -228,7 +228,7 @@ public class VwadTests bars.Add(new TBar(time.AddMinutes(1), 10, 12, 8, 12, 200)); bars.Add(new TBar(time.AddMinutes(2), 12, 12, 8, 8, 100)); - var result = Vwad.Calculate(bars, 3); + var result = Vwad.Batch(bars, 3); Assert.Equal(3, result.Count); } @@ -242,7 +242,7 @@ public class VwadTests double[] volume = [100, 200, 100]; double[] output = new double[3]; - Vwad.Calculate(high, low, close, volume, output, 3); + Vwad.Batch(high, low, close, volume, output, 3); // Bar 0: MFM=0, Vol=100, SumVol=100, VolWeight=1, WeightedMFV=0 Assert.Equal(0, output[0]); @@ -268,7 +268,7 @@ public class VwadTests double[] output = new double[2]; Assert.Throws(() => - Vwad.Calculate(high, low, close, volume, output, 3)); + Vwad.Batch(high, low, close, volume, output, 3)); } [Fact] @@ -281,14 +281,14 @@ public class VwadTests double[] output = new double[1]; Assert.Throws(() => - Vwad.Calculate(high, low, close, volume, output, 0)); + Vwad.Batch(high, low, close, volume, output, 0)); } [Fact] public void Vwad_Calculate_EmptySeries_ReturnsEmpty() { var bars = new TBarSeries(); - var result = Vwad.Calculate(bars); + var result = Vwad.Batch(bars); Assert.Empty(result); } @@ -312,7 +312,7 @@ public class VwadTests } // Batch - var batchResult = Vwad.Calculate(bars, 20); + var batchResult = Vwad.Batch(bars, 20); // Compare all values for (int i = 0; i < 100; i++) @@ -366,7 +366,7 @@ public class VwadTests double[] volume = [100, 200, 100, double.PositiveInfinity, 100]; double[] output = new double[5]; - Vwad.Calculate(high, low, close, volume, output, 3); + Vwad.Batch(high, low, close, volume, output, 3); // All outputs should be finite foreach (var val in output) @@ -386,7 +386,7 @@ public class VwadTests bars.Add(gbm.Next()); } - var result = Vwad.Calculate(bars, 10); + var result = Vwad.Batch(bars, 10); // In a bullish trend, VWAD should generally be positive and growing // (this is a statistical expectation, not a guarantee) @@ -406,12 +406,12 @@ public class VwadTests var bars = gbm.Fetch(100, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1)); // 1. Batch Mode - var batchSeries = Vwad.Calculate(bars, period); + var batchSeries = Vwad.Batch(bars, period); double expected = batchSeries.Last.Value; // 2. Span Mode var spanOutput = new double[bars.Count]; - Vwad.Calculate(bars.High.Values, bars.Low.Values, bars.Close.Values, bars.Volume.Values, spanOutput, period); + Vwad.Batch(bars.High.Values, bars.Low.Values, bars.Close.Values, bars.Volume.Values, spanOutput, period); double spanResult = spanOutput[^1]; // 3. Streaming Mode diff --git a/lib/volume/vwad/Vwad.Validation.Tests.cs b/lib/volume/vwad/Vwad.Validation.Tests.cs index 992f4b73..59e2f13e 100644 --- a/lib/volume/vwad/Vwad.Validation.Tests.cs +++ b/lib/volume/vwad/Vwad.Validation.Tests.cs @@ -50,7 +50,7 @@ public class VwadValidationTests } // Batch - var batchResult = Vwad.Calculate(_data.Bars, DefaultPeriod); + var batchResult = Vwad.Batch(_data.Bars, DefaultPeriod); var batchValues = batchResult.Values.ToArray(); // Cumulative indicators accumulate floating-point errors over many bars @@ -76,7 +76,7 @@ public class VwadValidationTests var volume = _data.Bars.Volume.Values.ToArray(); var spanValues = new double[high.Length]; - Vwad.Calculate(high, low, close, volume, spanValues, DefaultPeriod); + Vwad.Batch(high, low, close, volume, spanValues, DefaultPeriod); // Cumulative indicators accumulate floating-point errors over many bars // 1e-10 tolerance is appropriate for ~5000 bar cumulative calculations @@ -87,7 +87,7 @@ public class VwadValidationTests public void Vwad_Batch_Matches_Span() { // Batch - var batchResult = Vwad.Calculate(_data.Bars, DefaultPeriod); + var batchResult = Vwad.Batch(_data.Bars, DefaultPeriod); var batchValues = batchResult.Values.ToArray(); // Span @@ -97,7 +97,7 @@ public class VwadValidationTests var volume = _data.Bars.Volume.Values.ToArray(); var spanValues = new double[high.Length]; - Vwad.Calculate(high, low, close, volume, spanValues, DefaultPeriod); + Vwad.Batch(high, low, close, volume, spanValues, DefaultPeriod); // Batch and Span use identical code path, should match exactly ValidationHelper.VerifyData(batchValues, spanValues, 0, 100, 1e-12); diff --git a/lib/volume/vwad/Vwad.cs b/lib/volume/vwad/Vwad.cs index 1c436629..a5aa4de1 100644 --- a/lib/volume/vwad/Vwad.cs +++ b/lib/volume/vwad/Vwad.cs @@ -229,7 +229,7 @@ public sealed class Vwad : ITValuePublisher /// Source bar series /// Lookback period for volume weighting /// TSeries containing VWAD values - public static TSeries Calculate(TBarSeries source, int period = 20) + public static TSeries Batch(TBarSeries source, int period = 20) { if (source.Count == 0) { @@ -239,7 +239,7 @@ public sealed class Vwad : ITValuePublisher var t = source.Open.Times.ToArray(); var v = new double[source.Count]; - Calculate(source.High.Values, source.Low.Values, source.Close.Values, source.Volume.Values, v, period); + Batch(source.High.Values, source.Low.Values, source.Close.Values, source.Volume.Values, v, period); return new TSeries(t, v); } @@ -254,7 +254,7 @@ public sealed class Vwad : ITValuePublisher /// Output span for VWAD values /// Lookback period for volume weighting [MethodImpl(MethodImplOptions.AggressiveOptimization)] - public static void Calculate(ReadOnlySpan high, ReadOnlySpan low, ReadOnlySpan close, ReadOnlySpan volume, Span output, int period = 20) + public static void Batch(ReadOnlySpan high, ReadOnlySpan low, ReadOnlySpan close, ReadOnlySpan volume, Span output, int period = 20) { if (high.Length != low.Length) { @@ -378,4 +378,11 @@ public sealed class Vwad : ITValuePublisher output[i] = cumulativeVwad; } } + + public static (TSeries Results, Vwad Indicator) Calculate(TBarSeries source, int period = 20) + { + var indicator = new Vwad(period); + TSeries results = indicator.Update(source); + return (results, indicator); + } } \ No newline at end of file diff --git a/lib/volume/vwap/Vwap.Tests.cs b/lib/volume/vwap/Vwap.Tests.cs index 68c535bb..24dccd02 100644 --- a/lib/volume/vwap/Vwap.Tests.cs +++ b/lib/volume/vwap/Vwap.Tests.cs @@ -316,7 +316,7 @@ public class VwapTests [Fact] public void Calculate_Static_ShouldReturnTSeries() { - var result = Vwap.Calculate(_bars); + var result = Vwap.Batch(_bars); Assert.NotNull(result); Assert.Equal(_bars.Count, result.Count); @@ -325,7 +325,7 @@ public class VwapTests [Fact] public void Calculate_Static_WithPeriod_ShouldWork() { - var result = Vwap.Calculate(_bars, 100); + var result = Vwap.Batch(_bars, 100); Assert.NotNull(result); Assert.Equal(_bars.Count, result.Count); @@ -336,7 +336,7 @@ public class VwapTests [Fact] public void Calculate_Span_ShouldMatchBatch() { - var batchResult = Vwap.Calculate(_bars); + var batchResult = Vwap.Batch(_bars); var high = _bars.High.Values.ToArray(); var low = _bars.Low.Values.ToArray(); @@ -344,7 +344,7 @@ public class VwapTests var volume = _bars.Volume.Values.ToArray(); var spanOutput = new double[_bars.Count]; - Vwap.Calculate(high, low, close, volume, spanOutput); + Vwap.Batch(high, low, close, volume, spanOutput); for (int i = 0; i < _bars.Count; i++) { @@ -361,7 +361,7 @@ public class VwapTests var volume = new double[100]; var output = new double[100]; - Assert.Throws(() => Vwap.Calculate(high, low, close, volume, output)); + Assert.Throws(() => Vwap.Batch(high, low, close, volume, output)); } [Fact] @@ -373,7 +373,7 @@ public class VwapTests var volume = new double[100]; var output = new double[50]; // Mismatched - Assert.Throws(() => Vwap.Calculate(high, low, close, volume, output)); + Assert.Throws(() => Vwap.Batch(high, low, close, volume, output)); } [Fact] @@ -385,7 +385,7 @@ public class VwapTests var volume = new double[100]; var output = new double[100]; - Assert.Throws(() => Vwap.Calculate(high, low, close, volume, output, -1)); + Assert.Throws(() => Vwap.Batch(high, low, close, volume, output, -1)); } // ============ Event Tests ============ @@ -418,7 +418,7 @@ public class VwapTests } // Batch - var batchResult = Vwap.Calculate(_bars); + var batchResult = Vwap.Batch(_bars); // Compare last 100 values for (int i = _bars.Count - 100; i < _bars.Count; i++) diff --git a/lib/volume/vwap/Vwap.Validation.Tests.cs b/lib/volume/vwap/Vwap.Validation.Tests.cs index 9711671f..27170b11 100644 --- a/lib/volume/vwap/Vwap.Validation.Tests.cs +++ b/lib/volume/vwap/Vwap.Validation.Tests.cs @@ -50,7 +50,7 @@ public class VwapValidationTests } // Batch - var batchResult = Vwap.Calculate(_data.Bars); + var batchResult = Vwap.Batch(_data.Bars); var batchValues = batchResult.Values.ToArray(); // Cumulative indicators accumulate floating-point errors over many bars @@ -75,7 +75,7 @@ public class VwapValidationTests var volume = _data.Bars.Volume.Values.ToArray(); var spanValues = new double[high.Length]; - Vwap.Calculate(high, low, close, volume, spanValues); + Vwap.Batch(high, low, close, volume, spanValues); // Cumulative indicators accumulate floating-point errors over many bars ValidationHelper.VerifyData(streamingValues.ToArray(), spanValues, 0, 100, 1e-10); @@ -85,7 +85,7 @@ public class VwapValidationTests public void Vwap_Batch_Matches_Span() { // Batch - var batchResult = Vwap.Calculate(_data.Bars); + var batchResult = Vwap.Batch(_data.Bars); var batchValues = batchResult.Values.ToArray(); // Span @@ -95,7 +95,7 @@ public class VwapValidationTests var volume = _data.Bars.Volume.Values.ToArray(); var spanValues = new double[high.Length]; - Vwap.Calculate(high, low, close, volume, spanValues); + Vwap.Batch(high, low, close, volume, spanValues); // Batch and Span use identical code path, should match exactly ValidationHelper.VerifyData(batchValues, spanValues, 0, 100, 1e-12); diff --git a/lib/volume/vwap/Vwap.cs b/lib/volume/vwap/Vwap.cs index 1b9e93cc..53deee62 100644 --- a/lib/volume/vwap/Vwap.cs +++ b/lib/volume/vwap/Vwap.cs @@ -220,7 +220,7 @@ public sealed class Vwap : ITValuePublisher /// Source bar series /// Period for VWAP reset (0 = no reset) /// TSeries containing VWAP values - public static TSeries Calculate(TBarSeries source, int period = 0) + public static TSeries Batch(TBarSeries source, int period = 0) { if (source.Count == 0) { @@ -230,7 +230,7 @@ public sealed class Vwap : ITValuePublisher var t = source.Open.Times.ToArray(); var v = new double[source.Count]; - Calculate(source.High.Values, source.Low.Values, source.Close.Values, source.Volume.Values, v, period); + Batch(source.High.Values, source.Low.Values, source.Close.Values, source.Volume.Values, v, period); return new TSeries(t, v); } @@ -245,7 +245,7 @@ public sealed class Vwap : ITValuePublisher /// Output span for VWAP values /// Period for VWAP reset (0 = no reset) [MethodImpl(MethodImplOptions.AggressiveOptimization)] - public static void Calculate(ReadOnlySpan high, ReadOnlySpan low, ReadOnlySpan close, ReadOnlySpan volume, Span output, int period = 0) + public static void Batch(ReadOnlySpan high, ReadOnlySpan low, ReadOnlySpan close, ReadOnlySpan volume, Span output, int period = 0) { if (high.Length != low.Length) { @@ -368,4 +368,11 @@ public sealed class Vwap : ITValuePublisher barsSinceReset++; } } + + public static (TSeries Results, Vwap Indicator) Calculate(TBarSeries source, int period = 0) + { + var indicator = new Vwap(period); + TSeries results = indicator.Update(source); + return (results, indicator); + } } \ No newline at end of file diff --git a/lib/volume/vwma/Vwma.Tests.cs b/lib/volume/vwma/Vwma.Tests.cs index d6b3cc8d..613d60f6 100644 --- a/lib/volume/vwma/Vwma.Tests.cs +++ b/lib/volume/vwma/Vwma.Tests.cs @@ -304,7 +304,7 @@ public class VwmaTests [Fact] public void Calculate_Static_ShouldReturnTSeries() { - var result = Vwma.Calculate(_bars, 10); + var result = Vwma.Batch(_bars, 10); Assert.NotNull(result); Assert.Equal(_bars.Count, result.Count); @@ -313,8 +313,8 @@ public class VwmaTests [Fact] public void Calculate_Static_WithDifferentPeriods_ShouldWork() { - var result14 = Vwma.Calculate(_bars, 14); - var result50 = Vwma.Calculate(_bars, 50); + var result14 = Vwma.Batch(_bars, 14); + var result50 = Vwma.Batch(_bars, 50); Assert.NotNull(result14); Assert.NotNull(result50); @@ -327,13 +327,13 @@ public class VwmaTests [Fact] public void Calculate_Span_ShouldMatchBatch() { - var batchResult = Vwma.Calculate(_bars, 20); + var batchResult = Vwma.Batch(_bars, 20); var price = _bars.Close.Values.ToArray(); var volume = _bars.Volume.Values.ToArray(); var spanOutput = new double[_bars.Count]; - Vwma.Calculate(price, volume, spanOutput, 20); + Vwma.Batch(price, volume, spanOutput, 20); for (int i = 0; i < _bars.Count; i++) { @@ -348,7 +348,7 @@ public class VwmaTests var volume = new double[99]; // Mismatched var output = new double[100]; - Assert.Throws(() => Vwma.Calculate(price, volume, output, 10)); + Assert.Throws(() => Vwma.Batch(price, volume, output, 10)); } [Fact] @@ -358,7 +358,7 @@ public class VwmaTests var volume = new double[100]; var output = new double[50]; // Mismatched - Assert.Throws(() => Vwma.Calculate(price, volume, output, 10)); + Assert.Throws(() => Vwma.Batch(price, volume, output, 10)); } [Fact] @@ -368,7 +368,7 @@ public class VwmaTests var volume = new double[100]; var output = new double[100]; - Assert.Throws(() => Vwma.Calculate(price, volume, output, 0)); + Assert.Throws(() => Vwma.Batch(price, volume, output, 0)); } [Fact] @@ -378,7 +378,7 @@ public class VwmaTests var volume = new double[100]; var output = new double[100]; - Assert.Throws(() => Vwma.Calculate(price, volume, output, -1)); + Assert.Throws(() => Vwma.Batch(price, volume, output, -1)); } // ============ Event Tests ============ @@ -411,7 +411,7 @@ public class VwmaTests } // Batch - var batchResult = Vwma.Calculate(_bars, 20); + var batchResult = Vwma.Batch(_bars, 20); // Compare last 100 values for (int i = _bars.Count - 100; i < _bars.Count; i++) @@ -426,7 +426,7 @@ public class VwmaTests public void Calculate_TSeries_ShouldWork() { var sourceSeries = _bars.Close; - var result = Vwma.Calculate(sourceSeries, 20); + var result = Vwma.Batch(sourceSeries, 20); Assert.NotNull(result); Assert.Equal(sourceSeries.Count, result.Count); @@ -436,7 +436,7 @@ public class VwmaTests public void Calculate_TSeries_ShouldMatchTValueStreaming() { var sourceSeries = _bars.Close; - var batchResult = Vwma.Calculate(sourceSeries, 20); + var batchResult = Vwma.Batch(sourceSeries, 20); // Streaming with TValue var vwma = new Vwma(20); diff --git a/lib/volume/vwma/Vwma.Validation.Tests.cs b/lib/volume/vwma/Vwma.Validation.Tests.cs index 5bff59b6..67d2017f 100644 --- a/lib/volume/vwma/Vwma.Validation.Tests.cs +++ b/lib/volume/vwma/Vwma.Validation.Tests.cs @@ -19,7 +19,7 @@ public class VwmaValidationTests int period = 20; // QuanTAlib batch - var quantalibResult = Vwma.Calculate(_data.Bars, period); + var quantalibResult = Vwma.Batch(_data.Bars, period); var quantalibValues = quantalibResult.Values.ToArray(); // Skender @@ -91,7 +91,7 @@ public class VwmaValidationTests var price = _data.Bars.Close.Values.ToArray(); var volume = _data.Bars.Volume.Values.ToArray(); var quantalibValues = new double[price.Length]; - Vwma.Calculate(price, volume, quantalibValues, period); + Vwma.Batch(price, volume, quantalibValues, period); // Skender var quotes = _data.Bars.Select(b => new Quote @@ -153,7 +153,7 @@ public class VwmaValidationTests } // Batch - var batchResult = Vwma.Calculate(_data.Bars, period); + var batchResult = Vwma.Batch(_data.Bars, period); var batchValues = batchResult.Values.ToArray(); ValidationHelper.VerifyData(streamingValues.ToArray(), batchValues, 0, 100, 1e-10); @@ -176,7 +176,7 @@ public class VwmaValidationTests var price = _data.Bars.Close.Values.ToArray(); var volume = _data.Bars.Volume.Values.ToArray(); var spanValues = new double[price.Length]; - Vwma.Calculate(price, volume, spanValues, period); + Vwma.Batch(price, volume, spanValues, period); ValidationHelper.VerifyData(streamingValues.ToArray(), spanValues, 0, 100, 1e-10); } @@ -187,14 +187,14 @@ public class VwmaValidationTests int period = 20; // Batch - var batchResult = Vwma.Calculate(_data.Bars, period); + var batchResult = Vwma.Batch(_data.Bars, period); var batchValues = batchResult.Values.ToArray(); // Span var price = _data.Bars.Close.Values.ToArray(); var volume = _data.Bars.Volume.Values.ToArray(); var spanValues = new double[price.Length]; - Vwma.Calculate(price, volume, spanValues, period); + Vwma.Batch(price, volume, spanValues, period); // Batch and Span use identical code path, should match exactly ValidationHelper.VerifyData(batchValues, spanValues, 0, 100, 1e-12); diff --git a/lib/volume/vwma/Vwma.cs b/lib/volume/vwma/Vwma.cs index 4028e3c1..236ce6e3 100644 --- a/lib/volume/vwma/Vwma.cs +++ b/lib/volume/vwma/Vwma.cs @@ -275,7 +275,7 @@ public sealed class Vwma : ITValuePublisher /// Source bar series /// Lookback period for VWMA /// TSeries containing VWMA values - public static TSeries Calculate(TBarSeries source, int period = 20) + public static TSeries Batch(TBarSeries source, int period = 20) { if (source.Count == 0) { @@ -285,7 +285,7 @@ public sealed class Vwma : ITValuePublisher var t = source.Open.Times.ToArray(); var v = new double[source.Count]; - Calculate(source.Close.Values, source.Volume.Values, v, period); + Batch(source.Close.Values, source.Volume.Values, v, period); return new TSeries(t, v); } @@ -296,7 +296,7 @@ public sealed class Vwma : ITValuePublisher /// Source value series /// Lookback period for VWMA /// TSeries containing VWMA values - public static TSeries Calculate(TSeries source, int period = 20) + public static TSeries Batch(TSeries source, int period = 20) { if (source.Count == 0) { @@ -310,7 +310,7 @@ public sealed class Vwma : ITValuePublisher Span unitVolume = stackalloc double[source.Count]; unitVolume.Fill(1.0); - Calculate(source.Values, unitVolume, v, period); + Batch(source.Values, unitVolume, v, period); return new TSeries(t, v); } @@ -323,7 +323,7 @@ public sealed class Vwma : ITValuePublisher /// Output span for VWMA values /// Lookback period for VWMA [MethodImpl(MethodImplOptions.AggressiveOptimization)] - public static void Calculate(ReadOnlySpan source, ReadOnlySpan volume, Span output, int period = 20) + public static void Batch(ReadOnlySpan source, ReadOnlySpan volume, Span output, int period = 20) { if (source.Length != volume.Length) { @@ -477,4 +477,11 @@ public sealed class Vwma : ITValuePublisher } } } + + public static (TSeries Results, Vwma Indicator) Calculate(TBarSeries source, int period = 20) + { + var indicator = new Vwma(period); + TSeries results = indicator.Update(source); + return (results, indicator); + } } \ No newline at end of file diff --git a/lib/volume/wad/Wad.Tests.cs b/lib/volume/wad/Wad.Tests.cs index d63d8c03..096f6ba5 100644 --- a/lib/volume/wad/Wad.Tests.cs +++ b/lib/volume/wad/Wad.Tests.cs @@ -145,7 +145,7 @@ public class WadTests bars.Add(new TBar(time.AddMinutes(1), 100, 115, 92, 110, 2000)); bars.Add(new TBar(time.AddMinutes(2), 110, 108, 102, 105, 1500)); - var result = Wad.Calculate(bars); + var result = Wad.Batch(bars); Assert.Equal(3, result.Count); Assert.Equal(0, result[0].Value); @@ -162,7 +162,7 @@ public class WadTests double[] volume = { 1000, 2000, 1500 }; double[] output = new double[3]; - Wad.Calculate(high, low, close, volume, output); + Wad.Batch(high, low, close, volume, output); Assert.Equal(0, output[0]); Assert.Equal(36000, output[1]); @@ -179,14 +179,14 @@ public class WadTests double[] output = new double[2]; Assert.Throws(() => - Wad.Calculate(high, low, close, volume, output)); + Wad.Batch(high, low, close, volume, output)); } [Fact] public void Wad_Calculate_EmptySeries_ReturnsEmpty() { var bars = new TBarSeries(); - var result = Wad.Calculate(bars); + var result = Wad.Batch(bars); Assert.Empty(result); } @@ -209,7 +209,7 @@ public class WadTests volume[i] = 100; } - Wad.Calculate(high, low, close, volume, output); + Wad.Batch(high, low, close, volume, output); // First bar should be 0 Assert.Equal(0, output[0]); @@ -234,7 +234,7 @@ public class WadTests } // Batch calculation - var batchResult = Wad.Calculate(bars); + var batchResult = Wad.Batch(bars); // Streaming calculation var wad = new Wad(); diff --git a/lib/volume/wad/Wad.Validation.Tests.cs b/lib/volume/wad/Wad.Validation.Tests.cs index f65a22cb..f2bde43e 100644 --- a/lib/volume/wad/Wad.Validation.Tests.cs +++ b/lib/volume/wad/Wad.Validation.Tests.cs @@ -13,7 +13,7 @@ public class WadValidationTests public void Wad_BatchMatchesStreaming() { // Batch calculation - var batchResult = Wad.Calculate(_data.Bars); + var batchResult = Wad.Batch(_data.Bars); // Streaming calculation var wad = new Wad(); @@ -37,7 +37,7 @@ public class WadValidationTests var spanOutput = new double[high.Length]; // Span calculation - Wad.Calculate(high, low, close, volume, spanOutput); + Wad.Batch(high, low, close, volume, spanOutput); // Streaming calculation var wad = new Wad(); diff --git a/lib/volume/wad/Wad.cs b/lib/volume/wad/Wad.cs index 6dcd8472..010cfbdd 100644 --- a/lib/volume/wad/Wad.cs +++ b/lib/volume/wad/Wad.cs @@ -159,7 +159,7 @@ public sealed class Wad : ITValuePublisher return new TSeries(t, v); } - public static TSeries Calculate(TBarSeries source) + public static TSeries Batch(TBarSeries source) { if (source.Count == 0) { @@ -169,13 +169,13 @@ public sealed class Wad : ITValuePublisher var t = source.Open.Times.ToArray(); var v = new double[source.Count]; - Calculate(source.High.Values, source.Low.Values, source.Close.Values, source.Volume.Values, v); + Batch(source.High.Values, source.Low.Values, source.Close.Values, source.Volume.Values, v); return new TSeries(t, v); } [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void Calculate(ReadOnlySpan high, ReadOnlySpan low, ReadOnlySpan close, ReadOnlySpan volume, Span output) + public static void Batch(ReadOnlySpan high, ReadOnlySpan low, ReadOnlySpan close, ReadOnlySpan volume, Span output) { if (high.Length != low.Length || high.Length != close.Length || high.Length != volume.Length || high.Length != output.Length) { @@ -225,4 +225,11 @@ public sealed class Wad : ITValuePublisher prevClose = c; } } + + public static (TSeries Results, Wad Indicator) Calculate(TBarSeries source) + { + var indicator = new Wad(); + TSeries results = indicator.Update(source); + return (results, indicator); + } } \ No newline at end of file diff --git a/quantower/Channels.csproj b/quantower/Channels.csproj index cc17f712..f91a3915 100644 --- a/quantower/Channels.csproj +++ b/quantower/Channels.csproj @@ -20,6 +20,7 @@ + diff --git a/quantower/Mocks/TradingPlatformMocks.cs b/quantower/Mocks/TradingPlatformMocks.cs index 2284dbef..9c00ffbb 100644 --- a/quantower/Mocks/TradingPlatformMocks.cs +++ b/quantower/Mocks/TradingPlatformMocks.cs @@ -349,6 +349,15 @@ public class LineSeries(string name, Color color, int width, LineStyle style) public IReadOnlyList Values => _values; } +/// +/// Line level for indicator horizontal lines +/// +public class LineLevel(double value, string name, Color color, int width, LineStyle style) + : Line(name, color, width, style) +{ + public double Value { get; set; } = value; +} + #endregion #region Paint Chart Event Args @@ -424,6 +433,7 @@ public interface IWatchlistIndicator public abstract class Indicator { private readonly List _lineSeries = []; + private readonly List _lineLevels = []; public string Name { get; set; } = string.Empty; public string Description { get; set; } = string.Empty; @@ -439,12 +449,18 @@ public abstract class Indicator public int Count => HistoricalData.Count; public IReadOnlyList LinesSeries => _lineSeries; + public IReadOnlyList LineLevels => _lineLevels; protected void AddLineSeries(LineSeries series) { _lineSeries.Add(series); } + protected void AddLineLevel(double value, string name, Color color, int width, LineStyle style) + { + _lineLevels.Add(new LineLevel(value, name, color, width, style)); + } + /// /// Called when indicator is initialized ///