From e59665c8f06c7e4cb4d0303154427028bdbe673a Mon Sep 17 00:00:00 2001 From: Miha Kralj Date: Sun, 25 Jan 2026 16:01:45 -0800 Subject: [PATCH] style patterns --- .editorconfig | 4 + lib/channels/abber/Abber.Quantower.cs | 5 +- lib/channels/abber/Abber.Validation.Tests.cs | 2 +- lib/channels/abber/Abber.cs | 40 ++- .../accbands/AccBands.Quantower.Tests.cs | 2 +- lib/channels/accbands/AccBands.Quantower.cs | 7 +- lib/channels/accbands/AccBands.cs | 115 ++++++- .../apchannel/Apchannel.Quantower.Tests.cs | 2 +- lib/channels/apchannel/Apchannel.Quantower.cs | 7 +- .../apchannel/apchannel.Validation.Tests.cs | 11 +- lib/channels/apchannel/apchannel.cs | 41 ++- lib/channels/apz/Apz.Quantower.Tests.cs | 2 +- lib/channels/apz/Apz.Quantower.cs | 7 +- lib/channels/apz/Apz.Validation.Tests.cs | 5 + lib/channels/apz/Apz.cs | 92 +++++- .../atrbands/AtrBands.Quantower.Tests.cs | 2 +- lib/channels/atrbands/AtrBands.Quantower.cs | 7 +- lib/channels/atrbands/AtrBands.Tests.cs | 2 +- lib/channels/atrbands/AtrBands.cs | 134 +++++++- lib/channels/bbands/Bbands.Quantower.Tests.cs | 2 +- lib/channels/bbands/Bbands.Quantower.cs | 2 +- lib/channels/bbands/Bbands.Tests.cs | 2 +- .../bbands/Bbands.Validation.Tests.cs | 2 +- lib/channels/bbands/Bbands.cs | 4 +- lib/channels/dchannel/Dchannel.Quantower.cs | 2 + lib/channels/dchannel/Dchannel.cs | 37 ++- .../decaychannel/Decaychannel.Quantower.cs | 2 + lib/channels/decaychannel/Decaychannel.cs | 49 ++- lib/channels/fcb/Fcb.Quantower.cs | 2 + lib/channels/fcb/Fcb.cs | 58 +++- lib/channels/jbands/Jbands.Quantower.cs | 2 + lib/channels/jbands/Jbands.Tests.cs | 2 + .../jbands/Jbands.Validation.Tests.cs | 2 + lib/channels/jbands/Jbands.cs | 59 +++- lib/channels/kchannel/Kchannel.Quantower.cs | 2 + .../kchannel/Kchannel.Validation.Tests.cs | 2 + lib/channels/kchannel/Kchannel.cs | 45 ++- lib/channels/maenv/Maenv.Quantower.cs | 2 + lib/channels/maenv/Maenv.cs | 53 ++- lib/channels/mmchannel/Mmchannel.Quantower.cs | 2 + lib/channels/mmchannel/Mmchannel.cs | 37 ++- lib/channels/pchannel/Pchannel.Quantower.cs | 2 + .../pchannel/Pchannel.Validation.Tests.cs | 11 +- lib/channels/pchannel/Pchannel.cs | 37 ++- .../regchannel/Regchannel.Quantower.cs | 2 + lib/channels/regchannel/Regchannel.cs | 28 +- lib/channels/sdchannel/Sdchannel.Quantower.cs | 2 + lib/channels/sdchannel/Sdchannel.cs | 28 +- .../starchannel/Starchannel.Quantower.cs | 2 + lib/channels/starchannel/Starchannel.cs | 41 ++- .../stbands/Stbands.Quantower.Tests.cs | 2 +- lib/channels/stbands/Stbands.Quantower.cs | 2 +- lib/channels/stbands/Stbands.Tests.cs | 2 +- .../stbands/Stbands.Validation.Tests.cs | 2 +- lib/channels/stbands/Stbands.cs | 12 +- lib/channels/ubands/Ubands.Quantower.Tests.cs | 2 +- lib/channels/ubands/Ubands.Quantower.cs | 2 +- lib/channels/ubands/Ubands.Tests.cs | 2 +- .../ubands/Ubands.Validation.Tests.cs | 2 +- lib/channels/ubands/Ubands.cs | 2 +- lib/channels/uchannel/Uchannel.Quantower.cs | 2 +- lib/channels/uchannel/Uchannel.Tests.cs | 2 +- .../uchannel/Uchannel.Validation.Tests.cs | 2 +- lib/channels/uchannel/Uchannel.cs | 19 +- .../vwapbands/Vwapbands.Quantower.Tests.cs | 4 +- lib/channels/vwapbands/Vwapbands.Quantower.cs | 2 +- lib/channels/vwapbands/Vwapbands.Tests.cs | 2 +- .../vwapbands/Vwapbands.Validation.Tests.cs | 2 +- lib/channels/vwapbands/Vwapbands.cs | 2 +- lib/channels/vwapsd/Vwapsd.Quantower.Tests.cs | 2 +- lib/channels/vwapsd/Vwapsd.Quantower.cs | 2 +- lib/channels/vwapsd/Vwapsd.Tests.cs | 2 +- .../vwapsd/Vwapsd.Validation.Tests.cs | 2 +- lib/channels/vwapsd/Vwapsd.cs | 2 +- lib/core/BiInputIndicatorBase.cs | 15 +- lib/core/collections/MonotonicDeque.cs | 14 +- lib/core/ringbuffer/RingBuffer.cs | 57 +++- lib/core/simd/ErrorHelpers.cs | 308 ++++++++++++++++-- lib/core/simd/SimdExtensions.Tests.cs | 4 +- lib/core/simd/SimdExtensions.cs | 184 +++++++++-- lib/core/tbar/TBar.Tests.cs | 2 +- lib/core/tbar/tbar.cs | 2 +- lib/core/tbarseries/tbarseries.cs | 16 +- lib/core/tseries/ITValuePublisher.cs | 2 +- lib/core/tseries/tseries.cs | 4 +- lib/core/tvalue/TValue.Tests.cs | 2 +- lib/core/tvalue/tvalue.cs | 25 ++ lib/cycles/stc/Stc.Validation.Tests.cs | 16 +- lib/cycles/stc/Stc.cs | 126 ++++++- lib/dynamics/adx/Adx.Tests.cs | 16 +- lib/dynamics/adx/Adx.cs | 82 ++++- lib/dynamics/adxr/Adxr.cs | 16 +- lib/dynamics/amat/Amat.Tests.cs | 2 + lib/dynamics/amat/Amat.Validation.Tests.cs | 4 +- lib/dynamics/amat/Amat.cs | 55 +++- lib/dynamics/aroon/Aroon.cs | 14 +- lib/dynamics/aroonosc/AroonOsc.cs | 14 +- lib/dynamics/dmx/Dmx.Tests.cs | 9 + lib/dynamics/dmx/Dmx.cs | 20 +- lib/dynamics/super/Super.cs | 2 +- lib/errors/huber/Huber.Tests.cs | 13 +- lib/errors/huber/Huber.cs | 17 +- lib/errors/logcosh/LogCosh.cs | 10 +- lib/errors/maape/Maape.cs | 26 +- lib/errors/mae/Mae.Tests.cs | 13 +- lib/errors/mae/Mae.cs | 7 +- lib/errors/mapd/Mapd.Tests.cs | 13 +- lib/errors/mapd/Mapd.cs | 20 +- lib/errors/mape/Mape.Tests.cs | 13 +- lib/errors/mape/Mape.cs | 7 +- lib/errors/mase/Mase.cs | 84 ++++- lib/errors/mdae/Mdae.cs | 104 +++++- lib/errors/mdape/Mdape.cs | 65 +++- lib/errors/me/Me.Tests.cs | 13 +- lib/errors/me/Me.cs | 7 +- lib/errors/mpe/Mpe.cs | 43 ++- lib/errors/mrae/Mrae.Tests.cs | 13 +- lib/errors/mrae/Mrae.cs | 22 +- lib/errors/mse/Mse.Tests.cs | 2 +- lib/errors/mse/Mse.cs | 5 +- lib/errors/msle/Msle.cs | 41 ++- lib/errors/pseudohuber/PseudoHuber.cs | 19 +- lib/errors/quantile/QuantileLoss.cs | 77 ++++- lib/errors/rae/Rae.cs | 77 ++++- lib/errors/rmse/Rmse.Tests.cs | 2 +- lib/errors/rmse/Rmse.cs | 5 +- lib/errors/rmsle/Rmsle.cs | 41 ++- lib/errors/rse/Rse.cs | 79 ++++- lib/errors/rsquared/Rsquared.cs | 77 ++++- lib/errors/smape/Smape.cs | 41 ++- lib/errors/theilu/TheilU.cs | 79 ++++- lib/errors/tukey/TukeyBiweight.cs | 21 +- lib/errors/wmape/Wmape.cs | 88 ++++- lib/errors/wrmse/Wrmse.Tests.cs | 12 +- lib/errors/wrmse/Wrmse.cs | 40 ++- lib/feeds/csv/CsvFeed.Tests.cs | 8 +- lib/feeds/csv/CsvFeed.cs | 30 +- lib/feeds/gbm/Gbm.Tests.cs | 5 + lib/feeds/gbm/ValidationHelper.cs | 95 +++++- lib/feeds/gbm/ValidationTestData.cs | 4 +- lib/feeds/gbm/gbm.cs | 56 +++- lib/filters/bessel/Bessel.cs | 34 ++ .../bilateral/Bilateral.Validation.Tests.cs | 10 +- lib/filters/bilateral/Bilateral.cs | 28 +- lib/filters/bpf/Bpf.Quantower.Tests.cs | 2 +- lib/filters/bpf/Bpf.Tests.cs | 2 +- lib/filters/bpf/Bpf.Validation.Tests.cs | 3 +- lib/filters/bpf/Bpf.cs | 19 +- lib/filters/cheby1/Cheby1.Tests.cs | 2 +- lib/filters/cheby1/Cheby1.Validation.Tests.cs | 5 +- lib/filters/cheby1/Cheby1.cs | 26 +- lib/filters/cheby2/Cheby2.Tests.cs | 2 +- lib/filters/cheby2/Cheby2.Validation.Tests.cs | 6 +- lib/filters/cheby2/Cheby2.cs | 24 +- .../elliptic/Elliptic.Quantower.Tests.cs | 2 +- lib/filters/elliptic/Elliptic.Tests.cs | 2 +- .../elliptic/Elliptic.Validation.Tests.cs | 8 +- lib/filters/elliptic/Elliptic.cs | 36 +- lib/filters/gauss/Gauss.Quantower.Tests.cs | 2 +- lib/filters/gauss/Gauss.Tests.cs | 8 +- lib/filters/gauss/Gauss.Validation.Tests.cs | 12 +- lib/filters/gauss/Gauss.cs | 13 +- lib/filters/hann/Hann.Quantower.Tests.cs | 2 +- lib/filters/hann/Hann.Tests.cs | 8 +- lib/filters/hann/Hann.Validation.Tests.cs | 10 +- lib/filters/hann/Hann.cs | 5 +- lib/filters/hp/Hp.Quantower.Tests.cs | 2 +- lib/filters/hp/Hp.Validation.Tests.cs | 2 +- lib/filters/hp/Hp.cs | 14 +- lib/filters/hpf/Hpf.Quantower.Tests.cs | 2 +- lib/filters/hpf/Hpf.cs | 34 +- lib/filters/kalman/Kalman.Quantower.Tests.cs | 2 +- lib/filters/kalman/Kalman.Tests.cs | 4 +- lib/filters/kalman/Kalman.Validation.Tests.cs | 2 +- lib/filters/kalman/Kalman.cs | 32 +- lib/filters/loess/Loess.Validation.Tests.cs | 4 +- lib/filters/loess/Loess.cs | 49 ++- lib/filters/notch/Notch.Quantower.Tests.cs | 2 +- lib/filters/notch/Notch.Tests.cs | 6 +- lib/filters/notch/Notch.Validation.Tests.cs | 4 +- lib/filters/sgf/Sgf.Quantower.Tests.cs | 2 +- lib/filters/sgf/Sgf.Tests.cs | 2 +- lib/filters/sgf/Sgf.Validation.Tests.cs | 12 +- lib/filters/sgf/Sgf.cs | 9 +- lib/filters/ssf/Ssf.cs | 41 ++- lib/filters/usf/Usf.Quantower.cs | 2 + lib/filters/usf/Usf.Tests.cs | 4 + lib/filters/usf/Usf.Validation.Tests.cs | 6 +- lib/filters/usf/Usf.cs | 41 ++- lib/filters/wiener/Wiener.Quantower.Tests.cs | 2 +- lib/filters/wiener/Wiener.Validation.Tests.cs | 8 +- lib/forecasts/afirma/Afirma.Tests.cs | 7 +- lib/forecasts/afirma/Afirma.cs | 240 ++++++++------ lib/momentum/bop/Bop.Tests.cs | 3 + lib/momentum/bop/Bop.cs | 7 +- lib/momentum/cfb/Cfb.Tests.cs | 9 + lib/momentum/cfb/Cfb.cs | 54 ++- lib/momentum/macd/Macd.Quantower.Tests.cs | 4 +- lib/momentum/macd/Macd.Tests.cs | 25 +- lib/momentum/macd/Macd.cs | 9 +- lib/momentum/roc/Roc.Quantower.cs | 12 +- lib/momentum/roc/Roc.Tests.cs | 2 + lib/momentum/roc/Roc.cs | 14 + lib/momentum/rsi/Rsi.Tests.cs | 2 +- lib/momentum/rsi/Rsi.cs | 17 +- lib/momentum/rsx/Rsx.cs | 19 +- lib/momentum/vel/Vel.Validation.Tests.cs | 11 +- lib/momentum/vel/Vel.cs | 15 +- lib/numerics/Atan2Validation.Tests.cs | 11 +- lib/numerics/accel/Accel.Quantower.cs | 21 +- lib/numerics/accel/Accel.Tests.cs | 4 +- lib/numerics/accel/Accel.cs | 58 +++- lib/numerics/change/Change.Quantower.cs | 23 +- lib/numerics/change/Change.Tests.cs | 2 +- lib/numerics/change/Change.cs | 16 + lib/numerics/exptrans/Exptrans.Quantower.cs | 7 +- lib/numerics/exptrans/Exptrans.Tests.cs | 2 +- .../exptrans/Exptrans.Validation.Tests.cs | 2 +- lib/numerics/exptrans/Exptrans.cs | 9 + lib/numerics/highest/Highest.Quantower.cs | 5 +- lib/numerics/highest/Highest.Tests.cs | 2 +- lib/numerics/highest/Highest.cs | 37 ++- lib/numerics/jerk/Jerk.Quantower.Tests.cs | 8 +- lib/numerics/jerk/Jerk.Quantower.cs | 12 +- lib/numerics/jerk/Jerk.Tests.cs | 6 +- lib/numerics/jerk/Jerk.cs | 82 ++++- .../Lineartrans.Quantower.Tests.cs | 2 +- .../lineartrans/Lineartrans.Quantower.cs | 7 +- lib/numerics/lineartrans/Lineartrans.Tests.cs | 8 +- .../Lineartrans.Validation.Tests.cs | 4 +- lib/numerics/lineartrans/Lineartrans.cs | 20 ++ .../logtrans/Logtrans.Quantower.Tests.cs | 2 +- lib/numerics/logtrans/Logtrans.Quantower.cs | 7 +- lib/numerics/logtrans/Logtrans.Tests.cs | 2 +- lib/numerics/logtrans/Logtrans.cs | 9 + lib/numerics/lowest/Lowest.Quantower.cs | 5 +- lib/numerics/lowest/Lowest.cs | 26 ++ lib/numerics/midpoint/Midpoint.Quantower.cs | 5 +- lib/numerics/midpoint/Midpoint.cs | 15 + lib/numerics/normalize/Normalize.Quantower.cs | 5 +- lib/numerics/normalize/Normalize.Tests.cs | 8 +- .../normalize/Normalize.Validation.Tests.cs | 14 +- lib/numerics/normalize/Normalize.cs | 38 ++- lib/numerics/relu/Relu.Quantower.cs | 5 +- lib/numerics/relu/Relu.cs | 16 +- .../sigmoid/Sigmoid.Quantower.Tests.cs | 2 +- lib/numerics/sigmoid/Sigmoid.Quantower.cs | 5 +- lib/numerics/sigmoid/Sigmoid.Tests.cs | 14 +- .../sigmoid/Sigmoid.Validation.Tests.cs | 4 +- lib/numerics/sigmoid/Sigmoid.cs | 26 +- lib/numerics/slope/Slope.Quantower.cs | 12 +- lib/numerics/slope/Slope.Tests.cs | 4 +- lib/numerics/slope/Slope.cs | 17 +- .../sqrttrans/Sqrttrans.Quantower.Tests.cs | 2 +- lib/numerics/sqrttrans/Sqrttrans.Quantower.cs | 7 +- lib/numerics/sqrttrans/Sqrttrans.Tests.cs | 2 +- .../sqrttrans/Sqrttrans.Validation.Tests.cs | 2 +- lib/numerics/sqrttrans/Sqrttrans.cs | 9 + lib/oscillators/ao/Ao.Quantower.cs | 2 + lib/oscillators/ao/Ao.Validation.Tests.cs | 5 +- lib/oscillators/ao/Ao.cs | 27 +- lib/oscillators/apo/Apo.cs | 10 + lib/oscillators/ultosc/Ultosc.Tests.cs | 5 +- lib/oscillators/ultosc/Ultosc.cs | 68 +++- lib/statistics/beta/Beta.cs | 2 +- lib/statistics/cma/Cma.Quantower.Tests.cs | 2 +- lib/statistics/cma/Cma.Tests.cs | 7 +- lib/statistics/cma/Cma.cs | 23 +- .../covariance/Covariance.Simd.Tests.cs | 11 +- lib/statistics/covariance/Covariance.cs | 68 +++- lib/statistics/linreg/LinReg.cs | 145 +++++---- lib/statistics/median/Median.Tests.cs | 11 +- .../median/Median.Validation.Tests.cs | 5 +- lib/statistics/median/Median.cs | 46 ++- lib/statistics/skew/Skew.Tests.cs | 10 +- lib/statistics/skew/Skew.cs | 60 +++- .../stddev/StdDev.Validation.Tests.cs | 11 +- lib/statistics/stddev/StdDev.cs | 7 +- lib/statistics/sum/Sum.Tests.cs | 4 + lib/statistics/sum/Sum.Validation.Tests.cs | 11 +- lib/statistics/sum/Sum.cs | 147 +++++---- lib/statistics/variance/Variance.Quantower.cs | 2 + lib/statistics/variance/Variance.Tests.cs | 17 +- lib/statistics/variance/Variance.cs | 88 ++++- lib/trends_FIR/alma/Alma.cs | 40 ++- lib/trends_FIR/blma/Blma.Tests.cs | 2 +- lib/trends_FIR/blma/Blma.cs | 20 +- lib/trends_FIR/bwma/Bwma.Quantower.Tests.cs | 2 +- lib/trends_FIR/bwma/Bwma.Quantower.cs | 4 +- lib/trends_FIR/bwma/Bwma.Validation.Tests.cs | 8 +- lib/trends_FIR/bwma/Bwma.cs | 79 ++++- lib/trends_FIR/conv/Conv.cs | 27 +- lib/trends_FIR/dwma/Dwma.cs | 27 +- lib/trends_FIR/gwma/Gwma.Quantower.Tests.cs | 2 +- lib/trends_FIR/gwma/Gwma.Quantower.cs | 2 +- lib/trends_FIR/gwma/Gwma.Validation.Tests.cs | 2 +- lib/trends_FIR/gwma/Gwma.cs | 75 ++++- lib/trends_FIR/hamma/Hamma.Quantower.Tests.cs | 2 +- lib/trends_FIR/hamma/Hamma.Quantower.cs | 2 +- lib/trends_FIR/hamma/Hamma.Tests.cs | 2 +- .../hamma/Hamma.Validation.Tests.cs | 2 +- lib/trends_FIR/hamma/Hamma.cs | 28 +- lib/trends_FIR/hanma/Hanma.Quantower.Tests.cs | 2 +- lib/trends_FIR/hanma/Hanma.Quantower.cs | 2 +- lib/trends_FIR/hanma/Hanma.Tests.cs | 2 +- .../hanma/Hanma.Validation.Tests.cs | 2 +- lib/trends_FIR/hanma/Hanma.cs | 37 ++- lib/trends_FIR/hma/Hma.cs | 25 +- lib/trends_FIR/hwma/Hwma.Quantower.Tests.cs | 2 +- lib/trends_FIR/hwma/Hwma.Quantower.cs | 2 +- lib/trends_FIR/hwma/Hwma.Tests.cs | 2 +- lib/trends_FIR/hwma/Hwma.cs | 35 +- lib/trends_FIR/lsma/Lsma.cs | 25 +- lib/trends_FIR/pwma/Pwma.cs | 37 ++- lib/trends_FIR/sgma/Sgma.Tests.cs | 13 +- lib/trends_FIR/sgma/Sgma.cs | 83 ++++- .../sinema/Sinema.Quantower.Tests.cs | 2 +- lib/trends_FIR/sinema/Sinema.Quantower.cs | 2 +- lib/trends_FIR/sinema/Sinema.Tests.cs | 9 +- .../sinema/Sinema.Validation.Tests.cs | 8 +- lib/trends_FIR/sinema/Sinema.cs | 46 ++- lib/trends_FIR/sma/Sma.Tests.cs | 7 +- lib/trends_FIR/sma/Sma.cs | 40 ++- lib/trends_FIR/trima/Trima.Quantower.cs | 2 + lib/trends_FIR/trima/Trima.cs | 17 +- lib/trends_FIR/wma/Wma.Coverage.Tests.cs | 22 +- lib/trends_FIR/wma/Wma.Quantower.cs | 2 + lib/trends_FIR/wma/Wma.Validation.Tests.cs | 11 +- lib/trends_FIR/wma/Wma.cs | 46 ++- lib/trends_IIR/dema/Dema.cs | 45 ++- lib/trends_IIR/dsma/Dsma.Quantower.Tests.cs | 2 +- lib/trends_IIR/dsma/Dsma.Tests.cs | 4 +- lib/trends_IIR/dsma/Dsma.Validation.Tests.cs | 21 +- lib/trends_IIR/dsma/Dsma.cs | 16 +- lib/trends_IIR/ema/Ema.Tests.cs | 17 +- lib/trends_IIR/ema/Ema.cs | 85 ++++- .../frama/Frama.Validation.Tests.cs | 44 ++- lib/trends_IIR/frama/Frama.cs | 46 ++- lib/trends_IIR/hema/Hema.Validation.Tests.cs | 4 + lib/trends_IIR/hema/Hema.cs | 47 ++- lib/trends_IIR/htit/Htit.cs | 58 +++- lib/trends_IIR/jma/Jma.Tests.cs | 2 +- lib/trends_IIR/jma/Jma.ZeroDiv.Tests.cs | 2 +- lib/trends_IIR/jma/Jma.cs | 51 ++- lib/trends_IIR/kama/Kama.cs | 64 +++- lib/trends_IIR/mama/Mama.Tests.cs | 41 ++- lib/trends_IIR/mama/Mama.cs | 67 +++- lib/trends_IIR/mgdi/Mgdi.cs | 25 +- lib/trends_IIR/mma/Mma.Validation.Tests.cs | 14 + lib/trends_IIR/mma/Mma.cs | 45 ++- lib/trends_IIR/qema/Qema.Tests.cs | 12 +- lib/trends_IIR/qema/Qema.cs | 42 ++- lib/trends_IIR/rema/Rema.Quantower.Tests.cs | 2 +- lib/trends_IIR/rema/Rema.Quantower.cs | 2 +- lib/trends_IIR/rema/Rema.Tests.cs | 23 +- lib/trends_IIR/rema/Rema.Validation.Tests.cs | 12 +- lib/trends_IIR/rema/Rema.cs | 47 ++- lib/trends_IIR/rgma/Rgma.Tests.cs | 6 + lib/trends_IIR/rgma/Rgma.Validation.Tests.cs | 12 + lib/trends_IIR/rgma/Rgma.cs | 66 +++- lib/trends_IIR/rma/Rma.cs | 8 +- lib/trends_IIR/t3/T3.cs | 37 ++- lib/trends_IIR/tema/Tema.Quantower.cs | 2 + lib/trends_IIR/tema/Tema.cs | 62 +++- lib/trends_IIR/vama/Vama.Tests.cs | 10 +- lib/trends_IIR/vama/Vama.cs | 41 ++- lib/trends_IIR/vidya/Vidya.Quantower.cs | 2 + lib/trends_IIR/vidya/Vidya.cs | 42 ++- lib/trends_IIR/yzvama/Yzvama.Tests.cs | 2 + .../yzvama/Yzvama.Validation.Tests.cs | 2 + lib/trends_IIR/yzvama/Yzvama.cs | 51 ++- .../zlema/Zlema.Validation.Tests.cs | 6 + lib/trends_IIR/zlema/Zlema.cs | 52 ++- lib/volatility/adr/Adr.Quantower.Tests.cs | 2 +- lib/volatility/adr/Adr.Quantower.cs | 2 +- lib/volatility/adr/Adr.Tests.cs | 7 +- lib/volatility/adr/Adr.Validation.Tests.cs | 2 +- lib/volatility/adr/Adr.cs | 9 +- lib/volatility/atr/Atr.Quantower.Tests.cs | 2 +- lib/volatility/atr/Atr.Tests.cs | 5 +- lib/volatility/atr/Atr.cs | 14 +- lib/volatility/atrn/Atrn.Quantower.Tests.cs | 2 +- lib/volatility/atrn/Atrn.Quantower.cs | 2 +- lib/volatility/atrn/Atrn.Validation.Tests.cs | 13 +- lib/volatility/atrn/Atrn.cs | 32 +- lib/volatility/atrp/Atrp.Quantower.Tests.cs | 2 +- lib/volatility/atrp/Atrp.Quantower.cs | 2 +- lib/volatility/atrp/Atrp.Tests.cs | 7 +- lib/volatility/atrp/Atrp.Validation.Tests.cs | 2 +- lib/volatility/atrp/Atrp.cs | 42 ++- lib/volume/adl/Adl.Tests.cs | 2 +- lib/volume/adl/Adl.cs | 9 +- lib/volume/adosc/Adosc.cs | 13 +- lib/volume/cmf/Cmf.cs | 21 +- quantower/IndicatorExtensions.Tests.cs | 22 +- quantower/IndicatorExtensions.cs | 24 +- quantower/Mocks/TradingPlatformMocks.cs | 12 + quantower/Trends/Sgma.Quantower.Tests.cs | 2 +- quantower/Trends/Sgma.Quantower.cs | 7 +- 399 files changed, 6892 insertions(+), 1323 deletions(-) diff --git a/.editorconfig b/.editorconfig index 25e49047..eca3c256 100644 --- a/.editorconfig +++ b/.editorconfig @@ -37,6 +37,10 @@ dotnet_diagnostic.S3236.severity = none dotnet_diagnostic.MA0046.severity = none dotnet_diagnostic.MA0003.severity = suggestion +# Require curly braces on control structures +csharp_prefer_braces = true:warning +dotnet_diagnostic.IDE0011.severity = warning + csharp_style_var_for_built_in_types = false:silent csharp_style_var_when_type_is_apparent = true:suggestion csharp_style_var_elsewhere = false:silent diff --git a/lib/channels/abber/Abber.Quantower.cs b/lib/channels/abber/Abber.Quantower.cs index c3a2ad84..0cd1060c 100644 --- a/lib/channels/abber/Abber.Quantower.cs +++ b/lib/channels/abber/Abber.Quantower.cs @@ -73,7 +73,10 @@ public sealed class AbberIndicator : Indicator, IWatchlistIndicator [MethodImpl(MethodImplOptions.AggressiveInlining)] protected override void OnUpdate(UpdateArgs args) { - if (HistoricalData.Count == 0 || _abber is null || _selector is null) return; + if (HistoricalData.Count == 0 || _abber is null || _selector is null) + { + return; + } var item = HistoricalData[0, SeekOriginHistory.End]; double value = _selector(item); diff --git a/lib/channels/abber/Abber.Validation.Tests.cs b/lib/channels/abber/Abber.Validation.Tests.cs index 3f9e081a..08fb770c 100644 --- a/lib/channels/abber/Abber.Validation.Tests.cs +++ b/lib/channels/abber/Abber.Validation.Tests.cs @@ -421,4 +421,4 @@ public sealed class AbberValidationTests(ITestOutputHelper output) : IDisposable output.WriteLine($"Abber consistency across {periods.Length} periods validated successfully"); } -} \ No newline at end of file +} diff --git a/lib/channels/abber/Abber.cs b/lib/channels/abber/Abber.cs index d7202ccd..0205d777 100644 --- a/lib/channels/abber/Abber.cs +++ b/lib/channels/abber/Abber.cs @@ -93,9 +93,14 @@ public sealed class Abber : ITValuePublisher public Abber(int period, double multiplier = 2.0) { if (period <= 0) + { throw new ArgumentException("Period must be greater than 0", nameof(period)); + } + if (multiplier <= 0) + { throw new ArgumentException("Multiplier must be greater than 0", nameof(multiplier)); + } _period = period; _multiplier = multiplier; @@ -235,7 +240,9 @@ public sealed class Abber : ITValuePublisher public (TSeries Middle, TSeries Upper, TSeries Lower) Update(TSeries source) { if (source.Count == 0) + { return (new TSeries([], []), new TSeries([], []), new TSeries([], [])); + } int len = source.Count; var tMiddle = new List(len); @@ -302,7 +309,10 @@ public sealed class Abber : ITValuePublisher /// public void Prime(TSeries source) { - if (source.Count == 0) return; + if (source.Count == 0) + { + return; + } // Reset state _sourceBuffer.Clear(); @@ -483,13 +493,24 @@ public sealed class Abber : ITValuePublisher { int len = source.Length; if (middle.Length < len || upper.Length < len || lower.Length < len) + { throw new ArgumentException("Output buffers must be at least as long as input", nameof(middle)); - if (period <= 0) - throw new ArgumentException("Period must be greater than 0", nameof(period)); - if (multiplier <= 0) - throw new ArgumentException("Multiplier must be greater than 0", nameof(multiplier)); + } - if (len == 0) return; + if (period <= 0) + { + throw new ArgumentException("Period must be greater than 0", nameof(period)); + } + + if (multiplier <= 0) + { + throw new ArgumentException("Multiplier must be greater than 0", nameof(multiplier)); + } + + if (len == 0) + { + return; + } // Scalar implementation with NaN handling var outputs = new BatchOutputs(middle, upper, lower); @@ -626,7 +647,10 @@ public sealed class Abber : ITValuePublisher buffers.Deviation[state.BufferIndex] = deviation; state.BufferIndex++; - if (state.BufferIndex >= period) state.BufferIndex = 0; + if (state.BufferIndex >= period) + { + state.BufferIndex = 0; + } double middle = state.SumSource / period; double avgDeviation = state.SumDeviation / period; @@ -649,4 +673,4 @@ public sealed class Abber : ITValuePublisher var results = abber.Update(source); return (results, abber); } -} \ No newline at end of file +} diff --git a/lib/channels/accbands/AccBands.Quantower.Tests.cs b/lib/channels/accbands/AccBands.Quantower.Tests.cs index 2d843278..59922409 100644 --- a/lib/channels/accbands/AccBands.Quantower.Tests.cs +++ b/lib/channels/accbands/AccBands.Quantower.Tests.cs @@ -189,4 +189,4 @@ public class AccBandsIndicatorTests indicator.Factor = 3.5; Assert.Equal(3.5, indicator.Factor); } -} \ No newline at end of file +} diff --git a/lib/channels/accbands/AccBands.Quantower.cs b/lib/channels/accbands/AccBands.Quantower.cs index 2ecf662a..01d9925c 100644 --- a/lib/channels/accbands/AccBands.Quantower.cs +++ b/lib/channels/accbands/AccBands.Quantower.cs @@ -51,7 +51,10 @@ public sealed class AccBandsIndicator : Indicator, IWatchlistIndicator protected override void OnUpdate(UpdateArgs args) { - if (_accBands == null) return; + if (_accBands == null) + { + return; + } var item = HistoricalData[0, SeekOriginHistory.End]; bool isNew = args.IsNewBar(); @@ -78,4 +81,4 @@ public sealed class AccBandsIndicator : Indicator, IWatchlistIndicator // Lower band LinesSeries[2].SetValue(_accBands.Lower.Value, isHot, ShowColdValues); } -} \ No newline at end of file +} diff --git a/lib/channels/accbands/AccBands.cs b/lib/channels/accbands/AccBands.cs index fcc4731a..fa447b11 100644 --- a/lib/channels/accbands/AccBands.cs +++ b/lib/channels/accbands/AccBands.cs @@ -96,9 +96,14 @@ public sealed class AccBands : ITValuePublisher, IDisposable public AccBands(int period, double factor = 2.0) { if (period <= 0) + { throw new ArgumentException("Period must be greater than 0", nameof(period)); + } + if (factor <= 0) + { throw new ArgumentException("Factor must be greater than 0", nameof(factor)); + } _period = period; _factor = factor; @@ -125,7 +130,11 @@ public sealed class AccBands : ITValuePublisher, IDisposable /// public void Dispose() { - if (_disposed) return; + if (_disposed) + { + return; + } + _disposed = true; if (_source != null) @@ -271,7 +280,9 @@ public sealed class AccBands : ITValuePublisher, IDisposable public (TSeries Middle, TSeries Upper, TSeries Lower) Update(TBarSeries source) { if (source.Count == 0) + { return (new TSeries([], []), new TSeries([], []), new TSeries([], [])); + } int len = source.Count; var tMiddle = new List(len); @@ -315,7 +326,10 @@ public sealed class AccBands : ITValuePublisher, IDisposable // skipcq: CS-R1140 public void Prime(TBarSeries source) { - if (source.Count == 0) return; + if (source.Count == 0) + { + return; + } // Reset state _highBuffer.Clear(); @@ -336,13 +350,24 @@ public sealed class AccBands : ITValuePublisher, IDisposable { var bar = source[i]; if (double.IsFinite(bar.High) && double.IsNaN(_state.LastValidHigh)) + { _state.LastValidHigh = bar.High; + } + if (double.IsFinite(bar.Low) && double.IsNaN(_state.LastValidLow)) + { _state.LastValidLow = bar.Low; + } + if (double.IsFinite(bar.Close) && double.IsNaN(_state.LastValidClose)) + { _state.LastValidClose = bar.Close; + } + if (!double.IsNaN(_state.LastValidHigh) && !double.IsNaN(_state.LastValidLow) && !double.IsNaN(_state.LastValidClose)) + { break; + } } // Find valid values in warmup window if not found @@ -352,13 +377,24 @@ public sealed class AccBands : ITValuePublisher, IDisposable { var bar = source[i]; if (double.IsFinite(bar.High) && double.IsNaN(_state.LastValidHigh)) + { _state.LastValidHigh = bar.High; + } + if (double.IsFinite(bar.Low) && double.IsNaN(_state.LastValidLow)) + { _state.LastValidLow = bar.Low; + } + if (double.IsFinite(bar.Close) && double.IsNaN(_state.LastValidClose)) + { _state.LastValidClose = bar.Close; + } + if (!double.IsNaN(_state.LastValidHigh) && !double.IsNaN(_state.LastValidLow) && !double.IsNaN(_state.LastValidClose)) + { break; + } } } @@ -578,15 +614,29 @@ public sealed class AccBands : ITValuePublisher, IDisposable { int len = close.Length; if (high.Length != len || low.Length != len) + { throw new ArgumentException("High, Low, and Close must have the same length", nameof(high)); - if (middle.Length < len || upper.Length < len || lower.Length < len) - throw new ArgumentException("Output buffers must be at least as long as input", nameof(middle)); - if (period <= 0) - throw new ArgumentException("Period must be greater than 0", nameof(period)); - if (factor <= 0) - throw new ArgumentException("Factor must be greater than 0", nameof(factor)); + } - if (len == 0) return; + if (middle.Length < len || upper.Length < len || lower.Length < len) + { + throw new ArgumentException("Output buffers must be at least as long as input", nameof(middle)); + } + + if (period <= 0) + { + throw new ArgumentException("Period must be greater than 0", nameof(period)); + } + + if (factor <= 0) + { + throw new ArgumentException("Factor must be greater than 0", nameof(factor)); + } + + if (len == 0) + { + return; + } // Scalar implementation with NaN handling var inputs = new BatchInputs(high, low, close); @@ -643,13 +693,24 @@ public sealed class AccBands : ITValuePublisher, IDisposable for (int k = 0; k < len; k++) { if (double.IsFinite(inputs.High[k]) && double.IsNaN(state.LastValidHigh)) + { state.LastValidHigh = inputs.High[k]; + } + if (double.IsFinite(inputs.Low[k]) && double.IsNaN(state.LastValidLow)) + { state.LastValidLow = inputs.Low[k]; + } + if (double.IsFinite(inputs.Close[k]) && double.IsNaN(state.LastValidClose)) + { state.LastValidClose = inputs.Close[k]; + } + if (!double.IsNaN(state.LastValidHigh) && !double.IsNaN(state.LastValidLow) && !double.IsNaN(state.LastValidClose)) + { break; + } } } @@ -660,9 +721,32 @@ public sealed class AccBands : ITValuePublisher, IDisposable double l = inputs.Low[i]; double c = inputs.Close[i]; - if (double.IsFinite(h)) state.LastValidHigh = h; else h = state.LastValidHigh; - if (double.IsFinite(l)) state.LastValidLow = l; else l = state.LastValidLow; - if (double.IsFinite(c)) state.LastValidClose = c; else c = state.LastValidClose; + if (double.IsFinite(h)) + { + state.LastValidHigh = h; + } + else + { + h = state.LastValidHigh; + } + + if (double.IsFinite(l)) + { + state.LastValidLow = l; + } + else + { + l = state.LastValidLow; + } + + if (double.IsFinite(c)) + { + state.LastValidClose = c; + } + else + { + c = state.LastValidClose; + } return (h, l, c); } @@ -726,7 +810,10 @@ public sealed class AccBands : ITValuePublisher, IDisposable buffers.Close[state.BufferIndex] = c; state.BufferIndex++; - if (state.BufferIndex >= period) state.BufferIndex = 0; + if (state.BufferIndex >= period) + { + state.BufferIndex = 0; + } WriteBandOutputs(outputs, i, state.SumHigh / period, state.SumLow / period, state.SumClose / period, factor); @@ -759,4 +846,4 @@ public sealed class AccBands : ITValuePublisher, IDisposable var results = accBands.Update(source); return (results, accBands); } -} \ No newline at end of file +} diff --git a/lib/channels/apchannel/Apchannel.Quantower.Tests.cs b/lib/channels/apchannel/Apchannel.Quantower.Tests.cs index fc8b9299..c4242fee 100644 --- a/lib/channels/apchannel/Apchannel.Quantower.Tests.cs +++ b/lib/channels/apchannel/Apchannel.Quantower.Tests.cs @@ -217,4 +217,4 @@ public class ApchannelIndicatorTests double expectedMiddle = (upper + lower) / 2.0; Assert.Equal(expectedMiddle, middle, 6); // 6 decimal precision } -} \ No newline at end of file +} diff --git a/lib/channels/apchannel/Apchannel.Quantower.cs b/lib/channels/apchannel/Apchannel.Quantower.cs index 9ff73d62..01fc89b6 100644 --- a/lib/channels/apchannel/Apchannel.Quantower.cs +++ b/lib/channels/apchannel/Apchannel.Quantower.cs @@ -48,7 +48,10 @@ public sealed class ApchannelIndicator : Indicator, IWatchlistIndicator protected override void OnUpdate(UpdateArgs args) { - if (_apchannel == null) return; + if (_apchannel == null) + { + return; + } var item = HistoricalData[0, SeekOriginHistory.End]; bool isNew = args.IsNewBar(); @@ -75,4 +78,4 @@ public sealed class ApchannelIndicator : Indicator, IWatchlistIndicator // Lower band LinesSeries[2].SetValue(_apchannel.LowerBand, isHot, ShowColdValues); } -} \ No newline at end of file +} diff --git a/lib/channels/apchannel/apchannel.Validation.Tests.cs b/lib/channels/apchannel/apchannel.Validation.Tests.cs index cae51f0b..7ed971dc 100644 --- a/lib/channels/apchannel/apchannel.Validation.Tests.cs +++ b/lib/channels/apchannel/apchannel.Validation.Tests.cs @@ -22,9 +22,16 @@ public sealed class ApchannelValidationTests : IDisposable private void Dispose(bool disposing) { - if (_disposed) return; + if (_disposed) + { + return; + } + _disposed = true; - if (disposing) _testData?.Dispose(); + if (disposing) + { + _testData?.Dispose(); + } } /// diff --git a/lib/channels/apchannel/apchannel.cs b/lib/channels/apchannel/apchannel.cs index c808770f..d7940ff2 100644 --- a/lib/channels/apchannel/apchannel.cs +++ b/lib/channels/apchannel/apchannel.cs @@ -83,7 +83,11 @@ public sealed class Apchannel : AbstractBase /// protected override void Dispose(bool disposing) { - if (_disposed) return; + if (_disposed) + { + return; + } + _disposed = true; if (disposing && _source != null) @@ -174,7 +178,10 @@ public sealed class Apchannel : AbstractBase [MethodImpl(MethodImplOptions.AggressiveInlining)] public TSeries Update(TBarSeries source) { - if (source.Count == 0) return []; + if (source.Count == 0) + { + return []; + } int len = source.Count; var t = new List(len); @@ -207,7 +214,10 @@ public sealed class Apchannel : AbstractBase [MethodImpl(MethodImplOptions.AggressiveInlining)] public override TSeries Update(TSeries source) { - if (source.Count == 0) return []; + if (source.Count == 0) + { + return []; + } int len = source.Count; var t = new List(len); @@ -234,7 +244,9 @@ public sealed class Apchannel : AbstractBase { Init(); if (source.Length == 0) + { return; + } long time = DateTime.UtcNow.Ticks; long dt = step?.Ticks ?? TimeSpan.TicksPerMinute; @@ -281,9 +293,15 @@ public sealed class Apchannel : AbstractBase int length = sourceHigh.Length; if (sourceLow.Length != length) + { throw new ArgumentException("Source arrays must have the same length.", nameof(sourceLow)); + } + if (upperBand.Length != length) + { throw new ArgumentException("Upper band array must match source length.", nameof(upperBand)); + } + if (lowerBand.Length != length) { throw new ArgumentException("Lower band array must match source length.", nameof(lowerBand)); @@ -295,7 +313,9 @@ public sealed class Apchannel : AbstractBase } if (length == 0) + { return; + } double decay = 1.0 - alpha; @@ -324,7 +344,9 @@ public sealed class Apchannel : AbstractBase // Early return for single-element arrays if (length == 1) + { return; + } for (int i = 1; i < length; i++) { @@ -332,8 +354,15 @@ public sealed class Apchannel : AbstractBase double low = sourceLow[i]; // Handle NaN/Infinity - if (!double.IsFinite(high)) high = lastValidHigh; - if (!double.IsFinite(low)) low = lastValidLow; + if (!double.IsFinite(high)) + { + high = lastValidHigh; + } + + if (!double.IsFinite(low)) + { + low = lastValidLow; + } // Use FMA for optimal performance and precision highEma = Math.FusedMultiplyAdd(decay, highEma, alpha * high); @@ -346,4 +375,4 @@ public sealed class Apchannel : AbstractBase lastValidLow = low; } } -} \ No newline at end of file +} diff --git a/lib/channels/apz/Apz.Quantower.Tests.cs b/lib/channels/apz/Apz.Quantower.Tests.cs index e74c845d..a6aa1000 100644 --- a/lib/channels/apz/Apz.Quantower.Tests.cs +++ b/lib/channels/apz/Apz.Quantower.Tests.cs @@ -213,4 +213,4 @@ public class ApzIndicatorTests Assert.Equal(upperDistance, lowerDistance, 6); // 6 decimal precision } -} \ No newline at end of file +} diff --git a/lib/channels/apz/Apz.Quantower.cs b/lib/channels/apz/Apz.Quantower.cs index af073315..6b27e269 100644 --- a/lib/channels/apz/Apz.Quantower.cs +++ b/lib/channels/apz/Apz.Quantower.cs @@ -51,7 +51,10 @@ public sealed class ApzIndicator : Indicator, IWatchlistIndicator protected override void OnUpdate(UpdateArgs args) { - if (_apz == null) return; + if (_apz == null) + { + return; + } var item = HistoricalData[0, SeekOriginHistory.End]; bool isNew = args.IsNewBar(); @@ -78,4 +81,4 @@ public sealed class ApzIndicator : Indicator, IWatchlistIndicator // Lower band LinesSeries[2].SetValue(_apz.Lower.Value, isHot, ShowColdValues); } -} \ No newline at end of file +} diff --git a/lib/channels/apz/Apz.Validation.Tests.cs b/lib/channels/apz/Apz.Validation.Tests.cs index 3266712e..7ea911c3 100644 --- a/lib/channels/apz/Apz.Validation.Tests.cs +++ b/lib/channels/apz/Apz.Validation.Tests.cs @@ -378,9 +378,14 @@ public sealed class ApzValidationTests : IDisposable apzResults.Add(apz.Last.Value); if (emaResults.Count == 0) + { ema = bar.Close; + } else + { ema = alpha * bar.Close + (1 - alpha) * ema; + } + emaResults.Add(ema); } diff --git a/lib/channels/apz/Apz.cs b/lib/channels/apz/Apz.cs index bd2e83a1..a4f5f01d 100644 --- a/lib/channels/apz/Apz.cs +++ b/lib/channels/apz/Apz.cs @@ -116,9 +116,14 @@ public sealed class Apz : ITValuePublisher public Apz(int period, double multiplier = 2.0) { if (period <= 0) + { throw new ArgumentException("Period must be greater than 0", nameof(period)); + } + if (multiplier <= 0) + { throw new ArgumentException("Multiplier must be greater than 0", nameof(multiplier)); + } _period = period; _multiplier = multiplier; @@ -163,19 +168,31 @@ public sealed class Apz : ITValuePublisher private (double price, double high, double low) GetValidValues(double price, double high, double low) { if (double.IsFinite(price)) + { _state.LastValidPrice = price; + } else + { price = _state.LastValidPrice; + } if (double.IsFinite(high)) + { _state.LastValidHigh = high; + } else + { high = _state.LastValidHigh; + } if (double.IsFinite(low)) + { _state.LastValidLow = low; + } else + { low = _state.LastValidLow; + } return (price, high, low); } @@ -206,7 +223,9 @@ public sealed class Apz : ITValuePublisher adaptiveRange *= compensator; if (_state.E <= ConvergenceThreshold) + { _state.IsHot = true; + } } double bandWidth = _multiplier * adaptiveRange; @@ -220,9 +239,13 @@ public sealed class Apz : ITValuePublisher public TValue Update(TBar input, bool isNew = true) { if (isNew) + { _p_state = _state; + } else + { _state = _p_state; + } var (price, high, low) = GetValidValues(input.Close, input.High, input.Low); @@ -237,7 +260,10 @@ public sealed class Apz : ITValuePublisher } double range = high - low; - if (range < 0) range = 0; // Safety check + if (range < 0) + { + range = 0; // Safety check + } var (middle, upper, lower) = Compute(price, range); @@ -255,7 +281,9 @@ public sealed class Apz : ITValuePublisher public (TSeries Middle, TSeries Upper, TSeries Lower) Update(TBarSeries source) { if (source.Count == 0) + { return (new TSeries([], []), new TSeries([], []), new TSeries([], [])); + } int len = source.Count; var tMiddle = new List(len); @@ -316,7 +344,10 @@ public sealed class Apz : ITValuePublisher /// public void Prime(TBarSeries source) { - if (source.Count == 0) return; + if (source.Count == 0) + { + return; + } // Reset state _state = State.New(); @@ -443,15 +474,29 @@ public sealed class Apz : ITValuePublisher { int len = close.Length; if (high.Length != len || low.Length != len) + { throw new ArgumentException("Input spans must have the same length", nameof(high)); - if (outputs.Middle.Length < len || outputs.Upper.Length < len || outputs.Lower.Length < len) - throw new ArgumentException("Output buffers must be at least as long as input", nameof(outputs)); - if (period <= 0) - throw new ArgumentException("Period must be greater than 0", nameof(period)); - if (multiplier <= 0) - throw new ArgumentException("Multiplier must be greater than 0", nameof(multiplier)); + } - if (len == 0) return; + if (outputs.Middle.Length < len || outputs.Upper.Length < len || outputs.Lower.Length < len) + { + throw new ArgumentException("Output buffers must be at least as long as input", nameof(outputs)); + } + + if (period <= 0) + { + throw new ArgumentException("Period must be greater than 0", nameof(period)); + } + + if (multiplier <= 0) + { + throw new ArgumentException("Multiplier must be greater than 0", nameof(multiplier)); + } + + if (len == 0) + { + return; + } CalculateScalarCore(high, low, close, outputs, period, multiplier); } @@ -470,16 +515,29 @@ public sealed class Apz : ITValuePublisher { int len = close.Length; if (high.Length != len || low.Length != len) + { throw new ArgumentException("Input spans must have the same length", nameof(high)); + } + if (outputs.Middle.Length < len || outputs.Upper.Length < len || outputs.Lower.Length < len) + { throw new ArgumentException("Output buffers must be at least as long as input", nameof(outputs)); + } + if (period <= 0) + { throw new ArgumentException("Period must be greater than 0", nameof(period)); + } + if (multiplier <= 0) + { throw new ArgumentException("Multiplier must be greater than 0", nameof(multiplier)); + } if (len == 0) + { return new ScalarState(); + } return CalculateScalarCoreWithState(high, low, close, outputs, period, multiplier); } @@ -540,19 +598,31 @@ public sealed class Apz : ITValuePublisher // Get valid values if (double.IsFinite(price)) + { state.LastValidPrice = price; + } else + { price = state.LastValidPrice; + } if (double.IsFinite(h)) + { state.LastValidHigh = h; + } else + { h = state.LastValidHigh; + } if (double.IsFinite(l)) + { state.LastValidLow = l; + } else + { l = state.LastValidLow; + } // Handle first valid value if (double.IsNaN(price)) @@ -585,7 +655,9 @@ public sealed class Apz : ITValuePublisher adaptiveRange *= compensator; if (state.E <= ConvergenceThreshold) + { state.IsHot = true; + } } double bandWidth = multiplier * adaptiveRange; @@ -626,4 +698,4 @@ public sealed class Apz : ITValuePublisher var results = apz.Update(source); return (results, apz); } -} \ No newline at end of file +} diff --git a/lib/channels/atrbands/AtrBands.Quantower.Tests.cs b/lib/channels/atrbands/AtrBands.Quantower.Tests.cs index 02818042..9167b0a2 100644 --- a/lib/channels/atrbands/AtrBands.Quantower.Tests.cs +++ b/lib/channels/atrbands/AtrBands.Quantower.Tests.cs @@ -189,4 +189,4 @@ public class AtrBandsIndicatorTests indicator.Multiplier = 3.5; Assert.Equal(3.5, indicator.Multiplier); } -} \ No newline at end of file +} diff --git a/lib/channels/atrbands/AtrBands.Quantower.cs b/lib/channels/atrbands/AtrBands.Quantower.cs index 0290e7b6..2087a6bc 100644 --- a/lib/channels/atrbands/AtrBands.Quantower.cs +++ b/lib/channels/atrbands/AtrBands.Quantower.cs @@ -51,7 +51,10 @@ public sealed class AtrBandsIndicator : Indicator, IWatchlistIndicator protected override void OnUpdate(UpdateArgs args) { - if (_atrBands == null) return; + if (_atrBands == null) + { + return; + } var item = HistoricalData[0, SeekOriginHistory.End]; bool isNew = args.IsNewBar(); @@ -78,4 +81,4 @@ public sealed class AtrBandsIndicator : Indicator, IWatchlistIndicator // Lower band LinesSeries[2].SetValue(_atrBands.Lower.Value, isHot, ShowColdValues); } -} \ No newline at end of file +} diff --git a/lib/channels/atrbands/AtrBands.Tests.cs b/lib/channels/atrbands/AtrBands.Tests.cs index ddfb8b7d..fc1ffe93 100644 --- a/lib/channels/atrbands/AtrBands.Tests.cs +++ b/lib/channels/atrbands/AtrBands.Tests.cs @@ -708,4 +708,4 @@ public class AtrBandsTests // Bands should be symmetric around middle Assert.Equal(upperDist, lowerDist, 1e-10); } -} \ No newline at end of file +} diff --git a/lib/channels/atrbands/AtrBands.cs b/lib/channels/atrbands/AtrBands.cs index 4c97bc20..272bc0cf 100644 --- a/lib/channels/atrbands/AtrBands.cs +++ b/lib/channels/atrbands/AtrBands.cs @@ -114,9 +114,14 @@ public sealed class AtrBands : ITValuePublisher, IDisposable public AtrBands(int period, double multiplier = 2.0) { if (period <= 0) + { throw new ArgumentException("Period must be greater than 0", nameof(period)); + } + if (multiplier <= 0) + { throw new ArgumentException("Multiplier must be greater than 0", nameof(multiplier)); + } _period = period; _multiplier = multiplier; @@ -145,7 +150,11 @@ public sealed class AtrBands : ITValuePublisher, IDisposable /// public void Dispose() { - if (_disposed) return; + if (_disposed) + { + return; + } + _disposed = true; if (_source is not null) @@ -173,7 +182,9 @@ public sealed class AtrBands : ITValuePublisher, IDisposable private double CalculateTrueRange(double high, double low, double prevClose) { if (double.IsNaN(prevClose)) + { return high - low; + } double hl = high - low; double hpc = Math.Abs(high - prevClose); @@ -188,9 +199,13 @@ public sealed class AtrBands : ITValuePublisher, IDisposable public TValue Update(TBar input, bool isNew = true) { if (isNew) + { _p_state = _state; + } else + { _state = _p_state; + } // Get valid values with last-value substitution double source = input.Close; @@ -198,10 +213,41 @@ public sealed class AtrBands : ITValuePublisher, IDisposable double low = input.Low; double close = input.Close; - if (double.IsFinite(source)) _state.LastValidSource = source; else source = _state.LastValidSource; - if (double.IsFinite(high)) _state.LastValidHigh = high; else high = _state.LastValidHigh; - if (double.IsFinite(low)) _state.LastValidLow = low; else low = _state.LastValidLow; - if (double.IsFinite(close)) _state.LastValidClose = close; else close = _state.LastValidClose; + if (double.IsFinite(source)) + { + _state.LastValidSource = source; + } + else + { + source = _state.LastValidSource; + } + + if (double.IsFinite(high)) + { + _state.LastValidHigh = high; + } + else + { + high = _state.LastValidHigh; + } + + if (double.IsFinite(low)) + { + _state.LastValidLow = low; + } + else + { + low = _state.LastValidLow; + } + + if (double.IsFinite(close)) + { + _state.LastValidClose = close; + } + else + { + close = _state.LastValidClose; + } // Handle first valid value initialization if (double.IsNaN(source)) @@ -248,7 +294,9 @@ public sealed class AtrBands : ITValuePublisher, IDisposable double width = atr * _multiplier; if (isNew) + { _state.PrevClose = close; + } Last = new TValue(input.Time, middle); Upper = new TValue(input.Time, middle + width); @@ -264,7 +312,9 @@ public sealed class AtrBands : ITValuePublisher, IDisposable public (TSeries Middle, TSeries Upper, TSeries Lower) Update(TBarSeries source) { if (source.Count == 0) + { return (new TSeries([], []), new TSeries([], []), new TSeries([], [])); + } int len = source.Count; var tMiddle = new List(len); @@ -305,7 +355,10 @@ public sealed class AtrBands : ITValuePublisher, IDisposable /// public void Prime(TBarSeries source) { - if (source.Count == 0) return; + if (source.Count == 0) + { + return; + } // Reset state _sourceBuffer.Clear(); @@ -325,11 +378,19 @@ public sealed class AtrBands : ITValuePublisher, IDisposable _state.LastValidClose = bar.Close; } if (double.IsFinite(bar.High) && double.IsNaN(_state.LastValidHigh)) + { _state.LastValidHigh = bar.High; + } + if (double.IsFinite(bar.Low) && double.IsNaN(_state.LastValidLow)) + { _state.LastValidLow = bar.Low; + } + if (!double.IsNaN(_state.LastValidSource) && !double.IsNaN(_state.LastValidHigh) && !double.IsNaN(_state.LastValidLow)) + { break; + } } // Find valid values in warmup window if not found @@ -396,15 +457,29 @@ public sealed class AtrBands : ITValuePublisher, IDisposable { int len = input.Close.Length; if (input.High.Length != len || input.Low.Length != len) + { throw new ArgumentException("High, Low, and Close must have the same length", nameof(input)); - if (output.Middle.Length < len || output.Upper.Length < len || output.Lower.Length < len) - throw new ArgumentException("Output buffers must be at least as long as input", nameof(output)); - if (period <= 0) - throw new ArgumentException("Period must be greater than 0", nameof(period)); - if (multiplier <= 0) - throw new ArgumentException("Multiplier must be greater than 0", nameof(multiplier)); + } - if (len == 0) return; + if (output.Middle.Length < len || output.Upper.Length < len || output.Lower.Length < len) + { + throw new ArgumentException("Output buffers must be at least as long as input", nameof(output)); + } + + if (period <= 0) + { + throw new ArgumentException("Period must be greater than 0", nameof(period)); + } + + if (multiplier <= 0) + { + throw new ArgumentException("Multiplier must be greater than 0", nameof(multiplier)); + } + + if (len == 0) + { + return; + } CalculateScalarCore(input.High, input.Low, input.Close, output.Middle, output.Upper, output.Lower, period, multiplier); } @@ -521,9 +596,32 @@ public sealed class AtrBands : ITValuePublisher, IDisposable double c = close[i]; // Get valid values - if (double.IsFinite(h)) lastValidHigh = h; else h = lastValidHigh; - if (double.IsFinite(l)) lastValidLow = l; else l = lastValidLow; - if (double.IsFinite(c)) lastValidClose = c; else c = lastValidClose; + if (double.IsFinite(h)) + { + lastValidHigh = h; + } + else + { + h = lastValidHigh; + } + + if (double.IsFinite(l)) + { + lastValidLow = l; + } + else + { + l = lastValidLow; + } + + if (double.IsFinite(c)) + { + lastValidClose = c; + } + else + { + c = lastValidClose; + } if (double.IsNaN(c)) { @@ -536,7 +634,9 @@ public sealed class AtrBands : ITValuePublisher, IDisposable // Calculate True Range double tr; if (double.IsNaN(prevClose)) + { tr = h - l; + } else { double hl = h - l; @@ -591,4 +691,4 @@ public sealed class AtrBands : ITValuePublisher, IDisposable var results = atrBands.Update(source); return (results, atrBands); } -} \ No newline at end of file +} diff --git a/lib/channels/bbands/Bbands.Quantower.Tests.cs b/lib/channels/bbands/Bbands.Quantower.Tests.cs index 7debecac..831080df 100644 --- a/lib/channels/bbands/Bbands.Quantower.Tests.cs +++ b/lib/channels/bbands/Bbands.Quantower.Tests.cs @@ -184,4 +184,4 @@ public class BbandsIndicatorTests Assert.True(double.IsFinite(series.GetValue(0))); } } -} \ No newline at end of file +} diff --git a/lib/channels/bbands/Bbands.Quantower.cs b/lib/channels/bbands/Bbands.Quantower.cs index 9fdebf9d..621e4506 100644 --- a/lib/channels/bbands/Bbands.Quantower.cs +++ b/lib/channels/bbands/Bbands.Quantower.cs @@ -72,4 +72,4 @@ public class BbandsIndicator : Indicator, IWatchlistIndicator WidthSeries!.SetValue(bbands.Width.Value, bbands.IsHot, ShowColdValues); PercentBSeries!.SetValue(bbands.PercentB.Value, bbands.IsHot, ShowColdValues); } -} \ No newline at end of file +} diff --git a/lib/channels/bbands/Bbands.Tests.cs b/lib/channels/bbands/Bbands.Tests.cs index 7cd9068f..002a346f 100644 --- a/lib/channels/bbands/Bbands.Tests.cs +++ b/lib/channels/bbands/Bbands.Tests.cs @@ -318,4 +318,4 @@ public class BbandsTests Assert.Equal(batchResult[^1].Value, streamingBbands.Middle.Value, precision: 8); Assert.Equal(middleArray[^1], streamingBbands.Middle.Value, precision: 8); } -} \ No newline at end of file +} diff --git a/lib/channels/bbands/Bbands.Validation.Tests.cs b/lib/channels/bbands/Bbands.Validation.Tests.cs index 8cca14ab..40b21a33 100644 --- a/lib/channels/bbands/Bbands.Validation.Tests.cs +++ b/lib/channels/bbands/Bbands.Validation.Tests.cs @@ -372,4 +372,4 @@ public sealed class BbandsValidationTests : IDisposable } _output.WriteLine("Bbands Batch(TSeries) validated successfully against Ooples"); } -} \ No newline at end of file +} diff --git a/lib/channels/bbands/Bbands.cs b/lib/channels/bbands/Bbands.cs index 8348cc45..4dc9ba5a 100644 --- a/lib/channels/bbands/Bbands.cs +++ b/lib/channels/bbands/Bbands.cs @@ -204,7 +204,7 @@ public sealed class Bbands : AbstractBase { step ??= TimeSpan.FromSeconds(1); DateTime startTime = DateTime.UtcNow; - + for (int i = 0; i < source.Length; i++) { Update(new TValue(startTime + i * step.Value, source[i]), isNew: true); @@ -320,4 +320,4 @@ public sealed class Bbands : AbstractBase lower[i] = middle[i] - offset; } } -} \ No newline at end of file +} diff --git a/lib/channels/dchannel/Dchannel.Quantower.cs b/lib/channels/dchannel/Dchannel.Quantower.cs index 29020988..03a6f700 100644 --- a/lib/channels/dchannel/Dchannel.Quantower.cs +++ b/lib/channels/dchannel/Dchannel.Quantower.cs @@ -42,7 +42,9 @@ public sealed class DchannelIndicator : Indicator, IWatchlistIndicator protected override void OnUpdate(UpdateArgs args) { if (_indicator is null) + { return; + } var item = HistoricalData[0, SeekOriginHistory.End]; bool isNew = args.IsNewBar(); diff --git a/lib/channels/dchannel/Dchannel.cs b/lib/channels/dchannel/Dchannel.cs index 28eda783..73ab2059 100644 --- a/lib/channels/dchannel/Dchannel.cs +++ b/lib/channels/dchannel/Dchannel.cs @@ -42,7 +42,9 @@ public sealed class Dchannel : ITValuePublisher public Dchannel(int period) { if (period <= 0) + { throw new ArgumentException("Period must be greater than 0", nameof(period)); + } _period = period; _hBuf = new double[_period]; @@ -74,14 +76,22 @@ public sealed class Dchannel : ITValuePublisher private (double high, double low) GetValid(double high, double low) { if (double.IsFinite(high)) + { _state = _state with { LastValidHigh = high }; + } else + { high = _state.LastValidHigh; + } if (double.IsFinite(low)) + { _state = _state with { LastValidLow = low }; + } else + { low = _state.LastValidLow; + } return (high, low); } @@ -90,15 +100,21 @@ public sealed class Dchannel : ITValuePublisher public TValue Update(TBar input, bool isNew = true) { if (isNew) + { _p_state = _state; + } else + { _state = _p_state; + } if (isNew) { _index++; if (_count < _period) + { _count++; + } } int bufIdx = (int)(_index % _period); @@ -134,7 +150,9 @@ public sealed class Dchannel : ITValuePublisher double mid = (top + bot) * 0.5; if (!IsHot && _count >= _period) + { _state = _state with { IsHot = true }; + } Last = new TValue(input.Time, mid); Upper = new TValue(input.Time, top); @@ -147,7 +165,9 @@ public sealed class Dchannel : ITValuePublisher public (TSeries Middle, TSeries Upper, TSeries Lower) Update(TBarSeries source) { if (source.Count == 0) + { return (new TSeries([], []), new TSeries([], []), new TSeries([], [])); + } int len = source.Count; var tMiddle = new List(len); @@ -191,7 +211,9 @@ public sealed class Dchannel : ITValuePublisher Reset(); if (source.Count == 0) + { return; + } for (int i = 0; i < source.Count; i++) { @@ -226,14 +248,25 @@ public sealed class Dchannel : ITValuePublisher int period) { if (period <= 0) + { throw new ArgumentException("Period must be greater than 0", nameof(period)); + } + if (high.Length != low.Length) + { throw new ArgumentException("High and Low spans must have the same length", nameof(high)); + } + if (middle.Length < high.Length || upper.Length < high.Length || lower.Length < high.Length) + { throw new ArgumentException("Output spans must be at least as long as inputs", nameof(middle)); + } int len = high.Length; - if (len == 0) return; + if (len == 0) + { + return; + } double[] top = ArrayPool.Shared.Rent(len); double[] bot = ArrayPool.Shared.Rent(len); @@ -295,4 +328,4 @@ public sealed class Dchannel : ITValuePublisher var results = indicator.Update(source); return (results, indicator); } -} \ No newline at end of file +} diff --git a/lib/channels/decaychannel/Decaychannel.Quantower.cs b/lib/channels/decaychannel/Decaychannel.Quantower.cs index e28e6d1b..c9682e0f 100644 --- a/lib/channels/decaychannel/Decaychannel.Quantower.cs +++ b/lib/channels/decaychannel/Decaychannel.Quantower.cs @@ -42,7 +42,9 @@ public sealed class DecaychannelIndicator : Indicator, IWatchlistIndicator protected override void OnUpdate(UpdateArgs args) { if (_indicator is null) + { return; + } var item = HistoricalData[0, SeekOriginHistory.End]; bool isNew = args.IsNewBar(); diff --git a/lib/channels/decaychannel/Decaychannel.cs b/lib/channels/decaychannel/Decaychannel.cs index 60ef6c26..dd4607d3 100644 --- a/lib/channels/decaychannel/Decaychannel.cs +++ b/lib/channels/decaychannel/Decaychannel.cs @@ -58,7 +58,9 @@ public sealed class Decaychannel : ITValuePublisher public Decaychannel(int period) { if (period <= 0) + { throw new ArgumentException("Period must be greater than 0", nameof(period)); + } _period = period; _decayLambda = Math.Log(2.0) / period; @@ -98,14 +100,22 @@ public sealed class Decaychannel : ITValuePublisher private (double high, double low) GetValid(double high, double low) { if (double.IsFinite(high)) + { _state = _state with { LastValidHigh = high }; + } else + { high = _state.LastValidHigh; + } if (double.IsFinite(low)) + { _state = _state with { LastValidLow = low }; + } else + { low = _state.LastValidLow; + } return (high, low); } @@ -151,7 +161,9 @@ public sealed class Decaychannel : ITValuePublisher { int len = Math.Min(_count, _period); if (len == 0) + { return (double.NaN, double.NaN); + } double max = double.MinValue; double min = double.MaxValue; @@ -159,13 +171,23 @@ public sealed class Decaychannel : ITValuePublisher for (int i = 0; i < len; i++) { int idx = (int)((_index - i) % _period); - if (idx < 0) idx += _period; + if (idx < 0) + { + idx += _period; + } double h = _hBuf[idx]; double l = _lBuf[idx]; - if (h > max) max = h; - if (l < min) min = l; + if (h > max) + { + max = h; + } + + if (l < min) + { + min = l; + } } return (max, min); @@ -183,7 +205,9 @@ public sealed class Decaychannel : ITValuePublisher // Now advance to new bar _index++; if (_count < _period) + { _count++; + } } else { @@ -193,7 +217,9 @@ public sealed class Decaychannel : ITValuePublisher // Re-advance to current bar position (we're reprocessing current bar) _index++; if (_count < _period) + { _count++; + } } int bufIdx = (int)(_index % _period); @@ -286,7 +312,9 @@ public sealed class Decaychannel : ITValuePublisher public (TSeries Middle, TSeries Upper, TSeries Lower) Update(TBarSeries source) { if (source.Count == 0) + { return (new TSeries([], []), new TSeries([], []), new TSeries([], [])); + } int len = source.Count; var tMiddle = new List(len); @@ -329,7 +357,9 @@ public sealed class Decaychannel : ITValuePublisher Reset(); if (source.Count == 0) + { return; + } for (int i = 0; i < source.Count; i++) { @@ -370,14 +400,25 @@ public sealed class Decaychannel : ITValuePublisher int period) { if (period <= 0) + { throw new ArgumentException("Period must be greater than 0", nameof(period)); + } + if (high.Length != low.Length) + { throw new ArgumentException("High and Low spans must have the same length", nameof(high)); + } + if (middle.Length < high.Length || upper.Length < high.Length || lower.Length < high.Length) + { throw new ArgumentException("Output spans must be at least as long as inputs", nameof(middle)); + } int len = high.Length; - if (len == 0) return; + if (len == 0) + { + return; + } double decayLambda = Math.Log(2.0) / period; diff --git a/lib/channels/fcb/Fcb.Quantower.cs b/lib/channels/fcb/Fcb.Quantower.cs index af15d89f..f3afbffa 100644 --- a/lib/channels/fcb/Fcb.Quantower.cs +++ b/lib/channels/fcb/Fcb.Quantower.cs @@ -44,7 +44,9 @@ public sealed class FcbIndicator : Indicator, IWatchlistIndicator protected override void OnUpdate(UpdateArgs args) { if (_indicator is null) + { return; + } var item = HistoricalData[0, SeekOriginHistory.End]; bool isNew = args.IsNewBar(); diff --git a/lib/channels/fcb/Fcb.cs b/lib/channels/fcb/Fcb.cs index b0429664..1254a7de 100644 --- a/lib/channels/fcb/Fcb.cs +++ b/lib/channels/fcb/Fcb.cs @@ -59,7 +59,9 @@ public sealed class Fcb : ITValuePublisher public Fcb(int period = 20) { if (period < 1) + { throw new ArgumentOutOfRangeException(nameof(period), "Period must be >= 1."); + } _period = period; WarmupPeriod = period + 2; // Need 2 extra bars for fractal detection @@ -91,14 +93,22 @@ public sealed class Fcb : ITValuePublisher private (double high, double low) GetValid(double high, double low) { if (double.IsFinite(high)) + { _state = _state with { LastValidHigh = high }; + } else + { high = _state.LastValidHigh; + } if (double.IsFinite(low)) + { _state = _state with { LastValidLow = low }; + } else + { low = _state.LastValidLow; + } return (high, low); } @@ -120,9 +130,13 @@ public sealed class Fcb : ITValuePublisher int backIdx = (_hHead + _hCount - 1) % _period; int bufIdx = _hDeque[backIdx] % _period; if (_hBuf[bufIdx] <= value) + { _hCount--; + } else + { break; + } } int tail = (_hHead + _hCount) % _period; @@ -145,9 +159,13 @@ public sealed class Fcb : ITValuePublisher int backIdx = (_lHead + _lCount - 1) % _period; int bufIdx = _lDeque[backIdx] % _period; if (_lBuf[bufIdx] >= value) + { _lCount--; + } else + { break; + } } int tail = (_lHead + _lCount) % _period; @@ -163,7 +181,9 @@ public sealed class Fcb : ITValuePublisher _lCount = 0; if (_count == 0) + { return; + } long startLogical = _index - _count + 1; for (int i = 0; i < _count; i++) @@ -199,15 +219,21 @@ public sealed class Fcb : ITValuePublisher public TValue Update(TBar input, bool isNew = true) { if (isNew) + { _p_state = _state; + } else + { _state = _p_state; + } if (isNew) { _index++; if (_count < _period) + { _count++; + } } var (high, low) = GetValid(input.High, input.Low); @@ -272,7 +298,9 @@ public sealed class Fcb : ITValuePublisher double mid = (top + bot) * 0.5; if (!_state.IsHot && _index + 1 >= WarmupPeriod) + { _state = _state with { IsHot = true }; + } Last = new TValue(input.Time, mid); Upper = new TValue(input.Time, top); @@ -285,7 +313,9 @@ public sealed class Fcb : ITValuePublisher public (TSeries Middle, TSeries Upper, TSeries Lower) Update(TBarSeries source) { if (source.Count == 0) + { return (new TSeries([], []), new TSeries([], []), new TSeries([], [])); + } int len = source.Count; var tMiddle = new List(len); @@ -329,7 +359,9 @@ public sealed class Fcb : ITValuePublisher Reset(); if (source.Count == 0) + { return; + } for (int i = 0; i < source.Count; i++) { @@ -349,14 +381,25 @@ public sealed class Fcb : ITValuePublisher int period) { if (period < 1) + { throw new ArgumentOutOfRangeException(nameof(period), "Period must be >= 1."); + } + if (high.Length != low.Length) + { throw new ArgumentException("High and Low spans must have the same length", nameof(high)); + } + if (middle.Length < high.Length || upper.Length < high.Length || lower.Length < high.Length) + { throw new ArgumentException("Output spans must be at least as long as inputs", nameof(middle)); + } int len = high.Length; - if (len == 0) return; + if (len == 0) + { + return; + } // Allocate buffers for fractal tracking and deques double[] hBuf = ArrayPool.Shared.Rent(period); @@ -389,9 +432,14 @@ public sealed class Fcb : ITValuePublisher if (i >= 2) { if (h1 > h2 && h1 > h0) + { hiFractal = h1; + } + if (l1 < l2 && l1 < l0) + { loFractal = l1; + } } int bufIdx = i % period; @@ -410,9 +458,13 @@ public sealed class Fcb : ITValuePublisher int backIdx = (hHead + hCount - 1) % period; int bIdx = hDeque[backIdx] % period; if (hBuf[bIdx] <= hiFractal) + { hCount--; + } else + { break; + } } int tail = (hHead + hCount) % period; hDeque[tail] = i; @@ -429,9 +481,13 @@ public sealed class Fcb : ITValuePublisher int backIdx = (lHead + lCount - 1) % period; int bIdx = lDeque[backIdx] % period; if (lBuf[bIdx] >= loFractal) + { lCount--; + } else + { break; + } } tail = (lHead + lCount) % period; lDeque[tail] = i; diff --git a/lib/channels/jbands/Jbands.Quantower.cs b/lib/channels/jbands/Jbands.Quantower.cs index 5c857e11..4e40543b 100644 --- a/lib/channels/jbands/Jbands.Quantower.cs +++ b/lib/channels/jbands/Jbands.Quantower.cs @@ -49,7 +49,9 @@ public sealed class JbandsIndicator : Indicator, IWatchlistIndicator protected override void OnUpdate(UpdateArgs args) { if (_indicator is null) + { return; + } var item = HistoricalData[0, SeekOriginHistory.End]; bool isNew = args.IsNewBar(); diff --git a/lib/channels/jbands/Jbands.Tests.cs b/lib/channels/jbands/Jbands.Tests.cs index ceb5c977..4ad4b017 100644 --- a/lib/channels/jbands/Jbands.Tests.cs +++ b/lib/channels/jbands/Jbands.Tests.cs @@ -215,7 +215,9 @@ public class JbandsTests var gbm = new GBM(startPrice: 100, mu: 0.01, sigma: 0.1, seed: 123); double[] source = new double[100]; for (int i = 0; i < source.Length; i++) + { source[i] = gbm.Next().Close; + } double[] middle = new double[100]; double[] upper = new double[100]; diff --git a/lib/channels/jbands/Jbands.Validation.Tests.cs b/lib/channels/jbands/Jbands.Validation.Tests.cs index 14aea175..595e677e 100644 --- a/lib/channels/jbands/Jbands.Validation.Tests.cs +++ b/lib/channels/jbands/Jbands.Validation.Tests.cs @@ -95,7 +95,9 @@ public class JbandsValidationTests var gbm = new GBM(startPrice: 100, mu: 0.01, sigma: 0.1, seed: 777); double[] source = new double[200]; for (int i = 0; i < source.Length; i++) + { source[i] = gbm.Next().Close; + } double[] middle = new double[200]; double[] upper = new double[200]; diff --git a/lib/channels/jbands/Jbands.cs b/lib/channels/jbands/Jbands.cs index b7486838..deb644d5 100644 --- a/lib/channels/jbands/Jbands.cs +++ b/lib/channels/jbands/Jbands.cs @@ -59,17 +59,28 @@ public sealed class Jbands : ITValuePublisher public Jbands(int period, int phase = 0, double power = 0.45) { if (period < 1) + { throw new ArgumentOutOfRangeException(nameof(period), "Period must be >= 1."); + } + if (!double.IsFinite(power)) + { throw new ArgumentException("Power must be finite.", nameof(power)); + } // Phase parameter: maps -100..100 -> 0.5..2.5 if (phase < -100) + { _phaseParam = 0.5; + } else if (phase > 100) + { _phaseParam = 2.5; + } else + { _phaseParam = (phase * 0.01) + 1.5; + } // Length / log / divider parameters from decompiled JMA double lengthParam = period < 1.0000000002 @@ -129,7 +140,10 @@ public sealed class Jbands : ITValuePublisher if (!double.IsFinite(value)) { if (_state.Bars == 0) + { return (double.NaN, double.NaN, double.NaN); + } + value = _state.LastPrice; } else @@ -139,7 +153,9 @@ public sealed class Jbands : ITValuePublisher _state.Bars++; if (_state.Bars == 1) + { return InitializeFirstBar(value); + } return CalculateJbands(value); } @@ -210,8 +226,16 @@ public sealed class Jbands : ITValuePublisher { double ratio = Math.Max(absValue / refVolatility, 0.0); double d = Math.Pow(ratio, _pExponent); - if (d > _logParam) d = _logParam; - if (d < 1.0) d = 1.0; + if (d > _logParam) + { + d = _logParam; + } + + if (d < 1.0) + { + d = 1.0; + } + return d; } @@ -266,7 +290,9 @@ public sealed class Jbands : ITValuePublisher public (TSeries Middle, TSeries Upper, TSeries Lower) Update(TSeries source) { if (source.Count == 0) + { return (new TSeries([], []), new TSeries([], []), new TSeries([], [])); + } int len = source.Count; var tMiddle = new List(len); @@ -319,7 +345,10 @@ public sealed class Jbands : ITValuePublisher public void Prime(TSeries source) { Reset(); - if (source.Count == 0) return; + if (source.Count == 0) + { + return; + } for (int i = 0; i < source.Count; i++) { @@ -343,13 +372,24 @@ public sealed class Jbands : ITValuePublisher double power = 0.45) { if (middle.Length != source.Length) + { throw new ArgumentException("Source and middle must have the same length.", nameof(middle)); + } + if (upper.Length != source.Length) + { throw new ArgumentException("Source and upper must have the same length.", nameof(upper)); + } + if (lower.Length != source.Length) + { throw new ArgumentException("Source and lower must have the same length.", nameof(lower)); + } + if (source.Length == 0) + { return; + } var jbands = new Jbands(period, phase, power); for (int i = 0; i < source.Length; i++) @@ -366,7 +406,9 @@ public sealed class Jbands : ITValuePublisher { int count = _volBuffer.Count; if (count < 16) + { return fallback; + } Span sorted = stackalloc double[count]; _volBuffer.CopyTo(sorted); @@ -387,8 +429,15 @@ public sealed class Jbands : ITValuePublisher end = drop + slice - 1; } - if (start < 0) start = 0; - if (end >= count) end = count - 1; + if (start < 0) + { + start = 0; + } + + if (end >= count) + { + end = count - 1; + } int len = end - start + 1; return sorted.Slice(start, len).SumSIMD() / len; diff --git a/lib/channels/kchannel/Kchannel.Quantower.cs b/lib/channels/kchannel/Kchannel.Quantower.cs index 6d64d55a..50ea1bc3 100644 --- a/lib/channels/kchannel/Kchannel.Quantower.cs +++ b/lib/channels/kchannel/Kchannel.Quantower.cs @@ -48,7 +48,9 @@ public sealed class KchannelIndicator : Indicator, IWatchlistIndicator protected override void OnUpdate(UpdateArgs args) { if (_indicator is null) + { return; + } var item = HistoricalData[0, SeekOriginHistory.End]; bool isNew = args.IsNewBar(); diff --git a/lib/channels/kchannel/Kchannel.Validation.Tests.cs b/lib/channels/kchannel/Kchannel.Validation.Tests.cs index 8e54a598..e9f711e4 100644 --- a/lib/channels/kchannel/Kchannel.Validation.Tests.cs +++ b/lib/channels/kchannel/Kchannel.Validation.Tests.cs @@ -449,7 +449,9 @@ public sealed class KchannelValidationTests : IDisposable // After warmup, values should be within 5% (warmup methods may differ) if (midPct < 0.05) + { closeCount++; + } } } diff --git a/lib/channels/kchannel/Kchannel.cs b/lib/channels/kchannel/Kchannel.cs index a64b8bcf..bf2cc966 100644 --- a/lib/channels/kchannel/Kchannel.cs +++ b/lib/channels/kchannel/Kchannel.cs @@ -52,9 +52,14 @@ public sealed class Kchannel : ITValuePublisher public Kchannel(int period = 20, double multiplier = 2.0) { if (period < 1) + { throw new ArgumentOutOfRangeException(nameof(period), "Period must be >= 1."); + } + if (multiplier <= 0.0) + { throw new ArgumentOutOfRangeException(nameof(multiplier), "Multiplier must be > 0."); + } _period = period; _multiplier = multiplier; @@ -95,19 +100,31 @@ public sealed class Kchannel : ITValuePublisher private (double close, double high, double low) GetValid(double close, double high, double low) { if (double.IsFinite(close)) + { _state = _state with { LastValidClose = close }; + } else + { close = _state.LastValidClose; + } if (double.IsFinite(high)) + { _state = _state with { LastValidHigh = high }; + } else + { high = _state.LastValidHigh; + } if (double.IsFinite(low)) + { _state = _state with { LastValidLow = low }; + } else + { low = _state.LastValidLow; + } return (close, high, low); } @@ -116,9 +133,13 @@ public sealed class Kchannel : ITValuePublisher public TValue Update(TBar input, bool isNew = true) { if (isNew) + { _p_state = _state; + } else + { _state = _p_state; + } var (close, high, low) = GetValid(input.Close, input.High, input.Low); @@ -144,7 +165,9 @@ public sealed class Kchannel : ITValuePublisher } if (isNew) + { _state = _state with { Bars = _state.Bars + 1 }; + } // EMA with warmup compensation (sum/weight approach) double newSum = Math.FusedMultiplyAdd(_state.EmaSum, 1.0 - _emaAlpha, close * _emaAlpha); @@ -179,7 +202,9 @@ public sealed class Kchannel : ITValuePublisher double lower = emaValue - width; if (!_state.IsHot && _state.Bars >= WarmupPeriod) + { _state = _state with { IsHot = true }; + } Last = new TValue(input.Time, emaValue); Upper = new TValue(input.Time, upper); @@ -192,7 +217,9 @@ public sealed class Kchannel : ITValuePublisher public (TSeries Middle, TSeries Upper, TSeries Lower) Update(TBarSeries source) { if (source.Count == 0) + { return (new TSeries([], []), new TSeries([], []), new TSeries([], [])); + } int len = source.Count; var tMiddle = new List(len); @@ -237,7 +264,9 @@ public sealed class Kchannel : ITValuePublisher Reset(); if (source.Count == 0) + { return; + } for (int i = 0; i < source.Count; i++) { @@ -259,16 +288,30 @@ public sealed class Kchannel : ITValuePublisher double multiplier = 2.0) { if (period < 1) + { throw new ArgumentOutOfRangeException(nameof(period), "Period must be >= 1."); + } + if (multiplier <= 0.0) + { throw new ArgumentOutOfRangeException(nameof(multiplier), "Multiplier must be > 0."); + } + if (high.Length != low.Length || high.Length != close.Length) + { throw new ArgumentException("High, Low, and Close spans must have the same length", nameof(high)); + } + if (middle.Length < high.Length || upper.Length < high.Length || lower.Length < high.Length) + { throw new ArgumentException("Output spans must be at least as long as inputs", nameof(middle)); + } int len = high.Length; - if (len == 0) return; + if (len == 0) + { + return; + } double emaAlpha = 2.0 / (period + 1); double atrAlpha = 1.0 / period; diff --git a/lib/channels/maenv/Maenv.Quantower.cs b/lib/channels/maenv/Maenv.Quantower.cs index 5b9ed690..428be388 100644 --- a/lib/channels/maenv/Maenv.Quantower.cs +++ b/lib/channels/maenv/Maenv.Quantower.cs @@ -53,7 +53,9 @@ public sealed class MaenvIndicator : Indicator, IWatchlistIndicator protected override void OnUpdate(UpdateArgs args) { if (_indicator is null) + { return; + } var item = HistoricalData[0, SeekOriginHistory.End]; bool isNew = args.IsNewBar(); diff --git a/lib/channels/maenv/Maenv.cs b/lib/channels/maenv/Maenv.cs index ba0ac540..ed8baef4 100644 --- a/lib/channels/maenv/Maenv.cs +++ b/lib/channels/maenv/Maenv.cs @@ -72,9 +72,14 @@ public sealed class Maenv : ITValuePublisher public Maenv(int period = 20, double percentage = 1.0, MaenvType maType = MaenvType.EMA) { if (period < 1) + { throw new ArgumentOutOfRangeException(nameof(period), "Period must be >= 1."); + } + if (percentage <= 0.0) + { throw new ArgumentOutOfRangeException(nameof(percentage), "Percentage must be > 0."); + } _period = period; _percentage = percentage; @@ -142,7 +147,10 @@ public sealed class Maenv : ITValuePublisher if (double.IsFinite(value)) { if (isNew) + { _state = _state with { LastValid = value }; + } + return value; } return _state.LastValid; @@ -159,23 +167,35 @@ public sealed class Maenv : ITValuePublisher { _p_state = _state; if (_smaBuffer != null && _p_smaBuffer != null) + { Array.Copy(_smaBuffer, _p_smaBuffer, _period); + } + if (_wmaBuffer != null && _p_wmaBuffer != null) + { Array.Copy(_wmaBuffer, _p_wmaBuffer, _period); + } } else { _state = _p_state; if (_smaBuffer != null && _p_smaBuffer != null) + { Array.Copy(_p_smaBuffer, _smaBuffer, _period); + } + if (_wmaBuffer != null && _p_wmaBuffer != null) + { Array.Copy(_p_wmaBuffer, _wmaBuffer, _period); + } } double value = GetValid(input.Value, isNew); if (isNew) + { _state = _state with { Bars = _state.Bars + 1 }; + } double middle = _maType switch { @@ -190,7 +210,9 @@ public sealed class Maenv : ITValuePublisher double lower = middle - dist; if (!_state.IsHot && _state.Bars >= WarmupPeriod) + { _state = _state with { IsHot = true }; + } Last = new TValue(input.Time, middle); Upper = new TValue(input.Time, upper); @@ -203,7 +225,9 @@ public sealed class Maenv : ITValuePublisher public (TSeries Middle, TSeries Upper, TSeries Lower) Update(TSeries source) { if (source.Count == 0) + { return (new TSeries([], []), new TSeries([], []), new TSeries([], [])); + } int len = source.Count; var tMiddle = new List(len); @@ -249,7 +273,10 @@ public sealed class Maenv : ITValuePublisher [MethodImpl(MethodImplOptions.AggressiveInlining)] private double CalculateSMA(double value, bool isNew) { - if (_smaBuffer == null) return value; + if (_smaBuffer == null) + { + return value; + } // Calculate new count (always increment if not full, for both isNew cases) int currentCount = _state.SmaCount; @@ -323,7 +350,10 @@ public sealed class Maenv : ITValuePublisher [MethodImpl(MethodImplOptions.AggressiveInlining)] private double CalculateWMA(double value, bool isNew) { - if (_wmaBuffer == null) return value; + if (_wmaBuffer == null) + { + return value; + } // Calculate count for this bar (always increment if not full, for both isNew cases) int currentCount = _state.WmaCount; @@ -334,13 +364,17 @@ public sealed class Maenv : ITValuePublisher if (calcCount > 1) { for (int i = _period - 1; i > 0; i--) + { _wmaBuffer[i] = _wmaBuffer[i - 1]; + } } _wmaBuffer[0] = value; // Persist state only for isNew=true if (isNew) + { _state = _state with { WmaCount = calcCount }; + } // Calculate WMA double norm = 0.0; @@ -361,7 +395,9 @@ public sealed class Maenv : ITValuePublisher Reset(); if (source.Count == 0) + { return; + } for (int i = 0; i < source.Count; i++) { @@ -386,14 +422,25 @@ public sealed class Maenv : ITValuePublisher MaenvType maType = MaenvType.EMA) { if (period < 1) + { throw new ArgumentOutOfRangeException(nameof(period), "Period must be >= 1."); + } + if (percentage <= 0.0) + { throw new ArgumentOutOfRangeException(nameof(percentage), "Percentage must be > 0."); + } + if (middle.Length < source.Length || upper.Length < source.Length || lower.Length < source.Length) + { throw new ArgumentException("Output spans must be at least as long as input", nameof(middle)); + } int len = source.Length; - if (len == 0) return; + if (len == 0) + { + return; + } switch (maType) { diff --git a/lib/channels/mmchannel/Mmchannel.Quantower.cs b/lib/channels/mmchannel/Mmchannel.Quantower.cs index e84cc592..142ccb58 100644 --- a/lib/channels/mmchannel/Mmchannel.Quantower.cs +++ b/lib/channels/mmchannel/Mmchannel.Quantower.cs @@ -41,7 +41,9 @@ public sealed class MmchannelIndicator : Indicator, IWatchlistIndicator protected override void OnUpdate(UpdateArgs args) { if (_indicator is null) + { return; + } var item = HistoricalData[0, SeekOriginHistory.End]; bool isNew = args.IsNewBar(); diff --git a/lib/channels/mmchannel/Mmchannel.cs b/lib/channels/mmchannel/Mmchannel.cs index 245b9095..35d52aaa 100644 --- a/lib/channels/mmchannel/Mmchannel.cs +++ b/lib/channels/mmchannel/Mmchannel.cs @@ -41,7 +41,9 @@ public sealed class Mmchannel : ITValuePublisher public Mmchannel(int period) { if (period <= 0) + { throw new ArgumentException("Period must be greater than 0", nameof(period)); + } _period = period; _hBuf = new double[_period]; @@ -73,14 +75,22 @@ public sealed class Mmchannel : ITValuePublisher private (double high, double low) GetValid(double high, double low) { if (double.IsFinite(high)) + { _state = _state with { LastValidHigh = high }; + } else + { high = _state.LastValidHigh; + } if (double.IsFinite(low)) + { _state = _state with { LastValidLow = low }; + } else + { low = _state.LastValidLow; + } return (high, low); } @@ -89,15 +99,21 @@ public sealed class Mmchannel : ITValuePublisher public TValue Update(TBar input, bool isNew = true) { if (isNew) + { _p_state = _state; + } else + { _state = _p_state; + } if (isNew) { _index++; if (_count < _period) + { _count++; + } } int bufIdx = (int)(_index % _period); @@ -132,7 +148,9 @@ public sealed class Mmchannel : ITValuePublisher double bot = _minDeque.GetExtremum(_lBuf); if (!IsHot && _count >= _period) + { _state = _state with { IsHot = true }; + } // Last returns Upper by default for single-value compatibility Last = new TValue(input.Time, top); @@ -146,7 +164,9 @@ public sealed class Mmchannel : ITValuePublisher public (TSeries Upper, TSeries Lower) Update(TBarSeries source) { if (source.Count == 0) + { return (new TSeries([], []), new TSeries([], [])); + } int len = source.Count; var tUpper = new List(len); @@ -184,7 +204,9 @@ public sealed class Mmchannel : ITValuePublisher Reset(); if (source.Count == 0) + { return; + } for (int i = 0; i < source.Count; i++) { @@ -218,14 +240,25 @@ public sealed class Mmchannel : ITValuePublisher int period) { if (period <= 0) + { throw new ArgumentException("Period must be greater than 0", nameof(period)); + } + if (high.Length != low.Length) + { throw new ArgumentException("High and Low spans must have the same length", nameof(high)); + } + if (upper.Length < high.Length || lower.Length < high.Length) + { throw new ArgumentException("Output spans must be at least as long as inputs", nameof(upper)); + } int len = high.Length; - if (len == 0) return; + if (len == 0) + { + return; + } Highest.Calculate(high, upper, period); Lowest.Calculate(low, lower, period); @@ -261,4 +294,4 @@ public sealed class Mmchannel : ITValuePublisher var results = indicator.Update(source); return (results, indicator); } -} \ No newline at end of file +} diff --git a/lib/channels/pchannel/Pchannel.Quantower.cs b/lib/channels/pchannel/Pchannel.Quantower.cs index 48188ffc..92764354 100644 --- a/lib/channels/pchannel/Pchannel.Quantower.cs +++ b/lib/channels/pchannel/Pchannel.Quantower.cs @@ -42,7 +42,9 @@ public sealed class PchannelIndicator : Indicator, IWatchlistIndicator protected override void OnUpdate(UpdateArgs args) { if (_indicator is null) + { return; + } var item = HistoricalData[0, SeekOriginHistory.End]; bool isNew = args.IsNewBar(); diff --git a/lib/channels/pchannel/Pchannel.Validation.Tests.cs b/lib/channels/pchannel/Pchannel.Validation.Tests.cs index 2f10e2c9..3a07c49b 100644 --- a/lib/channels/pchannel/Pchannel.Validation.Tests.cs +++ b/lib/channels/pchannel/Pchannel.Validation.Tests.cs @@ -18,9 +18,16 @@ public sealed class PchannelValidationTests : IDisposable private void Dispose(bool disposing) { - if (_disposed) return; + if (_disposed) + { + return; + } + _disposed = true; - if (disposing) _testData?.Dispose(); + if (disposing) + { + _testData?.Dispose(); + } } [Fact] diff --git a/lib/channels/pchannel/Pchannel.cs b/lib/channels/pchannel/Pchannel.cs index 41bfd74d..a00a3244 100644 --- a/lib/channels/pchannel/Pchannel.cs +++ b/lib/channels/pchannel/Pchannel.cs @@ -49,7 +49,9 @@ public sealed class Pchannel : ITValuePublisher public Pchannel(int period) { if (period <= 0) + { throw new ArgumentException("Period must be greater than 0", nameof(period)); + } _period = period; _hBuf = new double[_period]; @@ -85,14 +87,22 @@ public sealed class Pchannel : ITValuePublisher private (double high, double low) GetValid(double high, double low) { if (double.IsFinite(high)) + { _state = _state with { LastValidHigh = high }; + } else + { high = _state.LastValidHigh; + } if (double.IsFinite(low)) + { _state = _state with { LastValidLow = low }; + } else + { low = _state.LastValidLow; + } return (high, low); } @@ -165,7 +175,9 @@ public sealed class Pchannel : ITValuePublisher _lCount = 0; if (_count == 0) + { return; + } long startLogical = _index - _count + 1; for (int i = 0; i < _count; i++) @@ -183,15 +195,21 @@ public sealed class Pchannel : ITValuePublisher public TValue Update(TBar input, bool isNew = true) { if (isNew) + { _p_state = _state; + } else + { _state = _p_state; + } if (isNew) { _index++; if (_count < _period) + { _count++; + } } int bufIdx = (int)(_index % _period); @@ -224,7 +242,9 @@ public sealed class Pchannel : ITValuePublisher double mid = (top + bot) * 0.5; if (!IsHot && _count >= _period) + { _state = _state with { IsHot = true }; + } Last = new TValue(input.Time, mid); Upper = new TValue(input.Time, top); @@ -237,7 +257,9 @@ public sealed class Pchannel : ITValuePublisher public (TSeries Middle, TSeries Upper, TSeries Lower) Update(TBarSeries source) { if (source.Count == 0) + { return (new TSeries([], []), new TSeries([], []), new TSeries([], [])); + } int len = source.Count; var tMiddle = new List(len); @@ -280,7 +302,9 @@ public sealed class Pchannel : ITValuePublisher Reset(); if (source.Count == 0) + { return; + } for (int i = 0; i < source.Count; i++) { @@ -317,14 +341,25 @@ public sealed class Pchannel : ITValuePublisher int period) { if (period <= 0) + { throw new ArgumentException("Period must be greater than 0", nameof(period)); + } + if (high.Length != low.Length) + { throw new ArgumentException("High and Low spans must have the same length", nameof(high)); + } + if (middle.Length < high.Length || upper.Length < high.Length || lower.Length < high.Length) + { throw new ArgumentException("Output spans must be at least as long as inputs", nameof(middle)); + } int len = high.Length; - if (len == 0) return; + if (len == 0) + { + return; + } double[] top = ArrayPool.Shared.Rent(len); double[] bot = ArrayPool.Shared.Rent(len); diff --git a/lib/channels/regchannel/Regchannel.Quantower.cs b/lib/channels/regchannel/Regchannel.Quantower.cs index afc49287..d9209f2d 100644 --- a/lib/channels/regchannel/Regchannel.Quantower.cs +++ b/lib/channels/regchannel/Regchannel.Quantower.cs @@ -50,7 +50,9 @@ public sealed class RegchannelIndicator : Indicator, IWatchlistIndicator protected override void OnUpdate(UpdateArgs args) { if (_indicator is null) + { return; + } var item = HistoricalData[0, SeekOriginHistory.End]; bool isNew = args.IsNewBar(); diff --git a/lib/channels/regchannel/Regchannel.cs b/lib/channels/regchannel/Regchannel.cs index f4e8a692..829c9597 100644 --- a/lib/channels/regchannel/Regchannel.cs +++ b/lib/channels/regchannel/Regchannel.cs @@ -83,9 +83,14 @@ public sealed class Regchannel : ITValuePublisher public Regchannel(int period = 20, double multiplier = 2.0) { if (period <= 1) + { throw new ArgumentOutOfRangeException(nameof(period), "Period must be greater than 1."); + } + if (multiplier <= 0) + { throw new ArgumentOutOfRangeException(nameof(multiplier), "Multiplier must be greater than 0."); + } _period = period; _multiplier = multiplier; @@ -163,13 +168,17 @@ public sealed class Regchannel : ITValuePublisher int head = _state.Head; if (count < _period) + { count++; + } _buffer[head] = value; int newHead = (head + 1) % _period; if (isNew) + { _state = _state with { Head = newHead, Count = count }; + } // Calculate linear regression and std dev of residuals if (count <= 1) @@ -241,7 +250,9 @@ public sealed class Regchannel : ITValuePublisher double band = _multiplier * stdDev; if (!_state.IsHot && count >= WarmupPeriod) + { _state = _state with { IsHot = true }; + } _state = _state with { Slope = slope, StdDev = stdDev }; @@ -256,7 +267,9 @@ public sealed class Regchannel : ITValuePublisher public (TSeries Middle, TSeries Upper, TSeries Lower) Update(TSeries source) { if (source.Count == 0) + { return (new TSeries([], []), new TSeries([], []), new TSeries([], [])); + } int len = source.Count; var tMiddle = new List(len); @@ -300,7 +313,9 @@ public sealed class Regchannel : ITValuePublisher Reset(); if (source.Count == 0) + { return; + } for (int i = 0; i < source.Count; i++) { @@ -320,14 +335,25 @@ public sealed class Regchannel : ITValuePublisher double multiplier = 2.0) { if (period <= 1) + { throw new ArgumentOutOfRangeException(nameof(period), "Period must be greater than 1."); + } + if (multiplier <= 0) + { throw new ArgumentOutOfRangeException(nameof(multiplier), "Multiplier must be greater than 0."); + } + if (middle.Length < source.Length || upper.Length < source.Length || lower.Length < source.Length) + { throw new ArgumentException("Output spans must be at least as long as input", nameof(middle)); + } int len = source.Length; - if (len == 0) return; + if (len == 0) + { + return; + } // Precompute constants for full period double sumXFull = 0.5 * period * (period - 1); diff --git a/lib/channels/sdchannel/Sdchannel.Quantower.cs b/lib/channels/sdchannel/Sdchannel.Quantower.cs index b6cdceaf..9bb6d835 100644 --- a/lib/channels/sdchannel/Sdchannel.Quantower.cs +++ b/lib/channels/sdchannel/Sdchannel.Quantower.cs @@ -50,7 +50,9 @@ public sealed class SdchannelIndicator : Indicator, IWatchlistIndicator protected override void OnUpdate(UpdateArgs args) { if (_indicator is null) + { return; + } var item = HistoricalData[0, SeekOriginHistory.End]; bool isNew = args.IsNewBar(); diff --git a/lib/channels/sdchannel/Sdchannel.cs b/lib/channels/sdchannel/Sdchannel.cs index fd46cad6..2fcb9ba7 100644 --- a/lib/channels/sdchannel/Sdchannel.cs +++ b/lib/channels/sdchannel/Sdchannel.cs @@ -83,9 +83,14 @@ public sealed class Sdchannel : ITValuePublisher public Sdchannel(int period = 20, double multiplier = 2.0) { if (period <= 1) + { throw new ArgumentOutOfRangeException(nameof(period), "Period must be greater than 1."); + } + if (multiplier <= 0) + { throw new ArgumentOutOfRangeException(nameof(multiplier), "Multiplier must be greater than 0."); + } _period = period; _multiplier = multiplier; @@ -163,13 +168,17 @@ public sealed class Sdchannel : ITValuePublisher int head = _state.Head; if (count < _period) + { count++; + } _buffer[head] = value; int newHead = (head + 1) % _period; if (isNew) + { _state = _state with { Head = newHead, Count = count }; + } // Calculate linear regression and std dev of residuals if (count <= 1) @@ -241,7 +250,9 @@ public sealed class Sdchannel : ITValuePublisher double band = _multiplier * stdDev; if (!_state.IsHot && count >= WarmupPeriod) + { _state = _state with { IsHot = true }; + } _state = _state with { Slope = slope, StdDev = stdDev }; @@ -256,7 +267,9 @@ public sealed class Sdchannel : ITValuePublisher public (TSeries Middle, TSeries Upper, TSeries Lower) Update(TSeries source) { if (source.Count == 0) + { return (new TSeries([], []), new TSeries([], []), new TSeries([], [])); + } int len = source.Count; var tMiddle = new List(len); @@ -300,7 +313,9 @@ public sealed class Sdchannel : ITValuePublisher Reset(); if (source.Count == 0) + { return; + } for (int i = 0; i < source.Count; i++) { @@ -320,14 +335,25 @@ public sealed class Sdchannel : ITValuePublisher double multiplier = 2.0) { if (period <= 1) + { throw new ArgumentOutOfRangeException(nameof(period), "Period must be greater than 1."); + } + if (multiplier <= 0) + { throw new ArgumentOutOfRangeException(nameof(multiplier), "Multiplier must be greater than 0."); + } + if (middle.Length < source.Length || upper.Length < source.Length || lower.Length < source.Length) + { throw new ArgumentException("Output spans must be at least as long as input", nameof(middle)); + } int len = source.Length; - if (len == 0) return; + if (len == 0) + { + return; + } // Precompute constants for full period double sumXFull = 0.5 * period * (period - 1); diff --git a/lib/channels/starchannel/Starchannel.Quantower.cs b/lib/channels/starchannel/Starchannel.Quantower.cs index d5a06ef9..91176d8b 100644 --- a/lib/channels/starchannel/Starchannel.Quantower.cs +++ b/lib/channels/starchannel/Starchannel.Quantower.cs @@ -48,7 +48,9 @@ public sealed class StarchannelIndicator : Indicator, IWatchlistIndicator protected override void OnUpdate(UpdateArgs args) { if (_indicator is null) + { return; + } var item = HistoricalData[0, SeekOriginHistory.End]; bool isNew = args.IsNewBar(); diff --git a/lib/channels/starchannel/Starchannel.cs b/lib/channels/starchannel/Starchannel.cs index 3d5e8a0f..b4f4e099 100644 --- a/lib/channels/starchannel/Starchannel.cs +++ b/lib/channels/starchannel/Starchannel.cs @@ -49,9 +49,14 @@ public sealed class Starchannel : ITValuePublisher public Starchannel(int period = 20, double multiplier = 2.0) { if (period < 1) + { throw new ArgumentOutOfRangeException(nameof(period), "Period must be >= 1."); + } + if (multiplier <= 0.0) + { throw new ArgumentOutOfRangeException(nameof(multiplier), "Multiplier must be > 0."); + } _period = period; _multiplier = multiplier; @@ -93,19 +98,31 @@ public sealed class Starchannel : ITValuePublisher private (double close, double high, double low) GetValid(double close, double high, double low) { if (double.IsFinite(close)) + { _state = _state with { LastValidClose = close }; + } else + { close = _state.LastValidClose; + } if (double.IsFinite(high)) + { _state = _state with { LastValidHigh = high }; + } else + { high = _state.LastValidHigh; + } if (double.IsFinite(low)) + { _state = _state with { LastValidLow = low }; + } else + { low = _state.LastValidLow; + } return (close, high, low); } @@ -147,7 +164,9 @@ public sealed class Starchannel : ITValuePublisher } if (isNew) + { _state = _state with { Bars = _state.Bars + 1 }; + } // SMA: use RingBuffer's running sum _smaBuffer.Add(close); @@ -179,7 +198,9 @@ public sealed class Starchannel : ITValuePublisher double lower = smaValue - width; if (!_state.IsHot && _state.Bars >= WarmupPeriod) + { _state = _state with { IsHot = true }; + } Last = new TValue(input.Time, smaValue); Upper = new TValue(input.Time, upper); @@ -192,7 +213,9 @@ public sealed class Starchannel : ITValuePublisher public (TSeries Middle, TSeries Upper, TSeries Lower) Update(TBarSeries source) { if (source.Count == 0) + { return (new TSeries([], []), new TSeries([], []), new TSeries([], [])); + } int len = source.Count; var tMiddle = new List(len); @@ -237,7 +260,9 @@ public sealed class Starchannel : ITValuePublisher Reset(); if (source.Count == 0) + { return; + } for (int i = 0; i < source.Count; i++) { @@ -259,16 +284,30 @@ public sealed class Starchannel : ITValuePublisher double multiplier = 2.0) { if (period < 1) + { throw new ArgumentOutOfRangeException(nameof(period), "Period must be >= 1."); + } + if (multiplier <= 0.0) + { throw new ArgumentOutOfRangeException(nameof(multiplier), "Multiplier must be > 0."); + } + if (high.Length != low.Length || high.Length != close.Length) + { throw new ArgumentException("High, Low, and Close spans must have the same length", nameof(high)); + } + if (middle.Length < high.Length || upper.Length < high.Length || lower.Length < high.Length) + { throw new ArgumentException("Output spans must be at least as long as inputs", nameof(middle)); + } int len = high.Length; - if (len == 0) return; + if (len == 0) + { + return; + } double atrAlpha = 1.0 / period; diff --git a/lib/channels/stbands/Stbands.Quantower.Tests.cs b/lib/channels/stbands/Stbands.Quantower.Tests.cs index 60c53269..65b701a0 100644 --- a/lib/channels/stbands/Stbands.Quantower.Tests.cs +++ b/lib/channels/stbands/Stbands.Quantower.Tests.cs @@ -203,4 +203,4 @@ public class StbandsIndicatorTests Assert.True(trend == 1 || trend == -1, $"Trend should be +1 or -1, got {trend}"); } } -} \ No newline at end of file +} diff --git a/lib/channels/stbands/Stbands.Quantower.cs b/lib/channels/stbands/Stbands.Quantower.cs index c7679afd..8cfb43a5 100644 --- a/lib/channels/stbands/Stbands.Quantower.cs +++ b/lib/channels/stbands/Stbands.Quantower.cs @@ -70,4 +70,4 @@ public class StbandsIndicator : Indicator, IWatchlistIndicator TrendSeries!.SetValue(stbands.Trend.Value, stbands.IsHot, ShowColdValues); WidthSeries!.SetValue(stbands.Width.Value, stbands.IsHot, ShowColdValues); } -} \ No newline at end of file +} diff --git a/lib/channels/stbands/Stbands.Tests.cs b/lib/channels/stbands/Stbands.Tests.cs index 665f387d..c832b37a 100644 --- a/lib/channels/stbands/Stbands.Tests.cs +++ b/lib/channels/stbands/Stbands.Tests.cs @@ -420,4 +420,4 @@ public class StbandsTests // Assert Assert.Equal(stbands.Upper.Value - stbands.Lower.Value, stbands.Width.Value, precision: 10); } -} \ No newline at end of file +} diff --git a/lib/channels/stbands/Stbands.Validation.Tests.cs b/lib/channels/stbands/Stbands.Validation.Tests.cs index 41df5424..0e213371 100644 --- a/lib/channels/stbands/Stbands.Validation.Tests.cs +++ b/lib/channels/stbands/Stbands.Validation.Tests.cs @@ -376,4 +376,4 @@ public sealed class StbandsValidationTests : IDisposable $"Higher multiplier should produce wider bands (mult={multipliers[i]})"); } } -} \ No newline at end of file +} diff --git a/lib/channels/stbands/Stbands.cs b/lib/channels/stbands/Stbands.cs index a12cbdbf..daebe5c8 100644 --- a/lib/channels/stbands/Stbands.cs +++ b/lib/channels/stbands/Stbands.cs @@ -175,11 +175,17 @@ public sealed class Stbands : AbstractBase // Determine trend if (close <= finalLower) + { trend = 1; // Bullish + } else if (close >= finalUpper) + { trend = -1; // Bearish + } else + { trend = prevTrend; + } } // Update state @@ -374,9 +380,13 @@ public sealed class Stbands : AbstractBase // Determine trend if (c <= finalLower) + { currentTrend = 1; + } else if (c >= finalUpper) + { currentTrend = -1; + } } upper[i] = finalUpper; @@ -385,4 +395,4 @@ public sealed class Stbands : AbstractBase prevClose = c; } } -} \ No newline at end of file +} diff --git a/lib/channels/ubands/Ubands.Quantower.Tests.cs b/lib/channels/ubands/Ubands.Quantower.Tests.cs index 48d506ce..e35cd589 100644 --- a/lib/channels/ubands/Ubands.Quantower.Tests.cs +++ b/lib/channels/ubands/Ubands.Quantower.Tests.cs @@ -216,4 +216,4 @@ public class UbandsIndicatorTests Assert.True(Math.Abs(width - (upper - lower)) < 0.0001, $"Width ({width}) should equal Upper - Lower ({upper - lower})"); } -} \ No newline at end of file +} diff --git a/lib/channels/ubands/Ubands.Quantower.cs b/lib/channels/ubands/Ubands.Quantower.cs index be8169df..ce12d076 100644 --- a/lib/channels/ubands/Ubands.Quantower.cs +++ b/lib/channels/ubands/Ubands.Quantower.cs @@ -68,4 +68,4 @@ public class UbandsIndicator : Indicator, IWatchlistIndicator LowerSeries!.SetValue(ubands.Lower.Value, ubands.IsHot, ShowColdValues); WidthSeries!.SetValue(ubands.Width.Value, ubands.IsHot, ShowColdValues); } -} \ No newline at end of file +} diff --git a/lib/channels/ubands/Ubands.Tests.cs b/lib/channels/ubands/Ubands.Tests.cs index 9573f7c5..959a1557 100644 --- a/lib/channels/ubands/Ubands.Tests.cs +++ b/lib/channels/ubands/Ubands.Tests.cs @@ -401,4 +401,4 @@ public class UbandsTests Assert.Equal(50, result.Count); Assert.True(double.IsFinite(result.Last.Value)); } -} \ No newline at end of file +} diff --git a/lib/channels/ubands/Ubands.Validation.Tests.cs b/lib/channels/ubands/Ubands.Validation.Tests.cs index abc84edc..414734d8 100644 --- a/lib/channels/ubands/Ubands.Validation.Tests.cs +++ b/lib/channels/ubands/Ubands.Validation.Tests.cs @@ -413,4 +413,4 @@ public sealed class UbandsValidationTests : IDisposable Assert.True(middleVar < sourceVar, "Smoothed signal should have lower variance"); } -} \ No newline at end of file +} diff --git a/lib/channels/ubands/Ubands.cs b/lib/channels/ubands/Ubands.cs index 2a4263dd..18cdebf4 100644 --- a/lib/channels/ubands/Ubands.cs +++ b/lib/channels/ubands/Ubands.cs @@ -385,4 +385,4 @@ public sealed class Ubands : AbstractBase lower[i] = usf - bandOffset; } } -} \ No newline at end of file +} diff --git a/lib/channels/uchannel/Uchannel.Quantower.cs b/lib/channels/uchannel/Uchannel.Quantower.cs index 58f8ea3b..512ec650 100644 --- a/lib/channels/uchannel/Uchannel.Quantower.cs +++ b/lib/channels/uchannel/Uchannel.Quantower.cs @@ -74,4 +74,4 @@ public class UchannelIndicator : Indicator, IWatchlistIndicator StrSeries!.SetValue(uchannel.STR.Value, uchannel.IsHot, ShowColdValues); WidthSeries!.SetValue(uchannel.Width.Value, uchannel.IsHot, ShowColdValues); } -} \ No newline at end of file +} diff --git a/lib/channels/uchannel/Uchannel.Tests.cs b/lib/channels/uchannel/Uchannel.Tests.cs index 54d42130..fbc8cd5b 100644 --- a/lib/channels/uchannel/Uchannel.Tests.cs +++ b/lib/channels/uchannel/Uchannel.Tests.cs @@ -497,4 +497,4 @@ public class UchannelTests Assert.Equal(uchannel1.Upper.Value, uchannel2.Upper.Value, precision: 10); Assert.Equal(uchannel1.Lower.Value, uchannel2.Lower.Value, precision: 10); } -} \ No newline at end of file +} diff --git a/lib/channels/uchannel/Uchannel.Validation.Tests.cs b/lib/channels/uchannel/Uchannel.Validation.Tests.cs index 53a3efe6..42cf6e51 100644 --- a/lib/channels/uchannel/Uchannel.Validation.Tests.cs +++ b/lib/channels/uchannel/Uchannel.Validation.Tests.cs @@ -436,4 +436,4 @@ public class UchannelValidationTests // Should produce same results Assert.Equal(valueBefore, valueAfter, precision: 10); } -} \ No newline at end of file +} diff --git a/lib/channels/uchannel/Uchannel.cs b/lib/channels/uchannel/Uchannel.cs index 07f7479d..021a21a0 100644 --- a/lib/channels/uchannel/Uchannel.cs +++ b/lib/channels/uchannel/Uchannel.cs @@ -87,6 +87,9 @@ public sealed class Uchannel : AbstractBase /// Gets the channel width (Upper - Lower). public TValue Width => new(Upper.Time, Upper.Value - Lower.Value); + /// + /// + /// /// Period for smoothing True Range. Must be >= 1. /// Period for smoothing centerline. Must be >= 1. /// Band multiplier for STR. Must be > 0. @@ -403,11 +406,19 @@ public sealed class Uchannel : AbstractBase int length = close.Length; if (high.Length != length || low.Length != length) + { throw new ArgumentException("All input arrays must have the same length", nameof(high)); - if (upper.Length != length || middle.Length != length || lower.Length != length) - throw new ArgumentException("Output arrays must match input length", nameof(upper)); + } - if (length == 0) return; + if (upper.Length != length || middle.Length != length || lower.Length != length) + { + throw new ArgumentException("Output arrays must match input length", nameof(upper)); + } + + if (length == 0) + { + return; + } // Compute USF coefficients double arg_str = Math.Sqrt(2) * Math.PI / strPeriod; @@ -503,4 +514,4 @@ public sealed class Uchannel : AbstractBase prevClose = c; } } -} \ No newline at end of file +} diff --git a/lib/channels/vwapbands/Vwapbands.Quantower.Tests.cs b/lib/channels/vwapbands/Vwapbands.Quantower.Tests.cs index f684d468..badc9fdd 100644 --- a/lib/channels/vwapbands/Vwapbands.Quantower.Tests.cs +++ b/lib/channels/vwapbands/Vwapbands.Quantower.Tests.cs @@ -211,7 +211,7 @@ public class VwapbandsIndicatorTests // Same prices but different volume distributions // Process both bars for each indicator - + // Indicator1: high volume on low price, low volume on high price indicator1.HistoricalData.AddBar(now, 100, 102, 98, 100, 10000); indicator1.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar)); @@ -231,4 +231,4 @@ public class VwapbandsIndicatorTests // VWAP2 should be higher (weighted toward 110 due to high volume at high price) Assert.True(vwap1 < vwap2, $"VWAP1 ({vwap1}) should be less than VWAP2 ({vwap2}) due to volume weighting"); } -} \ No newline at end of file +} diff --git a/lib/channels/vwapbands/Vwapbands.Quantower.cs b/lib/channels/vwapbands/Vwapbands.Quantower.cs index 561364c7..e025af9f 100644 --- a/lib/channels/vwapbands/Vwapbands.Quantower.cs +++ b/lib/channels/vwapbands/Vwapbands.Quantower.cs @@ -76,4 +76,4 @@ public class VwapbandsIndicator : Indicator, IWatchlistIndicator Lower2Series!.SetValue(vwapbands.Lower2.Value, vwapbands.IsHot, ShowColdValues); WidthSeries!.SetValue(vwapbands.Width.Value, vwapbands.IsHot, ShowColdValues); } -} \ No newline at end of file +} diff --git a/lib/channels/vwapbands/Vwapbands.Tests.cs b/lib/channels/vwapbands/Vwapbands.Tests.cs index 4bec1b74..aecbddbc 100644 --- a/lib/channels/vwapbands/Vwapbands.Tests.cs +++ b/lib/channels/vwapbands/Vwapbands.Tests.cs @@ -602,4 +602,4 @@ public class VwapbandsTests Assert.Equal(expectedVwap, vwapbands.Vwap.Value, precision: 10); Assert.True(vwapbands.Vwap.Value < 110, "VWAP should be heavily weighted toward 100"); } -} \ No newline at end of file +} diff --git a/lib/channels/vwapbands/Vwapbands.Validation.Tests.cs b/lib/channels/vwapbands/Vwapbands.Validation.Tests.cs index e3a0ff98..688dd294 100644 --- a/lib/channels/vwapbands/Vwapbands.Validation.Tests.cs +++ b/lib/channels/vwapbands/Vwapbands.Validation.Tests.cs @@ -518,4 +518,4 @@ public sealed class VwapbandsValidationTests : IDisposable _output.WriteLine("VWAPBANDS static Calculate validated successfully"); } -} \ No newline at end of file +} diff --git a/lib/channels/vwapbands/Vwapbands.cs b/lib/channels/vwapbands/Vwapbands.cs index cf5ab221..e33018b4 100644 --- a/lib/channels/vwapbands/Vwapbands.cs +++ b/lib/channels/vwapbands/Vwapbands.cs @@ -414,4 +414,4 @@ public sealed class Vwapbands : AbstractBase lower2[i] = vwapVal - 2.0 * multiplier * stdev; } } -} \ No newline at end of file +} diff --git a/lib/channels/vwapsd/Vwapsd.Quantower.Tests.cs b/lib/channels/vwapsd/Vwapsd.Quantower.Tests.cs index 53f28ea0..d9b19048 100644 --- a/lib/channels/vwapsd/Vwapsd.Quantower.Tests.cs +++ b/lib/channels/vwapsd/Vwapsd.Quantower.Tests.cs @@ -258,4 +258,4 @@ public class VwapsdIndicatorTests Assert.True(Math.Abs(width2 - 2 * width1) < 0.0001, $"Width2 ({width2}) should be ~2x Width1 ({width1})"); } -} \ No newline at end of file +} diff --git a/lib/channels/vwapsd/Vwapsd.Quantower.cs b/lib/channels/vwapsd/Vwapsd.Quantower.cs index 551f70f9..9594bd9d 100644 --- a/lib/channels/vwapsd/Vwapsd.Quantower.cs +++ b/lib/channels/vwapsd/Vwapsd.Quantower.cs @@ -68,4 +68,4 @@ public class VwapsdIndicator : Indicator, IWatchlistIndicator LowerSeries!.SetValue(vwapsd.Lower.Value, vwapsd.IsHot, ShowColdValues); WidthSeries!.SetValue(vwapsd.Width.Value, vwapsd.IsHot, ShowColdValues); } -} \ No newline at end of file +} diff --git a/lib/channels/vwapsd/Vwapsd.Tests.cs b/lib/channels/vwapsd/Vwapsd.Tests.cs index a3bf60ec..710cf85e 100644 --- a/lib/channels/vwapsd/Vwapsd.Tests.cs +++ b/lib/channels/vwapsd/Vwapsd.Tests.cs @@ -694,4 +694,4 @@ public class VwapsdTests Assert.Equal(400.0, vwapsd.Upper.Value, precision: 10); Assert.Equal(-100.0, vwapsd.Lower.Value, precision: 10); } -} \ No newline at end of file +} diff --git a/lib/channels/vwapsd/Vwapsd.Validation.Tests.cs b/lib/channels/vwapsd/Vwapsd.Validation.Tests.cs index a70c335b..de88bd26 100644 --- a/lib/channels/vwapsd/Vwapsd.Validation.Tests.cs +++ b/lib/channels/vwapsd/Vwapsd.Validation.Tests.cs @@ -610,4 +610,4 @@ public sealed class VwapsdValidationTests : IDisposable _output.WriteLine("VWAPSD boundary numDevs validation completed"); } -} \ No newline at end of file +} diff --git a/lib/channels/vwapsd/Vwapsd.cs b/lib/channels/vwapsd/Vwapsd.cs index 6cde04c3..c8568466 100644 --- a/lib/channels/vwapsd/Vwapsd.cs +++ b/lib/channels/vwapsd/Vwapsd.cs @@ -400,4 +400,4 @@ public sealed class Vwapsd : AbstractBase lower[i] = vwapVal - numDevs * stdev; } } -} \ No newline at end of file +} diff --git a/lib/core/BiInputIndicatorBase.cs b/lib/core/BiInputIndicatorBase.cs index 717ebebd..77f8472c 100644 --- a/lib/core/BiInputIndicatorBase.cs +++ b/lib/core/BiInputIndicatorBase.cs @@ -52,7 +52,9 @@ public abstract class BiInputIndicatorBase : AbstractBase protected BiInputIndicatorBase(int period, string name) { if (period <= 0) + { throw new ArgumentException("Period must be greater than 0", nameof(period)); + } _buffer = new RingBuffer(period); Name = name; @@ -176,9 +178,13 @@ public abstract class BiInputIndicatorBase : AbstractBase double error = ComputeError(actualVal, predictedVal); if (isNew) + { ProcessNewBar(error); + } else + { ProcessBarCorrection(error); + } double mean = _buffer.Count > 0 ? _state.Sum / _buffer.Count : error; double result = PostProcess(mean); @@ -248,7 +254,9 @@ public abstract class BiInputIndicatorBase : AbstractBase BiInputBatchDelegate batchMethod) { if (actual.Count != predicted.Count) + { throw new ArgumentException("Actual and predicted series must have the same length", nameof(predicted)); + } int len = actual.Count; var t = new List(len); @@ -276,8 +284,13 @@ public abstract class BiInputIndicatorBase : AbstractBase int period) { if (actual.Length != predicted.Length || actual.Length != output.Length) + { throw new ArgumentException("All spans must have the same length", nameof(output)); + } + if (period <= 0) + { throw new ArgumentException("Period must be greater than 0", nameof(period)); + } } -} \ No newline at end of file +} diff --git a/lib/core/collections/MonotonicDeque.cs b/lib/core/collections/MonotonicDeque.cs index 58dece0c..7eddead7 100644 --- a/lib/core/collections/MonotonicDeque.cs +++ b/lib/core/collections/MonotonicDeque.cs @@ -38,7 +38,9 @@ public sealed class MonotonicDeque public MonotonicDeque(int period) { if (period <= 0) + { throw new ArgumentOutOfRangeException(nameof(period), "Period must be greater than 0"); + } _period = period; _deque = new int[period]; @@ -155,7 +157,10 @@ public sealed class MonotonicDeque public void RebuildMax(double[] buffer, long currentIndex, int count) { Reset(); - if (count == 0) return; + if (count == 0) + { + return; + } long startLogical = currentIndex - count + 1; for (int i = 0; i < count; i++) @@ -176,7 +181,10 @@ public sealed class MonotonicDeque public void RebuildMin(double[] buffer, long currentIndex, int count) { Reset(); - if (count == 0) return; + if (count == 0) + { + return; + } long startLogical = currentIndex - count + 1; for (int i = 0; i < count; i++) @@ -186,4 +194,4 @@ public sealed class MonotonicDeque PushMin(logicalIndex, buffer[bufIdx], buffer); } } -} \ No newline at end of file +} diff --git a/lib/core/ringbuffer/RingBuffer.cs b/lib/core/ringbuffer/RingBuffer.cs index 7f70e7b2..8ba9999d 100644 --- a/lib/core/ringbuffer/RingBuffer.cs +++ b/lib/core/ringbuffer/RingBuffer.cs @@ -46,7 +46,9 @@ public sealed class RingBuffer : IEnumerable public RingBuffer(int capacity) { if (capacity <= 0) + { throw new ArgumentException("Capacity must be greater than 0", nameof(capacity)); + } Capacity = capacity; _buffer = GC.AllocateArray(capacity, pinned: true); @@ -130,7 +132,11 @@ public sealed class RingBuffer : IEnumerable [MethodImpl(MethodImplOptions.AggressiveInlining)] get { - if (_count == 0) return double.NaN; + if (_count == 0) + { + return double.NaN; + } + int idx = (_head - 1 + Capacity) % Capacity; return _buffer[idx]; } @@ -145,7 +151,11 @@ public sealed class RingBuffer : IEnumerable [MethodImpl(MethodImplOptions.AggressiveInlining)] get { - if (_count == 0) return double.NaN; + if (_count == 0) + { + return double.NaN; + } + int start = _count == Capacity ? _head : 0; return _buffer[start]; } @@ -224,7 +234,10 @@ public sealed class RingBuffer : IEnumerable [MethodImpl(MethodImplOptions.AggressiveInlining)] public void UpdateNewest(double value) { - if (_count == 0) return; + if (_count == 0) + { + return; + } int idx = (_head - 1 + Capacity) % Capacity; double oldValue = _buffer[idx]; @@ -283,7 +296,10 @@ public sealed class RingBuffer : IEnumerable [MethodImpl(MethodImplOptions.AggressiveInlining)] public ReadOnlySpan GetSpan() { - if (_count == 0) return ReadOnlySpan.Empty; + if (_count == 0) + { + return ReadOnlySpan.Empty; + } int start = _count == Capacity ? _head : 0; @@ -332,7 +348,11 @@ public sealed class RingBuffer : IEnumerable [MethodImpl(MethodImplOptions.AggressiveInlining)] public double Max() { - if (_count == 0) return double.NaN; + if (_count == 0) + { + return double.NaN; + } + return MaxSimd(); } @@ -342,7 +362,11 @@ public sealed class RingBuffer : IEnumerable [MethodImpl(MethodImplOptions.AggressiveInlining)] public double Min() { - if (_count == 0) return double.NaN; + if (_count == 0) + { + return double.NaN; + } + return MinSimd(); } @@ -461,7 +485,10 @@ public sealed class RingBuffer : IEnumerable [MethodImpl(MethodImplOptions.AggressiveInlining)] public double[] ToArray() { - if (_count == 0) return Array.Empty(); + if (_count == 0) + { + return Array.Empty(); + } double[] array = new double[_count]; CopyTo(array, 0); @@ -474,7 +501,10 @@ public sealed class RingBuffer : IEnumerable [MethodImpl(MethodImplOptions.AggressiveInlining)] public void CopyTo(double[] destination, int destinationIndex) { - if (_count == 0) return; + if (_count == 0) + { + return; + } int start = _count == Capacity ? _head : 0; @@ -497,7 +527,10 @@ public sealed class RingBuffer : IEnumerable [MethodImpl(MethodImplOptions.AggressiveInlining)] public void CopyTo(Span destination) { - if (_count == 0) return; + if (_count == 0) + { + return; + } int start = _count == Capacity ? _head : 0; @@ -533,7 +566,9 @@ public sealed class RingBuffer : IEnumerable public void CopyFrom(RingBuffer source) { if (source.Capacity != Capacity) + { throw new ArgumentException("Source buffer must have same capacity", nameof(source)); + } Array.Copy(source._buffer, _buffer, Capacity); _head = source._head; @@ -632,7 +667,9 @@ public sealed class RingBuffer : IEnumerable public bool MoveNext() { if (_index + 1 >= _count) + { return false; + } _index++; int bufferIdx = (_start + _index) % _buffer.Capacity; @@ -668,4 +705,4 @@ public sealed class RingBuffer : IEnumerable public static bool operator ==(Enumerator left, Enumerator right) => left.Equals(right); public static bool operator !=(Enumerator left, Enumerator right) => !left.Equals(right); } -} \ No newline at end of file +} diff --git a/lib/core/simd/ErrorHelpers.cs b/lib/core/simd/ErrorHelpers.cs index e506b888..cb9bdbf1 100644 --- a/lib/core/simd/ErrorHelpers.cs +++ b/lib/core/simd/ErrorHelpers.cs @@ -39,11 +39,15 @@ public static class ErrorHelpers Span output) { if (actual.Length != predicted.Length || actual.Length != output.Length) + { throw new ArgumentException(SpanLengthMismatchMessage, nameof(output)); + } int len = actual.Length; if (len == 0) + { return; + } double lastValidActual = FindFirstValidValue(actual); double lastValidPredicted = FindFirstValidValue(predicted); @@ -53,7 +57,9 @@ public static class ErrorHelpers { int processedCount = ComputeSignedErrorsSimdWithNaNDetection(actual, predicted, output, ref lastValidActual, ref lastValidPredicted); if (processedCount == len) + { return; // All processed via SIMD + } // Continue with scalar for remaining elements (NaN was detected) ComputeSignedErrorsScalar(actual.Slice(processedCount), predicted.Slice(processedCount), output.Slice(processedCount), lastValidActual, lastValidPredicted); return; @@ -74,11 +80,15 @@ public static class ErrorHelpers Span output) { if (actual.Length != predicted.Length || actual.Length != output.Length) + { throw new ArgumentException(SpanLengthMismatchMessage, nameof(output)); + } int len = actual.Length; if (len == 0) + { return; + } double lastValidActual = FindFirstValidValue(actual); double lastValidPredicted = FindFirstValidValue(predicted); @@ -88,7 +98,9 @@ public static class ErrorHelpers { int processedCount = ComputeAbsoluteErrorsSimdWithNaNDetection(actual, predicted, output, ref lastValidActual, ref lastValidPredicted); if (processedCount == len) + { return; // All processed via SIMD + } // Continue with scalar for remaining elements (NaN was detected) ComputeAbsoluteErrorsScalar(actual.Slice(processedCount), predicted.Slice(processedCount), output.Slice(processedCount), lastValidActual, lastValidPredicted); return; @@ -109,11 +121,15 @@ public static class ErrorHelpers Span output) { if (actual.Length != predicted.Length || actual.Length != output.Length) + { throw new ArgumentException(SpanLengthMismatchMessage, nameof(output)); + } int len = actual.Length; if (len == 0) + { return; + } double lastValidActual = FindFirstValidValue(actual); double lastValidPredicted = FindFirstValidValue(predicted); @@ -123,7 +139,9 @@ public static class ErrorHelpers { int processedCount = ComputeSquaredErrorsSimdWithNaNDetection(actual, predicted, output, ref lastValidActual, ref lastValidPredicted); if (processedCount == len) + { return; // All processed via SIMD + } // Continue with scalar for remaining elements (NaN was detected) ComputeSquaredErrorsScalar(actual.Slice(processedCount), predicted.Slice(processedCount), output.Slice(processedCount), lastValidActual, lastValidPredicted); return; @@ -145,11 +163,15 @@ public static class ErrorHelpers Span output) { if (actual.Length != predicted.Length || actual.Length != weights.Length || actual.Length != output.Length) + { throw new ArgumentException("All spans must have the same length", nameof(output)); + } int len = actual.Length; if (len == 0) + { return; + } double lastValidActual = FindFirstValidValue(actual); double lastValidPredicted = FindFirstValidValue(predicted); @@ -165,9 +187,32 @@ public static class ErrorHelpers double pred = predicted[i]; double wgt = weights[i]; - if (double.IsFinite(act)) currentValidActual = act; else act = currentValidActual; - if (double.IsFinite(pred)) currentValidPredicted = pred; else pred = currentValidPredicted; - if (double.IsFinite(wgt)) currentValidWeight = wgt; else wgt = currentValidWeight; + if (double.IsFinite(act)) + { + currentValidActual = act; + } + else + { + act = currentValidActual; + } + + if (double.IsFinite(pred)) + { + currentValidPredicted = pred; + } + else + { + pred = currentValidPredicted; + } + + if (double.IsFinite(wgt)) + { + currentValidWeight = wgt; + } + else + { + wgt = currentValidWeight; + } double diff = act - pred; output[i] = wgt * diff * diff; @@ -186,11 +231,15 @@ public static class ErrorHelpers double epsilon = 1e-10) { if (actual.Length != predicted.Length || actual.Length != output.Length) + { throw new ArgumentException(SpanLengthMismatchMessage, nameof(output)); + } int len = actual.Length; if (len == 0) + { return; + } double lastValidActual = FindFirstValidValue(actual); double lastValidPredicted = FindFirstValidValue(predicted); @@ -227,11 +276,15 @@ public static class ErrorHelpers double epsilon = 1e-10) { if (actual.Length != predicted.Length || actual.Length != output.Length) + { throw new ArgumentException(SpanLengthMismatchMessage, nameof(output)); + } int len = actual.Length; if (len == 0) + { return; + } double lastValidActual = FindFirstValidValue(actual); double lastValidPredicted = FindFirstValidValue(predicted); @@ -244,8 +297,23 @@ public static class ErrorHelpers double act = actual[i]; double pred = predicted[i]; - if (double.IsFinite(act)) currentValidActual = act; else act = currentValidActual; - if (double.IsFinite(pred)) currentValidPredicted = pred; else pred = currentValidPredicted; + if (double.IsFinite(act)) + { + currentValidActual = act; + } + else + { + act = currentValidActual; + } + + if (double.IsFinite(pred)) + { + currentValidPredicted = pred; + } + else + { + pred = currentValidPredicted; + } double denominator = (Math.Abs(act) + Math.Abs(pred)) / 2.0; output[i] = denominator < epsilon @@ -265,11 +333,15 @@ public static class ErrorHelpers Span output) { if (actual.Length != predicted.Length || actual.Length != output.Length) + { throw new ArgumentException(SpanLengthMismatchMessage, nameof(output)); + } int len = actual.Length; if (len == 0) + { return; + } double lastValidActual = FindFirstValidValue(actual); double lastValidPredicted = FindFirstValidValue(predicted); @@ -282,8 +354,23 @@ public static class ErrorHelpers double act = actual[i]; double pred = predicted[i]; - if (double.IsFinite(act)) currentValidActual = act; else act = currentValidActual; - if (double.IsFinite(pred)) currentValidPredicted = pred; else pred = currentValidPredicted; + if (double.IsFinite(act)) + { + currentValidActual = act; + } + else + { + act = currentValidActual; + } + + if (double.IsFinite(pred)) + { + currentValidPredicted = pred; + } + else + { + pred = currentValidPredicted; + } double diff = act - pred; // log(cosh(x)) ≈ |x| - log(2) for large |x|, numerically stable @@ -303,11 +390,15 @@ public static class ErrorHelpers double delta = 1.0) { if (actual.Length != predicted.Length || actual.Length != output.Length) + { throw new ArgumentException(SpanLengthMismatchMessage, nameof(output)); + } int len = actual.Length; if (len == 0) + { return; + } double lastValidActual = FindFirstValidValue(actual); double lastValidPredicted = FindFirstValidValue(predicted); @@ -321,8 +412,23 @@ public static class ErrorHelpers double act = actual[i]; double pred = predicted[i]; - if (double.IsFinite(act)) currentValidActual = act; else act = currentValidActual; - if (double.IsFinite(pred)) currentValidPredicted = pred; else pred = currentValidPredicted; + if (double.IsFinite(act)) + { + currentValidActual = act; + } + else + { + act = currentValidActual; + } + + if (double.IsFinite(pred)) + { + currentValidPredicted = pred; + } + else + { + pred = currentValidPredicted; + } double diff = act - pred; double ratio = diff / delta; @@ -345,11 +451,15 @@ public static class ErrorHelpers double c = 4.685) { if (actual.Length != predicted.Length || actual.Length != output.Length) + { throw new ArgumentException(SpanLengthMismatchMessage, nameof(output)); + } int len = actual.Length; if (len == 0) + { return; + } double lastValidActual = FindFirstValidValue(actual); double lastValidPredicted = FindFirstValidValue(predicted); @@ -363,8 +473,23 @@ public static class ErrorHelpers double act = actual[i]; double pred = predicted[i]; - if (double.IsFinite(act)) currentValidActual = act; else act = currentValidActual; - if (double.IsFinite(pred)) currentValidPredicted = pred; else pred = currentValidPredicted; + if (double.IsFinite(act)) + { + currentValidActual = act; + } + else + { + act = currentValidActual; + } + + if (double.IsFinite(pred)) + { + currentValidPredicted = pred; + } + else + { + pred = currentValidPredicted; + } double diff = act - pred; double absDiff = Math.Abs(diff); @@ -396,11 +521,15 @@ public static class ErrorHelpers double delta = 1.0) { if (actual.Length != predicted.Length || actual.Length != output.Length) + { throw new ArgumentException(SpanLengthMismatchMessage, nameof(output)); + } int len = actual.Length; if (len == 0) + { return; + } double lastValidActual = FindFirstValidValue(actual); double lastValidPredicted = FindFirstValidValue(predicted); @@ -414,8 +543,23 @@ public static class ErrorHelpers double act = actual[i]; double pred = predicted[i]; - if (double.IsFinite(act)) currentValidActual = act; else act = currentValidActual; - if (double.IsFinite(pred)) currentValidPredicted = pred; else pred = currentValidPredicted; + if (double.IsFinite(act)) + { + currentValidActual = act; + } + else + { + act = currentValidActual; + } + + if (double.IsFinite(pred)) + { + currentValidPredicted = pred; + } + else + { + pred = currentValidPredicted; + } double diff = act - pred; double absDiff = Math.Abs(diff); @@ -438,13 +582,20 @@ public static class ErrorHelpers int resyncInterval = 1000) { if (errors.Length != output.Length) + { throw new ArgumentException("Spans must have the same length", nameof(output)); + } + if (period <= 0) + { throw new ArgumentException("Period must be greater than 0", nameof(period)); + } int len = errors.Length; if (len == 0) + { return; + } double[]? rented = null; @@ -477,7 +628,10 @@ public static class ErrorHelpers buffer[bufferIndex] = error; bufferIndex++; - if (bufferIndex >= period) bufferIndex = 0; + if (bufferIndex >= period) + { + bufferIndex = 0; + } output[i] = sum / period; @@ -486,7 +640,11 @@ public static class ErrorHelpers { tickCount = 0; double recalcSum = 0; - for (int k = 0; k < period; k++) recalcSum += buffer[k]; + for (int k = 0; k < period; k++) + { + recalcSum += buffer[k]; + } + sum = recalcSum; } } @@ -511,13 +669,20 @@ public static class ErrorHelpers int resyncInterval = 1000) { if (squaredErrors.Length != output.Length) + { throw new ArgumentException("Spans must have the same length", nameof(output)); + } + if (period <= 0) + { throw new ArgumentException("Period must be greater than 0", nameof(period)); + } int len = squaredErrors.Length; if (len == 0) + { return; + } double[]? rented = null; @@ -550,7 +715,10 @@ public static class ErrorHelpers buffer[bufferIndex] = sqError; bufferIndex++; - if (bufferIndex >= period) bufferIndex = 0; + if (bufferIndex >= period) + { + bufferIndex = 0; + } output[i] = Math.Sqrt(sum / period); @@ -559,7 +727,11 @@ public static class ErrorHelpers { tickCount = 0; double recalcSum = 0; - for (int k = 0; k < period; k++) recalcSum += buffer[k]; + for (int k = 0; k < period; k++) + { + recalcSum += buffer[k]; + } + sum = recalcSum; } } @@ -586,13 +758,20 @@ public static class ErrorHelpers int resyncInterval = 1000) { if (weightedSquaredErrors.Length != output.Length || weightedSquaredErrors.Length != weights.Length) + { throw new ArgumentException("Spans must have the same length", nameof(output)); + } + if (period <= 0) + { throw new ArgumentException("Period must be greater than 0", nameof(period)); + } int len = weightedSquaredErrors.Length; if (len == 0) + { return; + } double[]? rentedErrors = null; double[]? rentedWeights = null; @@ -637,7 +816,10 @@ public static class ErrorHelpers weightBuffer[bufferIndex] = wgt; bufferIndex++; - if (bufferIndex >= period) bufferIndex = 0; + if (bufferIndex >= period) + { + bufferIndex = 0; + } output[i] = sumWeights > 1e-10 ? Math.Sqrt(sumErrors / sumWeights) : 0.0; @@ -686,11 +868,15 @@ public static class ErrorHelpers Span predictedOut) { if (actual.Length != predicted.Length || actual.Length != actualOut.Length || actual.Length != predictedOut.Length) + { throw new ArgumentException(SpanLengthMismatchMessage, nameof(predictedOut)); + } int len = actual.Length; if (len == 0) + { return; + } double lastValidActual = FindFirstValidValue(actual); double lastValidPredicted = FindFirstValidValue(predicted); @@ -700,8 +886,23 @@ public static class ErrorHelpers double act = actual[i]; double pred = predicted[i]; - if (double.IsFinite(act)) lastValidActual = act; else act = lastValidActual; - if (double.IsFinite(pred)) lastValidPredicted = pred; else pred = lastValidPredicted; + if (double.IsFinite(act)) + { + lastValidActual = act; + } + else + { + act = lastValidActual; + } + + if (double.IsFinite(pred)) + { + lastValidPredicted = pred; + } + else + { + pred = lastValidPredicted; + } actualOut[i] = act; predictedOut[i] = pred; @@ -717,7 +918,9 @@ public static class ErrorHelpers for (int i = 0; i < span.Length; i++) { if (double.IsFinite(span[i])) + { return span[i]; + } } return 0.0; } @@ -751,14 +954,18 @@ public static class ErrorHelpers // MoveMask returns a bitmask; all-ones means all finite (mask == 0b1111 for 4 doubles) int mask = Avx.MoveMask(combined); if (mask != 0b1111) + { return false; + } } // Scalar tail for (int i = vectorEnd; i < len; i++) { if (!double.IsFinite(actual[i]) || !double.IsFinite(predicted[i])) + { return false; + } } return true; } @@ -767,7 +974,9 @@ public static class ErrorHelpers for (int i = 0; i < len; i++) { if (!double.IsFinite(actual[i]) || !double.IsFinite(predicted[i])) + { return false; + } } return true; } @@ -890,8 +1099,23 @@ public static class ErrorHelpers double act = actual[i]; double pred = predicted[i]; - if (double.IsFinite(act)) currentValidActual = act; else act = currentValidActual; - if (double.IsFinite(pred)) currentValidPredicted = pred; else pred = currentValidPredicted; + if (double.IsFinite(act)) + { + currentValidActual = act; + } + else + { + act = currentValidActual; + } + + if (double.IsFinite(pred)) + { + currentValidPredicted = pred; + } + else + { + pred = currentValidPredicted; + } output[i] = act - pred; } @@ -1099,8 +1323,23 @@ public static class ErrorHelpers double act = actual[i]; double pred = predicted[i]; - if (double.IsFinite(act)) currentValidActual = act; else act = currentValidActual; - if (double.IsFinite(pred)) currentValidPredicted = pred; else pred = currentValidPredicted; + if (double.IsFinite(act)) + { + currentValidActual = act; + } + else + { + act = currentValidActual; + } + + if (double.IsFinite(pred)) + { + currentValidPredicted = pred; + } + else + { + pred = currentValidPredicted; + } output[i] = Math.Abs(act - pred); } @@ -1156,8 +1395,23 @@ public static class ErrorHelpers double act = actual[i]; double pred = predicted[i]; - if (double.IsFinite(act)) currentValidActual = act; else act = currentValidActual; - if (double.IsFinite(pred)) currentValidPredicted = pred; else pred = currentValidPredicted; + if (double.IsFinite(act)) + { + currentValidActual = act; + } + else + { + act = currentValidActual; + } + + if (double.IsFinite(pred)) + { + currentValidPredicted = pred; + } + else + { + pred = currentValidPredicted; + } double diff = act - pred; output[i] = diff * diff; @@ -1181,4 +1435,4 @@ public static class ErrorHelpers } #endregion -} \ No newline at end of file +} diff --git a/lib/core/simd/SimdExtensions.Tests.cs b/lib/core/simd/SimdExtensions.Tests.cs index 7e8583d9..01bb3a2e 100644 --- a/lib/core/simd/SimdExtensions.Tests.cs +++ b/lib/core/simd/SimdExtensions.Tests.cs @@ -112,7 +112,9 @@ public class SimdExtensionsTests { double[] data = new double[1000]; for (int i = 0; i < data.Length; i++) + { data[i] = i + 1.0; + } var span = new ReadOnlySpan(data); const double expected = 1000.0 * 1001.0 / 2.0; @@ -992,4 +994,4 @@ public class SimdScalarFallbackTests Assert.Throws(() => SimdExtensions.Subtract(left, right, result)); } -} \ No newline at end of file +} diff --git a/lib/core/simd/SimdExtensions.cs b/lib/core/simd/SimdExtensions.cs index 0a8c07c1..42b69d6c 100644 --- a/lib/core/simd/SimdExtensions.cs +++ b/lib/core/simd/SimdExtensions.cs @@ -20,7 +20,9 @@ public static class SimdExtensions for (int i = 0; i < span.Length; i++) { if (!double.IsFinite(span[i])) + { return true; + } } return false; } @@ -30,7 +32,10 @@ public static class SimdExtensions { double scalar = 0.0; for (int i = 0; i < span.Length; i++) + { scalar += span[i]; + } + return scalar; } @@ -38,13 +43,17 @@ public static class SimdExtensions internal static double MinScalar(ReadOnlySpan span) { if (span.Length == 0) + { throw new ArgumentException("Span must not be empty", nameof(span)); + } double min = span[0]; for (int i = 1; i < span.Length; i++) { if (span[i] < min) + { min = span[i]; + } } return min; } @@ -53,13 +62,17 @@ public static class SimdExtensions internal static double MaxScalar(ReadOnlySpan span) { if (span.Length == 0) + { throw new ArgumentException("Span must not be empty", nameof(span)); + } double max = span[0]; for (int i = 1; i < span.Length; i++) { if (span[i] > max) + { max = span[i]; + } } return max; } @@ -69,7 +82,9 @@ public static class SimdExtensions { // Match VarianceSIMD behavior: return 0.0 for length <= 1 to avoid divide-by-zero if (span.Length <= 1) + { return 0.0; + } double sumSquares = 0.0; for (int i = 0; i < span.Length; i++) @@ -84,14 +99,23 @@ public static class SimdExtensions internal static (double Min, double Max) MinMaxScalar(ReadOnlySpan span) { if (span.Length == 0) + { throw new ArgumentException("Span must not be empty", nameof(span)); + } double scalarMin = span[0]; double scalarMax = span[0]; for (int i = 1; i < span.Length; i++) { - if (span[i] < scalarMin) scalarMin = span[i]; - if (span[i] > scalarMax) scalarMax = span[i]; + if (span[i] < scalarMin) + { + scalarMin = span[i]; + } + + if (span[i] > scalarMax) + { + scalarMax = span[i]; + } } return (scalarMin, scalarMax); } @@ -105,7 +129,10 @@ public static class SimdExtensions [MethodImpl(MethodImplOptions.AggressiveInlining)] public static bool ContainsNonFinite(this ReadOnlySpan span) { - if (span.IsEmpty) return false; + if (span.IsEmpty) + { + return false; + } if (Vector.IsHardwareAccelerated && span.Length >= Vector.Count) { @@ -120,19 +147,25 @@ public static class SimdExtensions // NaN check: NaN != NaN, so Vector.Equals(v, v) will be false for NaN lanes var nanCheck = Vector.Equals(vector, vector); if (!nanCheck.Equals(Vector.AllBitsSet)) + { return true; + } // Infinity check: |v| > MaxValue (Infinity has magnitude > MaxValue) var absVec = Vector.Abs(vector); var infCheck = Vector.GreaterThan(absVec, maxValue); if (!infCheck.Equals(Vector.Zero)) + { return true; + } } for (; i < span.Length; i++) { if (!double.IsFinite(span[i])) + { return true; + } } return false; @@ -151,7 +184,10 @@ public static class SimdExtensions [MethodImpl(MethodImplOptions.AggressiveInlining)] public static double SumSIMD(this ReadOnlySpan span) { - if (span.IsEmpty) return 0.0; + if (span.IsEmpty) + { + return 0.0; + } if (Vector.IsHardwareAccelerated && span.Length >= Vector.Count) { @@ -167,16 +203,22 @@ public static class SimdExtensions double result = 0.0; for (int j = 0; j < vectorSize; j++) + { result += sum[j]; + } for (; i < span.Length; i++) + { result += span[i]; + } // Lazy check: if result is non-finite AND input contained non-finite values, return NaN // NaN + anything = NaN, Inf + anything finite = Inf // If result is infinite from overflow (no input NaN/Inf), return as-is if (!double.IsFinite(result) && span.ContainsNonFinite()) + { return double.NaN; + } return result; } @@ -194,11 +236,21 @@ public static class SimdExtensions [MethodImpl(MethodImplOptions.AggressiveInlining)] public static double MinSIMD(this ReadOnlySpan span) { - if (span.IsEmpty) return double.NaN; - if (span.Length == 1) return span[0]; + if (span.IsEmpty) + { + return double.NaN; + } + + if (span.Length == 1) + { + return span[0]; + } // Guard against non-finite inputs - if (span.ContainsNonFinite()) return double.NaN; + if (span.ContainsNonFinite()) + { + return double.NaN; + } if (Vector.IsHardwareAccelerated && span.Length >= Vector.Count) { @@ -216,13 +268,17 @@ public static class SimdExtensions for (int j = 1; j < vectorSize; j++) { if (minVec[j] < result) + { result = minVec[j]; + } } for (; i < span.Length; i++) { if (span[i] < result) + { result = span[i]; + } } return result; @@ -239,11 +295,21 @@ public static class SimdExtensions [MethodImpl(MethodImplOptions.AggressiveInlining)] public static double MaxSIMD(this ReadOnlySpan span) { - if (span.IsEmpty) return double.NaN; - if (span.Length == 1) return span[0]; + if (span.IsEmpty) + { + return double.NaN; + } + + if (span.Length == 1) + { + return span[0]; + } // Guard against non-finite inputs - if (span.ContainsNonFinite()) return double.NaN; + if (span.ContainsNonFinite()) + { + return double.NaN; + } if (Vector.IsHardwareAccelerated && span.Length >= Vector.Count) { @@ -261,13 +327,17 @@ public static class SimdExtensions for (int j = 1; j < vectorSize; j++) { if (maxVec[j] > result) + { result = maxVec[j]; + } } for (; i < span.Length; i++) { if (span[i] > result) + { result = span[i]; + } } return result; @@ -284,7 +354,10 @@ public static class SimdExtensions [MethodImpl(MethodImplOptions.AggressiveInlining)] public static double AverageSIMD(this ReadOnlySpan span) { - if (span.IsEmpty) return double.NaN; + if (span.IsEmpty) + { + return double.NaN; + } // SumSIMD already guards against non-finite, which will propagate NaN return span.SumSIMD() / span.Length; } @@ -300,13 +373,20 @@ public static class SimdExtensions public static double VarianceSIMD(this ReadOnlySpan span, double? mean = null) { // Match VarianceScalar behavior: return 0.0 for length <= 1 to avoid inconsistency - if (span.Length <= 1) return 0.0; + if (span.Length <= 1) + { + return 0.0; + } double m; if (mean.HasValue) { // Mean provided externally - need explicit non-finite check - if (span.ContainsNonFinite()) return double.NaN; + if (span.ContainsNonFinite()) + { + return double.NaN; + } + m = mean.Value; } else @@ -317,7 +397,10 @@ public static class SimdExtensions } // If mean is NaN (from input NaN or explicit NaN mean), return NaN - if (!double.IsFinite(m)) return double.NaN; + if (!double.IsFinite(m)) + { + return double.NaN; + } if (Vector.IsHardwareAccelerated && span.Length >= Vector.Count) { @@ -335,7 +418,9 @@ public static class SimdExtensions double result = 0.0; for (int j = 0; j < vectorSize; j++) + { result += sumSq[j]; + } for (; i < span.Length; i++) { @@ -368,11 +453,21 @@ public static class SimdExtensions [MethodImpl(MethodImplOptions.AggressiveInlining)] public static (double Min, double Max) MinMaxSIMD(this ReadOnlySpan span) { - if (span.IsEmpty) return (double.NaN, double.NaN); - if (span.Length == 1) return (span[0], span[0]); + if (span.IsEmpty) + { + return (double.NaN, double.NaN); + } + + if (span.Length == 1) + { + return (span[0], span[0]); + } // Guard against non-finite inputs - if (span.ContainsNonFinite()) return (double.NaN, double.NaN); + if (span.ContainsNonFinite()) + { + return (double.NaN, double.NaN); + } if (Vector.IsHardwareAccelerated && span.Length >= Vector.Count) { @@ -392,14 +487,28 @@ public static class SimdExtensions double max = maxVec[0]; for (int j = 1; j < vectorSize; j++) { - if (minVec[j] < min) min = minVec[j]; - if (maxVec[j] > max) max = maxVec[j]; + if (minVec[j] < min) + { + min = minVec[j]; + } + + if (maxVec[j] > max) + { + max = maxVec[j]; + } } for (; i < span.Length; i++) { - if (span[i] < min) min = span[i]; - if (span[i] > max) max = span[i]; + if (span[i] < min) + { + min = span[i]; + } + + if (span[i] > max) + { + max = span[i]; + } } return (min, max); @@ -416,7 +525,9 @@ public static class SimdExtensions public static void Add(ReadOnlySpan left, ReadOnlySpan right, Span result) { if (left.Length != right.Length || left.Length != result.Length) + { throw new ArgumentException("All spans must have the same length", nameof(result)); + } int i = 0; if (Vector.IsHardwareAccelerated && left.Length >= Vector.Count) @@ -444,7 +555,9 @@ public static class SimdExtensions public static void Scale(ReadOnlySpan source, double scalar, Span result) { if (source.Length != result.Length) + { throw new ArgumentException("Source and result spans must have the same length", nameof(result)); + } int i = 0; if (Vector.IsHardwareAccelerated && source.Length >= Vector.Count) @@ -472,7 +585,9 @@ public static class SimdExtensions public static void Subtract(ReadOnlySpan left, ReadOnlySpan right, Span result) { if (left.Length != right.Length || left.Length != result.Length) + { throw new ArgumentException("All spans must have the same length", nameof(result)); + } int i = 0; if (Vector.IsHardwareAccelerated && left.Length >= Vector.Count) @@ -500,9 +615,14 @@ public static class SimdExtensions public static double DotProduct(this ReadOnlySpan a, ReadOnlySpan b) { if (a.Length != b.Length) + { throw new ArgumentException("Spans must have equal length", nameof(b)); + } - if (a.IsEmpty) return 0.0; + if (a.IsEmpty) + { + return 0.0; + } int len = a.Length; @@ -513,19 +633,33 @@ public static class SimdExtensions ref double bRef = ref MemoryMarshal.GetReference(b); double sum = aRef * bRef; - if (len > 1) sum += Unsafe.Add(ref aRef, 1) * Unsafe.Add(ref bRef, 1); - if (len > 2) sum += Unsafe.Add(ref aRef, 2) * Unsafe.Add(ref bRef, 2); + if (len > 1) + { + sum += Unsafe.Add(ref aRef, 1) * Unsafe.Add(ref bRef, 1); + } + + if (len > 2) + { + sum += Unsafe.Add(ref aRef, 2) * Unsafe.Add(ref bRef, 2); + } + return sum; } if (Avx512F.IsSupported) + { return DotProductAvx512(a, b); + } if (Avx2.IsSupported) + { return DotProductAvx2(a, b); + } if (AdvSimd.Arm64.IsSupported) + { return DotProductNeon(a, b); + } double s1 = 0, s2 = 0, s3 = 0, s4 = 0; ref double ar = ref MemoryMarshal.GetReference(a); @@ -779,4 +913,4 @@ public static class SimdExtensions return sum; } -} \ No newline at end of file +} diff --git a/lib/core/tbar/TBar.Tests.cs b/lib/core/tbar/TBar.Tests.cs index fd90474b..d14405ca 100644 --- a/lib/core/tbar/TBar.Tests.cs +++ b/lib/core/tbar/TBar.Tests.cs @@ -508,4 +508,4 @@ public class TBarTests // (90 + 120 + 60) / 3 = 270 / 3 = 90 Assert.Equal(90.0, bar.OHL3); } -} \ No newline at end of file +} diff --git a/lib/core/tbar/tbar.cs b/lib/core/tbar/tbar.cs index 72f419d8..44517932 100644 --- a/lib/core/tbar/tbar.cs +++ b/lib/core/tbar/tbar.cs @@ -53,4 +53,4 @@ public readonly record struct TBar(long Time, double Open, double High, double L [MethodImpl(MethodImplOptions.AggressiveInlining)] public override string ToString() => $"[{AsDateTime:yyyy-MM-dd HH:mm:ss}: O={Open:F2}, H={High:F2}, L={Low:F2}, C={Close:F2}, V={Volume:F2}]"; -} \ No newline at end of file +} diff --git a/lib/core/tbarseries/tbarseries.cs b/lib/core/tbarseries/tbarseries.cs index fba07cf5..08eb701d 100644 --- a/lib/core/tbarseries/tbarseries.cs +++ b/lib/core/tbarseries/tbarseries.cs @@ -64,7 +64,9 @@ public struct TBarSeriesEnumerator : IEnumerator, IEquatable= _count) + { return false; + } _index++; _current = new TBar(_t[_index], _o[_index], _h[_index], _l[_index], _c[_index], _v[_index]); @@ -323,9 +325,14 @@ public class TBarSeries : IReadOnlyList { int len = t.Length; if (o.Length != len || h.Length != len || l.Length != len || c.Length != len || v.Length != len) + { throw new ArgumentException("All spans must have the same length", nameof(t)); + } - if (len == 0) return; + if (len == 0) + { + return; + } int oldCount = _c.Count; int newCount = oldCount + len; @@ -366,7 +373,10 @@ public class TBarSeries : IReadOnlyList public void AddRange(ReadOnlySpan bars) { int len = bars.Length; - if (len == 0) return; + if (len == 0) + { + return; + } int oldCount = _c.Count; int newCount = oldCount + len; @@ -417,4 +427,4 @@ public class TBarSeries : IReadOnlyList IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); -} \ No newline at end of file +} diff --git a/lib/core/tseries/ITValuePublisher.cs b/lib/core/tseries/ITValuePublisher.cs index b00a9968..b4183be4 100644 --- a/lib/core/tseries/ITValuePublisher.cs +++ b/lib/core/tseries/ITValuePublisher.cs @@ -49,4 +49,4 @@ public interface ITValuePublisher /// event TValuePublishedHandler? Pub; } -#pragma warning restore MA0046 \ No newline at end of file +#pragma warning restore MA0046 diff --git a/lib/core/tseries/tseries.cs b/lib/core/tseries/tseries.cs index a5b068e9..6ac28d7c 100644 --- a/lib/core/tseries/tseries.cs +++ b/lib/core/tseries/tseries.cs @@ -41,7 +41,9 @@ public struct TSeriesEnumerator : IEnumerator, IEquatable= _count) + { return false; + } _index++; _current = new TValue(_t[_index], _v[_index]); @@ -227,4 +229,4 @@ public class TSeries : IReadOnlyList, ITValuePublisher IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); -} \ No newline at end of file +} diff --git a/lib/core/tvalue/TValue.Tests.cs b/lib/core/tvalue/TValue.Tests.cs index 18ca6640..d6f94378 100644 --- a/lib/core/tvalue/TValue.Tests.cs +++ b/lib/core/tvalue/TValue.Tests.cs @@ -373,4 +373,4 @@ public class TValueTests Assert.Equal(long.MaxValue, tValue.Time); } -} \ No newline at end of file +} diff --git a/lib/core/tvalue/tvalue.cs b/lib/core/tvalue/tvalue.cs index 37632797..04b6901c 100644 --- a/lib/core/tvalue/tvalue.cs +++ b/lib/core/tvalue/tvalue.cs @@ -52,7 +52,9 @@ public readonly record struct TValue(long Time, double Value) : ISpanFormattable public string ToString(string? format, IFormatProvider? formatProvider) { if (!string.IsNullOrEmpty(format)) + { throw new NotSupportedException($"Custom format '{format}' is not supported by TValue. Use ToString() for the default format."); + } return ToString(); } @@ -70,7 +72,9 @@ public readonly record struct TValue(long Time, double Value) : ISpanFormattable // This is a heuristic check; actual buffer-overflow protection is performed // by the explicit length checks that guard each write operation below. if (destination.Length < 24) + { return false; + } // Write opening bracket destination[0] = '['; @@ -78,12 +82,18 @@ public readonly record struct TValue(long Time, double Value) : ISpanFormattable // Format datetime: yyyy-MM-dd HH:mm:ss (19 chars) if (!AsDateTime.TryFormat(destination.Slice(pos), out int dtChars, "yyyy-MM-dd HH:mm:ss", provider)) + { return false; + } + pos += dtChars; // Write separator if (pos + 2 > destination.Length) + { return false; + } + destination[pos++] = ','; destination[pos++] = ' '; @@ -91,20 +101,29 @@ public readonly record struct TValue(long Time, double Value) : ISpanFormattable if (double.IsPositiveInfinity(Value)) { if (pos + 1 > destination.Length) + { return false; + } + destination[pos++] = (char)0x221E; //  } else if (double.IsNegativeInfinity(Value)) { if (pos + 2 > destination.Length) + { return false; + } + destination[pos++] = '-'; destination[pos++] = (char)0x221E; // - } else if (double.IsNaN(Value)) { if (pos + 3 > destination.Length) + { return false; + } + destination[pos++] = 'N'; destination[pos++] = 'a'; destination[pos++] = 'N'; @@ -112,13 +131,19 @@ public readonly record struct TValue(long Time, double Value) : ISpanFormattable else { if (!Value.TryFormat(destination.Slice(pos), out int valueChars, "F2", provider)) + { return false; + } + pos += valueChars; } // Write closing bracket if (pos + 1 > destination.Length) + { return false; + } + destination[pos++] = ']'; charsWritten = pos; diff --git a/lib/cycles/stc/Stc.Validation.Tests.cs b/lib/cycles/stc/Stc.Validation.Tests.cs index 6badb92d..2e506c4f 100644 --- a/lib/cycles/stc/Stc.Validation.Tests.cs +++ b/lib/cycles/stc/Stc.Validation.Tests.cs @@ -50,14 +50,14 @@ public sealed class StcValidationTests : IDisposable for (int i = skip; i < qResult.Count; i++) { - double sVal = sResult[i].Stc ?? double.NaN; - double qVal = qResult[i].Value; + double sVal = sResult[i].Stc ?? double.NaN; + double qVal = qResult[i].Value; - if (!double.IsNaN(sVal) && !double.IsNaN(qVal)) - { - sumSq += (sVal - qVal) * (sVal - qVal); - count++; - } + if (!double.IsNaN(sVal) && !double.IsNaN(qVal)) + { + sumSq += (sVal - qVal) * (sVal - qVal); + count++; + } } double rmse = Math.Sqrt(sumSq / count); @@ -68,7 +68,7 @@ public sealed class StcValidationTests : IDisposable Assert.True(rmse > 5.0, "QuanTAlib STC matches Skender STC, which suggests regression to Single Smoothed logic."); // Assert values are valid - for(int i = skip; i < qResult.Count; i++) + for (int i = skip; i < qResult.Count; i++) { Assert.True(double.IsFinite(qResult[i].Value)); Assert.InRange(qResult[i].Value, 0, 100); diff --git a/lib/cycles/stc/Stc.cs b/lib/cycles/stc/Stc.cs index 974e2949..5c846c80 100644 --- a/lib/cycles/stc/Stc.cs +++ b/lib/cycles/stc/Stc.cs @@ -113,7 +113,11 @@ public sealed class Stc : AbstractBase [MethodImpl(MethodImplOptions.AggressiveInlining)] private static double Clamp100(double x) { - if (double.IsNaN(x)) return x; + if (double.IsNaN(x)) + { + return x; + } + return Math.Clamp(x, 0, 100); } @@ -139,9 +143,19 @@ public sealed class Stc : AbstractBase break; case StcSmoothing.Digital: - if (stoch2Raw > 75) stc = 100; - else if (stoch2Raw < 25) stc = 0; - else stc = double.IsNaN(prevStc) ? stoch2Raw : prevStc; + if (stoch2Raw > 75) + { + stc = 100; + } + else if (stoch2Raw < 25) + { + stc = 0; + } + else + { + stc = double.IsNaN(prevStc) ? stoch2Raw : prevStc; + } + break; default: // Includes StcSmoothing.None @@ -159,26 +173,48 @@ public sealed class Stc : AbstractBase [MethodImpl(MethodImplOptions.AggressiveInlining)] private static bool UpdateMinMaxCore(double added, double removed, bool hasRemoved, ref double min, ref double max) { - if (double.IsNaN(added)) return false; + if (double.IsNaN(added)) + { + return false; + } bool expandMin = added < min; bool expandMax = added > max; if (!hasRemoved) { - if (expandMin) min = added; - if (expandMax) max = added; + if (expandMin) + { + min = added; + } + + if (expandMax) + { + max = added; + } + return false; } // Use relative tolerance for floating-point comparison double tolerance = Math.Max(Math.Abs(min), Math.Abs(max)) * 1e-12; - if (tolerance < 1e-15) tolerance = 1e-15; // minimum absolute tolerance + if (tolerance < 1e-15) + { + tolerance = 1e-15; // minimum absolute tolerance + } + bool removedMin = Math.Abs(removed - min) <= tolerance; bool removedMax = Math.Abs(removed - max) <= tolerance; - if (expandMin) min = added; - if (expandMax) max = added; + if (expandMin) + { + min = added; + } + + if (expandMax) + { + max = added; + } return (removedMin && !expandMin) || (removedMax && !expandMax); } @@ -193,9 +229,20 @@ public sealed class Stc : AbstractBase max = double.NegativeInfinity; foreach (double v in span) { - if (double.IsNaN(v)) continue; - if (v < min) min = v; - if (v > max) max = v; + if (double.IsNaN(v)) + { + continue; + } + + if (v < min) + { + min = v; + } + + if (v > max) + { + max = v; + } } } @@ -225,8 +272,14 @@ public sealed class Stc : AbstractBase public override TValue Update(TValue input, bool isNew = true) { _isNew = isNew; - if (isNew) _ps = _s; - else _s = _ps; + if (isNew) + { + _ps = _s; + } + else + { + _s = _ps; + } var s = _s; @@ -276,9 +329,13 @@ public sealed class Stc : AbstractBase { double span = s.MacdMax - s.MacdMin; if (span > double.Epsilon) + { stoch1Raw = 100.0 * (macd - s.MacdMin) / span; + } else + { stoch1Raw = double.IsNaN(s.Stoch1Ema) ? 50.0 : s.Stoch1Ema; + } stoch1Raw = Clamp100(stoch1Raw); } @@ -322,9 +379,13 @@ public sealed class Stc : AbstractBase { double span = s.Stoch1Max - s.Stoch1Min; if (span > double.Epsilon) + { stoch2Raw = 100.0 * (stoch1 - s.Stoch1Min) / span; + } else + { stoch2Raw = double.IsNaN(s.Stoch2Ema) ? stoch1 : s.Stoch2Ema; + } stoch2Raw = Clamp100(stoch2Raw); } @@ -340,7 +401,10 @@ public sealed class Stc : AbstractBase stc = ApplySmoothing(stoch2Raw, _smoothing, _dAlpha, ref s.Stoch2Ema, ref s.PrevStc); } - if (isNew) _samples++; + if (isNew) + { + _samples++; + } _s = s; Last = new TValue(input.Time, stc); @@ -352,14 +416,19 @@ public sealed class Stc : AbstractBase { var result = new TSeries(); foreach (var item in source) + { result.Add(Update(item, isNew: true)); + } + return result; } public override void Prime(ReadOnlySpan source, TimeSpan? step = null) { foreach (double v in source) + { Update(new TValue(DateTime.MinValue, v), isNew: true); + } } [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -389,7 +458,9 @@ public sealed class Stc : AbstractBase int kPeriod = 10, int dPeriod = 3, int fastLength = 23, int slowLength = 50, StcSmoothing smoothing = StcSmoothing.Ema) { if (source.Length != output.Length) + { throw new ArgumentException("Source and output spans must be of equal length.", nameof(output)); + } double fastAlpha = 2.0 / (fastLength + 1.0); double slowAlpha = 2.0 / (slowLength + 1.0); @@ -465,7 +536,10 @@ public sealed class Stc : AbstractBase double macdRemoved = macdBuf[macdIdx]; macdBuf[macdIdx] = macd; macdIdx = (macdIdx + 1) % kPeriod; - if (!macdHasRemoved) macdCount++; + if (!macdHasRemoved) + { + macdCount++; + } ReadOnlySpan macdValidSpan = macdBuf.Slice(0, macdCount); UpdateMinMax(macd, macdRemoved, macdHasRemoved, macdValidSpan, ref macdMin, ref macdMax); @@ -476,9 +550,13 @@ public sealed class Stc : AbstractBase { double span = macdMax - macdMin; if (span > double.Epsilon) + { stoch1Raw = 100.0 * (macd - macdMin) / span; + } else + { stoch1Raw = double.IsNaN(stoch1Ema) ? 50.0 : stoch1Ema; + } stoch1Raw = Clamp100(stoch1Raw); } @@ -505,7 +583,10 @@ public sealed class Stc : AbstractBase double stochRemoved = stoch1Buf[stoch1Idx]; stoch1Buf[stoch1Idx] = stoch1; stoch1Idx = (stoch1Idx + 1) % kPeriod; - if (!stochHasRemoved) stoch1Count++; + if (!stochHasRemoved) + { + stoch1Count++; + } ReadOnlySpan stochValidSpan = stoch1Buf.Slice(0, stoch1Count); UpdateMinMax(stoch1, stochRemoved, stochHasRemoved, stochValidSpan, ref stoch1Min, ref stoch1Max); @@ -517,9 +598,13 @@ public sealed class Stc : AbstractBase { double span = stoch1Max - stoch1Min; if (span > double.Epsilon) + { stoch2Raw = 100.0 * (stoch1 - stoch1Min) / span; + } else + { stoch2Raw = double.IsNaN(stoch2Ema) ? stoch1 : stoch2Ema; + } stoch2Raw = Clamp100(stoch2Raw); } @@ -541,9 +626,14 @@ public sealed class Stc : AbstractBase finally { if (rentedMacd != null) + { ArrayPool.Shared.Return(rentedMacd); + } + if (rentedStoch1 != null) + { ArrayPool.Shared.Return(rentedStoch1); + } } } } diff --git a/lib/dynamics/adx/Adx.Tests.cs b/lib/dynamics/adx/Adx.Tests.cs index 28cc71b1..87f2fc63 100644 --- a/lib/dynamics/adx/Adx.Tests.cs +++ b/lib/dynamics/adx/Adx.Tests.cs @@ -59,7 +59,9 @@ public class AdxTests var bars = gbm.Fetch(100, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1)); for (int i = 0; i < 50; i++) + { adx.Update(bars[i]); + } var originalValue = adx.Last; @@ -110,7 +112,10 @@ public class AdxTests for (int i = 0; i < bars.Count; i++) { adx.Update(bars[i]); - if (adx.IsHot) break; + if (adx.IsHot) + { + break; + } } Assert.True(adx.IsHot); @@ -124,7 +129,9 @@ public class AdxTests var bars = gbm.Fetch(50, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1)); for (int i = 0; i < 40; i++) + { adx.Update(bars[i]); + } var nanBar = new TBar(DateTime.UtcNow, double.NaN, double.NaN, double.NaN, double.NaN, 100); var result = adx.Update(nanBar); @@ -140,7 +147,9 @@ public class AdxTests var bars = gbm.Fetch(50, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1)); for (int i = 0; i < 40; i++) + { adx.Update(bars[i]); + } var infBar = new TBar(DateTime.UtcNow, double.PositiveInfinity, double.PositiveInfinity, 0, 100, 100); var result = adx.Update(infBar); @@ -161,7 +170,10 @@ public class AdxTests // 2. Streaming Mode var streamAdx = new Adx(14); for (int i = 0; i < bars.Count; i++) + { streamAdx.Update(bars[i]); + } + double streamResult = streamAdx.Last.Value; Assert.Equal(expected, streamResult, 9); @@ -235,4 +247,4 @@ public class AdxTests Assert.Throws(() => new Adx(0)); Assert.Throws(() => new Adx(-1)); } -} \ No newline at end of file +} diff --git a/lib/dynamics/adx/Adx.cs b/lib/dynamics/adx/Adx.cs index 07ea8561..2a209245 100644 --- a/lib/dynamics/adx/Adx.cs +++ b/lib/dynamics/adx/Adx.cs @@ -92,7 +92,9 @@ public sealed class Adx : ITValuePublisher public Adx(int period) { if (period <= 0) + { throw new ArgumentException("Period must be greater than 0", nameof(period)); + } _period = period; _decay = (period - 1.0) / period; @@ -177,14 +179,17 @@ public sealed class Adx : ITValuePublisher double prevClose = double.IsFinite(_prevBar.Close) ? _prevBar.Close : high; double prevHigh = double.IsFinite(_prevBar.High) ? _prevBar.High : high; double prevLow = double.IsFinite(_prevBar.Low) ? _prevBar.Low : low; - + double hl = high - low; double hpc = Math.Abs(high - prevClose); double lpc = Math.Abs(low - prevClose); double tr = Math.Max(hl, Math.Max(hpc, lpc)); // Guard TR against non-finite values - if (!double.IsFinite(tr)) tr = 0; + if (!double.IsFinite(tr)) + { + tr = 0; + } // Calculate DM using guarded values double dmPlus = 0; @@ -193,14 +198,25 @@ public sealed class Adx : ITValuePublisher double downMove = prevLow - low; // Guard moves against non-finite values - if (!double.IsFinite(upMove)) upMove = 0; - if (!double.IsFinite(downMove)) downMove = 0; + if (!double.IsFinite(upMove)) + { + upMove = 0; + } + + if (!double.IsFinite(downMove)) + { + downMove = 0; + } if (upMove > downMove && upMove > 0) + { dmPlus = upMove; + } if (downMove > upMove && downMove > 0) + { dmMinus = downMove; + } if (isNew) { @@ -251,8 +267,15 @@ public sealed class Adx : ITValuePublisher } // Guard against NaN/Infinity in DI calculations - if (!double.IsFinite(diPlus)) diPlus = 0; - if (!double.IsFinite(diMinus)) diMinus = 0; + if (!double.IsFinite(diPlus)) + { + diPlus = 0; + } + + if (!double.IsFinite(diMinus)) + { + diMinus = 0; + } double diSum = diPlus + diMinus; if (diSum > 1e-10) @@ -261,7 +284,10 @@ public sealed class Adx : ITValuePublisher } // Guard against NaN/Infinity in DX calculation - if (!double.IsFinite(dx)) dx = 0; + if (!double.IsFinite(dx)) + { + dx = 0; + } // Smooth DX to get ADX if (_dxSamples < _period) @@ -281,17 +307,34 @@ public sealed class Adx : ITValuePublisher } // Final guard on ADX - if (!double.IsFinite(_adx)) _adx = _p_adx; + if (!double.IsFinite(_adx)) + { + _adx = _p_adx; + } } // Ensure all outputs are finite; if not, use previous values or 0 - if (!double.IsFinite(diPlus)) diPlus = double.IsFinite(DiPlus.Value) ? DiPlus.Value : 0; - if (!double.IsFinite(diMinus)) diMinus = double.IsFinite(DiMinus.Value) ? DiMinus.Value : 0; - + if (!double.IsFinite(diPlus)) + { + diPlus = double.IsFinite(DiPlus.Value) ? DiPlus.Value : 0; + } + + if (!double.IsFinite(diMinus)) + { + diMinus = double.IsFinite(DiMinus.Value) ? DiMinus.Value : 0; + } + // Final guard on ADX output - ensure we always return a finite value double finalAdx = _adx; - if (!double.IsFinite(finalAdx)) finalAdx = _p_adx; - if (!double.IsFinite(finalAdx)) finalAdx = 0; + if (!double.IsFinite(finalAdx)) + { + finalAdx = _p_adx; + } + + if (!double.IsFinite(finalAdx)) + { + finalAdx = 0; + } DiPlus = new TValue(input.Time, diPlus); DiMinus = new TValue(input.Time, diMinus); @@ -309,7 +352,10 @@ public sealed class Adx : ITValuePublisher public TSeries Update(TBarSeries source) { - if (source.Count == 0) return new TSeries([], []); + if (source.Count == 0) + { + return new TSeries([], []); + } var len = source.Count; var v = new double[len]; @@ -450,7 +496,11 @@ public sealed class Adx : ITValuePublisher [MethodImpl(MethodImplOptions.AggressiveInlining)] public static TSeries Batch(TBarSeries source, int period) { - if (source.Count == 0) return new TSeries([], []); + if (source.Count == 0) + { + return new TSeries([], []); + } + var len = source.Count; var v = new double[len]; Calculate(source.High.Values, source.Low.Values, source.Close.Values, period, v); @@ -464,4 +514,4 @@ public sealed class Adx : ITValuePublisher return new TSeries(tList, [.. v]); } -} \ No newline at end of file +} diff --git a/lib/dynamics/adxr/Adxr.cs b/lib/dynamics/adxr/Adxr.cs index c08a34f3..a52f7706 100644 --- a/lib/dynamics/adxr/Adxr.cs +++ b/lib/dynamics/adxr/Adxr.cs @@ -54,7 +54,9 @@ public sealed class Adxr : ITValuePublisher public Adxr(int period) { if (period <= 0) + { throw new ArgumentException("Period must be greater than 0", nameof(period)); + } _period = period; Name = $"Adxr({period})"; @@ -126,7 +128,10 @@ public sealed class Adxr : ITValuePublisher public TSeries Update(TBarSeries source) { - if (source.Count == 0) return new TSeries([], []); + if (source.Count == 0) + { + return new TSeries([], []); + } int len = source.Count; var v = new double[len]; @@ -205,14 +210,19 @@ public sealed class Adxr : ITValuePublisher finally { if (rentedAdx != null) + { ArrayPool.Shared.Return(rentedAdx); + } } } [MethodImpl(MethodImplOptions.AggressiveInlining)] public static TSeries Batch(TBarSeries source, int period) { - if (source.Count == 0) return new TSeries([], []); + if (source.Count == 0) + { + return new TSeries([], []); + } int len = source.Count; var v = new double[len]; @@ -228,4 +238,4 @@ public sealed class Adxr : ITValuePublisher return new TSeries(tList, [.. v]); } -} \ No newline at end of file +} diff --git a/lib/dynamics/amat/Amat.Tests.cs b/lib/dynamics/amat/Amat.Tests.cs index e0a53e79..1020e365 100644 --- a/lib/dynamics/amat/Amat.Tests.cs +++ b/lib/dynamics/amat/Amat.Tests.cs @@ -388,7 +388,9 @@ public class AmatTests for (int i = warmup; i < source.Length; i++) { if (Math.Abs(tseriesResult[i].Value - trend[i]) < 0.01) + { matched++; + } } // At least 95% of values after warmup should match double matchRate = (double)matched / (source.Length - warmup); diff --git a/lib/dynamics/amat/Amat.Validation.Tests.cs b/lib/dynamics/amat/Amat.Validation.Tests.cs index d6fa1368..e863dfc3 100644 --- a/lib/dynamics/amat/Amat.Validation.Tests.cs +++ b/lib/dynamics/amat/Amat.Validation.Tests.cs @@ -373,7 +373,7 @@ public sealed class AmatValidationTests : IDisposable double trendMatchRate = (double)trendMatchCount / totalCount; double strengthMatchRate = (double)strengthMatchCount / totalCount; - + Assert.True(trendMatchRate > 0.95, $"Expected >95% trend match rate after warmup, got {trendMatchRate:P2}"); Assert.True(strengthMatchRate > 0.95, $"Expected >95% strength match rate after warmup, got {strengthMatchRate:P2}"); @@ -437,4 +437,4 @@ public sealed class AmatValidationTests : IDisposable _output.WriteLine($"Period combination ({fastPeriod}, {slowPeriod}) validated: Trend={amat.Last.Value}, Strength={amat.Strength.Value:F2}%"); } -} \ No newline at end of file +} diff --git a/lib/dynamics/amat/Amat.cs b/lib/dynamics/amat/Amat.cs index e8d5318b..f50dda68 100644 --- a/lib/dynamics/amat/Amat.cs +++ b/lib/dynamics/amat/Amat.cs @@ -123,11 +123,19 @@ public sealed class Amat : ITValuePublisher, IDisposable public Amat(int fastPeriod = 10, int slowPeriod = 50) { if (fastPeriod <= 0) + { throw new ArgumentException("Fast period must be greater than 0", nameof(fastPeriod)); + } + if (slowPeriod <= 0) + { throw new ArgumentException("Slow period must be greater than 0", nameof(slowPeriod)); + } + if (fastPeriod >= slowPeriod) + { throw new ArgumentException("Fast period must be less than slow period", nameof(fastPeriod)); + } _fastAlpha = 2.0 / (fastPeriod + 1); _slowAlpha = 2.0 / (slowPeriod + 1); @@ -314,7 +322,10 @@ public sealed class Amat : ITValuePublisher, IDisposable /// Series of trend values public TSeries Update(TSeries source) { - if (source.Count == 0) return []; + if (source.Count == 0) + { + return []; + } int len = source.Count; var t = new List(len); @@ -342,7 +353,10 @@ public sealed class Amat : ITValuePublisher, IDisposable private static double GetCompensatedValue(double ema, double e, bool isCompensated) { if (isCompensated || e <= COMPENSATOR_THRESHOLD) + { return ema; + } + return ema / (1.0 - e); } @@ -358,7 +372,9 @@ public sealed class Amat : ITValuePublisher, IDisposable e *= decay; if (!isHot && e <= COVERAGE_THRESHOLD) + { isHot = true; + } if (e <= COMPENSATOR_THRESHOLD) { @@ -391,18 +407,35 @@ public sealed class Amat : ITValuePublisher, IDisposable int fastPeriod = 10, int slowPeriod = 50) { if (source.Length != trend.Length) + { throw new ArgumentException("Source and trend must have the same length", nameof(trend)); + } + if (source.Length != strength.Length) + { throw new ArgumentException("Source and strength must have the same length", nameof(strength)); + } + if (fastPeriod <= 0) + { throw new ArgumentException("Fast period must be greater than 0", nameof(fastPeriod)); + } + if (slowPeriod <= 0) + { throw new ArgumentException("Slow period must be greater than 0", nameof(slowPeriod)); + } + if (fastPeriod >= slowPeriod) + { throw new ArgumentException("Fast period must be less than slow period", nameof(fastPeriod)); + } int len = source.Length; - if (len == 0) return; + if (len == 0) + { + return; + } double fastAlpha = 2.0 / (fastPeriod + 1); double slowAlpha = 2.0 / (slowPeriod + 1); @@ -483,16 +516,30 @@ public sealed class Amat : ITValuePublisher, IDisposable int fastPeriod = 10, int slowPeriod = 50) { if (source.Length != trend.Length) + { throw new ArgumentException("Source and trend must have the same length", nameof(trend)); + } + if (fastPeriod <= 0) + { throw new ArgumentException("Fast period must be greater than 0", nameof(fastPeriod)); + } + if (slowPeriod <= 0) + { throw new ArgumentException("Slow period must be greater than 0", nameof(slowPeriod)); + } + if (fastPeriod >= slowPeriod) + { throw new ArgumentException("Fast period must be less than slow period", nameof(fastPeriod)); + } int len = source.Length; - if (len == 0) return; + if (len == 0) + { + return; + } double fastAlpha = 2.0 / (fastPeriod + 1); double slowAlpha = 2.0 / (slowPeriod + 1); @@ -575,4 +622,4 @@ public sealed class Amat : ITValuePublisher, IDisposable var amat = new Amat(fastPeriod, slowPeriod); return amat.Update(source); } -} \ No newline at end of file +} diff --git a/lib/dynamics/aroon/Aroon.cs b/lib/dynamics/aroon/Aroon.cs index 279eb608..0b2f33e3 100644 --- a/lib/dynamics/aroon/Aroon.cs +++ b/lib/dynamics/aroon/Aroon.cs @@ -67,7 +67,9 @@ public sealed class Aroon : ITValuePublisher public Aroon(int period) { if (period <= 0) + { throw new ArgumentException("Period must be greater than 0", nameof(period)); + } _period = period; Name = $"Aroon({period})"; @@ -163,7 +165,10 @@ public sealed class Aroon : ITValuePublisher public TSeries Update(TBarSeries source) { - if (source.Count == 0) return new TSeries([], []); + if (source.Count == 0) + { + return new TSeries([], []); + } int len = source.Count; var v = new double[len]; @@ -288,7 +293,10 @@ public sealed class Aroon : ITValuePublisher [MethodImpl(MethodImplOptions.AggressiveInlining)] public static TSeries Batch(TBarSeries source, int period) { - if (source.Count == 0) return new TSeries([], []); + if (source.Count == 0) + { + return new TSeries([], []); + } int len = source.Count; var v = new double[len]; @@ -304,4 +312,4 @@ public sealed class Aroon : ITValuePublisher return new TSeries(tList, [.. v]); } -} \ No newline at end of file +} diff --git a/lib/dynamics/aroonosc/AroonOsc.cs b/lib/dynamics/aroonosc/AroonOsc.cs index b3140168..3e9b4bc7 100644 --- a/lib/dynamics/aroonosc/AroonOsc.cs +++ b/lib/dynamics/aroonosc/AroonOsc.cs @@ -56,7 +56,9 @@ public sealed class AroonOsc : ITValuePublisher public AroonOsc(int period) { if (period <= 0) + { throw new ArgumentException("Period must be greater than 0", nameof(period)); + } _period = period; Name = $"AroonOsc({period})"; @@ -148,7 +150,10 @@ public sealed class AroonOsc : ITValuePublisher public TSeries Update(TBarSeries source) { - if (source.Count == 0) return new TSeries([], []); + if (source.Count == 0) + { + return new TSeries([], []); + } int len = source.Count; var v = new double[len]; @@ -189,7 +194,10 @@ public sealed class AroonOsc : ITValuePublisher [MethodImpl(MethodImplOptions.AggressiveInlining)] public static TSeries Batch(TBarSeries source, int period) { - if (source.Count == 0) return new TSeries([], []); + if (source.Count == 0) + { + return new TSeries([], []); + } int len = source.Count; var v = new double[len]; @@ -205,4 +213,4 @@ public sealed class AroonOsc : ITValuePublisher return new TSeries(tList, [.. v]); } -} \ No newline at end of file +} diff --git a/lib/dynamics/dmx/Dmx.Tests.cs b/lib/dynamics/dmx/Dmx.Tests.cs index 48567c7e..59ef2494 100644 --- a/lib/dynamics/dmx/Dmx.Tests.cs +++ b/lib/dynamics/dmx/Dmx.Tests.cs @@ -68,7 +68,9 @@ public class DmxTests var bars = gbm.Fetch(100, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1)); for (int i = 0; i < 50; i++) + { dmx.Update(bars[i]); + } var originalValue = dmx.Last; @@ -114,7 +116,9 @@ public class DmxTests var bars = gbm.Fetch(50, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1)); for (int i = 0; i < 30; i++) + { dmx.Update(bars[i]); + } var nanBar = new TBar(DateTime.UtcNow, double.NaN, double.NaN, double.NaN, double.NaN, 100); var result = dmx.Update(nanBar); @@ -130,7 +134,9 @@ public class DmxTests var bars = gbm.Fetch(50, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1)); for (int i = 0; i < 30; i++) + { dmx.Update(bars[i]); + } var infBar = new TBar(DateTime.UtcNow, double.PositiveInfinity, double.PositiveInfinity, 0, 100, 100); var result = dmx.Update(infBar); @@ -151,7 +157,10 @@ public class DmxTests // 2. Streaming Mode var streamDmx = new Dmx(14); for (int i = 0; i < bars.Count; i++) + { streamDmx.Update(bars[i]); + } + double streamResult = streamDmx.Last.Value; Assert.Equal(expected, streamResult, 9); diff --git a/lib/dynamics/dmx/Dmx.cs b/lib/dynamics/dmx/Dmx.cs index 5da83368..70e37bf7 100644 --- a/lib/dynamics/dmx/Dmx.cs +++ b/lib/dynamics/dmx/Dmx.cs @@ -109,10 +109,14 @@ public sealed class Dmx : ITValuePublisher double downMove = _prevBar.Low - input.Low; if (upMove > downMove && upMove > 0) + { dmPlusRaw = upMove; + } if (downMove > upMove && downMove > 0) + { dmMinusRaw = downMove; + } double tr1 = input.High - input.Low; double tr2 = Math.Abs(input.High - _prevBar.Close); @@ -147,7 +151,9 @@ public sealed class Dmx : ITValuePublisher { int count = source.Count; if (count == 0) + { return []; + } var t = new List(count); var v = new List(count); @@ -182,13 +188,19 @@ public sealed class Dmx : ITValuePublisher { int len = high.Length; if (len == 0) + { return; + } if (low.Length != len || close.Length != len || destination.Length != len) + { throw new ArgumentException("All input spans must have the same length", nameof(destination)); + } if (period <= 0) + { throw new ArgumentException("Period must be greater than zero.", nameof(period)); + } // Use single ArrayPool rent with slicing for better cache locality and fewer allocations // Need 6 buffers of len each: dmPlus, dmMinus, tr, dmPlusSmooth, dmMinusSmooth, trSmooth @@ -238,10 +250,14 @@ public sealed class Dmx : ITValuePublisher double dmMinusRaw = 0.0; if (upMove > downMove && upMove > 0.0) + { dmPlusRaw = upMove; + } if (downMove > upMove && downMove > 0.0) + { dmMinusRaw = downMove; + } double tr1 = h - l; double tr2 = Math.Abs(h - pc); @@ -275,7 +291,9 @@ public sealed class Dmx : ITValuePublisher finally { if (rented != null) + { ArrayPool.Shared.Return(rented); + } } } @@ -284,4 +302,4 @@ public sealed class Dmx : ITValuePublisher var dmx = new Dmx(period); return dmx.Update(source); } -} \ No newline at end of file +} diff --git a/lib/dynamics/super/Super.cs b/lib/dynamics/super/Super.cs index 79bcd26c..e8fcaba7 100644 --- a/lib/dynamics/super/Super.cs +++ b/lib/dynamics/super/Super.cs @@ -265,4 +265,4 @@ public sealed class Super : ITValuePublisher var indicator = new Super(period, multiplier); return indicator.Update(source); } -} \ No newline at end of file +} diff --git a/lib/errors/huber/Huber.Tests.cs b/lib/errors/huber/Huber.Tests.cs index 388c6c50..70dfb24f 100644 --- a/lib/errors/huber/Huber.Tests.cs +++ b/lib/errors/huber/Huber.Tests.cs @@ -345,8 +345,15 @@ public class HuberTests var actual = new TSeries(); var predicted = new TSeries(); - for (int i = 0; i < 10; i++) actual.Add(DateTime.UtcNow, i); - for (int i = 0; i < 5; i++) predicted.Add(DateTime.UtcNow, i); + for (int i = 0; i < 10; i++) + { + actual.Add(DateTime.UtcNow, i); + } + + for (int i = 0; i < 5; i++) + { + predicted.Add(DateTime.UtcNow, i); + } Assert.Throws(() => Huber.Calculate(actual, predicted, 3)); } @@ -403,4 +410,4 @@ public class HuberTests // With delta=5: linear region -> 5*10 - 12.5 = 37.5 Assert.NotEqual(huber1.Last.Value, huber2.Last.Value); } -} \ No newline at end of file +} diff --git a/lib/errors/huber/Huber.cs b/lib/errors/huber/Huber.cs index 7b7a6dcf..c06c798f 100644 --- a/lib/errors/huber/Huber.cs +++ b/lib/errors/huber/Huber.cs @@ -40,7 +40,9 @@ public sealed class Huber : BiInputIndicatorBase : base(period, $"Huber({period},{delta:F3})") { if (delta <= 0) + { throw new ArgumentException("Delta must be greater than 0", nameof(delta)); + } Delta = delta; _negHalfDeltaSquared = -0.5 * delta * delta; @@ -68,7 +70,9 @@ public sealed class Huber : BiInputIndicatorBase public static TSeries Calculate(TSeries actual, TSeries predicted, int period, double delta = 1.345) { if (actual.Count != predicted.Count) + { throw new ArgumentException("Actual and predicted series must have the same length", nameof(predicted)); + } int len = actual.Count; var t = new List(len); @@ -92,14 +96,25 @@ public sealed class Huber : BiInputIndicatorBase public static void Batch(ReadOnlySpan actual, ReadOnlySpan predicted, Span output, int period, double delta = 1.345) { if (actual.Length != predicted.Length || actual.Length != output.Length) + { throw new ArgumentException("All spans must have the same length", nameof(output)); + } + if (period <= 0) + { throw new ArgumentException("Period must be greater than 0", nameof(period)); + } + if (delta <= 0) + { throw new ArgumentException("Delta must be greater than 0", nameof(delta)); + } int len = actual.Length; - if (len == 0) return; + if (len == 0) + { + return; + } // Pre-compute Huber errors using shared helper const int StackAllocThreshold = 256; diff --git a/lib/errors/logcosh/LogCosh.cs b/lib/errors/logcosh/LogCosh.cs index 2671bbf3..c07d7d3b 100644 --- a/lib/errors/logcosh/LogCosh.cs +++ b/lib/errors/logcosh/LogCosh.cs @@ -48,7 +48,10 @@ public sealed class LogCosh : BiInputIndicatorBase double absX = Math.Abs(x); // For large values, use asymptotic approximation to avoid overflow if (absX > 20.0) + { return absX - 0.6931471805599453; // log(2) + } + return Math.Log(Math.Cosh(x)); } @@ -67,7 +70,10 @@ public sealed class LogCosh : BiInputIndicatorBase ValidateBatchInputs(actual, predicted, output, period); int len = actual.Length; - if (len == 0) return; + if (len == 0) + { + return; + } const int StackAllocThreshold = 256; if (len <= StackAllocThreshold) @@ -91,4 +97,4 @@ public sealed class LogCosh : BiInputIndicatorBase } } } -} \ No newline at end of file +} diff --git a/lib/errors/maape/Maape.cs b/lib/errors/maape/Maape.cs index e523f9ee..4a3ae12b 100644 --- a/lib/errors/maape/Maape.cs +++ b/lib/errors/maape/Maape.cs @@ -52,7 +52,9 @@ public sealed class Maape : BiInputIndicatorBase public static TSeries Calculate(TSeries actual, TSeries predicted, int period) { if (actual.Count != predicted.Count) + { throw new ArgumentException("Actual and predicted series must have the same length", nameof(predicted)); + } int len = actual.Count; var t = new List(len); @@ -76,12 +78,20 @@ public sealed class Maape : BiInputIndicatorBase public static void Batch(ReadOnlySpan actual, ReadOnlySpan predicted, Span output, int period) { if (actual.Length != predicted.Length || actual.Length != output.Length) + { throw new ArgumentException("All spans must have the same length", nameof(output)); + } + if (period <= 0) + { throw new ArgumentException("Period must be greater than 0", nameof(period)); + } int len = actual.Length; - if (len == 0) return; + if (len == 0) + { + return; + } // Pre-compute arctangent errors const int StackAllocThreshold = 256; @@ -100,7 +110,9 @@ public sealed class Maape : BiInputIndicatorBase finally { if (rented != null) + { ArrayPool.Shared.Return(rented); + } } } @@ -133,7 +145,9 @@ public sealed class Maape : BiInputIndicatorBase foundPredicted = true; } if (foundActual && foundPredicted) + { break; + } } for (int i = 0; i < len; i++) @@ -142,18 +156,26 @@ public sealed class Maape : BiInputIndicatorBase double pred = predicted[i]; if (double.IsFinite(act)) + { lastValidActual = act; + } else + { act = lastValidActual; + } if (double.IsFinite(pred)) + { lastValidPredicted = pred; + } else + { pred = lastValidPredicted; + } double absActual = Math.Abs(act); double absError = Math.Abs(act - pred); 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 801310a3..1c5c4bc7 100644 --- a/lib/errors/mae/Mae.Tests.cs +++ b/lib/errors/mae/Mae.Tests.cs @@ -321,8 +321,15 @@ public class MaeTests var actual = new TSeries(); var predicted = new TSeries(); - for (int i = 0; i < 10; i++) actual.Add(DateTime.UtcNow, i); - for (int i = 0; i < 5; i++) predicted.Add(DateTime.UtcNow, i); + for (int i = 0; i < 10; i++) + { + actual.Add(DateTime.UtcNow, i); + } + + for (int i = 0; i < 5; i++) + { + predicted.Add(DateTime.UtcNow, i); + } Assert.Throws(() => Mae.Calculate(actual, predicted, 3)); } @@ -356,4 +363,4 @@ public class MaeTests // After resync, result should still be correct Assert.Equal(10.0, mae.Last.Value, 10); } -} \ No newline at end of file +} diff --git a/lib/errors/mae/Mae.cs b/lib/errors/mae/Mae.cs index 0bd0d4a5..ab1366b4 100644 --- a/lib/errors/mae/Mae.cs +++ b/lib/errors/mae/Mae.cs @@ -60,7 +60,10 @@ public sealed class Mae : BiInputIndicatorBase public static void Batch(ReadOnlySpan actual, ReadOnlySpan predicted, Span output, int period) { ValidateBatchInputs(actual, predicted, output, period); - if (actual.Length == 0) return; + if (actual.Length == 0) + { + return; + } // Allocate temporary buffer for absolute errors const int StackAllocThreshold = 256; @@ -86,4 +89,4 @@ public sealed class Mae : BiInputIndicatorBase } } } -} \ No newline at end of file +} diff --git a/lib/errors/mapd/Mapd.Tests.cs b/lib/errors/mapd/Mapd.Tests.cs index cf1a9183..2291ceeb 100644 --- a/lib/errors/mapd/Mapd.Tests.cs +++ b/lib/errors/mapd/Mapd.Tests.cs @@ -305,8 +305,15 @@ public class MapdTests var actual = new TSeries(); var predicted = new TSeries(); - for (int i = 0; i < 10; i++) actual.Add(DateTime.UtcNow, i + 1); - for (int i = 0; i < 5; i++) predicted.Add(DateTime.UtcNow, i + 1); + for (int i = 0; i < 10; i++) + { + actual.Add(DateTime.UtcNow, i + 1); + } + + for (int i = 0; i < 5; i++) + { + predicted.Add(DateTime.UtcNow, i + 1); + } Assert.Throws(() => Mapd.Calculate(actual, predicted, 3)); } @@ -353,4 +360,4 @@ public class MapdTests var result = mapd.Update(10, 0); Assert.True(double.IsFinite(result.Value)); } -} \ No newline at end of file +} diff --git a/lib/errors/mapd/Mapd.cs b/lib/errors/mapd/Mapd.cs index 015aacc1..e90fdcde 100644 --- a/lib/errors/mapd/Mapd.cs +++ b/lib/errors/mapd/Mapd.cs @@ -51,7 +51,9 @@ public sealed class Mapd : BiInputIndicatorBase public static TSeries Calculate(TSeries actual, TSeries predicted, int period) { if (actual.Count != predicted.Count) + { throw new ArgumentException("Actual and predicted series must have the same length", nameof(predicted)); + } int len = actual.Count; var t = new List(len); @@ -75,12 +77,20 @@ public sealed class Mapd : BiInputIndicatorBase public static void Batch(ReadOnlySpan actual, ReadOnlySpan predicted, Span output, int period) { if (actual.Length != predicted.Length || actual.Length != output.Length) + { throw new ArgumentException("All spans must have the same length", nameof(output)); + } + if (period <= 0) + { throw new ArgumentException("Period must be greater than 0", nameof(period)); + } int len = actual.Length; - if (len == 0) return; + if (len == 0) + { + return; + } // Pre-compute percentage errors (divided by predicted, not actual) const int StackAllocThreshold = 256; @@ -131,14 +141,22 @@ public sealed class Mapd : BiInputIndicatorBase double pred = predicted[i]; if (double.IsFinite(act)) + { lastValidActual = act; + } else + { act = lastValidActual; + } if (double.IsFinite(pred) && Math.Abs(pred) >= Epsilon) + { lastValidPredicted = pred; + } else + { pred = lastValidPredicted; + } double absPredicted = Math.Abs(pred); output[i] = absPredicted > Epsilon diff --git a/lib/errors/mape/Mape.Tests.cs b/lib/errors/mape/Mape.Tests.cs index b0a1a8ee..70a96a1a 100644 --- a/lib/errors/mape/Mape.Tests.cs +++ b/lib/errors/mape/Mape.Tests.cs @@ -320,8 +320,15 @@ public class MapeTests var actual = new TSeries(); var predicted = new TSeries(); - for (int i = 0; i < 10; i++) actual.Add(DateTime.UtcNow, i + 1); - for (int i = 0; i < 5; i++) predicted.Add(DateTime.UtcNow, i + 1); + for (int i = 0; i < 10; i++) + { + actual.Add(DateTime.UtcNow, i + 1); + } + + for (int i = 0; i < 5; i++) + { + predicted.Add(DateTime.UtcNow, i + 1); + } Assert.Throws(() => Mape.Calculate(actual, predicted, 3)); } @@ -386,4 +393,4 @@ public class MapeTests // Over-prediction should have higher MAPE due to smaller denominator Assert.True(overPrediction.Value > underPrediction.Value); } -} \ No newline at end of file +} diff --git a/lib/errors/mape/Mape.cs b/lib/errors/mape/Mape.cs index 3fc10372..027abbc8 100644 --- a/lib/errors/mape/Mape.cs +++ b/lib/errors/mape/Mape.cs @@ -52,7 +52,10 @@ public sealed class Mape : BiInputIndicatorBase ValidateBatchInputs(actual, predicted, output, period); int len = actual.Length; - if (len == 0) return; + if (len == 0) + { + return; + } const int StackAllocThreshold = 256; Span percentErrors = len <= StackAllocThreshold @@ -62,4 +65,4 @@ public sealed class Mape : BiInputIndicatorBase ErrorHelpers.ComputePercentageErrors(actual, predicted, percentErrors, Epsilon); ErrorHelpers.ApplyRollingMean(percentErrors, output, period); } -} \ No newline at end of file +} diff --git a/lib/errors/mase/Mase.cs b/lib/errors/mase/Mase.cs index b6dd1fc6..36384540 100644 --- a/lib/errors/mase/Mase.cs +++ b/lib/errors/mase/Mase.cs @@ -44,7 +44,9 @@ public sealed class Mase : AbstractBase public Mase(int period) { if (period <= 0) + { throw new ArgumentException("Period must be greater than 0", nameof(period)); + } _errorBuffer = new RingBuffer(period); _scaleBuffer = new RingBuffer(period); @@ -63,14 +65,22 @@ public sealed class Mase : AbstractBase double predictedVal = predicted.Value; if (!double.IsFinite(actualVal)) + { actualVal = double.IsFinite(_state.LastValidActual) ? _state.LastValidActual : 0.0; + } else + { _state.LastValidActual = actualVal; + } if (!double.IsFinite(predictedVal)) + { predictedVal = double.IsFinite(_state.LastValidPredicted) ? _state.LastValidPredicted : 0.0; + } else + { _state.LastValidPredicted = predictedVal; + } double absError = Math.Abs(actualVal - predictedVal); double naiveDiff = double.IsFinite(_state.PrevActual) ? Math.Abs(actualVal - _state.PrevActual) : 0.0; @@ -124,9 +134,14 @@ public sealed class Mase : AbstractBase // TickCount is 1-based (incremented after adding), so use >= period+1 for post-warmup double scale; if (_state.TickCount > period) + { scale = _state.ScaleSum / period; + } else + { scale = count > 1 ? _state.ScaleSum / (count - 1) : 1.0; + } + double result = scale > 1e-10 ? mae / scale : mae; Last = new TValue(actual.Time, result); @@ -167,7 +182,9 @@ public sealed class Mase : AbstractBase public static TSeries Calculate(TSeries actual, TSeries predicted, int period) { if (actual.Count != predicted.Count) + { throw new ArgumentException("Actual and predicted series must have the same length", nameof(predicted)); + } int len = actual.Count; var t = new List(len); @@ -188,12 +205,20 @@ public sealed class Mase : AbstractBase public static void Batch(ReadOnlySpan actual, ReadOnlySpan predicted, Span output, int period) { if (actual.Length != predicted.Length || actual.Length != output.Length) + { throw new ArgumentException("All spans must have the same length", nameof(output)); + } + if (period <= 0) + { throw new ArgumentException("Period must be greater than 0", nameof(period)); + } int len = actual.Length; - if (len == 0) return; + if (len == 0) + { + return; + } const int StackAllocThreshold = 256; Span errorBuffer = period <= StackAllocThreshold @@ -211,11 +236,19 @@ public sealed class Mase : AbstractBase for (int k = 0; k < len; k++) { - if (double.IsFinite(actual[k])) { lastValidActual = actual[k]; break; } + if (double.IsFinite(actual[k])) + { + lastValidActual = actual[k]; + break; + } } for (int k = 0; k < len; k++) { - if (double.IsFinite(predicted[k])) { lastValidPredicted = predicted[k]; break; } + if (double.IsFinite(predicted[k])) + { + lastValidPredicted = predicted[k]; + break; + } } int bufferIndex = 0; @@ -227,8 +260,23 @@ public sealed class Mase : AbstractBase double act = actual[i]; double pred = predicted[i]; - if (double.IsFinite(act)) lastValidActual = act; else act = lastValidActual; - if (double.IsFinite(pred)) lastValidPredicted = pred; else pred = lastValidPredicted; + if (double.IsFinite(act)) + { + lastValidActual = act; + } + else + { + act = lastValidActual; + } + + if (double.IsFinite(pred)) + { + lastValidPredicted = pred; + } + else + { + pred = lastValidPredicted; + } double absError = Math.Abs(act - pred); double naiveDiff = double.IsFinite(prevActual) ? Math.Abs(act - prevActual) : 0.0; @@ -251,8 +299,23 @@ public sealed class Mase : AbstractBase double act = actual[i]; double pred = predicted[i]; - if (double.IsFinite(act)) lastValidActual = act; else act = lastValidActual; - if (double.IsFinite(pred)) lastValidPredicted = pred; else pred = lastValidPredicted; + if (double.IsFinite(act)) + { + lastValidActual = act; + } + else + { + act = lastValidActual; + } + + if (double.IsFinite(pred)) + { + lastValidPredicted = pred; + } + else + { + pred = lastValidPredicted; + } double absError = Math.Abs(act - pred); double naiveDiff = Math.Abs(act - prevActual); @@ -263,7 +326,10 @@ public sealed class Mase : AbstractBase scaleBuffer[bufferIndex] = naiveDiff; bufferIndex++; - if (bufferIndex >= period) bufferIndex = 0; + if (bufferIndex >= period) + { + bufferIndex = 0; + } double mae = errorSum / period; double scale = scaleSum / period; @@ -286,4 +352,4 @@ public sealed class Mase : AbstractBase } } } -} \ No newline at end of file +} diff --git a/lib/errors/mdae/Mdae.cs b/lib/errors/mdae/Mdae.cs index 3bcc76c9..953dc8f0 100644 --- a/lib/errors/mdae/Mdae.cs +++ b/lib/errors/mdae/Mdae.cs @@ -35,7 +35,9 @@ public sealed class Mdae : AbstractBase public Mdae(int period) { if (period <= 0) + { throw new ArgumentException("Period must be greater than 0", nameof(period)); + } _buffer = new RingBuffer(period); _sortBuffer = new double[period]; @@ -62,14 +64,22 @@ public sealed class Mdae : AbstractBase } if (!double.IsFinite(actualVal)) + { actualVal = double.IsFinite(_state.LastValidActual) ? _state.LastValidActual : 0.0; + } else + { _state.LastValidActual = actualVal; + } if (!double.IsFinite(predictedVal)) + { predictedVal = double.IsFinite(_state.LastValidPredicted) ? _state.LastValidPredicted : 0.0; + } else + { _state.LastValidPredicted = predictedVal; + } double absError = Math.Abs(actualVal - predictedVal); @@ -124,7 +134,10 @@ public sealed class Mdae : AbstractBase private double CalculateMedian() { int count = _buffer.Count; - if (count == 0) return 0.0; + if (count == 0) + { + return 0.0; + } // Copy buffer contents to sort buffer using GetSequencedSpans to handle wraparound _buffer.GetSequencedSpans(out var first, out var second); @@ -151,7 +164,9 @@ public sealed class Mdae : AbstractBase public static TSeries Calculate(TSeries actual, TSeries predicted, int period) { if (actual.Count != predicted.Count) + { throw new ArgumentException("Actual and predicted series must have the same length", nameof(predicted)); + } int len = actual.Count; var t = new List(len); @@ -172,12 +187,20 @@ public sealed class Mdae : AbstractBase public static void Batch(ReadOnlySpan actual, ReadOnlySpan predicted, Span output, int period) { if (actual.Length != predicted.Length || actual.Length != output.Length) + { throw new ArgumentException("All spans must have the same length", nameof(output)); + } + if (period <= 0) + { throw new ArgumentException("Period must be greater than 0", nameof(period)); + } int len = actual.Length; - if (len == 0) return; + if (len == 0) + { + return; + } // Use stackalloc for small periods, heap for larger scoped Span buffer; @@ -199,11 +222,19 @@ public sealed class Mdae : AbstractBase for (int k = 0; k < len; k++) { - if (double.IsFinite(actual[k])) { lastValidActual = actual[k]; break; } + if (double.IsFinite(actual[k])) + { + lastValidActual = actual[k]; + break; + } } for (int k = 0; k < len; k++) { - if (double.IsFinite(predicted[k])) { lastValidPredicted = predicted[k]; break; } + if (double.IsFinite(predicted[k])) + { + lastValidPredicted = predicted[k]; + break; + } } int bufferIndex = 0; @@ -214,16 +245,38 @@ public sealed class Mdae : AbstractBase double act = actual[i]; double pred = predicted[i]; - if (double.IsFinite(act)) lastValidActual = act; else act = lastValidActual; - if (double.IsFinite(pred)) lastValidPredicted = pred; else pred = lastValidPredicted; + if (double.IsFinite(act)) + { + lastValidActual = act; + } + else + { + act = lastValidActual; + } + + if (double.IsFinite(pred)) + { + lastValidPredicted = pred; + } + else + { + pred = lastValidPredicted; + } double absError = Math.Abs(act - pred); // Add to circular buffer buffer[bufferIndex] = absError; bufferIndex++; - if (bufferIndex >= period) bufferIndex = 0; - if (bufferCount < period) bufferCount++; + if (bufferIndex >= period) + { + bufferIndex = 0; + } + + if (bufferCount < period) + { + bufferCount++; + } // Copy and use QuickSelect for median buffer.Slice(0, bufferCount).CopyTo(sortBuffer); @@ -277,9 +330,20 @@ public sealed class Mdae : AbstractBase // Median-of-three pivot selection for better pivot choice int mid = left + (right - left) / 2; - if (span[mid] < span[left]) (span[left], span[mid]) = (span[mid], span[left]); - if (span[right] < span[left]) (span[left], span[right]) = (span[right], span[left]); - if (span[right] < span[mid]) (span[mid], span[right]) = (span[right], span[mid]); + if (span[mid] < span[left]) + { + (span[left], span[mid]) = (span[mid], span[left]); + } + + if (span[right] < span[left]) + { + (span[left], span[right]) = (span[right], span[left]); + } + + if (span[right] < span[mid]) + { + (span[mid], span[right]) = (span[right], span[mid]); + } // Use median as pivot, move to right-1 position double pivot = span[mid]; @@ -297,11 +361,21 @@ public sealed class Mdae : AbstractBase } (span[storeIndex], span[right - 1]) = (span[right - 1], span[storeIndex]); - if (k == storeIndex) return span[storeIndex]; - if (k < storeIndex) right = storeIndex - 1; - else left = storeIndex + 1; + if (k == storeIndex) + { + return span[storeIndex]; + } + + if (k < storeIndex) + { + right = storeIndex - 1; + } + else + { + left = storeIndex + 1; + } } return span[left]; } -} \ No newline at end of file +} diff --git a/lib/errors/mdape/Mdape.cs b/lib/errors/mdape/Mdape.cs index c9677c66..5ce2a07b 100644 --- a/lib/errors/mdape/Mdape.cs +++ b/lib/errors/mdape/Mdape.cs @@ -33,7 +33,9 @@ public sealed class Mdape : AbstractBase public Mdape(int period) { if (period <= 0) + { throw new ArgumentException("Period must be greater than 0", nameof(period)); + } _buffer = new RingBuffer(period); _sortBuffer = new double[period]; @@ -73,14 +75,22 @@ public sealed class Mdape : AbstractBase private TValue UpdateCore(DateTime time, double actualVal, double predictedVal, bool isNew) { if (!double.IsFinite(actualVal)) + { actualVal = double.IsFinite(_state.LastValidActual) ? _state.LastValidActual : 1.0; + } else + { _state.LastValidActual = actualVal; + } if (!double.IsFinite(predictedVal)) + { predictedVal = double.IsFinite(_state.LastValidPredicted) ? _state.LastValidPredicted : 0.0; + } else + { _state.LastValidPredicted = predictedVal; + } // Calculate absolute percentage error double absActual = Math.Abs(actualVal); @@ -124,7 +134,10 @@ public sealed class Mdape : AbstractBase private double CalculateMedian() { int count = _buffer.Count; - if (count == 0) return 0.0; + if (count == 0) + { + return 0.0; + } // Copy buffer contents to sort buffer using GetSequencedSpans to handle wraparound _buffer.GetSequencedSpans(out var first, out var second); @@ -151,7 +164,9 @@ public sealed class Mdape : AbstractBase public static TSeries Calculate(TSeries actual, TSeries predicted, int period) { if (actual.Count != predicted.Count) + { throw new ArgumentException("Actual and predicted series must have the same length", nameof(predicted)); + } int len = actual.Count; var t = new List(len); @@ -172,12 +187,20 @@ public sealed class Mdape : AbstractBase public static void Batch(ReadOnlySpan actual, ReadOnlySpan predicted, Span output, int period) { if (actual.Length != predicted.Length || actual.Length != output.Length) + { throw new ArgumentException("All spans must have the same length", nameof(output)); + } + if (period <= 0) + { throw new ArgumentException("Period must be greater than 0", nameof(period)); + } int len = actual.Length; - if (len == 0) return; + if (len == 0) + { + return; + } // Use dual-heap sliding median for O(log n) updates instead of O(n log n) sort per element var slidingMedian = new SlidingMedianHeap(period); @@ -187,11 +210,19 @@ public sealed class Mdape : AbstractBase for (int k = 0; k < len; k++) { - if (double.IsFinite(actual[k]) && Math.Abs(actual[k]) >= 1e-10) { lastValidActual = actual[k]; break; } + if (double.IsFinite(actual[k]) && Math.Abs(actual[k]) >= 1e-10) + { + lastValidActual = actual[k]; + break; + } } for (int k = 0; k < len; k++) { - if (double.IsFinite(predicted[k])) { lastValidPredicted = predicted[k]; break; } + if (double.IsFinite(predicted[k])) + { + lastValidPredicted = predicted[k]; + break; + } } for (int i = 0; i < len; i++) @@ -199,8 +230,23 @@ public sealed class Mdape : AbstractBase double act = actual[i]; double pred = predicted[i]; - if (double.IsFinite(act) && Math.Abs(act) >= 1e-10) lastValidActual = act; else act = lastValidActual; - if (double.IsFinite(pred)) lastValidPredicted = pred; else pred = lastValidPredicted; + if (double.IsFinite(act) && Math.Abs(act) >= 1e-10) + { + lastValidActual = act; + } + else + { + act = lastValidActual; + } + + if (double.IsFinite(pred)) + { + lastValidPredicted = pred; + } + else + { + pred = lastValidPredicted; + } double absActual = Math.Abs(act); double absError = Math.Abs(act - pred); @@ -255,7 +301,10 @@ public sealed class Mdape : AbstractBase [MethodImpl(MethodImplOptions.AggressiveInlining)] public double GetMedian() { - if (_lowerCount == 0 && _upperCount == 0) return 0.0; + if (_lowerCount == 0 && _upperCount == 0) + { + return 0.0; + } if (_lowerCount > _upperCount) { @@ -353,4 +402,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 f9a43bce..bbed0415 100644 --- a/lib/errors/me/Me.Tests.cs +++ b/lib/errors/me/Me.Tests.cs @@ -347,8 +347,15 @@ public class MeTests var actual = new TSeries(); var predicted = new TSeries(); - for (int i = 0; i < 10; i++) actual.Add(DateTime.UtcNow, i); - for (int i = 0; i < 5; i++) predicted.Add(DateTime.UtcNow, i); + for (int i = 0; i < 10; i++) + { + actual.Add(DateTime.UtcNow, i); + } + + for (int i = 0; i < 5; i++) + { + predicted.Add(DateTime.UtcNow, i); + } Assert.Throws(() => Me.Calculate(actual, predicted, 3)); } @@ -382,4 +389,4 @@ public class MeTests // After resync, result should still be correct Assert.Equal(10.0, me.Last.Value, 10); } -} \ No newline at end of file +} diff --git a/lib/errors/me/Me.cs b/lib/errors/me/Me.cs index 58f9b217..9d06b631 100644 --- a/lib/errors/me/Me.cs +++ b/lib/errors/me/Me.cs @@ -51,7 +51,10 @@ public sealed class Me : BiInputIndicatorBase ValidateBatchInputs(actual, predicted, output, period); int len = actual.Length; - if (len == 0) return; + if (len == 0) + { + return; + } const int StackAllocThreshold = 256; if (len <= StackAllocThreshold) @@ -75,4 +78,4 @@ public sealed class Me : BiInputIndicatorBase } } } -} \ No newline at end of file +} diff --git a/lib/errors/mpe/Mpe.cs b/lib/errors/mpe/Mpe.cs index 263052b3..a02609b7 100644 --- a/lib/errors/mpe/Mpe.cs +++ b/lib/errors/mpe/Mpe.cs @@ -64,7 +64,10 @@ public sealed class Mpe : BiInputIndicatorBase ValidateBatchInputs(actual, predicted, output, period); int len = actual.Length; - if (len == 0) return; + if (len == 0) + { + return; + } const int StackAllocThreshold = 256; Span errors = len <= StackAllocThreshold @@ -82,17 +85,45 @@ public sealed class Mpe : BiInputIndicatorBase double lastValidActual = 1.0, lastValidPredicted = 0; for (int i = 0; i < len; i++) - if (double.IsFinite(actual[i]) && Math.Abs(actual[i]) >= Epsilon) { lastValidActual = actual[i]; break; } + { + if (double.IsFinite(actual[i]) && Math.Abs(actual[i]) >= Epsilon) + { + lastValidActual = actual[i]; + break; + } + } + for (int i = 0; i < len; i++) - if (double.IsFinite(predicted[i])) { lastValidPredicted = predicted[i]; break; } + { + if (double.IsFinite(predicted[i])) + { + lastValidPredicted = predicted[i]; + break; + } + } for (int i = 0; i < len; i++) { double act = actual[i]; double pred = predicted[i]; - if (double.IsFinite(act) && Math.Abs(act) >= Epsilon) lastValidActual = act; else act = lastValidActual; - if (double.IsFinite(pred)) lastValidPredicted = pred; else pred = lastValidPredicted; + if (double.IsFinite(act) && Math.Abs(act) >= Epsilon) + { + lastValidActual = act; + } + else + { + act = lastValidActual; + } + + if (double.IsFinite(pred)) + { + lastValidPredicted = pred; + } + else + { + pred = lastValidPredicted; + } // Use signed epsilon to preserve the original sign when actual is near zero double divisor; @@ -108,4 +139,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 499020b0..2bf6b1bb 100644 --- a/lib/errors/mrae/Mrae.Tests.cs +++ b/lib/errors/mrae/Mrae.Tests.cs @@ -296,8 +296,15 @@ public class MraeTests var actual = new TSeries(); var predicted = new TSeries(); - for (int i = 1; i <= 10; i++) actual.Add(DateTime.UtcNow, i * 10); - for (int i = 1; i <= 5; i++) predicted.Add(DateTime.UtcNow, i * 10); + for (int i = 1; i <= 10; i++) + { + actual.Add(DateTime.UtcNow, i * 10); + } + + for (int i = 1; i <= 5; i++) + { + predicted.Add(DateTime.UtcNow, i * 10); + } Assert.Throws(() => Mrae.Calculate(actual, predicted, 3)); } @@ -330,4 +337,4 @@ public class MraeTests Assert.Equal(0.1, mrae.Last.Value, 10); } -} \ No newline at end of file +} diff --git a/lib/errors/mrae/Mrae.cs b/lib/errors/mrae/Mrae.cs index 3cf19697..e3a918e9 100644 --- a/lib/errors/mrae/Mrae.cs +++ b/lib/errors/mrae/Mrae.cs @@ -52,7 +52,9 @@ public sealed class Mrae : BiInputIndicatorBase public static TSeries Calculate(TSeries actual, TSeries predicted, int period) { if (actual.Count != predicted.Count) + { throw new ArgumentException("Actual and predicted series must have the same length", nameof(predicted)); + } int len = actual.Count; var t = new List(len); @@ -75,12 +77,20 @@ public sealed class Mrae : BiInputIndicatorBase public static void Batch(ReadOnlySpan actual, ReadOnlySpan predicted, Span output, int period) { if (actual.Length != predicted.Length || actual.Length != output.Length) + { throw new ArgumentException("All spans must have the same length", nameof(output)); + } + if (period <= 0) + { throw new ArgumentException("Period must be greater than 0", nameof(period)); + } int len = actual.Length; - if (len == 0) return; + if (len == 0) + { + return; + } // Pre-compute relative errors (same as percentage errors but without *100) const int StackAllocThreshold = 256; @@ -130,14 +140,22 @@ public sealed class Mrae : BiInputIndicatorBase double pred = predicted[i]; if (double.IsFinite(act) && Math.Abs(act) >= Epsilon) + { lastValidActual = act; + } else + { act = lastValidActual; + } if (double.IsFinite(pred)) + { lastValidPredicted = pred; + } else + { pred = lastValidPredicted; + } double absActual = Math.Abs(act); output[i] = absActual > Epsilon @@ -145,4 +163,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 c018b1e7..82a77a17 100644 --- a/lib/errors/mse/Mse.Tests.cs +++ b/lib/errors/mse/Mse.Tests.cs @@ -308,4 +308,4 @@ public class MseTests Assert.True(double.IsFinite(val), $"Expected finite value but got {val}"); } } -} \ No newline at end of file +} diff --git a/lib/errors/mse/Mse.cs b/lib/errors/mse/Mse.cs index dab6d71e..c6561b78 100644 --- a/lib/errors/mse/Mse.cs +++ b/lib/errors/mse/Mse.cs @@ -61,7 +61,10 @@ public sealed class Mse : BiInputIndicatorBase public static void Batch(ReadOnlySpan actual, ReadOnlySpan predicted, Span output, int period) { ValidateBatchInputs(actual, predicted, output, period); - if (actual.Length == 0) return; + if (actual.Length == 0) + { + return; + } // Allocate temporary buffer for squared errors const int StackAllocThreshold = 256; diff --git a/lib/errors/msle/Msle.cs b/lib/errors/msle/Msle.cs index 1b5f2efc..0488c421 100644 --- a/lib/errors/msle/Msle.cs +++ b/lib/errors/msle/Msle.cs @@ -58,7 +58,10 @@ public sealed class Msle : BiInputIndicatorBase ValidateBatchInputs(actual, predicted, output, period); int len = actual.Length; - if (len == 0) return; + if (len == 0) + { + return; + } const int StackAllocThreshold = 256; Span errors = len <= StackAllocThreshold @@ -77,9 +80,22 @@ public sealed class Msle : BiInputIndicatorBase // Find first valid non-negative values for (int i = 0; i < len; i++) - if (double.IsFinite(actual[i]) && actual[i] >= 0) { lastValidActual = actual[i]; break; } + { + if (double.IsFinite(actual[i]) && actual[i] >= 0) + { + lastValidActual = actual[i]; + break; + } + } + for (int i = 0; i < len; i++) - if (double.IsFinite(predicted[i]) && predicted[i] >= 0) { lastValidPredicted = predicted[i]; break; } + { + if (double.IsFinite(predicted[i]) && predicted[i] >= 0) + { + lastValidPredicted = predicted[i]; + break; + } + } for (int i = 0; i < len; i++) { @@ -87,8 +103,23 @@ public sealed class Msle : BiInputIndicatorBase double pred = predicted[i]; // Handle NaN/Infinity and negative values - if (double.IsFinite(act) && act >= 0) lastValidActual = act; else act = lastValidActual; - if (double.IsFinite(pred) && pred >= 0) lastValidPredicted = pred; else pred = lastValidPredicted; + if (double.IsFinite(act) && act >= 0) + { + lastValidActual = act; + } + else + { + act = lastValidActual; + } + + if (double.IsFinite(pred) && pred >= 0) + { + lastValidPredicted = pred; + } + else + { + pred = lastValidPredicted; + } double logActual = Math.Log(1.0 + act); double logPredicted = Math.Log(1.0 + pred); diff --git a/lib/errors/pseudohuber/PseudoHuber.cs b/lib/errors/pseudohuber/PseudoHuber.cs index 95afd53d..9bb7d7c2 100644 --- a/lib/errors/pseudohuber/PseudoHuber.cs +++ b/lib/errors/pseudohuber/PseudoHuber.cs @@ -41,7 +41,9 @@ public sealed class PseudoHuber : BiInputIndicatorBase : base(period, $"PseudoHuber({period},{delta:F3})") { if (delta <= 0) + { throw new ArgumentException("Delta must be positive", nameof(delta)); + } Delta = delta; _deltaSquared = delta * delta; @@ -65,7 +67,9 @@ public sealed class PseudoHuber : BiInputIndicatorBase public static TSeries Calculate(TSeries actual, TSeries predicted, int period, double delta = 1.0) { if (actual.Count != predicted.Count) + { throw new ArgumentException("Actual and predicted series must have the same length", nameof(predicted)); + } int len = actual.Count; var t = new List(len); @@ -89,14 +93,25 @@ public sealed class PseudoHuber : BiInputIndicatorBase public static void Batch(ReadOnlySpan actual, ReadOnlySpan predicted, Span output, int period, double delta = 1.0) { if (actual.Length != predicted.Length || actual.Length != output.Length) + { throw new ArgumentException("All spans must have the same length", nameof(output)); + } + if (period <= 0) + { throw new ArgumentException("Period must be greater than 0", nameof(period)); + } + if (delta <= 0) + { throw new ArgumentException("Delta must be positive", nameof(delta)); + } int len = actual.Length; - if (len == 0) return; + if (len == 0) + { + return; + } // Pre-compute Pseudo-Huber errors using shared helper const int StackAllocThreshold = 256; @@ -109,4 +124,4 @@ public sealed class PseudoHuber : BiInputIndicatorBase // Apply rolling mean ErrorHelpers.ApplyRollingMean(errors, output, period); } -} \ No newline at end of file +} diff --git a/lib/errors/quantile/QuantileLoss.cs b/lib/errors/quantile/QuantileLoss.cs index 37ed8091..9e214abb 100644 --- a/lib/errors/quantile/QuantileLoss.cs +++ b/lib/errors/quantile/QuantileLoss.cs @@ -38,7 +38,9 @@ public sealed class QuantileLoss : BiInputIndicatorBase : base(period, $"QuantileLoss({period},{quantile:F2})") { if (quantile <= 0.0 || quantile >= 1.0) + { throw new ArgumentException("Quantile must be between 0 and 1 (exclusive)", nameof(quantile)); + } Quantile = quantile; } @@ -61,7 +63,9 @@ public sealed class QuantileLoss : BiInputIndicatorBase public static TSeries Calculate(TSeries actual, TSeries predicted, int period, double quantile = 0.5) { if (actual.Count != predicted.Count) + { throw new ArgumentException("Actual and predicted series must have the same length", nameof(predicted)); + } int len = actual.Count; var t = new List(len); @@ -82,14 +86,25 @@ public sealed class QuantileLoss : BiInputIndicatorBase public static void Batch(ReadOnlySpan actual, ReadOnlySpan predicted, Span output, int period, double quantile = 0.5) { if (actual.Length != predicted.Length || actual.Length != output.Length) + { throw new ArgumentException("All spans must have the same length", nameof(output)); + } + if (period <= 0) + { throw new ArgumentException("Period must be greater than 0", nameof(period)); + } + if (quantile <= 0.0 || quantile >= 1.0) + { throw new ArgumentException("Quantile must be between 0 and 1 (exclusive)", nameof(quantile)); + } int len = actual.Length; - if (len == 0) return; + if (len == 0) + { + return; + } const int StackAllocThreshold = 256; Span lossBuffer = period <= StackAllocThreshold @@ -102,11 +117,19 @@ public sealed class QuantileLoss : BiInputIndicatorBase for (int k = 0; k < len; k++) { - if (double.IsFinite(actual[k])) { lastValidActual = actual[k]; break; } + if (double.IsFinite(actual[k])) + { + lastValidActual = actual[k]; + break; + } } for (int k = 0; k < len; k++) { - if (double.IsFinite(predicted[k])) { lastValidPredicted = predicted[k]; break; } + if (double.IsFinite(predicted[k])) + { + lastValidPredicted = predicted[k]; + break; + } } int bufferIndex = 0; @@ -118,8 +141,23 @@ public sealed class QuantileLoss : BiInputIndicatorBase double act = actual[i]; double pred = predicted[i]; - if (double.IsFinite(act)) lastValidActual = act; else act = lastValidActual; - if (double.IsFinite(pred)) lastValidPredicted = pred; else pred = lastValidPredicted; + if (double.IsFinite(act)) + { + lastValidActual = act; + } + else + { + act = lastValidActual; + } + + if (double.IsFinite(pred)) + { + lastValidPredicted = pred; + } + else + { + pred = lastValidPredicted; + } double diff = act - pred; double loss = diff >= 0 ? quantile * diff : (quantile - 1.0) * diff; @@ -136,8 +174,23 @@ public sealed class QuantileLoss : BiInputIndicatorBase double act = actual[i]; double pred = predicted[i]; - if (double.IsFinite(act)) lastValidActual = act; else act = lastValidActual; - if (double.IsFinite(pred)) lastValidPredicted = pred; else pred = lastValidPredicted; + if (double.IsFinite(act)) + { + lastValidActual = act; + } + else + { + act = lastValidActual; + } + + if (double.IsFinite(pred)) + { + lastValidPredicted = pred; + } + else + { + pred = lastValidPredicted; + } double diff = act - pred; double loss = diff >= 0 ? quantile * diff : (quantile - 1.0) * diff; @@ -146,7 +199,10 @@ public sealed class QuantileLoss : BiInputIndicatorBase lossBuffer[bufferIndex] = loss; bufferIndex++; - if (bufferIndex >= period) bufferIndex = 0; + if (bufferIndex >= period) + { + bufferIndex = 0; + } output[i] = lossSum / period; @@ -156,9 +212,12 @@ public sealed class QuantileLoss : BiInputIndicatorBase tickCount = 0; double recalcSum = 0; for (int k = 0; k < period; k++) + { recalcSum += lossBuffer[k]; + } + lossSum = recalcSum; } } } -} \ No newline at end of file +} diff --git a/lib/errors/rae/Rae.cs b/lib/errors/rae/Rae.cs index 413efc6f..172380d9 100644 --- a/lib/errors/rae/Rae.cs +++ b/lib/errors/rae/Rae.cs @@ -43,7 +43,9 @@ public sealed class Rae : AbstractBase public Rae(int period) { if (period <= 0) + { throw new ArgumentException("Period must be greater than 0", nameof(period)); + } _actualBuffer = new RingBuffer(period); _absErrorBuffer = new RingBuffer(period); @@ -61,14 +63,22 @@ public sealed class Rae : AbstractBase double predictedVal = predicted.Value; if (!double.IsFinite(actualVal)) + { actualVal = double.IsFinite(_state.LastValidActual) ? _state.LastValidActual : 0.0; + } else + { _state.LastValidActual = actualVal; + } if (!double.IsFinite(predictedVal)) + { predictedVal = double.IsFinite(_state.LastValidPredicted) ? _state.LastValidPredicted : 0.0; + } else + { _state.LastValidPredicted = predictedVal; + } if (isNew) { @@ -163,7 +173,9 @@ public sealed class Rae : AbstractBase public static TSeries Calculate(TSeries actual, TSeries predicted, int period) { if (actual.Count != predicted.Count) + { throw new ArgumentException("Actual and predicted series must have the same length", nameof(predicted)); + } int len = actual.Count; var t = new List(len); @@ -184,12 +196,20 @@ public sealed class Rae : AbstractBase public static void Batch(ReadOnlySpan actual, ReadOnlySpan predicted, Span output, int period) { if (actual.Length != predicted.Length || actual.Length != output.Length) + { throw new ArgumentException("All spans must have the same length", nameof(output)); + } + if (period <= 0) + { throw new ArgumentException("Period must be greater than 0", nameof(period)); + } int len = actual.Length; - if (len == 0) return; + if (len == 0) + { + return; + } const int StackAllocThreshold = 256; Span actualBuffer = period <= StackAllocThreshold @@ -210,11 +230,19 @@ public sealed class Rae : AbstractBase for (int k = 0; k < len; k++) { - if (double.IsFinite(actual[k])) { lastValidActual = actual[k]; break; } + if (double.IsFinite(actual[k])) + { + lastValidActual = actual[k]; + break; + } } for (int k = 0; k < len; k++) { - if (double.IsFinite(predicted[k])) { lastValidPredicted = predicted[k]; break; } + if (double.IsFinite(predicted[k])) + { + lastValidPredicted = predicted[k]; + break; + } } int bufferIndex = 0; @@ -226,8 +254,23 @@ public sealed class Rae : AbstractBase double act = actual[i]; double pred = predicted[i]; - if (double.IsFinite(act)) lastValidActual = act; else act = lastValidActual; - if (double.IsFinite(pred)) lastValidPredicted = pred; else pred = lastValidPredicted; + if (double.IsFinite(act)) + { + lastValidActual = act; + } + else + { + act = lastValidActual; + } + + if (double.IsFinite(pred)) + { + lastValidPredicted = pred; + } + else + { + pred = lastValidPredicted; + } actualSum += act; actualBuffer[i] = act; @@ -250,8 +293,23 @@ public sealed class Rae : AbstractBase double act = actual[i]; double pred = predicted[i]; - if (double.IsFinite(act)) lastValidActual = act; else act = lastValidActual; - if (double.IsFinite(pred)) lastValidPredicted = pred; else pred = lastValidPredicted; + if (double.IsFinite(act)) + { + lastValidActual = act; + } + else + { + act = lastValidActual; + } + + if (double.IsFinite(pred)) + { + lastValidPredicted = pred; + } + else + { + pred = lastValidPredicted; + } actualSum = actualSum - actualBuffer[bufferIndex] + act; actualBuffer[bufferIndex] = act; @@ -266,7 +324,10 @@ public sealed class Rae : AbstractBase absBaselineBuffer[bufferIndex] = absBaseline; bufferIndex++; - if (bufferIndex >= period) bufferIndex = 0; + if (bufferIndex >= period) + { + bufferIndex = 0; + } output[i] = absBaselineSum > 1e-10 ? absErrorSum / absBaselineSum : 1.0; diff --git a/lib/errors/rmse/Rmse.Tests.cs b/lib/errors/rmse/Rmse.Tests.cs index 723907db..c7e6067a 100644 --- a/lib/errors/rmse/Rmse.Tests.cs +++ b/lib/errors/rmse/Rmse.Tests.cs @@ -247,4 +247,4 @@ public class RmseTests // All errors are 5, MSE = 25, RMSE = 5 Assert.Equal(5.0, results.Last.Value, 10); } -} \ No newline at end of file +} diff --git a/lib/errors/rmse/Rmse.cs b/lib/errors/rmse/Rmse.cs index b7d78616..51fe3c7c 100644 --- a/lib/errors/rmse/Rmse.cs +++ b/lib/errors/rmse/Rmse.cs @@ -57,7 +57,10 @@ public sealed class Rmse : BiInputIndicatorBase ValidateBatchInputs(actual, predicted, output, period); int len = actual.Length; - if (len == 0) return; + if (len == 0) + { + return; + } const int StackAllocThreshold = 256; Span sqErrors = len <= StackAllocThreshold diff --git a/lib/errors/rmsle/Rmsle.cs b/lib/errors/rmsle/Rmsle.cs index 44cf26b6..07e0b1de 100644 --- a/lib/errors/rmsle/Rmsle.cs +++ b/lib/errors/rmsle/Rmsle.cs @@ -61,7 +61,10 @@ public sealed class Rmsle : BiInputIndicatorBase ValidateBatchInputs(actual, predicted, output, period); int len = actual.Length; - if (len == 0) return; + if (len == 0) + { + return; + } const int StackAllocThreshold = 256; Span errors = len <= StackAllocThreshold @@ -80,9 +83,22 @@ public sealed class Rmsle : BiInputIndicatorBase // Find first valid non-negative values for (int i = 0; i < len; i++) - if (double.IsFinite(actual[i]) && actual[i] >= 0) { lastValidActual = actual[i]; break; } + { + if (double.IsFinite(actual[i]) && actual[i] >= 0) + { + lastValidActual = actual[i]; + break; + } + } + for (int i = 0; i < len; i++) - if (double.IsFinite(predicted[i]) && predicted[i] >= 0) { lastValidPredicted = predicted[i]; break; } + { + if (double.IsFinite(predicted[i]) && predicted[i] >= 0) + { + lastValidPredicted = predicted[i]; + break; + } + } for (int i = 0; i < len; i++) { @@ -90,8 +106,23 @@ public sealed class Rmsle : BiInputIndicatorBase double pred = predicted[i]; // Handle NaN/Infinity and negative values - if (double.IsFinite(act) && act >= 0) lastValidActual = act; else act = lastValidActual; - if (double.IsFinite(pred) && pred >= 0) lastValidPredicted = pred; else pred = lastValidPredicted; + if (double.IsFinite(act) && act >= 0) + { + lastValidActual = act; + } + else + { + act = lastValidActual; + } + + if (double.IsFinite(pred) && pred >= 0) + { + lastValidPredicted = pred; + } + else + { + pred = lastValidPredicted; + } double logActual = Math.Log(1.0 + act); double logPredicted = Math.Log(1.0 + pred); diff --git a/lib/errors/rse/Rse.cs b/lib/errors/rse/Rse.cs index 5756caa7..f122e9db 100644 --- a/lib/errors/rse/Rse.cs +++ b/lib/errors/rse/Rse.cs @@ -43,7 +43,9 @@ public sealed class Rse : AbstractBase public Rse(int period) { if (period <= 0) + { throw new ArgumentException("Period must be greater than 0", nameof(period)); + } _actualBuffer = new RingBuffer(period); _sqErrorBuffer = new RingBuffer(period); @@ -67,14 +69,22 @@ public sealed class Rse : AbstractBase } if (!double.IsFinite(actualVal)) + { actualVal = double.IsFinite(_state.LastValidActual) ? _state.LastValidActual : 0.0; + } else + { _state.LastValidActual = actualVal; + } if (!double.IsFinite(predictedVal)) + { predictedVal = double.IsFinite(_state.LastValidPredicted) ? _state.LastValidPredicted : 0.0; + } else + { _state.LastValidPredicted = predictedVal; + } if (isNew) { @@ -177,7 +187,9 @@ public sealed class Rse : AbstractBase public static TSeries Calculate(TSeries actual, TSeries predicted, int period) { if (actual.Count != predicted.Count) + { throw new ArgumentException("Actual and predicted series must have the same length", nameof(predicted)); + } int len = actual.Count; var t = new List(len); @@ -198,12 +210,20 @@ public sealed class Rse : AbstractBase public static void Batch(ReadOnlySpan actual, ReadOnlySpan predicted, Span output, int period) { if (actual.Length != predicted.Length || actual.Length != output.Length) + { throw new ArgumentException("All spans must have the same length", nameof(output)); + } + if (period <= 0) + { throw new ArgumentException("Period must be greater than 0", nameof(period)); + } int len = actual.Length; - if (len == 0) return; + if (len == 0) + { + return; + } const int StackAllocThreshold = 256; Span actualBuffer = period <= StackAllocThreshold @@ -224,11 +244,19 @@ public sealed class Rse : AbstractBase for (int k = 0; k < len; k++) { - if (double.IsFinite(actual[k])) { lastValidActual = actual[k]; break; } + if (double.IsFinite(actual[k])) + { + lastValidActual = actual[k]; + break; + } } for (int k = 0; k < len; k++) { - if (double.IsFinite(predicted[k])) { lastValidPredicted = predicted[k]; break; } + if (double.IsFinite(predicted[k])) + { + lastValidPredicted = predicted[k]; + break; + } } int bufferIndex = 0; @@ -240,8 +268,23 @@ public sealed class Rse : AbstractBase double act = actual[i]; double pred = predicted[i]; - if (double.IsFinite(act)) lastValidActual = act; else act = lastValidActual; - if (double.IsFinite(pred)) lastValidPredicted = pred; else pred = lastValidPredicted; + if (double.IsFinite(act)) + { + lastValidActual = act; + } + else + { + act = lastValidActual; + } + + if (double.IsFinite(pred)) + { + lastValidPredicted = pred; + } + else + { + pred = lastValidPredicted; + } actualSum += act; actualBuffer[i] = act; @@ -266,8 +309,23 @@ public sealed class Rse : AbstractBase double act = actual[i]; double pred = predicted[i]; - if (double.IsFinite(act)) lastValidActual = act; else act = lastValidActual; - if (double.IsFinite(pred)) lastValidPredicted = pred; else pred = lastValidPredicted; + if (double.IsFinite(act)) + { + lastValidActual = act; + } + else + { + act = lastValidActual; + } + + if (double.IsFinite(pred)) + { + lastValidPredicted = pred; + } + else + { + pred = lastValidPredicted; + } actualSum = actualSum - actualBuffer[bufferIndex] + act; actualBuffer[bufferIndex] = act; @@ -284,7 +342,10 @@ public sealed class Rse : AbstractBase sqBaselineBuffer[bufferIndex] = sqBaseline; bufferIndex++; - if (bufferIndex >= period) bufferIndex = 0; + if (bufferIndex >= period) + { + bufferIndex = 0; + } output[i] = sqBaselineSum > 1e-10 ? sqErrorSum / sqBaselineSum : 1.0; @@ -305,4 +366,4 @@ public sealed class Rse : AbstractBase } } } -} \ No newline at end of file +} diff --git a/lib/errors/rsquared/Rsquared.cs b/lib/errors/rsquared/Rsquared.cs index af9ee56b..58be0cd9 100644 --- a/lib/errors/rsquared/Rsquared.cs +++ b/lib/errors/rsquared/Rsquared.cs @@ -44,7 +44,9 @@ public sealed class Rsquared : AbstractBase public Rsquared(int period) { if (period <= 0) + { throw new ArgumentException("Period must be greater than 0", nameof(period)); + } _actualBuffer = new RingBuffer(period); _sqResidualBuffer = new RingBuffer(period); @@ -62,14 +64,22 @@ public sealed class Rsquared : AbstractBase double predictedVal = predicted.Value; if (!double.IsFinite(actualVal)) + { actualVal = double.IsFinite(_state.LastValidActual) ? _state.LastValidActual : 0.0; + } else + { _state.LastValidActual = actualVal; + } if (!double.IsFinite(predictedVal)) + { predictedVal = double.IsFinite(_state.LastValidPredicted) ? _state.LastValidPredicted : 0.0; + } else + { _state.LastValidPredicted = predictedVal; + } if (isNew) { @@ -171,7 +181,9 @@ public sealed class Rsquared : AbstractBase public static TSeries Calculate(TSeries actual, TSeries predicted, int period) { if (actual.Count != predicted.Count) + { throw new ArgumentException("Actual and predicted series must have the same length", nameof(predicted)); + } int len = actual.Count; var t = new List(len); @@ -192,12 +204,20 @@ public sealed class Rsquared : AbstractBase public static void Batch(ReadOnlySpan actual, ReadOnlySpan predicted, Span output, int period) { if (actual.Length != predicted.Length || actual.Length != output.Length) + { throw new ArgumentException("All spans must have the same length", nameof(output)); + } + if (period <= 0) + { throw new ArgumentException("Period must be greater than 0", nameof(period)); + } int len = actual.Length; - if (len == 0) return; + if (len == 0) + { + return; + } const int StackAllocThreshold = 256; Span actualBuffer = period <= StackAllocThreshold @@ -218,11 +238,19 @@ public sealed class Rsquared : AbstractBase for (int k = 0; k < len; k++) { - if (double.IsFinite(actual[k])) { lastValidActual = actual[k]; break; } + if (double.IsFinite(actual[k])) + { + lastValidActual = actual[k]; + break; + } } for (int k = 0; k < len; k++) { - if (double.IsFinite(predicted[k])) { lastValidPredicted = predicted[k]; break; } + if (double.IsFinite(predicted[k])) + { + lastValidPredicted = predicted[k]; + break; + } } int bufferIndex = 0; @@ -234,8 +262,23 @@ public sealed class Rsquared : AbstractBase double act = actual[i]; double pred = predicted[i]; - if (double.IsFinite(act)) lastValidActual = act; else act = lastValidActual; - if (double.IsFinite(pred)) lastValidPredicted = pred; else pred = lastValidPredicted; + if (double.IsFinite(act)) + { + lastValidActual = act; + } + else + { + act = lastValidActual; + } + + if (double.IsFinite(pred)) + { + lastValidPredicted = pred; + } + else + { + pred = lastValidPredicted; + } actualSum += act; actualBuffer[i] = act; @@ -260,8 +303,23 @@ public sealed class Rsquared : AbstractBase double act = actual[i]; double pred = predicted[i]; - if (double.IsFinite(act)) lastValidActual = act; else act = lastValidActual; - if (double.IsFinite(pred)) lastValidPredicted = pred; else pred = lastValidPredicted; + if (double.IsFinite(act)) + { + lastValidActual = act; + } + else + { + act = lastValidActual; + } + + if (double.IsFinite(pred)) + { + lastValidPredicted = pred; + } + else + { + pred = lastValidPredicted; + } actualSum = actualSum - actualBuffer[bufferIndex] + act; actualBuffer[bufferIndex] = act; @@ -278,7 +336,10 @@ public sealed class Rsquared : AbstractBase sqTotalBuffer[bufferIndex] = sqTotal; bufferIndex++; - if (bufferIndex >= period) bufferIndex = 0; + if (bufferIndex >= period) + { + bufferIndex = 0; + } output[i] = sqTotalSum > 1e-10 ? 1.0 - (sqResidualSum / sqTotalSum) : 1.0; diff --git a/lib/errors/smape/Smape.cs b/lib/errors/smape/Smape.cs index 379c1f7c..315c1a7f 100644 --- a/lib/errors/smape/Smape.cs +++ b/lib/errors/smape/Smape.cs @@ -55,7 +55,10 @@ public sealed class Smape : BiInputIndicatorBase ValidateBatchInputs(actual, predicted, output, period); int len = actual.Length; - if (len == 0) return; + if (len == 0) + { + return; + } const int StackAllocThreshold = 256; Span symErrors = len <= StackAllocThreshold @@ -74,17 +77,45 @@ public sealed class Smape : BiInputIndicatorBase double lastValidActual = 0, lastValidPredicted = 0; for (int i = 0; i < len; i++) - if (double.IsFinite(actual[i])) { lastValidActual = actual[i]; break; } + { + if (double.IsFinite(actual[i])) + { + lastValidActual = actual[i]; + break; + } + } + for (int i = 0; i < len; i++) - if (double.IsFinite(predicted[i])) { lastValidPredicted = predicted[i]; break; } + { + if (double.IsFinite(predicted[i])) + { + lastValidPredicted = predicted[i]; + break; + } + } for (int i = 0; i < len; i++) { double act = actual[i]; double pred = predicted[i]; - if (double.IsFinite(act)) lastValidActual = act; else act = lastValidActual; - if (double.IsFinite(pred)) lastValidPredicted = pred; else pred = lastValidPredicted; + if (double.IsFinite(act)) + { + lastValidActual = act; + } + else + { + act = lastValidActual; + } + + if (double.IsFinite(pred)) + { + lastValidPredicted = pred; + } + else + { + pred = lastValidPredicted; + } double absDiff = Math.Abs(act - pred); double sumAbs = Math.Abs(act) + Math.Abs(pred); diff --git a/lib/errors/theilu/TheilU.cs b/lib/errors/theilu/TheilU.cs index 02255867..139c4659 100644 --- a/lib/errors/theilu/TheilU.cs +++ b/lib/errors/theilu/TheilU.cs @@ -39,7 +39,9 @@ public sealed class TheilU : AbstractBase public TheilU(int period) { if (period <= 0) + { throw new ArgumentException("Period must be greater than 0", nameof(period)); + } _sqErrorBuffer = new RingBuffer(period); _sqActualBuffer = new RingBuffer(period); @@ -80,14 +82,22 @@ public sealed class TheilU : AbstractBase private TValue UpdateCore(DateTime time, double actualVal, double predictedVal, bool isNew) { if (!double.IsFinite(actualVal)) + { actualVal = double.IsFinite(_state.LastValidActual) ? _state.LastValidActual : 0.0; + } else + { _state.LastValidActual = actualVal; + } if (!double.IsFinite(predictedVal)) + { predictedVal = double.IsFinite(_state.LastValidPredicted) ? _state.LastValidPredicted : 0.0; + } else + { _state.LastValidPredicted = predictedVal; + } double error = predictedVal - actualVal; double sqError = error * error; @@ -163,7 +173,9 @@ public sealed class TheilU : AbstractBase public static TSeries Calculate(TSeries actual, TSeries predicted, int period) { if (actual.Count != predicted.Count) + { throw new ArgumentException("Actual and predicted series must have the same length", nameof(predicted)); + } int len = actual.Count; var t = new List(len); @@ -184,12 +196,20 @@ public sealed class TheilU : AbstractBase public static void Batch(ReadOnlySpan actual, ReadOnlySpan predicted, Span output, int period) { if (actual.Length != predicted.Length || actual.Length != output.Length) + { throw new ArgumentException("All spans must have the same length", nameof(output)); + } + if (period <= 0) + { throw new ArgumentException("Period must be greater than 0", nameof(period)); + } int len = actual.Length; - if (len == 0) return; + if (len == 0) + { + return; + } const int StackAllocThreshold = 256; Span sqErrorBuffer = period <= StackAllocThreshold @@ -210,11 +230,19 @@ public sealed class TheilU : AbstractBase for (int k = 0; k < len; k++) { - if (double.IsFinite(actual[k])) { lastValidActual = actual[k]; break; } + if (double.IsFinite(actual[k])) + { + lastValidActual = actual[k]; + break; + } } for (int k = 0; k < len; k++) { - if (double.IsFinite(predicted[k])) { lastValidPredicted = predicted[k]; break; } + if (double.IsFinite(predicted[k])) + { + lastValidPredicted = predicted[k]; + break; + } } int bufferIndex = 0; @@ -226,8 +254,23 @@ public sealed class TheilU : AbstractBase double act = actual[i]; double pred = predicted[i]; - if (double.IsFinite(act)) lastValidActual = act; else act = lastValidActual; - if (double.IsFinite(pred)) lastValidPredicted = pred; else pred = lastValidPredicted; + if (double.IsFinite(act)) + { + lastValidActual = act; + } + else + { + act = lastValidActual; + } + + if (double.IsFinite(pred)) + { + lastValidPredicted = pred; + } + else + { + pred = lastValidPredicted; + } double error = pred - act; double sqError = error * error; @@ -251,8 +294,23 @@ public sealed class TheilU : AbstractBase double act = actual[i]; double pred = predicted[i]; - if (double.IsFinite(act)) lastValidActual = act; else act = lastValidActual; - if (double.IsFinite(pred)) lastValidPredicted = pred; else pred = lastValidPredicted; + if (double.IsFinite(act)) + { + lastValidActual = act; + } + else + { + act = lastValidActual; + } + + if (double.IsFinite(pred)) + { + lastValidPredicted = pred; + } + else + { + pred = lastValidPredicted; + } double error = pred - act; double sqError = error * error; @@ -269,7 +327,10 @@ public sealed class TheilU : AbstractBase sqPredBuffer[bufferIndex] = sqPred; bufferIndex++; - if (bufferIndex >= period) bufferIndex = 0; + if (bufferIndex >= period) + { + bufferIndex = 0; + } double denom = Math.Sqrt(sqActualSum + sqPredSum); output[i] = denom > 1e-10 ? Math.Sqrt(sqErrorSum) / denom : 0.0; @@ -291,4 +352,4 @@ public sealed class TheilU : AbstractBase } } } -} \ No newline at end of file +} diff --git a/lib/errors/tukey/TukeyBiweight.cs b/lib/errors/tukey/TukeyBiweight.cs index 3b74581b..0768390f 100644 --- a/lib/errors/tukey/TukeyBiweight.cs +++ b/lib/errors/tukey/TukeyBiweight.cs @@ -35,7 +35,9 @@ public sealed class TukeyBiweight : BiInputIndicatorBase : base(period, $"TukeyBiweight({period},{c:F3})") { if (c <= 0) + { throw new ArgumentException("Threshold c must be positive", nameof(c)); + } C = c; _cSquaredOver6 = (c * c) / 6.0; @@ -53,7 +55,9 @@ public sealed class TukeyBiweight : BiInputIndicatorBase double absError = Math.Abs(error); if (absError > C) + { return _cSquaredOver6; + } double ratio = error / C; double ratioSq = ratio * ratio; @@ -65,7 +69,9 @@ public sealed class TukeyBiweight : BiInputIndicatorBase public static TSeries Calculate(TSeries actual, TSeries predicted, int period, double c = DefaultC) { if (actual.Count != predicted.Count) + { throw new ArgumentException("Actual and predicted series must have the same length", nameof(predicted)); + } int len = actual.Count; var t = new List(len); @@ -86,14 +92,25 @@ public sealed class TukeyBiweight : BiInputIndicatorBase public static void Batch(ReadOnlySpan actual, ReadOnlySpan predicted, Span output, int period, double c = DefaultC) { if (actual.Length != predicted.Length || actual.Length != output.Length) + { throw new ArgumentException("All spans must have the same length", nameof(output)); + } + if (period <= 0) + { throw new ArgumentException("Period must be greater than 0", nameof(period)); + } + if (c <= 0) + { throw new ArgumentException("Threshold c must be positive", nameof(c)); + } int len = actual.Length; - if (len == 0) return; + if (len == 0) + { + return; + } // Rent buffer for intermediate Tukey biweight errors double[] rented = ArrayPool.Shared.Rent(len); @@ -112,4 +129,4 @@ public sealed class TukeyBiweight : BiInputIndicatorBase ArrayPool.Shared.Return(rented, clearArray: false); } } -} \ No newline at end of file +} diff --git a/lib/errors/wmape/Wmape.cs b/lib/errors/wmape/Wmape.cs index 73529848..82321eef 100644 --- a/lib/errors/wmape/Wmape.cs +++ b/lib/errors/wmape/Wmape.cs @@ -38,7 +38,9 @@ public sealed class Wmape : AbstractBase public Wmape(int period) { if (period <= 0) + { throw new ArgumentException("Period must be greater than 0", nameof(period)); + } _absErrorBuffer = new RingBuffer(period); _absActualBuffer = new RingBuffer(period); @@ -65,14 +67,22 @@ public sealed class Wmape : AbstractBase } if (!double.IsFinite(actualVal)) + { actualVal = double.IsFinite(_state.LastValidActual) ? _state.LastValidActual : 0.0; + } else + { _state.LastValidActual = actualVal; + } if (!double.IsFinite(predictedVal)) + { predictedVal = double.IsFinite(_state.LastValidPredicted) ? _state.LastValidPredicted : 0.0; + } else + { _state.LastValidPredicted = predictedVal; + } double absError = Math.Abs(actualVal - predictedVal); double absActual = Math.Abs(actualVal); @@ -148,7 +158,9 @@ public sealed class Wmape : AbstractBase public static TSeries Calculate(TSeries actual, TSeries predicted, int period) { if (actual.Count != predicted.Count) + { throw new ArgumentException("Actual and predicted series must have the same length", nameof(predicted)); + } int len = actual.Count; var t = new List(len); @@ -169,12 +181,20 @@ public sealed class Wmape : AbstractBase public static void Batch(ReadOnlySpan actual, ReadOnlySpan predicted, Span output, int period) { if (actual.Length != predicted.Length || actual.Length != output.Length) + { throw new ArgumentException("All spans must have the same length", nameof(output)); + } + if (period <= 0) + { throw new ArgumentException("Period must be greater than 0", nameof(period)); + } int len = actual.Length; - if (len == 0) return; + if (len == 0) + { + return; + } // Use stackalloc for small periods, ArrayPool for larger scoped Span absErrorBuffer; @@ -202,14 +222,22 @@ public sealed class Wmape : AbstractBase double lastValidActual = 0; double lastValidPredicted = 0; - for (int k = 0; k < len; k++) + for (int k = 0; k < len; k++) + { + if (double.IsFinite(actual[k])) { - if (double.IsFinite(actual[k])) { lastValidActual = actual[k]; break; } + lastValidActual = actual[k]; + break; } - for (int k = 0; k < len; k++) + } + for (int k = 0; k < len; k++) + { + if (double.IsFinite(predicted[k])) { - if (double.IsFinite(predicted[k])) { lastValidPredicted = predicted[k]; break; } + lastValidPredicted = predicted[k]; + break; } + } int bufferIndex = 0; int i = 0; @@ -220,8 +248,23 @@ public sealed class Wmape : AbstractBase double act = actual[i]; double pred = predicted[i]; - if (double.IsFinite(act)) lastValidActual = act; else act = lastValidActual; - if (double.IsFinite(pred)) lastValidPredicted = pred; else pred = lastValidPredicted; + if (double.IsFinite(act)) + { + lastValidActual = act; + } + else + { + act = lastValidActual; + } + + if (double.IsFinite(pred)) + { + lastValidPredicted = pred; + } + else + { + pred = lastValidPredicted; + } double absError = Math.Abs(act - pred); double absActual = Math.Abs(act); @@ -240,8 +283,23 @@ public sealed class Wmape : AbstractBase double act = actual[i]; double pred = predicted[i]; - if (double.IsFinite(act)) lastValidActual = act; else act = lastValidActual; - if (double.IsFinite(pred)) lastValidPredicted = pred; else pred = lastValidPredicted; + if (double.IsFinite(act)) + { + lastValidActual = act; + } + else + { + act = lastValidActual; + } + + if (double.IsFinite(pred)) + { + lastValidPredicted = pred; + } + else + { + pred = lastValidPredicted; + } double absError = Math.Abs(act - pred); double absActual = Math.Abs(act); @@ -252,7 +310,10 @@ public sealed class Wmape : AbstractBase absActualBuffer[bufferIndex] = absActual; bufferIndex++; - if (bufferIndex >= period) bufferIndex = 0; + if (bufferIndex >= period) + { + bufferIndex = 0; + } output[i] = absActualSum > 1e-10 ? (absErrorSum / absActualSum) * 100.0 : 0.0; @@ -274,9 +335,14 @@ public sealed class Wmape : AbstractBase finally { if (rentedError != null) + { ArrayPool.Shared.Return(rentedError); + } + if (rentedActual != null) + { ArrayPool.Shared.Return(rentedActual); + } } } -} \ No newline at end of file +} diff --git a/lib/errors/wrmse/Wrmse.Tests.cs b/lib/errors/wrmse/Wrmse.Tests.cs index 0806e1cf..dd22b0c2 100644 --- a/lib/errors/wrmse/Wrmse.Tests.cs +++ b/lib/errors/wrmse/Wrmse.Tests.cs @@ -360,7 +360,10 @@ public class WrmseTests for (int i = 0; i < 10; i++) { actual.Add(now.AddMinutes(i), i * 10); - if (i < 5) predicted.Add(now.AddMinutes(i), i * 10 + 5); + if (i < 5) + { + predicted.Add(now.AddMinutes(i), i * 10 + 5); + } } Assert.Throws(() => Wrmse.Calculate(actual, predicted, 3)); @@ -378,7 +381,10 @@ public class WrmseTests { actual.Add(now.AddMinutes(i), i * 10); predicted.Add(now.AddMinutes(i), i * 10 + 5); - if (i < 5) weights.Add(now.AddMinutes(i), 1.0); + if (i < 5) + { + weights.Add(now.AddMinutes(i), 1.0); + } } Assert.Throws(() => Wrmse.Calculate(actual, predicted, weights, 3)); @@ -411,4 +417,4 @@ public class WrmseTests Assert.Equal(rmseResults[i], wrmseResults[i], 9); } } -} \ No newline at end of file +} diff --git a/lib/errors/wrmse/Wrmse.cs b/lib/errors/wrmse/Wrmse.cs index 368c6a38..e917ae8d 100644 --- a/lib/errors/wrmse/Wrmse.cs +++ b/lib/errors/wrmse/Wrmse.cs @@ -50,7 +50,9 @@ public sealed class Wrmse : AbstractBase public Wrmse(int period) { if (period <= 0) + { throw new ArgumentException("Period must be greater than 0", nameof(period)); + } _weightedErrorBuffer = new RingBuffer(period); _weightBuffer = new RingBuffer(period); @@ -86,19 +88,31 @@ public sealed class Wrmse : AbstractBase // Sanitize inputs if (!double.IsFinite(actualVal)) + { actualVal = double.IsFinite(_state.LastValidActual) ? _state.LastValidActual : 0.0; + } else + { _state.LastValidActual = actualVal; + } if (!double.IsFinite(predictedVal)) + { predictedVal = double.IsFinite(_state.LastValidPredicted) ? _state.LastValidPredicted : 0.0; + } else + { _state.LastValidPredicted = predictedVal; + } if (!double.IsFinite(weight) || weight < 0) + { weight = _state.LastValidWeight; + } else + { _state.LastValidWeight = weight; + } // Compute weighted squared error double diff = actualVal - predictedVal; @@ -210,7 +224,9 @@ public sealed class Wrmse : AbstractBase public static TSeries Calculate(TSeries actual, TSeries predicted, int period) { if (actual.Count != predicted.Count) + { throw new ArgumentException("Actual and predicted series must have the same length", nameof(predicted)); + } int len = actual.Count; var t = new List(len); @@ -233,7 +249,9 @@ public sealed class Wrmse : AbstractBase public static TSeries Calculate(TSeries actual, TSeries predicted, TSeries weights, int period) { if (actual.Count != predicted.Count || actual.Count != weights.Count) + { throw new ArgumentException("All series must have the same length", nameof(weights)); + } int len = actual.Count; var t = new List(len); @@ -258,12 +276,20 @@ public sealed class Wrmse : AbstractBase public static void Batch(ReadOnlySpan actual, ReadOnlySpan predicted, Span output, int period) { if (actual.Length != predicted.Length || actual.Length != output.Length) + { throw new ArgumentException("All spans must have the same length", nameof(output)); + } + if (period <= 0) + { throw new ArgumentException("Period must be greater than 0", nameof(period)); + } int len = actual.Length; - if (len == 0) return; + if (len == 0) + { + return; + } // With uniform weights, WRMSE = RMSE const int StackAllocThreshold = 256; @@ -283,12 +309,20 @@ public sealed class Wrmse : AbstractBase public static void Batch(ReadOnlySpan actual, ReadOnlySpan predicted, ReadOnlySpan weights, Span output, int period) { if (actual.Length != predicted.Length || actual.Length != weights.Length || actual.Length != output.Length) + { throw new ArgumentException("All spans must have the same length", nameof(output)); + } + if (period <= 0) + { throw new ArgumentException("Period must be greater than 0", nameof(period)); + } int len = actual.Length; - if (len == 0) return; + if (len == 0) + { + return; + } const int StackAllocThreshold = 256; Span weightedErrors = len <= StackAllocThreshold @@ -298,4 +332,4 @@ public sealed class Wrmse : AbstractBase ErrorHelpers.ComputeWeightedErrors(actual, predicted, weights, weightedErrors); ErrorHelpers.ApplyRollingWeightedMeanSqrt(weightedErrors, weights, output, period); } -} \ No newline at end of file +} diff --git a/lib/feeds/csv/CsvFeed.Tests.cs b/lib/feeds/csv/CsvFeed.Tests.cs index 36314ac0..c020a400 100644 --- a/lib/feeds/csv/CsvFeed.Tests.cs +++ b/lib/feeds/csv/CsvFeed.Tests.cs @@ -24,7 +24,11 @@ public sealed class CsvFeedTests : IDisposable public void Dispose() { - if (_disposed) return; + if (_disposed) + { + return; + } + _disposed = true; GC.SuppressFinalize(this); @@ -883,4 +887,4 @@ public sealed class CsvFeedTests : IDisposable } #endregion -} \ No newline at end of file +} diff --git a/lib/feeds/csv/CsvFeed.cs b/lib/feeds/csv/CsvFeed.cs index 3e519402..72edb1be 100644 --- a/lib/feeds/csv/CsvFeed.cs +++ b/lib/feeds/csv/CsvFeed.cs @@ -74,10 +74,14 @@ public sealed class CsvFeed : IFeed public CsvFeed(string filePath) { if (string.IsNullOrWhiteSpace(filePath)) + { throw new ArgumentException("File path cannot be null or empty", nameof(filePath)); + } if (!File.Exists(filePath)) + { throw new FileNotFoundException($"CSV file not found: {filePath}", filePath); + } FilePath = filePath; Data = LoadFromCsv(filePath); @@ -97,18 +101,24 @@ public sealed class CsvFeed : IFeed { var header = reader.ReadLine(); if (header is null) + { throw new InvalidDataException("CSV file is empty"); + } while (!reader.EndOfStream) { var line = reader.ReadLine(); if (!string.IsNullOrWhiteSpace(line)) + { dataLines.Add(line); + } } } if (dataLines.Count == 0) + { throw new InvalidDataException("CSV file contains only header, no data"); + } // Reverse in-place to chronological order (oldest first) dataLines.Reverse(); @@ -292,7 +302,9 @@ public sealed class CsvFeed : IFeed public TBarSeries Fetch(int count, long startTime, TimeSpan interval) { if (count <= 0) + { throw new ArgumentException("Count must be positive", nameof(count)); + } var result = new TBarSeries(count); @@ -300,7 +312,9 @@ public sealed class CsvFeed : IFeed int startIndex = FindStartIndex(startTime); if (startIndex == -1) + { return result; + } // Collect bars matching interval long expectedTime = startTime; @@ -349,13 +363,19 @@ public sealed class CsvFeed : IFeed private int FindStartIndex(long startTime) { if (Count == 0) + { return -1; + } if (Data[0].Time >= startTime) + { return 0; + } if (Data[Count - 1].Time < startTime) + { return -1; + } int left = 0; int right = Count - 1; @@ -365,9 +385,13 @@ public sealed class CsvFeed : IFeed int mid = left + (right - left) / 2; if (Data[mid].Time < startTime) + { left = mid + 1; + } else + { right = mid; + } } return left; @@ -391,7 +415,9 @@ public sealed class CsvFeed : IFeed public void Reset(int index) { if (index < 0 || index > Count) + { throw new ArgumentOutOfRangeException(nameof(index), index, $"Index must be between 0 and {Count}"); + } _currentIndex = index; _hasCurrentBar = false; @@ -407,7 +433,9 @@ public sealed class CsvFeed : IFeed public TBar GetBar(int index) { if (index < 0 || index >= Count) + { throw new ArgumentOutOfRangeException(nameof(index), index, $"Index must be between 0 and {Count - 1}"); + } return Data[index]; } @@ -416,4 +444,4 @@ public sealed class CsvFeed : IFeed /// Gets the underlying data series (read-only access). /// public TBarSeries Data { get; } -} \ No newline at end of file +} diff --git a/lib/feeds/gbm/Gbm.Tests.cs b/lib/feeds/gbm/Gbm.Tests.cs index 7de0a4b6..484572b1 100644 --- a/lib/feeds/gbm/Gbm.Tests.cs +++ b/lib/feeds/gbm/Gbm.Tests.cs @@ -584,12 +584,17 @@ public class GBMTests { double mean = 0; for (int i = 0; i < values.Length; i++) + { mean += values[i]; + } + mean /= values.Length; double sumSquares = 0; for (int i = 0; i < values.Length; i++) + { sumSquares += (values[i] - mean) * (values[i] - mean); + } return Math.Sqrt(sumSquares / values.Length); } diff --git a/lib/feeds/gbm/ValidationHelper.cs b/lib/feeds/gbm/ValidationHelper.cs index c53c1ea6..3fff83c1 100644 --- a/lib/feeds/gbm/ValidationHelper.cs +++ b/lib/feeds/gbm/ValidationHelper.cs @@ -75,7 +75,10 @@ public static class ValidationHelper double qValue = qSeries[i].Value; double? sValue = selector(sSeries[i]); - if (!sValue.HasValue) continue; + if (!sValue.HasValue) + { + continue; + } Assert.True( Math.Abs(qValue - sValue.Value) <= tolerance, @@ -103,7 +106,10 @@ public static class ValidationHelper double qValue = qResults[i]; double? sValue = selector(sSeries[i]); - if (!sValue.HasValue) continue; + if (!sValue.HasValue) + { + continue; + } Assert.True( Math.Abs(qValue - sValue.Value) <= tolerance, @@ -131,7 +137,10 @@ public static class ValidationHelper double qValue = qOutput[i]; double? sValue = selector(sSeries[i]); - if (!sValue.HasValue) continue; + if (!sValue.HasValue) + { + continue; + } Assert.True( Math.Abs(qValue - sValue.Value) <= tolerance, @@ -161,10 +170,16 @@ public static class ValidationHelper { double qValue = qSeries[i].Value; - if (i < lookback) continue; + if (i < lookback) + { + continue; + } int tIndex = i - lookback; - if (tIndex >= tOutput.Length) continue; + if (tIndex >= tOutput.Length) + { + continue; + } double tValue = tOutput[tIndex]; @@ -191,10 +206,16 @@ public static class ValidationHelper { double qValue = qResults[i]; - if (i < lookback) continue; + if (i < lookback) + { + continue; + } int tIndex = i - lookback; - if (tIndex >= tOutput.Length) continue; + if (tIndex >= tOutput.Length) + { + continue; + } double tValue = tOutput[tIndex]; @@ -221,10 +242,16 @@ public static class ValidationHelper { double qValue = qOutput[i]; - if (i < lookback) continue; + if (i < lookback) + { + continue; + } int tIndex = i - lookback; - if (tIndex >= tOutput.Length) continue; + if (tIndex >= tOutput.Length) + { + continue; + } double tValue = tOutput[tIndex]; @@ -253,10 +280,16 @@ public static class ValidationHelper { double qValue = qSeries[i].Value; - if (i < lookback) continue; + if (i < lookback) + { + continue; + } int tIndex = i - offset; - if (tIndex < 0 || tIndex >= length) continue; + if (tIndex < 0 || tIndex >= length) + { + continue; + } double tValue = tOutput[tIndex]; @@ -285,10 +318,16 @@ public static class ValidationHelper { double qValue = qResults[i]; - if (i < lookback) continue; + if (i < lookback) + { + continue; + } int tIndex = i - offset; - if (tIndex < 0 || tIndex >= length) continue; + if (tIndex < 0 || tIndex >= length) + { + continue; + } double tValue = tOutput[tIndex]; @@ -317,10 +356,16 @@ public static class ValidationHelper { double qValue = qOutput[i]; - if (i < lookback) continue; + if (i < lookback) + { + continue; + } int tIndex = i - offset; - if (tIndex < 0 || tIndex >= length) continue; + if (tIndex < 0 || tIndex >= length) + { + continue; + } double tValue = tOutput[tIndex]; @@ -388,18 +433,25 @@ public static class ValidationHelper Func selector) { if (qSeries.Count != sSeries.Count) + { throw new ArgumentException("Series must have the same count", nameof(sSeries)); + } double maxDiff = 0; for (int i = 0; i < qSeries.Count; i++) { double? sValue = selector(sSeries[i]); - if (!sValue.HasValue) continue; + if (!sValue.HasValue) + { + continue; + } double diff = Math.Abs(qSeries[i].Value - sValue.Value); if (diff > maxDiff) + { maxDiff = diff; + } } return maxDiff; @@ -415,20 +467,27 @@ public static class ValidationHelper Func selector) { if (qSeries.Count != sSeries.Count) + { throw new ArgumentException("Series must have the same count", nameof(sSeries)); + } double maxDiff = 0; for (int i = 0; i < qSeries.Count; i++) { double? sValue = selector(sSeries[i]); - if (!sValue.HasValue || Math.Abs(sValue.Value) < double.Epsilon) continue; + if (!sValue.HasValue || Math.Abs(sValue.Value) < double.Epsilon) + { + continue; + } double relDiff = Math.Abs((qSeries[i].Value - sValue.Value) / sValue.Value); if (relDiff > maxDiff) + { maxDiff = relDiff; + } } return maxDiff; } -} \ No newline at end of file +} diff --git a/lib/feeds/gbm/ValidationTestData.cs b/lib/feeds/gbm/ValidationTestData.cs index db8bedfc..6f5e023d 100644 --- a/lib/feeds/gbm/ValidationTestData.cs +++ b/lib/feeds/gbm/ValidationTestData.cs @@ -178,7 +178,9 @@ public sealed class ValidationTestData : IDisposable public ValidationTestData CreateSubset(int count) { if (count <= 0 || count > Count) + { throw new ArgumentOutOfRangeException(nameof(count), count, $"Count must be between 1 and {Count}"); + } return new ValidationTestData(count, DefaultStartPrice, DefaultMu, DefaultSigma, DefaultSeed); } @@ -216,4 +218,4 @@ public sealed class ValidationTestData : IDisposable // No unmanaged resources to dispose // Implemented for IDisposable pattern compatibility with test fixtures } -} \ No newline at end of file +} diff --git a/lib/feeds/gbm/gbm.cs b/lib/feeds/gbm/gbm.cs index 94368f33..02781ad0 100644 --- a/lib/feeds/gbm/gbm.cs +++ b/lib/feeds/gbm/gbm.cs @@ -75,22 +75,30 @@ public sealed class GBM : IFeed { // Validate startPrice if (startPrice <= 0 || !double.IsFinite(startPrice)) + { throw new ArgumentOutOfRangeException(nameof(startPrice), startPrice, "Start price must be positive and finite"); + } // Validate mu if (!double.IsFinite(mu)) + { throw new ArgumentOutOfRangeException(nameof(mu), mu, "Drift (mu) must be finite"); + } // Validate sigma if (sigma < 0 || !double.IsFinite(sigma)) + { throw new ArgumentOutOfRangeException(nameof(sigma), sigma, "Volatility (sigma) must be non-negative and finite"); + } // Use provided timeframe or default to 1 minute var timeframe = defaultTimeframe ?? TimeSpan.FromMinutes(1); // Validate timeframe if (timeframe <= TimeSpan.Zero) + { throw new ArgumentOutOfRangeException(nameof(defaultTimeframe), defaultTimeframe, "Timeframe must be positive"); + } _rnd = seed.HasValue ? new Random(seed.Value) : null; StartPrice = startPrice; @@ -170,7 +178,9 @@ public sealed class GBM : IFeed // Guard against log(0) which produces -Infinity if (u1 <= double.Epsilon) + { u1 = double.Epsilon; + } double mag = Math.Sqrt(-2.0 * Math.Log(u1)); double angle = 2.0 * Math.PI * u2; @@ -200,7 +210,9 @@ public sealed class GBM : IFeed // Ensure price stays positive and finite if (!double.IsFinite(price) || price <= 0) + { price = _lastPrice; + } double volume = 1000 + NextDouble() * 1000; @@ -232,7 +244,9 @@ public sealed class GBM : IFeed // Ensure price stays positive and finite if (!double.IsFinite(price) || price <= 0) + { price = _lastPrice; + } double additionalVolume = 1000 + NextDouble() * 1000; @@ -274,9 +288,14 @@ public sealed class GBM : IFeed public TBarSeries Fetch(int count, long startTime, TimeSpan interval) { if (count <= 0) + { throw new ArgumentException("Count must be positive", nameof(count)); + } + if (interval <= TimeSpan.Zero) + { throw new ArgumentOutOfRangeException(nameof(interval), interval, "Interval must be positive"); + } var series = new TBarSeries(count); @@ -333,12 +352,35 @@ public sealed class GBM : IFeed } finally { - if (rentedT != null) ArrayPool.Shared.Return(rentedT); - if (rentedO != null) ArrayPool.Shared.Return(rentedO); - if (rentedH != null) ArrayPool.Shared.Return(rentedH); - if (rentedL != null) ArrayPool.Shared.Return(rentedL); - if (rentedC != null) ArrayPool.Shared.Return(rentedC); - if (rentedV != null) ArrayPool.Shared.Return(rentedV); + if (rentedT != null) + { + ArrayPool.Shared.Return(rentedT); + } + + if (rentedO != null) + { + ArrayPool.Shared.Return(rentedO); + } + + if (rentedH != null) + { + ArrayPool.Shared.Return(rentedH); + } + + if (rentedL != null) + { + ArrayPool.Shared.Return(rentedL); + } + + if (rentedC != null) + { + ArrayPool.Shared.Return(rentedC); + } + + if (rentedV != null) + { + ArrayPool.Shared.Return(rentedV); + } } } @@ -371,7 +413,9 @@ public sealed class GBM : IFeed // Ensure price stays positive and finite if (!double.IsFinite(price) || price <= 0) + { price = currentPrice; + } double open = currentPrice; double close = price; diff --git a/lib/filters/bessel/Bessel.cs b/lib/filters/bessel/Bessel.cs index f76de936..f439ea3c 100644 --- a/lib/filters/bessel/Bessel.cs +++ b/lib/filters/bessel/Bessel.cs @@ -100,7 +100,9 @@ public sealed class Bessel : AbstractBase public Bessel(int length) { if (length < 2) + { throw new ArgumentException("Length must be at least 2 for 2nd-order Bessel filter", nameof(length)); + } double a = Math.Exp(-Math.PI / length); double b = 2.0 * a * Math.Cos(1.738 * Math.PI / length); @@ -171,7 +173,9 @@ public sealed class Bessel : AbstractBase public override void Prime(ReadOnlySpan source, TimeSpan? step = null) { if (source.Length == 0) + { return; + } Reset(); @@ -209,9 +213,13 @@ public sealed class Bessel : AbstractBase { double val = source[i]; if (double.IsFinite(val)) + { _state.LastValidValue = val; + } else + { val = _state.LastValidValue; + } _state.F2 = _state.F1; _state.F1 = val; @@ -223,9 +231,13 @@ public sealed class Bessel : AbstractBase { double val = source[i]; if (double.IsFinite(val)) + { _state.LastValidValue = val; + } else + { val = _state.LastValidValue; + } double filt = Math.FusedMultiplyAdd(_c3, _state.F2, Math.FusedMultiplyAdd(_c2, _state.F1, _c1 * val)); @@ -236,7 +248,9 @@ public sealed class Bessel : AbstractBase } if (_state.Count >= WarmupPeriod) + { _state.IsHot = true; + } Last = new TValue(DateTime.MinValue, _state.F1); @@ -310,7 +324,9 @@ public sealed class Bessel : AbstractBase } if (!_state.IsHot && _state.Count >= WarmupPeriod) + { _state.IsHot = true; + } Last = new TValue(input.Time, filt); PubEvent(Last); @@ -330,7 +346,9 @@ public sealed class Bessel : AbstractBase public override TSeries Update(TSeries source) { if (source.Count == 0) + { return []; + } int len = source.Count; var t = new List(len); @@ -404,9 +422,13 @@ public sealed class Bessel : AbstractBase { double val = source[i]; if (double.IsFinite(val)) + { state.LastValidValue = val; + } else + { val = state.LastValidValue; + } state.F2 = state.F1; state.F1 = val; @@ -419,9 +441,13 @@ public sealed class Bessel : AbstractBase { double val = source[i]; if (double.IsFinite(val)) + { state.LastValidValue = val; + } else + { val = state.LastValidValue; + } double filt = Math.FusedMultiplyAdd(c3, state.F2, Math.FusedMultiplyAdd(c2, state.F1, c1 * val)); @@ -433,7 +459,9 @@ public sealed class Bessel : AbstractBase } if (!state.IsHot && state.Count >= warmupPeriod) + { state.IsHot = true; + } } /// @@ -479,13 +507,19 @@ public sealed class Bessel : AbstractBase public static void Calculate(ReadOnlySpan source, Span output, int length) { if (length < 2) + { throw new ArgumentException("Length must be at least 2 for 2nd-order Bessel filter", nameof(length)); + } if (source.Length != output.Length) + { throw new ArgumentException("Source and output must have the same length", nameof(output)); + } if (source.Length == 0) + { return; + } double a = Math.Exp(-Math.PI / length); double b = 2.0 * a * Math.Cos(1.738 * Math.PI / length); diff --git a/lib/filters/bilateral/Bilateral.Validation.Tests.cs b/lib/filters/bilateral/Bilateral.Validation.Tests.cs index cb320793..e64aed48 100644 --- a/lib/filters/bilateral/Bilateral.Validation.Tests.cs +++ b/lib/filters/bilateral/Bilateral.Validation.Tests.cs @@ -142,7 +142,10 @@ public sealed class BilateralValidationTests : IDisposable _history.RemoveAt(0); } - if (_history.Count == 0) return double.NaN; + if (_history.Count == 0) + { + return double.NaN; + } double sigmaS = Math.Max(_length * _sigmaSRatio, 1e-10); @@ -178,7 +181,10 @@ public sealed class BilateralValidationTests : IDisposable private static double CalculateStDev(List values) { - if (values.Count < 2) return 0; + if (values.Count < 2) + { + return 0; + } double avg = values.Average(); double sumSqDiff = values.Sum(d => (d - avg) * (d - avg)); diff --git a/lib/filters/bilateral/Bilateral.cs b/lib/filters/bilateral/Bilateral.cs index e2e9a0a2..71ffa93a 100644 --- a/lib/filters/bilateral/Bilateral.cs +++ b/lib/filters/bilateral/Bilateral.cs @@ -46,7 +46,9 @@ public sealed class Bilateral : AbstractBase public Bilateral(int period, double sigmaSRatio = 0.5, double sigmaRMult = 1.0) { if (period <= 0) + { throw new ArgumentException("Period must be greater than 0", nameof(period)); + } _period = period; _sigmaSRatio = sigmaSRatio; @@ -74,7 +76,10 @@ public sealed class Bilateral : AbstractBase public override void Prime(ReadOnlySpan source, TimeSpan? step = null) { - if (source.Length == 0) return; + if (source.Length == 0) + { + return; + } _buffer.Clear(); _state = default; @@ -128,7 +133,10 @@ public sealed class Bilateral : AbstractBase public override TSeries Update(TSeries source) { - if (source.Count == 0) return []; + if (source.Count == 0) + { + return []; + } int len = source.Count; var t = new System.Collections.Generic.List(len); @@ -337,10 +345,14 @@ public sealed class Bilateral : AbstractBase public static void Calculate(ReadOnlySpan source, Span destination, int period, double sigmaSRatio = 0.5, double sigmaRMult = 1.0) { if (period <= 0) + { throw new ArgumentException("Period must be greater than 0", nameof(period)); + } if (destination.Length < source.Length) + { throw new ArgumentException("Destination must have length >= source length", nameof(destination)); + } // Rent arrays for large periods to avoid heap allocations double[]? rentedSpatialWeights = null; @@ -424,7 +436,10 @@ public sealed class Bilateral : AbstractBase int currentNewestIdx = windowIdx; windowIdx = (windowIdx + 1) % period; - if (count < period) count++; + if (count < period) + { + count++; + } // Calculate StDev double invCount = 1.0 / count; @@ -480,9 +495,14 @@ public sealed class Bilateral : AbstractBase finally { if (rentedSpatialWeights != null) + { ArrayPool.Shared.Return(rentedSpatialWeights); + } + if (rentedWindow != null) + { ArrayPool.Shared.Return(rentedWindow); + } } } @@ -514,4 +534,4 @@ public sealed class Bilateral : AbstractBase } base.Dispose(disposing); } -} \ No newline at end of file +} diff --git a/lib/filters/bpf/Bpf.Quantower.Tests.cs b/lib/filters/bpf/Bpf.Quantower.Tests.cs index b234fbce..e5a12949 100644 --- a/lib/filters/bpf/Bpf.Quantower.Tests.cs +++ b/lib/filters/bpf/Bpf.Quantower.Tests.cs @@ -134,4 +134,4 @@ public class BpfIndicatorTests Assert.Equal(60, indicator.LowerPeriod); Assert.Equal(20, indicator.UpperPeriod); } -} \ No newline at end of file +} diff --git a/lib/filters/bpf/Bpf.Tests.cs b/lib/filters/bpf/Bpf.Tests.cs index 912697aa..28abce10 100644 --- a/lib/filters/bpf/Bpf.Tests.cs +++ b/lib/filters/bpf/Bpf.Tests.cs @@ -116,4 +116,4 @@ public class BpfTests // Last value should be close to 0 Assert.True(Math.Abs(output[^1]) < 1e-6); } -} \ No newline at end of file +} diff --git a/lib/filters/bpf/Bpf.Validation.Tests.cs b/lib/filters/bpf/Bpf.Validation.Tests.cs index f88b7d15..32a094ee 100644 --- a/lib/filters/bpf/Bpf.Validation.Tests.cs +++ b/lib/filters/bpf/Bpf.Validation.Tests.cs @@ -20,7 +20,8 @@ public class BpfValidationTests double[] sine15 = new double[T]; // Period 15 (Inside band, 10 < P < 40). Should pass. double[] sine100 = new double[T]; // Period 100 (Too slow, should be blocked by HP(40)). HP(40) passes P < 40. - for (int i = 0; i < T; i++) { + for (int i = 0; i < T; i++) + { sine5[i] = Math.Sin(2 * Math.PI * i / 5.0); sine15[i] = Math.Sin(2 * Math.PI * i / 15.0); sine100[i] = Math.Sin(2 * Math.PI * i / 100.0); diff --git a/lib/filters/bpf/Bpf.cs b/lib/filters/bpf/Bpf.cs index 8eac2281..a6f92432 100644 --- a/lib/filters/bpf/Bpf.cs +++ b/lib/filters/bpf/Bpf.cs @@ -53,9 +53,14 @@ public sealed class Bpf : AbstractBase public Bpf(int lowerPeriod, int upperPeriod) { if (lowerPeriod < 1) + { throw new ArgumentOutOfRangeException(nameof(lowerPeriod), "Lower period must be >= 1"); + } + if (upperPeriod < 1) + { throw new ArgumentOutOfRangeException(nameof(upperPeriod), "Upper period must be >= 1"); + } LowerPeriod = lowerPeriod; UpperPeriod = upperPeriod; @@ -95,7 +100,10 @@ public sealed class Bpf : AbstractBase public override TSeries Update(TSeries source) { - if (source.Count == 0) return []; + if (source.Count == 0) + { + return []; + } double[] values = source.Values.ToArray(); double[] results = new double[values.Length]; @@ -186,7 +194,9 @@ public sealed class Bpf : AbstractBase public static void Calculate(ReadOnlySpan source, Span output, int lowerPeriod, int upperPeriod) { if (source.Length != output.Length) + { throw new ArgumentException("Source and output spans must be of the same length.", nameof(output)); + } // Coefficients double sqrt2Pi = Math.Sqrt(2.0) * Math.PI; @@ -213,7 +223,10 @@ public sealed class Bpf : AbstractBase if (source.Length > 0) { lastValid = source[0]; - if (!double.IsFinite(lastValid)) lastValid = 0; + if (!double.IsFinite(lastValid)) + { + lastValid = 0; + } } for (int i = 0; i < source.Length; i++) @@ -260,4 +273,4 @@ public sealed class Bpf : 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 3fd97979..8d2560e1 100644 --- a/lib/filters/cheby1/Cheby1.Tests.cs +++ b/lib/filters/cheby1/Cheby1.Tests.cs @@ -111,4 +111,4 @@ public class Cheby1Tests Assert.Equal(iterativeResults[i], spanResults[i], 1e-10); } } -} \ No newline at end of file +} diff --git a/lib/filters/cheby1/Cheby1.Validation.Tests.cs b/lib/filters/cheby1/Cheby1.Validation.Tests.cs index 49c189b5..c9f8a5c1 100644 --- a/lib/filters/cheby1/Cheby1.Validation.Tests.cs +++ b/lib/filters/cheby1/Cheby1.Validation.Tests.cs @@ -20,7 +20,10 @@ public class Cheby1ValidationTests var filter = new Cheby1(20, 1.0); // Wn = 1/20 var input = new TSeries(); - for (int i = 0; i < 100; i++) input.Add(new TValue(DateTime.UtcNow, 100)); // Impulse/Step + for (int i = 0; i < 100; i++) + { + input.Add(new TValue(DateTime.UtcNow, 100)); // Impulse/Step + } var output = filter.Update(input); diff --git a/lib/filters/cheby1/Cheby1.cs b/lib/filters/cheby1/Cheby1.cs index a4b8d6b8..01167a1b 100644 --- a/lib/filters/cheby1/Cheby1.cs +++ b/lib/filters/cheby1/Cheby1.cs @@ -46,9 +46,14 @@ public sealed class Cheby1 : AbstractBase public Cheby1(int period, double ripple = 1.0) { if (period < 2) + { throw new ArgumentOutOfRangeException(nameof(period), "Period must be >= 2"); + } + if (ripple <= 0) + { throw new ArgumentOutOfRangeException(nameof(ripple), "Ripple must be > 0"); + } Period = period; Ripple = ripple; @@ -105,7 +110,10 @@ public sealed class Cheby1 : AbstractBase public override TSeries Update(TSeries source) { - if (source.Count == 0) return []; + if (source.Count == 0) + { + return []; + } double[] values = source.Values.ToArray(); double[] results = new double[values.Length]; @@ -178,7 +186,9 @@ public sealed class Cheby1 : AbstractBase _state.Filt1 = filt; if (_state.Count < 2) + { _state.Count++; + } } Last = new TValue(input.Time, filt); @@ -197,12 +207,19 @@ public sealed class Cheby1 : AbstractBase public static void Calculate(ReadOnlySpan source, Span output, int period, double ripple = 1.0) { if (source.Length != output.Length) + { throw new ArgumentException("Source and output spans must be of the same length.", nameof(output)); + } if (period < 2) + { throw new ArgumentOutOfRangeException(nameof(period), "Period must be >= 2"); + } + if (ripple <= 0) + { throw new ArgumentOutOfRangeException(nameof(ripple), "Ripple must be > 0"); + } // Coefficients double safeRipple = Math.Max(ripple, 0.01); @@ -239,7 +256,10 @@ public sealed class Cheby1 : AbstractBase if (source.Length > 0) { lastValid = source[0]; - if (!double.IsFinite(lastValid)) lastValid = 0; + if (!double.IsFinite(lastValid)) + { + lastValid = 0; + } } for (int i = 0; i < source.Length; i++) @@ -285,4 +305,4 @@ public sealed class Cheby1 : AbstractBase } base.Dispose(disposing); } -} \ No newline at end of file +} diff --git a/lib/filters/cheby2/Cheby2.Tests.cs b/lib/filters/cheby2/Cheby2.Tests.cs index b42de461..ccbf72c9 100644 --- a/lib/filters/cheby2/Cheby2.Tests.cs +++ b/lib/filters/cheby2/Cheby2.Tests.cs @@ -109,4 +109,4 @@ public class Cheby2Tests Assert.Equal(iterativeResults[i], spanResults[i], 1e-10); } } -} \ No newline at end of file +} diff --git a/lib/filters/cheby2/Cheby2.Validation.Tests.cs b/lib/filters/cheby2/Cheby2.Validation.Tests.cs index 2f355ae8..da963f67 100644 --- a/lib/filters/cheby2/Cheby2.Validation.Tests.cs +++ b/lib/filters/cheby2/Cheby2.Validation.Tests.cs @@ -21,9 +21,11 @@ public class Cheby2ValidationTests Assert.True(Math.Abs(result1) > 0); // Verify stability (should decay towards zero for lowpass IIR) - for(int i=0; i<50; i++) + for (int i = 0; i < 50; i++) + { filter.Update(new TValue(DateTime.UtcNow, 0.0)); + } Assert.True(Math.Abs(filter.Last.Value) < 1e-5); } -} \ No newline at end of file +} diff --git a/lib/filters/cheby2/Cheby2.cs b/lib/filters/cheby2/Cheby2.cs index b2b974b2..b485dbef 100644 --- a/lib/filters/cheby2/Cheby2.cs +++ b/lib/filters/cheby2/Cheby2.cs @@ -46,9 +46,14 @@ public sealed class Cheby2 : AbstractBase public Cheby2(int period, double attenuation = 5.0) { if (period < 2) + { throw new ArgumentOutOfRangeException(nameof(period), "Period must be >= 2"); + } + if (attenuation <= 0) + { throw new ArgumentOutOfRangeException(nameof(attenuation), "Attenuation must be > 0"); + } Period = period; Attenuation = attenuation; @@ -123,7 +128,10 @@ public sealed class Cheby2 : AbstractBase public override TSeries Update(TSeries source) { - if (source.Count == 0) return []; + if (source.Count == 0) + { + return []; + } double[] values = source.Values.ToArray(); double[] results = new double[values.Length]; @@ -192,7 +200,9 @@ public sealed class Cheby2 : AbstractBase _state.Filt1 = filt; if (_state.Count < 2) + { _state.Count++; + } } Last = new TValue(input.Time, filt); @@ -211,12 +221,19 @@ public sealed class Cheby2 : AbstractBase public static void Calculate(ReadOnlySpan source, Span output, int period, double attenuation = 5.0) { if (source.Length != output.Length) + { throw new ArgumentException("Source and output spans must be of the same length.", nameof(output)); + } if (period < 2) + { throw new ArgumentOutOfRangeException(nameof(period), "Period must be >= 2"); + } + if (attenuation <= 0) + { throw new ArgumentOutOfRangeException(nameof(attenuation), "Attenuation must be > 0"); + } // Coefficients double safeAtten = Math.Max(attenuation, 0.1); @@ -268,7 +285,10 @@ public sealed class Cheby2 : AbstractBase if (source.Length > 0) { lastValid = source[0]; - if (!double.IsFinite(lastValid)) lastValid = 0; + if (!double.IsFinite(lastValid)) + { + lastValid = 0; + } } for (int i = 0; i < source.Length; i++) diff --git a/lib/filters/elliptic/Elliptic.Quantower.Tests.cs b/lib/filters/elliptic/Elliptic.Quantower.Tests.cs index 49d4d3ae..c45de95b 100644 --- a/lib/filters/elliptic/Elliptic.Quantower.Tests.cs +++ b/lib/filters/elliptic/Elliptic.Quantower.Tests.cs @@ -109,4 +109,4 @@ public class EllipticIndicatorTests indicator.Period = 50; Assert.Equal(50, indicator.Period); } -} \ No newline at end of file +} diff --git a/lib/filters/elliptic/Elliptic.Tests.cs b/lib/filters/elliptic/Elliptic.Tests.cs index 9a0cfade..fd77d7e8 100644 --- a/lib/filters/elliptic/Elliptic.Tests.cs +++ b/lib/filters/elliptic/Elliptic.Tests.cs @@ -101,4 +101,4 @@ public class EllipticTests Assert.False(filter.IsHot); Assert.Equal(0, filter.Last.Value); } -} \ No newline at end of file +} diff --git a/lib/filters/elliptic/Elliptic.Validation.Tests.cs b/lib/filters/elliptic/Elliptic.Validation.Tests.cs index e06b1c9d..e7a66a34 100644 --- a/lib/filters/elliptic/Elliptic.Validation.Tests.cs +++ b/lib/filters/elliptic/Elliptic.Validation.Tests.cs @@ -49,9 +49,13 @@ public sealed class EllipticValidationTests : IDisposable private static double StdDev(List values) { - if (values.Count < 2) return 0; + if (values.Count < 2) + { + return 0; + } + double avg = values.Average(); double sumSq = values.Sum(v => Math.Pow(v - avg, 2)); return Math.Sqrt(sumSq / (values.Count - 1)); } -} \ No newline at end of file +} diff --git a/lib/filters/elliptic/Elliptic.cs b/lib/filters/elliptic/Elliptic.cs index cfc625c5..518a6b75 100644 --- a/lib/filters/elliptic/Elliptic.cs +++ b/lib/filters/elliptic/Elliptic.cs @@ -46,7 +46,9 @@ public sealed class Elliptic : AbstractBase public Elliptic(int period) { if (period < 2) + { throw new ArgumentOutOfRangeException(nameof(period), "Period must be >= 2"); + } Period = period; Name = $"Elliptic({period})"; @@ -54,14 +56,20 @@ public sealed class Elliptic : AbstractBase // Precompute coefficients based on hardcoded Rp=1dB, Rs=40dB double Wc = Math.Tan(Math.PI / period); - if (Wc < 1e-9) Wc = 1e-9; + if (Wc < 1e-9) + { + Wc = 1e-9; + } double omega_z_scaled = C_wz * Wc; double sigma_scaled = C_sigma * Wc; double Kp_scaled = C_Kp_norm * Wc * Wc; double a0_denom = 1.0 - 2.0 * sigma_scaled + Kp_scaled; - if (Math.Abs(a0_denom) < 1e-9) a0_denom = 1e-9; + if (Math.Abs(a0_denom) < 1e-9) + { + a0_denom = 1e-9; + } const double norm_factor = C_Kp_norm / (C_k * C_wz * C_wz); @@ -111,7 +119,10 @@ public sealed class Elliptic : AbstractBase public override TSeries Update(TSeries source) { - if (source.Count == 0) return []; + if (source.Count == 0) + { + return []; + } double[] values = source.Values.ToArray(); double[] results = new double[values.Length]; @@ -206,21 +217,31 @@ public sealed class Elliptic : AbstractBase endState = default; if (source.Length != output.Length) + { throw new ArgumentException("Source and output spans must be of the same length.", nameof(output)); + } if (period < 2) + { throw new ArgumentOutOfRangeException(nameof(period), "Period must be >= 2"); + } // Precompute coefficients based on hardcoded Rp=1dB, Rs=40dB double Wc = Math.Tan(Math.PI / period); - if (Wc < 1e-9) Wc = 1e-9; + if (Wc < 1e-9) + { + Wc = 1e-9; + } double omega_z_scaled = C_wz * Wc; double sigma_scaled = C_sigma * Wc; double Kp_scaled = C_Kp_norm * Wc * Wc; double a0_denom = 1.0 - 2.0 * sigma_scaled + Kp_scaled; - if (Math.Abs(a0_denom) < 1e-9) a0_denom = 1e-9; + if (Math.Abs(a0_denom) < 1e-9) + { + a0_denom = 1e-9; + } const double norm_factor = C_Kp_norm / (C_k * C_wz * C_wz); @@ -249,7 +270,10 @@ public sealed class Elliptic : AbstractBase if (source.Length > 0) { lastValid = source[0]; - if (!double.IsFinite(lastValid)) lastValid = 0; + if (!double.IsFinite(lastValid)) + { + lastValid = 0; + } } for (int i = 0; i < source.Length; i++) diff --git a/lib/filters/gauss/Gauss.Quantower.Tests.cs b/lib/filters/gauss/Gauss.Quantower.Tests.cs index ceb53850..c46c2392 100644 --- a/lib/filters/gauss/Gauss.Quantower.Tests.cs +++ b/lib/filters/gauss/Gauss.Quantower.Tests.cs @@ -158,4 +158,4 @@ public class GaussIndicatorTests indicator.Sigma = 2.0; Assert.Equal(2.0, indicator.Sigma); } -} \ No newline at end of file +} diff --git a/lib/filters/gauss/Gauss.Tests.cs b/lib/filters/gauss/Gauss.Tests.cs index 57fe31f5..4778b9b0 100644 --- a/lib/filters/gauss/Gauss.Tests.cs +++ b/lib/filters/gauss/Gauss.Tests.cs @@ -85,8 +85,10 @@ public class GaussTests Assert.False(double.IsNaN(result.Value)); // Fill full buffer with NaNs - for (int i=0; i<20; i++) + for (int i = 0; i < 20; i++) + { gauss.Update(new TValue(DateTime.MinValue, double.NaN)); + } // Should evaluate to NaN if all are NaN Assert.True(double.IsNaN(gauss.Last.Value)); @@ -100,11 +102,13 @@ public class GaussTests Assert.False(gauss.IsHot); for (int i = 0; i < 6; i++) + { gauss.Update(new TValue(DateTime.MinValue, i)); + } Assert.False(gauss.IsHot); gauss.Update(new TValue(DateTime.MinValue, 6)); Assert.True(gauss.IsHot); } -} \ No newline at end of file +} diff --git a/lib/filters/gauss/Gauss.Validation.Tests.cs b/lib/filters/gauss/Gauss.Validation.Tests.cs index 61f0cd5e..90e2282c 100644 --- a/lib/filters/gauss/Gauss.Validation.Tests.cs +++ b/lib/filters/gauss/Gauss.Validation.Tests.cs @@ -24,7 +24,11 @@ public class GaussValidationTests : IDisposable protected virtual void Dispose(bool disposing) { - if (_disposed) return; + if (_disposed) + { + return; + } + if (disposing) { _testData?.Dispose(); @@ -96,9 +100,13 @@ public class GaussValidationTests : IDisposable } if (wSum > 0) + { result[i] = val / wSum; + } else + { result[i] = source[i]; // Fallback if no weights applied (shouldn't happen with valid sigma) + } } return result; @@ -163,4 +171,4 @@ public class GaussValidationTests : IDisposable } _output.WriteLine("Span mode successfully validated against reference implementation"); } -} \ No newline at end of file +} diff --git a/lib/filters/gauss/Gauss.cs b/lib/filters/gauss/Gauss.cs index 965d49e8..cb554942 100644 --- a/lib/filters/gauss/Gauss.cs +++ b/lib/filters/gauss/Gauss.cs @@ -148,9 +148,13 @@ public sealed class Gauss : AbstractBase } if (wSum > 0) + { result /= wSum; + } else + { result = input.Value; + } } else { @@ -187,7 +191,10 @@ public sealed class Gauss : AbstractBase public override TSeries Update(TSeries source) { - if (source.Count == 0) return []; + if (source.Count == 0) + { + return []; + } // Calculate using static method for performance var resultValues = new double[source.Count]; @@ -294,9 +301,13 @@ public sealed class Gauss : AbstractBase } if (wSum > 0) + { output[i] = result / wSum; + } else + { output[i] = double.NaN; + } } } diff --git a/lib/filters/hann/Hann.Quantower.Tests.cs b/lib/filters/hann/Hann.Quantower.Tests.cs index 369f4f5a..b31207d7 100644 --- a/lib/filters/hann/Hann.Quantower.Tests.cs +++ b/lib/filters/hann/Hann.Quantower.Tests.cs @@ -156,4 +156,4 @@ public class HannIndicatorTests indicator.Length = 10; Assert.Equal(10, indicator.Length); } -} \ No newline at end of file +} diff --git a/lib/filters/hann/Hann.Tests.cs b/lib/filters/hann/Hann.Tests.cs index d9fe5cb0..ea62fb2e 100644 --- a/lib/filters/hann/Hann.Tests.cs +++ b/lib/filters/hann/Hann.Tests.cs @@ -141,11 +141,15 @@ public class HannTests Assert.False(hann.IsHot); // Fill it - for(int i=0; i<5; i++) hann.Update(new TValue(DateTime.UtcNow, 100)); + for (int i = 0; i < 5; i++) + { + hann.Update(new TValue(DateTime.UtcNow, 100)); + } + Assert.True(hann.IsHot); hann.Reset(); Assert.False(hann.IsHot); Assert.Equal(0, hann.Last.Value); } -} \ No newline at end of file +} diff --git a/lib/filters/hann/Hann.Validation.Tests.cs b/lib/filters/hann/Hann.Validation.Tests.cs index f8ffaa7f..9243bea8 100644 --- a/lib/filters/hann/Hann.Validation.Tests.cs +++ b/lib/filters/hann/Hann.Validation.Tests.cs @@ -23,7 +23,11 @@ public class HannValidationTests : IDisposable protected virtual void Dispose(bool disposing) { - if (_disposed) return; + if (_disposed) + { + return; + } + if (disposing) { _testData?.Dispose(); @@ -71,9 +75,13 @@ public class HannValidationTests : IDisposable } if (wSum > double.Epsilon) + { result[i] = acc / wSum; + } else + { result[i] = source[i]; + } } return result; diff --git a/lib/filters/hann/Hann.cs b/lib/filters/hann/Hann.cs index f38a72ee..31b1cafe 100644 --- a/lib/filters/hann/Hann.cs +++ b/lib/filters/hann/Hann.cs @@ -150,7 +150,10 @@ public sealed class Hann : AbstractBase public override TSeries Update(TSeries source) { - if (source.Count == 0) return []; + if (source.Count == 0) + { + return []; + } var resultValues = new double[source.Count]; Calculate(source.Values, resultValues, Length); diff --git a/lib/filters/hp/Hp.Quantower.Tests.cs b/lib/filters/hp/Hp.Quantower.Tests.cs index 9c20b936..b82f0161 100644 --- a/lib/filters/hp/Hp.Quantower.Tests.cs +++ b/lib/filters/hp/Hp.Quantower.Tests.cs @@ -165,4 +165,4 @@ public class HpIndicatorTests indicator.Lambda = 500; Assert.Equal(500, indicator.Lambda); } -} \ No newline at end of file +} diff --git a/lib/filters/hp/Hp.Validation.Tests.cs b/lib/filters/hp/Hp.Validation.Tests.cs index 49ec3620..5a83d167 100644 --- a/lib/filters/hp/Hp.Validation.Tests.cs +++ b/lib/filters/hp/Hp.Validation.Tests.cs @@ -88,4 +88,4 @@ public class HpValidationTests : IDisposable return result; } -} \ No newline at end of file +} diff --git a/lib/filters/hp/Hp.cs b/lib/filters/hp/Hp.cs index e81e0d12..5329f8f7 100644 --- a/lib/filters/hp/Hp.cs +++ b/lib/filters/hp/Hp.cs @@ -149,7 +149,7 @@ public sealed class Hp : AbstractBase } else { - _state.Trend = currentTrend; + _state.Trend = currentTrend; } Last = new TValue(input.Time, currentTrend); @@ -159,7 +159,10 @@ public sealed class Hp : AbstractBase public override TSeries Update(TSeries source) { - if (source.Count == 0) return []; + if (source.Count == 0) + { + return []; + } var resultValues = new double[source.Count]; Calculate(source.Values, resultValues, Lambda); @@ -203,7 +206,10 @@ public sealed class Hp : AbstractBase throw new ArgumentException("Source and output spans must be of equal length.", nameof(output)); } - if (source.Length == 0) return; + if (source.Length == 0) + { + return; + } double s = Math.Sqrt(lambda); double alpha = (s * 0.5 - 1.0) / (s * 0.5 + 1.0); @@ -242,4 +248,4 @@ public sealed class Hp : AbstractBase } base.Dispose(disposing); } -} \ No newline at end of file +} diff --git a/lib/filters/hpf/Hpf.Quantower.Tests.cs b/lib/filters/hpf/Hpf.Quantower.Tests.cs index 6c1ec33a..6abc2a6b 100644 --- a/lib/filters/hpf/Hpf.Quantower.Tests.cs +++ b/lib/filters/hpf/Hpf.Quantower.Tests.cs @@ -152,4 +152,4 @@ public class HpfIndicatorTests indicator.Length = 50; Assert.Equal(50, indicator.Length); } -} \ No newline at end of file +} diff --git a/lib/filters/hpf/Hpf.cs b/lib/filters/hpf/Hpf.cs index 744cb446..d88e92de 100644 --- a/lib/filters/hpf/Hpf.cs +++ b/lib/filters/hpf/Hpf.cs @@ -36,7 +36,10 @@ public sealed class Hpf : AbstractBase public Hpf(int length = 40) { - if (length < 2) throw new ArgumentOutOfRangeException(nameof(length), "Length must be at least 2."); + if (length < 2) + { + throw new ArgumentOutOfRangeException(nameof(length), "Length must be at least 2."); + } Length = length; @@ -45,7 +48,9 @@ public sealed class Hpf : AbstractBase double sinW = Math.Sin(omega); if (Math.Abs(cosW) < 1e-15) + { throw new ArgumentOutOfRangeException(nameof(length), "Length produces an unstable coefficient set (cos(ω)≈0)."); + } double a = (cosW + sinW - 1.0) / cosW; double oneMinusA = 1.0 - a; @@ -86,14 +91,22 @@ public sealed class Hpf : AbstractBase public override void Prime(ReadOnlySpan source, TimeSpan? step = null) { foreach (double v in source) + { Update(new TValue(DateTime.MinValue, v), isNew: true); + } } [MethodImpl(MethodImplOptions.AggressiveInlining)] public override TValue Update(TValue input, bool isNew = true) { - if (isNew) _pState = _state; - else _state = _pState; + if (isNew) + { + _pState = _state; + } + else + { + _state = _pState; + } double price = input.Value; @@ -140,8 +153,8 @@ public sealed class Hpf : AbstractBase double src1 = _state.Src1; double src2 = _state.Src2; - double hp1 = _state.Hp1; - double hp2 = _state.Hp2; + double hp1 = _state.Hp1; + double hp2 = _state.Hp2; double d2 = Math.FusedMultiplyAdd(-2.0, src1, price + src2); @@ -161,7 +174,10 @@ public sealed class Hpf : AbstractBase public override TSeries Update(TSeries source) { - if (source.Count == 0) return []; + if (source.Count == 0) + { + return []; + } var output = new double[source.Count]; @@ -183,7 +199,9 @@ public sealed class Hpf : AbstractBase Last = new TValue(times[^1], output[^1]); for (int i = 0; i < source.Count; i++) + { result.Add(new TValue(times[i], output[i])); + } return result; } @@ -205,7 +223,9 @@ public sealed class Hpf : AbstractBase out (double Hp1, double Hp2, double Src1, double Src2, int Samples, bool HasSrc) state) { if (source.Length != output.Length) + { throw new ArgumentException("Source and output spans must be of equal length.", nameof(output)); + } if (source.Length == 0) { @@ -219,7 +239,9 @@ public sealed class Hpf : AbstractBase double sinW = Math.Sin(omega); if (Math.Abs(cosW) < 1e-15) + { throw new ArgumentOutOfRangeException(nameof(length), "Length produces an unstable coefficient set (cos(ω)≈0)."); + } double a = (cosW + sinW - 1.0) / cosW; double oneMinusA = 1.0 - a; diff --git a/lib/filters/kalman/Kalman.Quantower.Tests.cs b/lib/filters/kalman/Kalman.Quantower.Tests.cs index f91d4de2..6cec2aca 100644 --- a/lib/filters/kalman/Kalman.Quantower.Tests.cs +++ b/lib/filters/kalman/Kalman.Quantower.Tests.cs @@ -38,4 +38,4 @@ public class KalmanIndicatorTests Assert.Equal(1, indicator.LinesSeries[0].Count); Assert.True(double.IsFinite(indicator.LinesSeries[0].GetValue(0))); } -} \ No newline at end of file +} diff --git a/lib/filters/kalman/Kalman.Tests.cs b/lib/filters/kalman/Kalman.Tests.cs index f6b4cc13..1107f712 100644 --- a/lib/filters/kalman/Kalman.Tests.cs +++ b/lib/filters/kalman/Kalman.Tests.cs @@ -125,7 +125,7 @@ public class KalmanTests // 3. Streaming Update filter.Reset(); var streamingResults = new List(); - foreach(var item in series) + foreach (var item in series) { streamingResults.Add(filter.Update(item).Value); } @@ -136,4 +136,4 @@ public class KalmanTests Assert.Equal(tseriesResult[i].Value, streamingResults[i], 1e-9); } } -} \ No newline at end of file +} diff --git a/lib/filters/kalman/Kalman.Validation.Tests.cs b/lib/filters/kalman/Kalman.Validation.Tests.cs index caa0bf25..77bae293 100644 --- a/lib/filters/kalman/Kalman.Validation.Tests.cs +++ b/lib/filters/kalman/Kalman.Validation.Tests.cs @@ -85,4 +85,4 @@ public class KalmanValidationTests : IDisposable Assert.Equal(expected[i], actual[i], 1e-9); } } -} \ No newline at end of file +} diff --git a/lib/filters/kalman/Kalman.cs b/lib/filters/kalman/Kalman.cs index 6bf017a2..d884659d 100644 --- a/lib/filters/kalman/Kalman.cs +++ b/lib/filters/kalman/Kalman.cs @@ -71,8 +71,15 @@ public sealed class Kalman : AbstractBase /// Thrown when q or r are not positive. public Kalman(double q = 0.01, double r = 0.1) { - if (q <= 0) throw new ArgumentOutOfRangeException(nameof(q), "q must be positive."); - if (r <= 0) throw new ArgumentOutOfRangeException(nameof(r), "r must be positive."); + if (q <= 0) + { + throw new ArgumentOutOfRangeException(nameof(q), "q must be positive."); + } + + if (r <= 0) + { + throw new ArgumentOutOfRangeException(nameof(r), "r must be positive."); + } ProcessNoise = q; MeasurementNoise = r; @@ -112,8 +119,14 @@ public sealed class Kalman : AbstractBase public override TValue Update(TValue input, bool isNew = true) { // "rewind" behavior for isNew=false - if (isNew) _pState = _state; - else _state = _pState; + if (isNew) + { + _pState = _state; + } + else + { + _state = _pState; + } double z = input.Value; @@ -164,7 +177,10 @@ public sealed class Kalman : AbstractBase public override TSeries Update(TSeries source) { - if (source.Count == 0) return []; + if (source.Count == 0) + { + return []; + } var output = new double[source.Count]; @@ -174,7 +190,9 @@ public sealed class Kalman : AbstractBase var result = new TSeries(); var times = source.Times; for (int i = 0; i < source.Count; i++) + { result.Add(new TValue(times[i], output[i])); + } _state = new State { X = endX, P = endP, Samples = endSamples }; _pState = _state; @@ -186,7 +204,9 @@ public sealed class Kalman : AbstractBase public override void Prime(ReadOnlySpan source, TimeSpan? step = null) { foreach (double v in source) + { Update(new TValue(DateTime.MinValue, v), isNew: true); + } } /// @@ -203,7 +223,9 @@ public sealed class Kalman : AbstractBase out int endSamples) { if (source.Length != output.Length) + { throw new ArgumentException("Source and output must be same length.", nameof(output)); + } double x = 0.0; double p = 0.0; diff --git a/lib/filters/loess/Loess.Validation.Tests.cs b/lib/filters/loess/Loess.Validation.Tests.cs index bd41ce34..aeafa2a6 100644 --- a/lib/filters/loess/Loess.Validation.Tests.cs +++ b/lib/filters/loess/Loess.Validation.Tests.cs @@ -61,7 +61,7 @@ public class LoessValidationTests : IDisposable // Check after warmup for (int i = 10; i < 50; i++) { - Assert.Equal(expected[i], actual[i], 1e-6); + Assert.Equal(expected[i], actual[i], 1e-6); } } -} \ No newline at end of file +} diff --git a/lib/filters/loess/Loess.cs b/lib/filters/loess/Loess.cs index 97ee863b..236b6b0d 100644 --- a/lib/filters/loess/Loess.cs +++ b/lib/filters/loess/Loess.cs @@ -156,7 +156,10 @@ public sealed class Loess : AbstractBase public override TSeries Update(TSeries source) { - if (source.Count == 0) return []; + if (source.Count == 0) + { + return []; + } // Use static Calculate for performance on the whole series var resultValues = new double[source.Count]; @@ -175,13 +178,18 @@ public sealed class Loess : AbstractBase // Restore Snap history if possible if (startup > 0) { - double lastFinite = 0; - bool found = false; - for(int k=startup-1; k>=0; k--) - { - if (double.IsFinite(source.Values[k])) { lastFinite = source.Values[k]; found=true; break; } - } - if(found) { _snap.LastFiniteInput = lastFinite; _snap.HasFiniteInput = true; _pSnap = _snap; } + double lastFinite = 0; + bool found = false; + for (int k = startup - 1; k >= 0; k--) + { + if (double.IsFinite(source.Values[k])) + { + lastFinite = source.Values[k]; + found = true; + break; + } + } + if (found) { _snap.LastFiniteInput = lastFinite; _snap.HasFiniteInput = true; _pSnap = _snap; } } for (int i = startup; i < source.Count; i++) @@ -213,7 +221,10 @@ public sealed class Loess : AbstractBase for (int i = 0; i < period; i++) { double dist = Math.Abs(i - halfWindow) / bandwidth; - if (dist >= 1.0) dist = 0.9999; + if (dist >= 1.0) + { + dist = 0.9999; + } double t = 1.0 - dist * dist * dist; double w = t * t * t; @@ -226,14 +237,21 @@ public sealed class Loess : AbstractBase } double delta = weightSum * x2Sum - xSum * xSum; - if (Math.Abs(delta) < double.Epsilon) delta = 1.0; + if (Math.Abs(delta) < double.Epsilon) + { + delta = 1.0; + } double targetX = -halfWindow; for (int i = 0; i < period; i++) { double dist = Math.Abs(i - halfWindow) / bandwidth; - if (dist >= 1.0) dist = 0.9999; + if (dist >= 1.0) + { + dist = 0.9999; + } + double t = 1.0 - dist * dist * dist; double w = t * t * t; double xi = i - halfWindow; @@ -252,7 +270,10 @@ public sealed class Loess : AbstractBase { // a and b expected to be same length (slice called in Update ensures this) int length = a.Length; - if (length == 0) return 0; + if (length == 0) + { + return 0; + } int i = 0; double sum = 0; @@ -296,10 +317,14 @@ public sealed class Loess : AbstractBase public static void Calculate(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; diff --git a/lib/filters/notch/Notch.Quantower.Tests.cs b/lib/filters/notch/Notch.Quantower.Tests.cs index 319a4ce4..fdc3572b 100644 --- a/lib/filters/notch/Notch.Quantower.Tests.cs +++ b/lib/filters/notch/Notch.Quantower.Tests.cs @@ -291,4 +291,4 @@ public class NotchIndicatorTests $"Value at index {i} should be finite"); } } -} \ No newline at end of file +} diff --git a/lib/filters/notch/Notch.Tests.cs b/lib/filters/notch/Notch.Tests.cs index 4f00d3ca..a5f19947 100644 --- a/lib/filters/notch/Notch.Tests.cs +++ b/lib/filters/notch/Notch.Tests.cs @@ -74,7 +74,7 @@ public class NotchTests double output = 0; // Warmup to stabilize (IIR transient) - for(int i=0; i<100; i++) + for (int i = 0; i < 100; i++) { output = notch.Update(new TValue(DateTime.UtcNow, input)).Value; } @@ -101,7 +101,7 @@ public class NotchTests if (i > 50) // ignore transient { - maxAmp = Math.Max(maxAmp, Math.Abs(outVal)); + maxAmp = Math.Max(maxAmp, Math.Abs(outVal)); } } @@ -109,4 +109,4 @@ public class NotchTests // With Q=5, it should be very small. Assert.True(maxAmp < 0.1, $"Amplitude {maxAmp} should be attenuated ( < 0.1 )"); } -} \ No newline at end of file +} diff --git a/lib/filters/notch/Notch.Validation.Tests.cs b/lib/filters/notch/Notch.Validation.Tests.cs index 9ec9f36b..dc35faef 100644 --- a/lib/filters/notch/Notch.Validation.Tests.cs +++ b/lib/filters/notch/Notch.Validation.Tests.cs @@ -40,10 +40,10 @@ public class NotchValidationTests double[] expected = { 0.5, 0.0, 0.0, 0.0, 0.0, 0.0 }; - for(int i=0; i double.Epsilon) + { result[i] = val / wSum; + } else if (wSum <= double.Epsilon && wSum > -double.Epsilon) + { result[i] = double.NaN; + } else // Negative or small sum fallback usually just NaN or raw + { result[i] = ((i + 1) < adjPeriod) ? source[i] : double.NaN; // Match Sgf.cs partial window fallback logic roughly + } if (wSum <= double.Epsilon) { diff --git a/lib/filters/sgf/Sgf.cs b/lib/filters/sgf/Sgf.cs index eacdf01c..9ccee9be 100644 --- a/lib/filters/sgf/Sgf.cs +++ b/lib/filters/sgf/Sgf.cs @@ -129,9 +129,13 @@ public sealed class Sgf : AbstractBase } if (wSum > double.Epsilon) + { result /= wSum; + } else + { result = input.Value; + } } else { @@ -166,7 +170,10 @@ public sealed class Sgf : AbstractBase public override TSeries Update(TSeries source) { - if (source.Count == 0) return []; + if (source.Count == 0) + { + return []; + } var resultValues = new double[source.Count]; Calculate(source.Values, resultValues, _period, _polyOrder); diff --git a/lib/filters/ssf/Ssf.cs b/lib/filters/ssf/Ssf.cs index 703e2ad5..f4994e38 100644 --- a/lib/filters/ssf/Ssf.cs +++ b/lib/filters/ssf/Ssf.cs @@ -40,7 +40,9 @@ public sealed class Ssf : AbstractBase public Ssf(int period) { if (period <= 0) + { throw new ArgumentException("Period must be greater than 0", nameof(period)); + } // Use high precision constants // Note: Some implementations (like Ooples/PineScript) use 1.414 * 3.14159 which causes divergence @@ -88,7 +90,10 @@ public sealed class Ssf : AbstractBase public override void Prime(ReadOnlySpan source, TimeSpan? step = null) { - if (source.Length == 0) return; + if (source.Length == 0) + { + return; + } Reset(); @@ -126,9 +131,13 @@ public sealed class Ssf : AbstractBase { double val = source[i]; if (double.IsFinite(val)) + { _state.LastValidValue = val; + } else + { val = _state.LastValidValue; + } double ssf = (_state.Count < 4) ? val @@ -142,7 +151,9 @@ public sealed class Ssf : AbstractBase } if (_state.Count >= WarmupPeriod) + { _state.IsHot = true; + } Last = new TValue(DateTime.MinValue, _state.Ssf1); @@ -190,9 +201,15 @@ public sealed class Ssf : AbstractBase _state.Ssf1 = ssf; _state.PrevInput = val; - if (isNew) _state.Count++; + if (isNew) + { + _state.Count++; + } + if (!_state.IsHot && _state.Count >= WarmupPeriod) + { _state.IsHot = true; + } Last = new TValue(input.Time, ssf); PubEvent(Last, isNew); @@ -201,7 +218,10 @@ public sealed class Ssf : AbstractBase public override TSeries Update(TSeries source) { - if (source.Count == 0) return []; + if (source.Count == 0) + { + return []; + } int len = source.Count; var t = new List(len); @@ -269,9 +289,13 @@ public sealed class Ssf : AbstractBase { double val = source[i]; if (double.IsFinite(val)) + { state.LastValidValue = val; + } else + { val = state.LastValidValue; + } double ssf = (state.Count < 4) ? val @@ -286,7 +310,9 @@ public sealed class Ssf : AbstractBase } if (!state.IsHot && state.Count >= warmupPeriod) + { state.IsHot = true; + } } public static (TSeries Results, Ssf Indicator) Calculate(TSeries source, int period) @@ -300,7 +326,9 @@ public sealed class Ssf : AbstractBase public static void Calculate(ReadOnlySpan source, Span output, int period) { if (period <= 0) + { throw new ArgumentException("Period must be greater than 0", nameof(period)); + } double sqrt2_pi = Math.Sqrt(2) * Math.PI; double arg = sqrt2_pi / period; @@ -311,9 +339,14 @@ public sealed class Ssf : AbstractBase double c1 = 1.0 - c2 - c3; if (source.Length != output.Length) + { throw new ArgumentException("Source and output must have the same length", nameof(output)); + } - if (source.Length == 0) return; + if (source.Length == 0) + { + return; + } var state = State.New(); diff --git a/lib/filters/usf/Usf.Quantower.cs b/lib/filters/usf/Usf.Quantower.cs index f6ceb7d0..b3bd973a 100644 --- a/lib/filters/usf/Usf.Quantower.cs +++ b/lib/filters/usf/Usf.Quantower.cs @@ -48,7 +48,9 @@ public sealed class UsfIndicator : Indicator, IWatchlistIndicator protected override void OnUpdate(UpdateArgs args) { if (args.Reason != UpdateReason.NewBar && args.Reason != UpdateReason.HistoricalBar) + { return; + } var item = HistoricalData[Count - 1, SeekOriginHistory.Begin]; TValue result = _ma.Update(new TValue(item.TimeLeft.Ticks, _priceSelector(item)), args.IsNewBar()); diff --git a/lib/filters/usf/Usf.Tests.cs b/lib/filters/usf/Usf.Tests.cs index d8ef2b8a..5a20b257 100644 --- a/lib/filters/usf/Usf.Tests.cs +++ b/lib/filters/usf/Usf.Tests.cs @@ -413,7 +413,9 @@ public class UsfTests var gbm = new GBM(startPrice: 100, mu: 0.05, sigma: 0.2, seed: 42); for (int i = 0; i < source.Length; i++) + { source[i] = gbm.Next().Close; + } // Warm up Usf.Calculate(source.AsSpan(), output.AsSpan(), 100); @@ -509,7 +511,9 @@ public class UsfTests { var series = new TSeries(); for (int i = 1; i <= 10; i++) + { series.Add(DateTime.UtcNow, i * 10); + } var (results, indicator) = Usf.Calculate(series, 5); diff --git a/lib/filters/usf/Usf.Validation.Tests.cs b/lib/filters/usf/Usf.Validation.Tests.cs index f3423a1c..d8603f31 100644 --- a/lib/filters/usf/Usf.Validation.Tests.cs +++ b/lib/filters/usf/Usf.Validation.Tests.cs @@ -209,7 +209,11 @@ public sealed class UsfValidationTests : IDisposable private static double CalculateVariance(List values) { - if (values.Count == 0) return 0; + if (values.Count == 0) + { + return 0; + } + double mean = values.Average(); return values.Sum(v => (v - mean) * (v - mean)) / values.Count; } diff --git a/lib/filters/usf/Usf.cs b/lib/filters/usf/Usf.cs index 1424c0e5..6979bded 100644 --- a/lib/filters/usf/Usf.cs +++ b/lib/filters/usf/Usf.cs @@ -42,7 +42,9 @@ public sealed class Usf : AbstractBase public Usf(int period) { if (period <= 0) + { throw new ArgumentException("Period must be greater than 0", nameof(period)); + } double sqrt2_pi = Math.Sqrt(2) * Math.PI; double arg = sqrt2_pi / period; @@ -90,7 +92,10 @@ public sealed class Usf : AbstractBase public override void Prime(ReadOnlySpan source, TimeSpan? step = null) { - if (source.Length == 0) return; + if (source.Length == 0) + { + return; + } Reset(); @@ -117,9 +122,13 @@ public sealed class Usf : AbstractBase { double val = source[i]; if (double.IsFinite(val)) + { _state.LastValidValue = val; + } else + { val = _state.LastValidValue; + } double usf = (_state.Count < 4) ? val @@ -136,7 +145,9 @@ public sealed class Usf : AbstractBase } if (_state.Count >= WarmupPeriod) + { _state.IsHot = true; + } Last = new TValue(DateTime.MinValue, _state.Usf1); @@ -191,9 +202,15 @@ public sealed class Usf : AbstractBase _state.PrevInput2 = _state.PrevInput1; _state.PrevInput1 = val; - if (isNew && !initialized) _state.Count++; + if (isNew && !initialized) + { + _state.Count++; + } + if (!_state.IsHot && _state.Count >= WarmupPeriod) + { _state.IsHot = true; + } Last = new TValue(input.Time, usf); PubEvent(Last, isNew); @@ -202,7 +219,10 @@ public sealed class Usf : AbstractBase public override TSeries Update(TSeries source) { - if (source.Count == 0) return []; + if (source.Count == 0) + { + return []; + } int len = source.Count; var t = new List(len); @@ -266,9 +286,13 @@ public sealed class Usf : AbstractBase { double val = source[i]; if (double.IsFinite(val)) + { state.LastValidValue = val; + } else + { val = state.LastValidValue; + } double usf = (state.Count < 4) ? val @@ -286,7 +310,9 @@ public sealed class Usf : AbstractBase } if (!state.IsHot && state.Count >= warmupPeriod) + { state.IsHot = true; + } } public static (TSeries Results, Usf Indicator) Calculate(TSeries source, int period) @@ -300,7 +326,9 @@ public sealed class Usf : AbstractBase public static void Calculate(ReadOnlySpan source, Span output, int period) { if (period <= 0) + { throw new ArgumentException("Period must be greater than 0", nameof(period)); + } double sqrt2_pi = Math.Sqrt(2) * Math.PI; double arg = sqrt2_pi / period; @@ -311,9 +339,14 @@ public sealed class Usf : AbstractBase double c1 = (1.0 + c2 - c3) / 4.0; if (source.Length != output.Length) + { throw new ArgumentException("Source and output must have the same length", nameof(output)); + } - if (source.Length == 0) return; + if (source.Length == 0) + { + return; + } var state = State.New(); diff --git a/lib/filters/wiener/Wiener.Quantower.Tests.cs b/lib/filters/wiener/Wiener.Quantower.Tests.cs index 617a8df9..daa29dc1 100644 --- a/lib/filters/wiener/Wiener.Quantower.Tests.cs +++ b/lib/filters/wiener/Wiener.Quantower.Tests.cs @@ -137,4 +137,4 @@ public class WienerIndicatorTests Assert.Equal(20, indicator.Period); Assert.Equal(5, indicator.SmoothPeriod); } -} \ No newline at end of file +} diff --git a/lib/filters/wiener/Wiener.Validation.Tests.cs b/lib/filters/wiener/Wiener.Validation.Tests.cs index 52eeece9..4dd46098 100644 --- a/lib/filters/wiener/Wiener.Validation.Tests.cs +++ b/lib/filters/wiener/Wiener.Validation.Tests.cs @@ -24,7 +24,11 @@ public class WienerValidationTests : IDisposable protected virtual void Dispose(bool disposing) { - if (_disposed) return; + if (_disposed) + { + return; + } + if (disposing) { _testData?.Dispose(); @@ -182,4 +186,4 @@ public class WienerValidationTests : IDisposable } _output.WriteLine("Span mode successfully validated against reference implementation"); } -} \ No newline at end of file +} diff --git a/lib/forecasts/afirma/Afirma.Tests.cs b/lib/forecasts/afirma/Afirma.Tests.cs index 921426eb..ac4eeeb9 100644 --- a/lib/forecasts/afirma/Afirma.Tests.cs +++ b/lib/forecasts/afirma/Afirma.Tests.cs @@ -438,7 +438,9 @@ public class AfirmaTests var gbm = new GBM(startPrice: 100, mu: 0.05, sigma: 0.2, seed: 42); for (int i = 0; i < source.Length; i++) + { source[i] = gbm.Next().Close; + } // Warm up Afirma.Batch(source.AsSpan(), output.AsSpan(), 10); @@ -567,7 +569,10 @@ public class AfirmaTests public void Afirma_Calculate_ReturnsCorrectResultsAndHotIndicator() { var series = new TSeries(); - for (int i = 1; i <= 10; i++) series.Add(DateTime.UtcNow, i * 10); + for (int i = 1; i <= 10; i++) + { + series.Add(DateTime.UtcNow, i * 10); + } var (results, indicator) = Afirma.Calculate(series, 5); diff --git a/lib/forecasts/afirma/Afirma.cs b/lib/forecasts/afirma/Afirma.cs index 2882520e..4381eb46 100644 --- a/lib/forecasts/afirma/Afirma.cs +++ b/lib/forecasts/afirma/Afirma.cs @@ -77,7 +77,9 @@ public sealed class Afirma : AbstractBase public Afirma(int period, WindowType window = WindowType.BlackmanHarris, bool leastSquares = false) { if (period < 1) + { throw new ArgumentException("Period must be at least 1", nameof(period)); + } _period = period; _window = window; @@ -133,7 +135,10 @@ public sealed class Afirma : AbstractBase /// public override void Prime(ReadOnlySpan source, TimeSpan? step = null) { - if (source.Length == 0) return; + if (source.Length == 0) + { + return; + } // Reset state _buffer.Clear(); @@ -185,7 +190,10 @@ public sealed class Afirma : AbstractBase if (double.IsFinite(input)) { if (updateState) + { _state.LastValidValue = input; + } + return input; } return _state.LastValidValue; @@ -220,7 +228,10 @@ public sealed class Afirma : AbstractBase public override TSeries Update(TSeries source) { - if (source.Count == 0) return []; + if (source.Count == 0) + { + return []; + } int len = source.Count; var t = new List(len); @@ -244,7 +255,10 @@ public sealed class Afirma : AbstractBase private double CalculateAfirma() { int count = _buffer.Count; - if (count == 0) return double.NaN; + if (count == 0) + { + return double.NaN; + } double result; @@ -367,9 +381,14 @@ public sealed class Afirma : AbstractBase double kTwoPiDivP = k * twoPiDivP; double coef = a0 + a1 * Math.Cos(kTwoPiDivP); if (Math.Abs(a2) > 1e-9) + { coef += a2 * Math.Cos(2.0 * kTwoPiDivP); + } + if (Math.Abs(a3) > 1e-9) + { coef += a3 * Math.Cos(3.0 * kTwoPiDivP); + } _weights[k] = coef; wsum += coef; @@ -394,12 +413,20 @@ public sealed class Afirma : AbstractBase public static void Batch(ReadOnlySpan source, Span output, int period, WindowType window = WindowType.BlackmanHarris, bool leastSquares = false) { 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; + if (len == 0) + { + return; + } // If leastSquares is enabled, use standard Update loop via object or specialized loop. // Implementing LS efficiently in Batch/Span is complex because of regression in inner loop. @@ -425,115 +452,144 @@ public sealed class Afirma : AbstractBase try { - // Pre-calculate weights (Static version of CalculateWeights) - // ... (Copy of weights calc logic) - double a0 = 0.35875, a1 = -0.48829, a2 = 0.14128, a3 = -0.01168; - if (window == WindowType.Hanning) { a0 = 0.50; a1 = -0.50; a2 = 0.0; a3 = 0.0; } - else if (window == WindowType.Hamming) { a0 = 0.54; a1 = -0.46; a2 = 0.0; a3 = 0.0; } - else if (window == WindowType.Blackman) { a0 = 0.42; a1 = -0.50; a2 = 0.08; a3 = 0.0; } - else if (window == WindowType.Rectangular) { a0 = 1.0; a1 = 0.0; a2 = 0.0; a3 = 0.0; } + // Pre-calculate weights (Static version of CalculateWeights) + // ... (Copy of weights calc logic) + double a0 = 0.35875, a1 = -0.48829, a2 = 0.14128, a3 = -0.01168; + if (window == WindowType.Hanning) { a0 = 0.50; a1 = -0.50; a2 = 0.0; a3 = 0.0; } + else if (window == WindowType.Hamming) { a0 = 0.54; a1 = -0.46; a2 = 0.0; a3 = 0.0; } + else if (window == WindowType.Blackman) { a0 = 0.42; a1 = -0.50; a2 = 0.08; a3 = 0.0; } + else if (window == WindowType.Rectangular) { a0 = 1.0; a1 = 0.0; a2 = 0.0; a3 = 0.0; } - double twoPiDivP = 2.0 * Math.PI / period; - for (int k = 0; k < period; k++) - { - double kTwoPiDivP = k * twoPiDivP; - double coef = a0 + a1 * Math.Cos(kTwoPiDivP); - if (Math.Abs(a2) > 1e-9) coef += a2 * Math.Cos(2.0 * kTwoPiDivP); - if (Math.Abs(a3) > 1e-9) coef += a3 * Math.Cos(3.0 * kTwoPiDivP); - weights[k] = coef; - } - - double lastValid = double.NaN; - for (int k = 0; k < len; k++) - if (double.IsFinite(source[k])) { lastValid = source[k]; break; } - - int bufferIndex = 0; - int bufferCount = 0; - - for (int i = 0; i < len; i++) - { - double val = source[i]; - if (double.IsFinite(val)) lastValid = val; else val = lastValid; - - buffer[bufferIndex] = val; - bufferIndex = (bufferIndex + 1) % period; - if (bufferCount < period) bufferCount++; - - // Base AFIRMA (WMA) - double result = 0.0; - double effectiveWeightSum = 0.0; - int readIndex = (bufferIndex - bufferCount + period) % period; - - for (int k = 0; k < bufferCount; k++) + double twoPiDivP = 2.0 * Math.PI / period; + for (int k = 0; k < period; k++) { - // Match Streaming: weights[k] corresponds to Oldest + k - int idx = (readIndex + k) % period; - result = Math.FusedMultiplyAdd(buffer[idx], weights[k], result); - effectiveWeightSum += weights[k]; - } - output[i] = effectiveWeightSum > 0 ? result / effectiveWeightSum : val; - - // Least Squares Path - if (leastSquares && bufferCount > 2) - { - int n = Math.Min((bufferCount - 1) / 2, 50); - if (n >= 2) + double kTwoPiDivP = k * twoPiDivP; + double coef = a0 + a1 * Math.Cos(kTwoPiDivP); + if (Math.Abs(a2) > 1e-9) { - double sx = 0.0, sx2 = 0.0, sy = 0.0, sxy = 0.0; - double dn = (double)n; - sx = (dn - 1.0) * dn * 0.5; - sx2 = (dn - 1.0) * dn * (2.0 * dn - 1.0) / 6.0; + coef += a2 * Math.Cos(2.0 * kTwoPiDivP); + } - for (int j = 0; j < n; j++) + if (Math.Abs(a3) > 1e-9) + { + coef += a3 * Math.Cos(3.0 * kTwoPiDivP); + } + + weights[k] = coef; + } + + double lastValid = double.NaN; + for (int k = 0; k < len; k++) + { + if (double.IsFinite(source[k])) + { + lastValid = source[k]; + break; + } + } + + int bufferIndex = 0; + int bufferCount = 0; + + for (int i = 0; i < len; i++) + { + double val = source[i]; + if (double.IsFinite(val)) + { + lastValid = val; + } + else + { + val = lastValid; + } + + buffer[bufferIndex] = val; + bufferIndex = (bufferIndex + 1) % period; + if (bufferCount < period) + { + bufferCount++; + } + + // Base AFIRMA (WMA) + double result = 0.0; + double effectiveWeightSum = 0.0; + int readIndex = (bufferIndex - bufferCount + period) % period; + + for (int k = 0; k < bufferCount; k++) + { + // Match Streaming: weights[k] corresponds to Oldest + k + int idx = (readIndex + k) % period; + result = Math.FusedMultiplyAdd(buffer[idx], weights[k], result); + effectiveWeightSum += weights[k]; + } + output[i] = effectiveWeightSum > 0 ? result / effectiveWeightSum : val; + + // Least Squares Path + if (leastSquares && bufferCount > 2) + { + int n = Math.Min((bufferCount - 1) / 2, 50); + if (n >= 2) { - // lag j - int idx = (readIndex + bufferCount - 1 - j + period) % period; - double v = buffer[idx]; - sy += v; - sxy += j * v; - } + double sx = 0.0, sx2 = 0.0, sy = 0.0, sxy = 0.0; + double dn = (double)n; + sx = (dn - 1.0) * dn * 0.5; + sx2 = (dn - 1.0) * dn * (2.0 * dn - 1.0) / 6.0; - double denom = dn * sx2 - sx * sx; - if (Math.Abs(denom) > 1e-10) - { - double slope = (dn * sxy - sx * sy) / denom; - double intercept = (sy - slope * sx) / dn; - - double lsSum = 0.0; - double lsCount = 0.0; - - for (int j = 0; j < bufferCount; j++) + for (int j = 0; j < n; j++) { - // lag j - double v_ls; - if (j < n) - { - v_ls = intercept + slope * j; - } - else - { - int idx = (readIndex + bufferCount - 1 - j + period) % period; - v_ls = buffer[idx]; - } - lsSum += v_ls; - lsCount++; + // lag j + int idx = (readIndex + bufferCount - 1 - j + period) % period; + double v = buffer[idx]; + sy += v; + sxy += j * v; } - if (lsCount > 0) + + double denom = dn * sx2 - sx * sx; + if (Math.Abs(denom) > 1e-10) { - output[i] = lsSum / lsCount; + double slope = (dn * sxy - sx * sy) / denom; + double intercept = (sy - slope * sx) / dn; + + double lsSum = 0.0; + double lsCount = 0.0; + + for (int j = 0; j < bufferCount; j++) + { + // lag j + double v_ls; + if (j < n) + { + v_ls = intercept + slope * j; + } + else + { + int idx = (readIndex + bufferCount - 1 - j + period) % period; + v_ls = buffer[idx]; + } + lsSum += v_ls; + lsCount++; + } + if (lsCount > 0) + { + output[i] = lsSum / lsCount; + } } } } } } - } finally { // Return rented arrays to the pool if (rentedWeights != null) + { ArrayPool.Shared.Return(rentedWeights); + } + if (rentedBuffer != null) + { ArrayPool.Shared.Return(rentedBuffer); + } } } diff --git a/lib/momentum/bop/Bop.Tests.cs b/lib/momentum/bop/Bop.Tests.cs index f1059aa2..028c1410 100644 --- a/lib/momentum/bop/Bop.Tests.cs +++ b/lib/momentum/bop/Bop.Tests.cs @@ -219,7 +219,10 @@ public class BopTests // 3. Streaming Mode var streamBop = new Bop(); for (int i = 0; i < bars.Count; i++) + { streamBop.Update(bars[i]); + } + double streamResult = streamBop.Last.Value; Assert.Equal(expected, spanResult, 9); diff --git a/lib/momentum/bop/Bop.cs b/lib/momentum/bop/Bop.cs index 106751d1..41371591 100644 --- a/lib/momentum/bop/Bop.cs +++ b/lib/momentum/bop/Bop.cs @@ -112,7 +112,9 @@ public sealed class Bop : ITValuePublisher { int len = Math.Min(open.Length, Math.Min(high.Length, Math.Min(low.Length, close.Length))); if (destination.Length < len) + { len = destination.Length; + } int i = 0; if (Vector.IsHardwareAccelerated && len >= Vector.Count) @@ -162,7 +164,10 @@ public sealed class Bop : ITValuePublisher [MethodImpl(MethodImplOptions.AggressiveInlining)] public static TSeries Batch(TBarSeries source) { - if (source.Count == 0) return new TSeries([], []); + if (source.Count == 0) + { + return new TSeries([], []); + } var len = source.Count; diff --git a/lib/momentum/cfb/Cfb.Tests.cs b/lib/momentum/cfb/Cfb.Tests.cs index 72f44a90..1d939152 100644 --- a/lib/momentum/cfb/Cfb.Tests.cs +++ b/lib/momentum/cfb/Cfb.Tests.cs @@ -133,7 +133,9 @@ public class CfbTests var bars = gbm.Fetch(100, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1)); for (int i = 0; i < 50; i++) + { cfb.Update(new TValue(bars.Close.Times[i], bars.Close.Values[i])); + } var originalValue = cfb.Last; @@ -155,7 +157,9 @@ public class CfbTests var bars = gbm.Fetch(50, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1)); for (int i = 0; i < 30; i++) + { cfb.Update(new TValue(bars.Close.Times[i], bars.Close.Values[i])); + } var result = cfb.Update(new TValue(DateTime.UtcNow, double.NaN)); Assert.True(double.IsFinite(result.Value)); @@ -169,7 +173,9 @@ public class CfbTests var bars = gbm.Fetch(50, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1)); for (int i = 0; i < 30; i++) + { cfb.Update(new TValue(bars.Close.Times[i], bars.Close.Values[i])); + } var result = cfb.Update(new TValue(DateTime.UtcNow, double.PositiveInfinity)); Assert.True(double.IsFinite(result.Value)); @@ -193,7 +199,10 @@ public class CfbTests // 3. Streaming Mode var streamCfb = new Cfb(); for (int i = 0; i < bars.Count; i++) + { streamCfb.Update(new TValue(bars.Close.Times[i], bars.Close.Values[i])); + } + double streamResult = streamCfb.Last.Value; Assert.Equal(expected, spanResult, 9); diff --git a/lib/momentum/cfb/Cfb.cs b/lib/momentum/cfb/Cfb.cs index 0f9f73e2..027733ec 100644 --- a/lib/momentum/cfb/Cfb.cs +++ b/lib/momentum/cfb/Cfb.cs @@ -114,7 +114,9 @@ public sealed class Cfb : ITValuePublisher, IDisposable public void Dispose() { if (_disposed) + { return; + } _disposed = true; @@ -211,10 +213,16 @@ public sealed class Cfb : ITValuePublisher, IDisposable _runningSums[i] += vol - volToRemove; - if (count <= L) continue; + if (count <= L) + { + continue; + } // Safety check for very small volatility - if (_runningSums[i] < 1e-12) continue; + if (_runningSums[i] < 1e-12) + { + continue; + } // Net move over L bars // Price at Count-1 is current. Price at Count-1-L is L bars ago. @@ -240,11 +248,17 @@ public sealed class Cfb : ITValuePublisher, IDisposable cfb = (_state.PrevCfb > 1.0) ? _state.PrevCfb * 0.5 : 1.0; } - if (cfb < 1.0) cfb = 1.0; + if (cfb < 1.0) + { + cfb = 1.0; + } // Round to nearest integer cfb = Math.Round(cfb); - if (cfb < 1.0) cfb = 1.0; + if (cfb < 1.0) + { + cfb = 1.0; + } _state.PrevCfb = cfb; @@ -255,7 +269,10 @@ public sealed class Cfb : ITValuePublisher, IDisposable public TSeries Update(TSeries source) { - if (source.Count == 0) return []; + if (source.Count == 0) + { + return []; + } int len = source.Count; var t = new List(len); @@ -304,10 +321,14 @@ public sealed class Cfb : ITValuePublisher, IDisposable { int len = source.Length; if (len == 0) + { return; + } if (output.Length != len) + { throw new ArgumentException("Source and output must have the same length", nameof(output)); + } // Setup lengths - use cached default or sort a copy of user-provided int[] lens; @@ -378,10 +399,16 @@ public sealed class Cfb : ITValuePublisher, IDisposable runningSums[k] -= vol[i - L]; } - if (i < L) continue; + if (i < L) + { + continue; + } double totalMove = runningSums[k]; - if (totalMove < 1e-12) continue; + if (totalMove < 1e-12) + { + continue; + } double netMove = Math.Abs(price - source[i - L]); double ratio = netMove / totalMove; @@ -403,12 +430,19 @@ public sealed class Cfb : ITValuePublisher, IDisposable cfb = (prevCfb > 1.0) ? prevCfb * 0.5 : 1.0; } - if (cfb < 1.0) cfb = 1.0; + if (cfb < 1.0) + { + cfb = 1.0; + } + cfb = Math.Round(cfb); - if (cfb < 1.0) cfb = 1.0; + if (cfb < 1.0) + { + cfb = 1.0; + } output[i] = cfb; prevCfb = cfb; } } -} \ No newline at end of file +} diff --git a/lib/momentum/macd/Macd.Quantower.Tests.cs b/lib/momentum/macd/Macd.Quantower.Tests.cs index 37e6dae4..42c8289d 100644 --- a/lib/momentum/macd/Macd.Quantower.Tests.cs +++ b/lib/momentum/macd/Macd.Quantower.Tests.cs @@ -77,7 +77,7 @@ public class MacdIndicatorTests // Add historical data var now = DateTime.UtcNow; - for(int i=0; i<10; i++) + for (int i = 0; i < 10; i++) { indicator.HistoricalData.AddBar(now.AddMinutes(i), 100, 105, 95, 100 + i); } @@ -85,7 +85,7 @@ public class MacdIndicatorTests // Process updates var args = new UpdateArgs(UpdateReason.HistoricalBar); - for(int i=0; i<10; i++) + for (int i = 0; i < 10; i++) { indicator.ProcessUpdate(args); } diff --git a/lib/momentum/macd/Macd.Tests.cs b/lib/momentum/macd/Macd.Tests.cs index 679926d5..b04d9933 100644 --- a/lib/momentum/macd/Macd.Tests.cs +++ b/lib/momentum/macd/Macd.Tests.cs @@ -38,7 +38,9 @@ public class MacdTests var series = gbm.Fetch(50, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1)); for (int i = 0; i < 49; i++) + { macd.Update(series.Close[i], isNew: true); + } var val1 = macd.Update(series.Close[49], isNew: true); var val2 = macd.Update(new TValue(DateTime.UtcNow, series.Close[49].Value + 1), isNew: true); @@ -54,7 +56,9 @@ public class MacdTests var series = gbm.Fetch(50, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1)); for (int i = 0; i < 49; i++) + { macd.Update(series.Close[i]); + } var val1 = macd.Update(series.Close[49], isNew: true); var val2 = macd.Update(new TValue(series.Close[49].Time, series.Close[49].Value + 5), isNew: false); @@ -71,7 +75,9 @@ public class MacdTests var series = gbm.Fetch(100, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1)); for (int i = 0; i < 50; i++) + { macd.Update(series.Close[i]); + } var originalValue = macd.Last; @@ -93,7 +99,9 @@ public class MacdTests var series = gbm.Fetch(50, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1)); for (int i = 0; i < series.Count; i++) + { macd.Update(series.Close[i]); + } macd.Reset(); @@ -113,7 +121,10 @@ public class MacdTests for (int i = 0; i < series.Count; i++) { macd.Update(series.Close[i]); - if (i >= 40) break; + if (i >= 40) + { + break; + } } Assert.True(macd.IsHot); @@ -127,7 +138,9 @@ public class MacdTests var series = gbm.Fetch(50, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1)); for (int i = 0; i < 40; i++) + { macd.Update(series.Close[i]); + } var result = macd.Update(new TValue(DateTime.UtcNow, double.NaN)); Assert.True(double.IsFinite(result.Value)); @@ -141,7 +154,9 @@ public class MacdTests var series = gbm.Fetch(50, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1)); for (int i = 0; i < 40; i++) + { macd.Update(series.Close[i]); + } var result = macd.Update(new TValue(DateTime.UtcNow, double.PositiveInfinity)); Assert.True(double.IsFinite(result.Value)); @@ -213,14 +228,20 @@ public class MacdTests // 3. Streaming Mode var streamMacd = new Macd(12, 26, 9); for (int i = 0; i < series.Count; i++) + { streamMacd.Update(series.Close[i]); + } + double streamResult = streamMacd.Last.Value; // 4. Eventing Mode var pubSource = new TSeries(); var eventMacd = new Macd(pubSource, 12, 26, 9); for (int i = 0; i < series.Count; i++) + { pubSource.Add(series.Close[i]); + } + double eventResult = eventMacd.Last.Value; Assert.Equal(expected, spanResult, 9); @@ -239,4 +260,4 @@ public class MacdTests Assert.Throws(() => Macd.Calculate(source, output, 0, 26)); Assert.Throws(() => Macd.Calculate(source, output, 12, 0)); } -} \ No newline at end of file +} diff --git a/lib/momentum/macd/Macd.cs b/lib/momentum/macd/Macd.cs index 131f9c9f..88174a3d 100644 --- a/lib/momentum/macd/Macd.cs +++ b/lib/momentum/macd/Macd.cs @@ -108,7 +108,10 @@ public sealed class Macd : ITValuePublisher, IDisposable public TSeries Update(TSeries source) { - if (source.Count == 0) return []; + if (source.Count == 0) + { + return []; + } var len = source.Count; var t = new List(len); @@ -142,7 +145,9 @@ public sealed class Macd : ITValuePublisher, IDisposable public static void Calculate(ReadOnlySpan source, Span destination, int fastPeriod = 12, int slowPeriod = 26) { if (source.Length != destination.Length) + { throw new ArgumentException("Source and destination must be same length", nameof(destination)); + } int len = source.Length; double[] fastBuffer = ArrayPool.Shared.Rent(len); @@ -164,4 +169,4 @@ public sealed class Macd : ITValuePublisher, IDisposable ArrayPool.Shared.Return(slowBuffer); } } -} \ No newline at end of file +} diff --git a/lib/momentum/roc/Roc.Quantower.cs b/lib/momentum/roc/Roc.Quantower.cs index 2083719c..1f68a9c0 100644 --- a/lib/momentum/roc/Roc.Quantower.cs +++ b/lib/momentum/roc/Roc.Quantower.cs @@ -45,7 +45,10 @@ public class RocIndicator : Indicator, IWatchlistIndicator protected override void OnUpdate(UpdateArgs args) { - if (_roc == null || _selector == null) return; + if (_roc == null || _selector == null) + { + return; + } var item = HistoricalData[0, SeekOriginHistory.End]; double value = _selector(item); @@ -64,11 +67,18 @@ public class RocIndicator : Indicator, IWatchlistIndicator double roc = _roc.Last.Value; Color color; if (roc > 0) + { color = Color.Green; + } else if (roc < 0) + { color = Color.Red; + } else + { color = Color.Gray; + } + LinesSeries[0].SetMarker(0, new IndicatorLineMarker(color)); } } diff --git a/lib/momentum/roc/Roc.Tests.cs b/lib/momentum/roc/Roc.Tests.cs index 794c8ef1..34095342 100644 --- a/lib/momentum/roc/Roc.Tests.cs +++ b/lib/momentum/roc/Roc.Tests.cs @@ -373,7 +373,9 @@ public class RocTests double[] output = new double[largeSize]; for (int i = 0; i < largeSize; i++) + { source[i] = 100.0 + i * 0.1; + } // Should not throw Roc.Calculate(source, output, TestPeriod); diff --git a/lib/momentum/roc/Roc.cs b/lib/momentum/roc/Roc.cs index b13371cd..e3202b10 100644 --- a/lib/momentum/roc/Roc.cs +++ b/lib/momentum/roc/Roc.cs @@ -34,7 +34,9 @@ public sealed class Roc : AbstractBase public Roc(int period = 9) { if (period < 1) + { throw new ArgumentException("Period must be >= 1", nameof(period)); + } _period = period; _buffer = new RingBuffer(period + 1); @@ -59,9 +61,13 @@ public sealed class Roc : AbstractBase public override TValue Update(TValue input, bool isNew = true) { if (isNew) + { _p_state = _state; + } else + { _state = _p_state; + } double value = double.IsFinite(input.Value) ? input.Value : _state.LastValid; _state = new State(value); @@ -122,11 +128,19 @@ public sealed class Roc : AbstractBase public static void Calculate(ReadOnlySpan source, Span output, int period = 9) { if (source.Length == 0) + { throw new ArgumentException("Source cannot be empty", nameof(source)); + } + if (output.Length < source.Length) + { throw new ArgumentException("Output length must be >= source length", nameof(output)); + } + if (period < 1) + { throw new ArgumentException("Period must be >= 1", nameof(period)); + } for (int i = 0; i < source.Length; i++) { diff --git a/lib/momentum/rsi/Rsi.Tests.cs b/lib/momentum/rsi/Rsi.Tests.cs index bef54cda..ef56a039 100644 --- a/lib/momentum/rsi/Rsi.Tests.cs +++ b/lib/momentum/rsi/Rsi.Tests.cs @@ -76,7 +76,7 @@ public class RsiTests // Get stable state var val1 = rsi.Update(series.Close[10], isNew: true); - + // Advance with a significantly different value var nextTime = series.Close[10].Time + TimeSpan.FromMinutes(1).Ticks; var nextValue = series.Close[10].Value * 1.05; // 5% increase diff --git a/lib/momentum/rsi/Rsi.cs b/lib/momentum/rsi/Rsi.cs index 5aa7d384..5b37c2d9 100644 --- a/lib/momentum/rsi/Rsi.cs +++ b/lib/momentum/rsi/Rsi.cs @@ -34,7 +34,9 @@ public sealed class Rsi : AbstractBase public Rsi(int period = 14) { if (period <= 0) + { throw new ArgumentException("Period must be greater than 0", nameof(period)); + } _period = period; _avgGain = new Rma(period); @@ -117,7 +119,10 @@ public sealed class Rsi : AbstractBase public override TSeries Update(TSeries source) { - if (source.Count == 0) return []; + if (source.Count == 0) + { + return []; + } int len = source.Count; var t = new List(len); @@ -169,12 +174,20 @@ public sealed class Rsi : AbstractBase public static void Calculate(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 <= 0) + { throw new ArgumentException("Period must be greater than 0", nameof(period)); + } int len = source.Length; - if (len == 0) return; + if (len == 0) + { + return; + } double[] gains = System.Buffers.ArrayPool.Shared.Rent(len); double[] losses = System.Buffers.ArrayPool.Shared.Rent(len); diff --git a/lib/momentum/rsx/Rsx.cs b/lib/momentum/rsx/Rsx.cs index 1abca086..498e2681 100644 --- a/lib/momentum/rsx/Rsx.cs +++ b/lib/momentum/rsx/Rsx.cs @@ -68,7 +68,9 @@ public sealed class Rsx : ITValuePublisher public Rsx(int period) { if (period <= 0) + { throw new ArgumentException("Period must be greater than 0", nameof(period)); + } _period = period; WarmupPeriod = period; @@ -190,7 +192,10 @@ public sealed class Rsx : ITValuePublisher public TSeries Update(TSeries source) { - if (source.Count == 0) return []; + if (source.Count == 0) + { + return []; + } int len = source.Count; var t = new List(len); @@ -226,12 +231,20 @@ public sealed class Rsx : ITValuePublisher 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 <= 0) + { throw new ArgumentException("Period must be greater than 0", nameof(period)); + } int len = source.Length; - if (len == 0) return; + if (len == 0) + { + return; + } double alpha = 3.0 / (period + 2.0); double decay = 1.0 - alpha; @@ -321,4 +334,4 @@ public sealed class Rsx : ITValuePublisher _p_state = default; 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 51c1546b..5abf8cd1 100644 --- a/lib/momentum/vel/Vel.Validation.Tests.cs +++ b/lib/momentum/vel/Vel.Validation.Tests.cs @@ -17,9 +17,16 @@ public sealed class VelValidationTests : IDisposable private void Dispose(bool disposing) { - if (_disposed) return; + if (_disposed) + { + return; + } + _disposed = true; - if (disposing) _testData?.Dispose(); + if (disposing) + { + _testData?.Dispose(); + } } [Fact] diff --git a/lib/momentum/vel/Vel.cs b/lib/momentum/vel/Vel.cs index 67671f82..c0982a6e 100644 --- a/lib/momentum/vel/Vel.cs +++ b/lib/momentum/vel/Vel.cs @@ -33,7 +33,10 @@ public sealed class Vel : ITValuePublisher, IDisposable public Vel(int period) { - if (period <= 0) throw new ArgumentException("Period must be greater than 0", nameof(period)); + if (period <= 0) + { + throw new ArgumentException("Period must be greater than 0", nameof(period)); + } _pwma = new Pwma(period); _wma = new Wma(period); @@ -55,7 +58,9 @@ public sealed class Vel : ITValuePublisher, IDisposable public void Dispose() { if (_disposed) + { return; + } _disposed = true; @@ -85,7 +90,9 @@ public sealed class Vel : ITValuePublisher, IDisposable { int len = source.Count; if (len == 0) + { return []; + } var t = new List(len); var v = new List(len); @@ -115,7 +122,9 @@ public sealed class Vel : ITValuePublisher, IDisposable { int len = source.Count; if (len == 0) + { return []; + } var t = new List(len); var v = new List(len); @@ -135,7 +144,9 @@ public sealed class Vel : ITValuePublisher, IDisposable 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)); + } Span pwma = source.Length <= 1024 ? stackalloc double[source.Length] : new double[source.Length]; Span wma = source.Length <= 1024 ? stackalloc double[source.Length] : new double[source.Length]; @@ -153,4 +164,4 @@ public sealed class Vel : ITValuePublisher, IDisposable _wma.Reset(); Last = default; } -} \ No newline at end of file +} diff --git a/lib/numerics/Atan2Validation.Tests.cs b/lib/numerics/Atan2Validation.Tests.cs index dab902ca..6d44a0fc 100644 --- a/lib/numerics/Atan2Validation.Tests.cs +++ b/lib/numerics/Atan2Validation.Tests.cs @@ -34,7 +34,9 @@ public class Atan2ValidationTests private static double PineScriptAtan2(double y, double x) { if (y == 0.0 && x == 0.0) + { throw new ArgumentException("atan2: Both y and x cannot be zero", nameof(y)); + } double ay = Math.Abs(y); double ax = Math.Abs(x); @@ -50,9 +52,14 @@ public class Atan2ValidationTests } if (x < 0.0) + { angle = Math.PI - angle; + } + if (y < 0.0) + { angle = -angle; + } return angle; } @@ -303,7 +310,9 @@ public class Atan2ValidationTests // Skip origin (angle = 0 with x=1, y=0 is fine, but need to handle numerical zeros) if (Math.Abs(x) < 1e-15 && Math.Abs(y) < 1e-15) + { continue; + } double dotNet = Math.Atan2(y, x); double pine = PineScriptAtan2(y, x); @@ -348,4 +357,4 @@ public class Atan2ValidationTests Assert.True(true); // This is a documentation test } -} \ No newline at end of file +} diff --git a/lib/numerics/accel/Accel.Quantower.cs b/lib/numerics/accel/Accel.Quantower.cs index e9dc7a93..db867519 100644 --- a/lib/numerics/accel/Accel.Quantower.cs +++ b/lib/numerics/accel/Accel.Quantower.cs @@ -47,7 +47,10 @@ public class AccelIndicator : Indicator, IWatchlistIndicator protected override void OnUpdate(UpdateArgs args) { - if (_accel == null || _selector == null) return; + if (_accel == null || _selector == null) + { + return; + } var item = HistoricalData[0, SeekOriginHistory.End]; double value = _selector(item); @@ -64,7 +67,9 @@ public class AccelIndicator : Indicator, IWatchlistIndicator { value = _accel!.Last.Value; if (!double.IsFinite(value)) + { value = 0.0; + } } TValue input = new(time, value); @@ -87,8 +92,16 @@ public class AccelIndicator : Indicator, IWatchlistIndicator [MethodImpl(MethodImplOptions.AggressiveInlining)] private static IndicatorLineMarker GetMarker(double value) { - if (value > 0) return GreenMarker; - if (value < 0) return RedMarker; + if (value > 0) + { + return GreenMarker; + } + + if (value < 0) + { + return RedMarker; + } + return GrayMarker; } -} \ No newline at end of file +} diff --git a/lib/numerics/accel/Accel.Tests.cs b/lib/numerics/accel/Accel.Tests.cs index 3788ec4e..74ad5920 100644 --- a/lib/numerics/accel/Accel.Tests.cs +++ b/lib/numerics/accel/Accel.Tests.cs @@ -254,11 +254,11 @@ public class AccelTests { var source = new TSeries(); var accel = new Accel(source); - + source.Add(new TValue(DateTime.UtcNow, 10)); source.Add(new TValue(DateTime.UtcNow, 20)); source.Add(new TValue(DateTime.UtcNow, 35)); - + Assert.True(accel.IsHot); Assert.Equal(5, accel.Last.Value); // 35 - 2*20 + 10 = 5 } diff --git a/lib/numerics/accel/Accel.cs b/lib/numerics/accel/Accel.cs index 80d83bc2..f201f6b6 100644 --- a/lib/numerics/accel/Accel.cs +++ b/lib/numerics/accel/Accel.cs @@ -110,7 +110,10 @@ public sealed class Accel : AbstractBase public override TSeries Update(TSeries source) { - if (source.Count == 0) return []; + if (source.Count == 0) + { + return []; + } int len = source.Count; @@ -187,16 +190,28 @@ public sealed class Accel : AbstractBase public static void Calculate(ReadOnlySpan source, Span output) { if (source.Length != output.Length) + { throw new ArgumentException("Source and output must have the same length", nameof(output)); + } int len = source.Length; - if (len == 0) return; + if (len == 0) + { + return; + } // First two elements have insufficient history output[0] = 0.0; - if (len == 1) return; + if (len == 1) + { + return; + } + output[1] = 0.0; - if (len == 2) return; + if (len == 2) + { + return; + } int i = 2; @@ -279,9 +294,20 @@ public sealed class Accel : AbstractBase // Handle NaN/Infinity by substitution (find first finite value) double fallback = FindFinite(curr, p1, p2); - if (!double.IsFinite(curr)) curr = fallback; - if (!double.IsFinite(p1)) p1 = fallback; - if (!double.IsFinite(p2)) p2 = fallback; + if (!double.IsFinite(curr)) + { + curr = fallback; + } + + if (!double.IsFinite(p1)) + { + p1 = fallback; + } + + if (!double.IsFinite(p2)) + { + p2 = fallback; + } // accel = curr - 2*prev1 + prev2 output[i] = Math.FusedMultiplyAdd(-2.0, p1, curr + p2); @@ -291,9 +317,21 @@ public sealed class Accel : AbstractBase [MethodImpl(MethodImplOptions.AggressiveInlining)] private static double FindFinite(double a, double b, double c) { - if (double.IsFinite(a)) return a; - if (double.IsFinite(b)) return b; - if (double.IsFinite(c)) return c; + if (double.IsFinite(a)) + { + return a; + } + + if (double.IsFinite(b)) + { + return b; + } + + if (double.IsFinite(c)) + { + return c; + } + return 0.0; } } diff --git a/lib/numerics/change/Change.Quantower.cs b/lib/numerics/change/Change.Quantower.cs index d54fd594..1587c4e1 100644 --- a/lib/numerics/change/Change.Quantower.cs +++ b/lib/numerics/change/Change.Quantower.cs @@ -50,7 +50,10 @@ public class ChangeIndicator : Indicator, IWatchlistIndicator protected override void OnUpdate(UpdateArgs args) { - if (_change == null || _selector == null) return; + if (_change == null || _selector == null) + { + return; + } var item = HistoricalData[0, SeekOriginHistory.End]; double value = _selector(item); @@ -76,9 +79,21 @@ public class ChangeIndicator : Indicator, IWatchlistIndicator [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] private static IndicatorLineMarker GetMarker(double value) { - if (!double.IsFinite(value)) return GrayMarker; - if (value > 0) return GreenMarker; - if (value < 0) return RedMarker; + if (!double.IsFinite(value)) + { + return GrayMarker; + } + + if (value > 0) + { + return GreenMarker; + } + + if (value < 0) + { + return RedMarker; + } + return GrayMarker; } } diff --git a/lib/numerics/change/Change.Tests.cs b/lib/numerics/change/Change.Tests.cs index 7bd073ce..177bfe98 100644 --- a/lib/numerics/change/Change.Tests.cs +++ b/lib/numerics/change/Change.Tests.cs @@ -220,4 +220,4 @@ public class ChangeTests Assert.True(change.IsHot); Assert.NotEqual(0.0, change.Last.Value); } -} \ No newline at end of file +} diff --git a/lib/numerics/change/Change.cs b/lib/numerics/change/Change.cs index 59c2785b..aae5a886 100644 --- a/lib/numerics/change/Change.cs +++ b/lib/numerics/change/Change.cs @@ -34,7 +34,9 @@ public sealed class Change : AbstractBase public Change(int period = 1) { if (period < 1) + { throw new ArgumentException("Period must be >= 1", nameof(period)); + } _period = period; _buffer = new RingBuffer(period + 1); @@ -59,9 +61,13 @@ public sealed class Change : AbstractBase public override TValue Update(TValue input, bool isNew = true) { if (isNew) + { _p_state = _state; + } else + { _state = _p_state; + } double value = double.IsFinite(input.Value) ? input.Value : _state.LastValid; _state = new State(value); @@ -122,11 +128,19 @@ public sealed class Change : AbstractBase public static void Calculate(ReadOnlySpan source, Span output, int period = 1) { if (source.Length == 0) + { throw new ArgumentException("Source cannot be empty", nameof(source)); + } + if (output.Length < source.Length) + { throw new ArgumentException("Output length must be >= source length", nameof(output)); + } + if (period < 1) + { throw new ArgumentException("Period must be >= 1", nameof(period)); + } // Use ArrayPool for large periods to track past valid values const int StackAllocThreshold = 256; @@ -184,7 +198,9 @@ public sealed class Change : AbstractBase finally { if (pastValidRented != null) + { System.Buffers.ArrayPool.Shared.Return(pastValidRented); + } } } diff --git a/lib/numerics/exptrans/Exptrans.Quantower.cs b/lib/numerics/exptrans/Exptrans.Quantower.cs index 90241ea2..02642b9d 100644 --- a/lib/numerics/exptrans/Exptrans.Quantower.cs +++ b/lib/numerics/exptrans/Exptrans.Quantower.cs @@ -40,7 +40,10 @@ public class ExptransIndicator : Indicator, IWatchlistIndicator protected override void OnUpdate(UpdateArgs args) { - if (_exptrans == null || _selector == null) return; + if (_exptrans == null || _selector == null) + { + return; + } var item = HistoricalData[0, SeekOriginHistory.End]; double value = _selector(item); @@ -53,4 +56,4 @@ public class ExptransIndicator : Indicator, IWatchlistIndicator LinesSeries[0].SetValue(_exptrans.Last.Value, isHot, ShowColdValues); } -} \ No newline at end of file +} diff --git a/lib/numerics/exptrans/Exptrans.Tests.cs b/lib/numerics/exptrans/Exptrans.Tests.cs index b435904b..05852ee4 100644 --- a/lib/numerics/exptrans/Exptrans.Tests.cs +++ b/lib/numerics/exptrans/Exptrans.Tests.cs @@ -274,4 +274,4 @@ public class ExptransTests Assert.True(indicator.Last.Value > 0); } } -} \ No newline at end of file +} diff --git a/lib/numerics/exptrans/Exptrans.Validation.Tests.cs b/lib/numerics/exptrans/Exptrans.Validation.Tests.cs index 0c7820f7..e60a9fe8 100644 --- a/lib/numerics/exptrans/Exptrans.Validation.Tests.cs +++ b/lib/numerics/exptrans/Exptrans.Validation.Tests.cs @@ -170,4 +170,4 @@ public class ExptransValidationTests Assert.Equal(Math.Pow(expA, n), expNA, 1e-12); } -} \ No newline at end of file +} diff --git a/lib/numerics/exptrans/Exptrans.cs b/lib/numerics/exptrans/Exptrans.cs index a2810b3a..82b4c9a0 100644 --- a/lib/numerics/exptrans/Exptrans.cs +++ b/lib/numerics/exptrans/Exptrans.cs @@ -46,9 +46,13 @@ public sealed class Exptrans : AbstractBase public override TValue Update(TValue input, bool isNew = true) { if (isNew) + { _p_state = _state; + } else + { _state = _p_state; + } double value = input.Value; double result; @@ -114,9 +118,14 @@ public sealed class Exptrans : AbstractBase public static void Calculate(ReadOnlySpan source, Span output) { if (source.Length == 0) + { throw new ArgumentException("Source cannot be empty", nameof(source)); + } + if (output.Length < source.Length) + { throw new ArgumentException("Output length must be >= source length", nameof(output)); + } double lastValid = 1.0; // exp(0) = 1 diff --git a/lib/numerics/highest/Highest.Quantower.cs b/lib/numerics/highest/Highest.Quantower.cs index 964ab5d0..d626265e 100644 --- a/lib/numerics/highest/Highest.Quantower.cs +++ b/lib/numerics/highest/Highest.Quantower.cs @@ -43,7 +43,10 @@ public class HighestIndicator : Indicator, IWatchlistIndicator protected override void OnUpdate(UpdateArgs args) { - if (_highest == null || _selector == null) return; + if (_highest == null || _selector == null) + { + return; + } var item = HistoricalData[0, SeekOriginHistory.End]; double value = _selector(item); diff --git a/lib/numerics/highest/Highest.Tests.cs b/lib/numerics/highest/Highest.Tests.cs index 7de69d80..ad1a173d 100644 --- a/lib/numerics/highest/Highest.Tests.cs +++ b/lib/numerics/highest/Highest.Tests.cs @@ -297,4 +297,4 @@ public class HighestTests indicator.Update(new TValue(time.AddMinutes(5), 5.0)); Assert.Equal(9.0, indicator.Last.Value, Tolerance); } -} \ No newline at end of file +} diff --git a/lib/numerics/highest/Highest.cs b/lib/numerics/highest/Highest.cs index 414b1abe..7c2c01fe 100644 --- a/lib/numerics/highest/Highest.cs +++ b/lib/numerics/highest/Highest.cs @@ -33,7 +33,9 @@ public sealed class Highest : AbstractBase public Highest(int period) { if (period < 1) + { throw new ArgumentException("Period must be >= 1", nameof(period)); + } _period = period; _buffer = new RingBuffer(period); @@ -58,9 +60,13 @@ public sealed class Highest : AbstractBase public override TValue Update(TValue input, bool isNew = true) { if (isNew) + { _p_state = _state; + } else + { _state = _p_state; + } double value = double.IsFinite(input.Value) ? input.Value : _state.LastValid; _state = new State(value); @@ -112,11 +118,19 @@ public sealed class Highest : AbstractBase public static void Calculate(ReadOnlySpan source, Span output, int period) { if (source.Length == 0) + { throw new ArgumentException("Source cannot be empty", nameof(source)); + } + if (output.Length < source.Length) + { throw new ArgumentException("Output length must be >= source length", nameof(output)); + } + if (period < 1) + { throw new ArgumentException("Period must be >= 1", nameof(period)); + } int len = source.Length; @@ -169,7 +183,11 @@ public sealed class Highest : AbstractBase while (count > 0 && deque[head] <= i - period) { head++; - if (head >= capacity) head -= capacity; + if (head >= capacity) + { + head -= capacity; + } + count--; } @@ -177,7 +195,11 @@ public sealed class Highest : AbstractBase while (count > 0) { int backIdx = tail - 1; - if (backIdx < 0) backIdx += capacity; + if (backIdx < 0) + { + backIdx += capacity; + } + if (values[deque[backIdx]] <= value) { tail = backIdx; @@ -192,7 +214,11 @@ public sealed class Highest : AbstractBase // Add current index at tail deque[tail] = i; tail++; - if (tail >= capacity) tail -= capacity; + if (tail >= capacity) + { + tail -= capacity; + } + count++; output[i] = values[deque[head]]; @@ -201,9 +227,14 @@ public sealed class Highest : AbstractBase finally { if (rentedDeque != null) + { System.Buffers.ArrayPool.Shared.Return(rentedDeque); + } + if (rentedValues != null) + { System.Buffers.ArrayPool.Shared.Return(rentedValues); + } } } diff --git a/lib/numerics/jerk/Jerk.Quantower.Tests.cs b/lib/numerics/jerk/Jerk.Quantower.Tests.cs index 883dfd9e..95974cd9 100644 --- a/lib/numerics/jerk/Jerk.Quantower.Tests.cs +++ b/lib/numerics/jerk/Jerk.Quantower.Tests.cs @@ -184,8 +184,8 @@ public class JerkIndicatorTests var now = DateTime.UtcNow; - // Cubic trend: f(x) = x³ has third derivative = 6 - // Using f(i) = i³, the discrete third differences converge to 6 + // Cubic trend: f(x) = x³ has third derivative = 6 + // Using f(i) = i³, the discrete third differences converge to 6 for (int i = 0; i < 10; i++) { double price = 100 + i * i * i; // cubic growth @@ -194,7 +194,7 @@ public class JerkIndicatorTests } double lastJerk = indicator.LinesSeries[0].GetValue(0); - // For f(x) = x³, discrete third difference = 6 + // For f(x) = x³, discrete third difference = 6 Assert.Equal(6.0, lastJerk, 6); } @@ -217,4 +217,4 @@ public class JerkIndicatorTests double lastJerk = indicator.LinesSeries[0].GetValue(0); Assert.Equal(0, lastJerk, 6); } -} \ No newline at end of file +} diff --git a/lib/numerics/jerk/Jerk.Quantower.cs b/lib/numerics/jerk/Jerk.Quantower.cs index 3939abfa..17465dcf 100644 --- a/lib/numerics/jerk/Jerk.Quantower.cs +++ b/lib/numerics/jerk/Jerk.Quantower.cs @@ -41,7 +41,10 @@ public class JerkIndicator : Indicator, IWatchlistIndicator protected override void OnUpdate(UpdateArgs args) { - if (_jerk == null || _selector == null) return; + if (_jerk == null || _selector == null) + { + return; + } var item = HistoricalData[0, SeekOriginHistory.End]; double value = _selector(item); @@ -60,11 +63,18 @@ public class JerkIndicator : Indicator, IWatchlistIndicator double jerk = _jerk.Last.Value; Color color; if (jerk > 0) + { color = Color.Green; + } else if (jerk < 0) + { color = Color.Red; + } else + { color = Color.Gray; + } + LinesSeries[0].SetMarker(0, new IndicatorLineMarker(color)); } } diff --git a/lib/numerics/jerk/Jerk.Tests.cs b/lib/numerics/jerk/Jerk.Tests.cs index 5e9ffb4c..0ed3b1ad 100644 --- a/lib/numerics/jerk/Jerk.Tests.cs +++ b/lib/numerics/jerk/Jerk.Tests.cs @@ -258,12 +258,12 @@ public class JerkTests { var source = new TSeries(); var jerk = new Jerk(source); - + source.Add(new TValue(DateTime.UtcNow, 10)); source.Add(new TValue(DateTime.UtcNow, 20)); source.Add(new TValue(DateTime.UtcNow, 35)); source.Add(new TValue(DateTime.UtcNow, 40)); - + Assert.True(jerk.IsHot); // jerk = 40 - 3*35 + 3*20 - 10 = 40 - 105 + 60 - 10 = -15 Assert.Equal(-15, jerk.Last.Value); @@ -304,4 +304,4 @@ public class JerkTests Assert.Equal(jerkResults[i], chainResults[i], precision: 9); } } -} \ No newline at end of file +} diff --git a/lib/numerics/jerk/Jerk.cs b/lib/numerics/jerk/Jerk.cs index 846b7a52..f9e3f20f 100644 --- a/lib/numerics/jerk/Jerk.cs +++ b/lib/numerics/jerk/Jerk.cs @@ -78,8 +78,8 @@ public sealed class Jerk : AbstractBase if (_state.Count >= 3) { - // jerk = val - 3*prev1 + 3*prev2 - prev3 - // Using FMA: val - 3*prev1 + 3*prev2 - prev3 + // jerk = val - 3*prev1 + 3*prev2 - prev3 + // Using FMA: val - 3*prev1 + 3*prev2 - prev3 // = FMA(-3, prev1, val) + FMA(3, prev2, -prev3) double term1 = Math.FusedMultiplyAdd(-3.0, _state.Prev1, val); double term2 = Math.FusedMultiplyAdd(3.0, _state.Prev2, -_state.Prev3); @@ -127,7 +127,10 @@ public sealed class Jerk : AbstractBase public override TSeries Update(TSeries source) { - if (source.Count == 0) return []; + if (source.Count == 0) + { + return []; + } int len = source.Count; @@ -211,18 +214,34 @@ public sealed class Jerk : AbstractBase public static void Calculate(ReadOnlySpan source, Span output) { if (source.Length != output.Length) + { throw new ArgumentException("Source and output must have the same length", nameof(output)); + } int len = source.Length; - if (len == 0) return; + if (len == 0) + { + return; + } // First three elements have insufficient history output[0] = 0.0; - if (len == 1) return; + if (len == 1) + { + return; + } + output[1] = 0.0; - if (len == 2) return; + if (len == 2) + { + return; + } + output[2] = 0.0; - if (len == 3) return; + if (len == 3) + { + return; + } int i = 3; @@ -339,10 +358,25 @@ public sealed class Jerk : AbstractBase // Handle NaN/Infinity by substitution (find first finite value) double fallback = FindFinite(curr, p1, p2, p3); - if (!double.IsFinite(curr)) curr = fallback; - if (!double.IsFinite(p1)) p1 = fallback; - if (!double.IsFinite(p2)) p2 = fallback; - if (!double.IsFinite(p3)) p3 = fallback; + if (!double.IsFinite(curr)) + { + curr = fallback; + } + + if (!double.IsFinite(p1)) + { + p1 = fallback; + } + + if (!double.IsFinite(p2)) + { + p2 = fallback; + } + + if (!double.IsFinite(p3)) + { + p3 = fallback; + } // jerk = curr - 3*prev1 + 3*prev2 - prev3 double term1 = Math.FusedMultiplyAdd(-3.0, p1, curr); @@ -354,10 +388,26 @@ public sealed class Jerk : AbstractBase [MethodImpl(MethodImplOptions.AggressiveInlining)] private static double FindFinite(double a, double b, double c, double d) { - if (double.IsFinite(a)) return a; - if (double.IsFinite(b)) return b; - if (double.IsFinite(c)) return c; - if (double.IsFinite(d)) return d; + if (double.IsFinite(a)) + { + return a; + } + + if (double.IsFinite(b)) + { + return b; + } + + if (double.IsFinite(c)) + { + return c; + } + + if (double.IsFinite(d)) + { + return d; + } + return 0.0; } -} \ No newline at end of file +} diff --git a/lib/numerics/lineartrans/Lineartrans.Quantower.Tests.cs b/lib/numerics/lineartrans/Lineartrans.Quantower.Tests.cs index f4226699..273ae6aa 100644 --- a/lib/numerics/lineartrans/Lineartrans.Quantower.Tests.cs +++ b/lib/numerics/lineartrans/Lineartrans.Quantower.Tests.cs @@ -134,4 +134,4 @@ public class LineartransIndicatorTests // Identity transform: 1.0 * 42.5 + 0.0 = 42.5 Assert.Equal(42.5, indicator.LinesSeries[0].GetValue(0), 1e-10); } -} \ No newline at end of file +} diff --git a/lib/numerics/lineartrans/Lineartrans.Quantower.cs b/lib/numerics/lineartrans/Lineartrans.Quantower.cs index 4f697efe..51ada4bf 100644 --- a/lib/numerics/lineartrans/Lineartrans.Quantower.cs +++ b/lib/numerics/lineartrans/Lineartrans.Quantower.cs @@ -46,7 +46,10 @@ public class LineartransIndicator : Indicator, IWatchlistIndicator protected override void OnUpdate(UpdateArgs args) { - if (_lineartrans == null || _selector == null) return; + if (_lineartrans == null || _selector == null) + { + return; + } var item = HistoricalData[0, SeekOriginHistory.End]; double value = _selector(item); @@ -59,4 +62,4 @@ public class LineartransIndicator : Indicator, IWatchlistIndicator LinesSeries[0].SetValue(_lineartrans.Last.Value, isHot, ShowColdValues); } -} \ No newline at end of file +} diff --git a/lib/numerics/lineartrans/Lineartrans.Tests.cs b/lib/numerics/lineartrans/Lineartrans.Tests.cs index 4afd4b57..7a7d58a2 100644 --- a/lib/numerics/lineartrans/Lineartrans.Tests.cs +++ b/lib/numerics/lineartrans/Lineartrans.Tests.cs @@ -172,7 +172,9 @@ public class LineartransTests var time = DateTime.UtcNow; for (int i = 0; i < 5; i++) + { series.Add(new TValue(time.AddSeconds(i), i * 10.0), true); + } var result = linear.Update(series); @@ -191,7 +193,9 @@ public class LineartransTests var time = DateTime.UtcNow; for (int i = 0; i < 3; i++) + { series.Add(new TValue(time.AddSeconds(i), 10.0 * (i + 1)), true); + } var result = Lineartrans.Calculate(series, slope: 0.5, intercept: 5.0); @@ -257,7 +261,9 @@ public class LineartransTests var streamIndicator = new Lineartrans(slope, intercept); var streamResult = new TSeries(); for (int i = 0; i < series.Count; i++) + { streamResult.Add(streamIndicator.Update(series[i], true), true); + } // Span var spanOutput = new double[series.Count]; @@ -270,4 +276,4 @@ public class LineartransTests Assert.Equal(batchResult[i].Value, spanOutput[i], 1e-10); } } -} \ No newline at end of file +} diff --git a/lib/numerics/lineartrans/Lineartrans.Validation.Tests.cs b/lib/numerics/lineartrans/Lineartrans.Validation.Tests.cs index 3098c25c..f7f4df11 100644 --- a/lib/numerics/lineartrans/Lineartrans.Validation.Tests.cs +++ b/lib/numerics/lineartrans/Lineartrans.Validation.Tests.cs @@ -153,7 +153,9 @@ public class LineartransValidationTests // Create shifted series var shifted = new TSeries(); for (int i = 0; i < series.Count; i++) + { shifted.Add(new TValue(series[i].Time, series[i].Value + offset), true); + } // a * (x + offset) should equal a*x + a*offset var scaledSum = Lineartrans.Calculate(shifted, a, 0.0); @@ -264,4 +266,4 @@ public class LineartransValidationTests Assert.Equal(expected, result[i].Value, 1e-5); } } -} \ No newline at end of file +} diff --git a/lib/numerics/lineartrans/Lineartrans.cs b/lib/numerics/lineartrans/Lineartrans.cs index f7744da4..e03321bd 100644 --- a/lib/numerics/lineartrans/Lineartrans.cs +++ b/lib/numerics/lineartrans/Lineartrans.cs @@ -38,9 +38,14 @@ public sealed class Lineartrans : AbstractBase public Lineartrans(double slope = 1.0, double intercept = 0.0) { if (!double.IsFinite(slope)) + { throw new ArgumentException("Slope must be a finite number", nameof(slope)); + } + if (!double.IsFinite(intercept)) + { throw new ArgumentException("Intercept must be a finite number", nameof(intercept)); + } _slope = slope; _intercept = intercept; @@ -67,9 +72,13 @@ public sealed class Lineartrans : AbstractBase public override TValue Update(TValue input, bool isNew = true) { if (isNew) + { _p_state = _state; + } else + { _state = _p_state; + } double value = input.Value; double result; @@ -129,13 +138,24 @@ public sealed class Lineartrans : AbstractBase double slope = 1.0, double intercept = 0.0) { if (source.Length == 0) + { throw new ArgumentException("Source cannot be empty", nameof(source)); + } + if (output.Length < source.Length) + { throw new ArgumentException("Output length must be >= source length", nameof(output)); + } + if (!double.IsFinite(slope)) + { throw new ArgumentException("Slope must be a finite number", nameof(slope)); + } + if (!double.IsFinite(intercept)) + { throw new ArgumentException("Intercept must be a finite number", nameof(intercept)); + } // Check for non-finite values - if any exist, use scalar path only // Note: For very large arrays, SIMD-based NaN detection could be faster, diff --git a/lib/numerics/logtrans/Logtrans.Quantower.Tests.cs b/lib/numerics/logtrans/Logtrans.Quantower.Tests.cs index eceaeae9..9f5ee58d 100644 --- a/lib/numerics/logtrans/Logtrans.Quantower.Tests.cs +++ b/lib/numerics/logtrans/Logtrans.Quantower.Tests.cs @@ -117,4 +117,4 @@ public class LogtransIndicatorTests Assert.True(double.IsFinite(indicator.LinesSeries[0].GetValue(0))); } } -} \ No newline at end of file +} diff --git a/lib/numerics/logtrans/Logtrans.Quantower.cs b/lib/numerics/logtrans/Logtrans.Quantower.cs index 6539c6d4..ca9549ca 100644 --- a/lib/numerics/logtrans/Logtrans.Quantower.cs +++ b/lib/numerics/logtrans/Logtrans.Quantower.cs @@ -40,7 +40,10 @@ public class LogtransIndicator : Indicator, IWatchlistIndicator protected override void OnUpdate(UpdateArgs args) { - if (_logtrans == null || _selector == null) return; + if (_logtrans == null || _selector == null) + { + return; + } var item = HistoricalData[0, SeekOriginHistory.End]; double value = _selector(item); @@ -53,4 +56,4 @@ public class LogtransIndicator : Indicator, IWatchlistIndicator LinesSeries[0].SetValue(_logtrans.Last.Value, isHot, ShowColdValues); } -} \ No newline at end of file +} diff --git a/lib/numerics/logtrans/Logtrans.Tests.cs b/lib/numerics/logtrans/Logtrans.Tests.cs index d0c01c9c..e758b10a 100644 --- a/lib/numerics/logtrans/Logtrans.Tests.cs +++ b/lib/numerics/logtrans/Logtrans.Tests.cs @@ -258,4 +258,4 @@ public class LogtransTests // exp(logtrans(x)) should equal x Assert.Equal(original, Math.Exp(logtransResult), Tolerance); } -} \ No newline at end of file +} diff --git a/lib/numerics/logtrans/Logtrans.cs b/lib/numerics/logtrans/Logtrans.cs index 2aff50e0..c33c0c01 100644 --- a/lib/numerics/logtrans/Logtrans.cs +++ b/lib/numerics/logtrans/Logtrans.cs @@ -46,9 +46,13 @@ public sealed class Logtrans : AbstractBase public override TValue Update(TValue input, bool isNew = true) { if (isNew) + { _p_state = _state; + } else + { _state = _p_state; + } // Handle non-positive and non-finite values double value = input.Value; @@ -108,9 +112,14 @@ public sealed class Logtrans : AbstractBase public static void Calculate(ReadOnlySpan source, Span output) { if (source.Length == 0) + { throw new ArgumentException("Source cannot be empty", nameof(source)); + } + if (output.Length < source.Length) + { throw new ArgumentException("Output length must be >= source length", nameof(output)); + } double lastValid = 0.0; diff --git a/lib/numerics/lowest/Lowest.Quantower.cs b/lib/numerics/lowest/Lowest.Quantower.cs index fcd443e3..581d2cf9 100644 --- a/lib/numerics/lowest/Lowest.Quantower.cs +++ b/lib/numerics/lowest/Lowest.Quantower.cs @@ -43,7 +43,10 @@ public class LowestIndicator : Indicator, IWatchlistIndicator protected override void OnUpdate(UpdateArgs args) { - if (_lowest == null || _selector == null) return; + if (_lowest == null || _selector == null) + { + return; + } var item = HistoricalData[0, SeekOriginHistory.End]; double value = _selector(item); diff --git a/lib/numerics/lowest/Lowest.cs b/lib/numerics/lowest/Lowest.cs index 11bf164d..a20714a6 100644 --- a/lib/numerics/lowest/Lowest.cs +++ b/lib/numerics/lowest/Lowest.cs @@ -33,7 +33,9 @@ public sealed class Lowest : AbstractBase public Lowest(int period) { if (period < 1) + { throw new ArgumentException("Period must be >= 1", nameof(period)); + } _period = period; _buffer = new RingBuffer(period); @@ -58,9 +60,13 @@ public sealed class Lowest : AbstractBase public override TValue Update(TValue input, bool isNew = true) { if (isNew) + { _p_state = _state; + } else + { _state = _p_state; + } double value = double.IsFinite(input.Value) ? input.Value : _state.LastValid; _state = new State(value); @@ -112,11 +118,19 @@ public sealed class Lowest : AbstractBase public static void Calculate(ReadOnlySpan source, Span output, int period) { if (source.Length == 0) + { throw new ArgumentException("Source cannot be empty", nameof(source)); + } + if (output.Length < source.Length) + { throw new ArgumentException("Output length must be >= source length", nameof(output)); + } + if (period < 1) + { throw new ArgumentException("Period must be >= 1", nameof(period)); + } int len = source.Length; @@ -162,18 +176,25 @@ public sealed class Lowest : AbstractBase // Remove indices outside window while (dequeEnd > dequeStart && deque[dequeStart] <= i - period) + { dequeStart++; + } // Remove larger values from back (use values[] for corrected values) while (dequeEnd > dequeStart && values[deque[dequeEnd - 1]] >= value) + { dequeEnd--; + } // Compact deque if needed if (dequeEnd >= deque.Length) { int count = dequeEnd - dequeStart; for (int j = 0; j < count; j++) + { deque[j] = deque[dequeStart + j]; + } + dequeStart = 0; dequeEnd = count; } @@ -185,9 +206,14 @@ public sealed class Lowest : AbstractBase finally { if (rentedDeque != null) + { System.Buffers.ArrayPool.Shared.Return(rentedDeque); + } + if (rentedValues != null) + { System.Buffers.ArrayPool.Shared.Return(rentedValues); + } } } diff --git a/lib/numerics/midpoint/Midpoint.Quantower.cs b/lib/numerics/midpoint/Midpoint.Quantower.cs index f8098dcd..2f49c384 100644 --- a/lib/numerics/midpoint/Midpoint.Quantower.cs +++ b/lib/numerics/midpoint/Midpoint.Quantower.cs @@ -43,7 +43,10 @@ public class MidpointIndicator : Indicator, IWatchlistIndicator protected override void OnUpdate(UpdateArgs args) { - if (_midpoint == null || _selector == null) return; + if (_midpoint == null || _selector == null) + { + return; + } var item = HistoricalData[0, SeekOriginHistory.End]; double value = _selector(item); diff --git a/lib/numerics/midpoint/Midpoint.cs b/lib/numerics/midpoint/Midpoint.cs index 16f3b047..39692a05 100644 --- a/lib/numerics/midpoint/Midpoint.cs +++ b/lib/numerics/midpoint/Midpoint.cs @@ -33,7 +33,9 @@ public sealed class Midpoint : AbstractBase public Midpoint(int period) { if (period < 1) + { throw new ArgumentException("Period must be >= 1", nameof(period)); + } _highest = new Highest(period); _lowest = new Lowest(period); @@ -116,11 +118,19 @@ public sealed class Midpoint : AbstractBase public static void Calculate(ReadOnlySpan source, Span output, int period) { if (source.Length == 0) + { throw new ArgumentException("Source cannot be empty", nameof(source)); + } + if (output.Length < source.Length) + { throw new ArgumentException("Output length must be >= source length", nameof(output)); + } + if (period < 1) + { throw new ArgumentException("Period must be >= 1", nameof(period)); + } int len = source.Length; @@ -151,9 +161,14 @@ public sealed class Midpoint : AbstractBase finally { if (rentedHigh != null) + { System.Buffers.ArrayPool.Shared.Return(rentedHigh); + } + if (rentedLow != null) + { System.Buffers.ArrayPool.Shared.Return(rentedLow); + } } } diff --git a/lib/numerics/normalize/Normalize.Quantower.cs b/lib/numerics/normalize/Normalize.Quantower.cs index f2c31c7a..ff28fa99 100644 --- a/lib/numerics/normalize/Normalize.Quantower.cs +++ b/lib/numerics/normalize/Normalize.Quantower.cs @@ -43,7 +43,10 @@ public class NormalizeIndicator : Indicator, IWatchlistIndicator protected override void OnUpdate(UpdateArgs args) { - if (_normalize == null || _selector == null) return; + if (_normalize == null || _selector == null) + { + return; + } var item = HistoricalData[0, SeekOriginHistory.End]; double value = _selector(item); diff --git a/lib/numerics/normalize/Normalize.Tests.cs b/lib/numerics/normalize/Normalize.Tests.cs index f4f807d3..b96cf8da 100644 --- a/lib/numerics/normalize/Normalize.Tests.cs +++ b/lib/numerics/normalize/Normalize.Tests.cs @@ -152,7 +152,9 @@ public class NormalizeTests var norm = new Normalize(5); for (int i = 0; i < 10; i++) + { norm.Update(new TValue(DateTime.UtcNow, i * 10)); + } Assert.True(norm.IsHot); @@ -198,7 +200,9 @@ public class NormalizeTests var series = _gbm.Fetch(100, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1)); var tseries = new TSeries(); foreach (var bar in series) + { tseries.Add(new TValue(bar.Time, bar.Close), true); + } // Static calculation var staticResult = Normalize.Calculate(tseries, 14); @@ -207,7 +211,9 @@ public class NormalizeTests var streamNorm = new Normalize(14); var streamResult = new TSeries(); foreach (var bar in series) + { streamResult.Add(streamNorm.Update(new TValue(bar.Time, bar.Close)), true); + } // Compare last 50 values for (int i = 50; i < 100; i++) @@ -261,4 +267,4 @@ public class NormalizeTests var result = norm.Update(new TValue(DateTime.UtcNow, 60)); Assert.Equal(0.2, result.Value, 1e-10); } -} \ No newline at end of file +} diff --git a/lib/numerics/normalize/Normalize.Validation.Tests.cs b/lib/numerics/normalize/Normalize.Validation.Tests.cs index f4686df4..278f1d92 100644 --- a/lib/numerics/normalize/Normalize.Validation.Tests.cs +++ b/lib/numerics/normalize/Normalize.Validation.Tests.cs @@ -40,7 +40,9 @@ public class NormalizeValidationTests double[] values = { 10, 20, 30, 40, 50 }; foreach (var v in values) + { norm.Update(new TValue(DateTime.UtcNow, v)); + } // Max value (50) should normalize to 1.0 Assert.Equal(1.0, norm.Last.Value, 1e-10); @@ -55,7 +57,9 @@ public class NormalizeValidationTests double[] values = { 50, 40, 30, 20, 10 }; foreach (var v in values) + { norm.Update(new TValue(DateTime.UtcNow, v)); + } // Min value (10) should normalize to 0.0 Assert.Equal(0.0, norm.Last.Value, 1e-10); @@ -98,7 +102,9 @@ public class NormalizeValidationTests // All same values for (int i = 0; i < 20; i++) + { norm.Update(new TValue(DateTime.UtcNow, 42.0)); + } // Flat range: should return 0.5 Assert.Equal(0.5, norm.Last.Value, 1e-10); @@ -223,7 +229,10 @@ public class NormalizeValidationTests // Mode 2: Batch via Update(TSeries) var tseries = new TSeries(); foreach (var bar in series) + { tseries.Add(new TValue(bar.Time, bar.Close), true); + } + var results2 = Normalize.Calculate(tseries, period); // Mode 3: Static span Calculate @@ -235,7 +244,10 @@ public class NormalizeValidationTests var source = new TSeries(); var norm4 = new Normalize(source, period); foreach (var bar in series) + { source.Add(new TValue(bar.Time, bar.Close), true); + } + var results4 = norm4.Last.Value; // Compare all modes (use last 50 values for stability) @@ -300,4 +312,4 @@ public class NormalizeValidationTests Assert.True(norm.IsHot); } -} \ No newline at end of file +} diff --git a/lib/numerics/normalize/Normalize.cs b/lib/numerics/normalize/Normalize.cs index 81b505b0..61c828b8 100644 --- a/lib/numerics/normalize/Normalize.cs +++ b/lib/numerics/normalize/Normalize.cs @@ -37,7 +37,9 @@ public sealed class Normalize : AbstractBase public Normalize(int period = 14) { if (period < 1) + { throw new ArgumentException("Period must be >= 1", nameof(period)); + } _period = period; _buffer = new RingBuffer(period); @@ -64,7 +66,9 @@ public sealed class Normalize : AbstractBase private static (double min, double max) FindMinMax(ReadOnlySpan values) { if (values.Length == 0) + { return (double.MaxValue, double.MinValue); + } double min = values[0]; double max = values[0]; @@ -72,8 +76,15 @@ public sealed class Normalize : AbstractBase for (int i = 1; i < values.Length; i++) { double v = values[i]; - if (v < min) min = v; - if (v > max) max = v; + if (v < min) + { + min = v; + } + + if (v > max) + { + max = v; + } } return (min, max); @@ -83,9 +94,13 @@ public sealed class Normalize : AbstractBase public override TValue Update(TValue input, bool isNew = true) { if (isNew) + { _p_state = _state; + } else + { _state = _p_state; + } double value = input.Value; double result; @@ -151,11 +166,19 @@ public sealed class Normalize : AbstractBase public static void Calculate(ReadOnlySpan source, Span output, int period = 14) { if (source.Length == 0) + { throw new ArgumentException("Source cannot be empty", nameof(source)); + } + if (output.Length < source.Length) + { throw new ArgumentException("Output length must be >= source length", nameof(output)); + } + if (period < 1) + { throw new ArgumentException("Period must be >= 1", nameof(period)); + } double lastValid = 0.5; @@ -181,8 +204,15 @@ public sealed class Normalize : AbstractBase double v = source[j]; if (double.IsFinite(v)) { - if (v < min) min = v; - if (v > max) max = v; + if (v < min) + { + min = v; + } + + if (v > max) + { + max = v; + } } } diff --git a/lib/numerics/relu/Relu.Quantower.cs b/lib/numerics/relu/Relu.Quantower.cs index 412509c6..df05bf12 100644 --- a/lib/numerics/relu/Relu.Quantower.cs +++ b/lib/numerics/relu/Relu.Quantower.cs @@ -40,7 +40,10 @@ public class ReluIndicator : Indicator, IWatchlistIndicator protected override void OnUpdate(UpdateArgs args) { - if (_relu == null || _selector == null) return; + if (_relu == null || _selector == null) + { + return; + } var item = HistoricalData[0, SeekOriginHistory.End]; double value = _selector(item); diff --git a/lib/numerics/relu/Relu.cs b/lib/numerics/relu/Relu.cs index 3d3f3755..1b5d2091 100644 --- a/lib/numerics/relu/Relu.cs +++ b/lib/numerics/relu/Relu.cs @@ -61,9 +61,13 @@ public sealed class Relu : AbstractBase public override TValue Update(TValue input, bool isNew = true) { if (isNew) + { _p_state = _state; + } else + { _state = _p_state; + } double value = input.Value; double result; @@ -85,7 +89,10 @@ public sealed class Relu : AbstractBase public override TSeries Update(TSeries source) { - if (source.Count == 0) return new TSeries([], []); + if (source.Count == 0) + { + return new TSeries([], []); + } int len = source.Count; var t = new List(len); @@ -135,9 +142,14 @@ public sealed class Relu : AbstractBase public static void Calculate(ReadOnlySpan source, Span output) { if (source.Length == 0) + { throw new ArgumentException("Source cannot be empty", nameof(source)); + } + if (output.Length < source.Length) + { throw new ArgumentException("Output length must be >= source length", nameof(output)); + } double lastValid = 0.0; int i = 0; @@ -207,4 +219,4 @@ public sealed class Relu : AbstractBase _p_state = default; Last = default; } -} \ No newline at end of file +} diff --git a/lib/numerics/sigmoid/Sigmoid.Quantower.Tests.cs b/lib/numerics/sigmoid/Sigmoid.Quantower.Tests.cs index 9499eb25..b9b2b204 100644 --- a/lib/numerics/sigmoid/Sigmoid.Quantower.Tests.cs +++ b/lib/numerics/sigmoid/Sigmoid.Quantower.Tests.cs @@ -162,4 +162,4 @@ public class SigmoidIndicatorTests now = now.AddMinutes(1); } } -} \ No newline at end of file +} diff --git a/lib/numerics/sigmoid/Sigmoid.Quantower.cs b/lib/numerics/sigmoid/Sigmoid.Quantower.cs index 8add6e81..4d5c1dcc 100644 --- a/lib/numerics/sigmoid/Sigmoid.Quantower.cs +++ b/lib/numerics/sigmoid/Sigmoid.Quantower.cs @@ -46,7 +46,10 @@ public class SigmoidIndicator : Indicator, IWatchlistIndicator protected override void OnUpdate(UpdateArgs args) { - if (_sigmoid == null || _selector == null) return; + if (_sigmoid == null || _selector == null) + { + return; + } var item = HistoricalData[0, SeekOriginHistory.End]; double value = _selector(item); diff --git a/lib/numerics/sigmoid/Sigmoid.Tests.cs b/lib/numerics/sigmoid/Sigmoid.Tests.cs index 6bf5a3ac..fee4bf34 100644 --- a/lib/numerics/sigmoid/Sigmoid.Tests.cs +++ b/lib/numerics/sigmoid/Sigmoid.Tests.cs @@ -219,7 +219,9 @@ public class SigmoidTests var series = new TSeries(); for (int i = 0; i < 100; i++) + { series.Add(new TValue(DateTime.UtcNow.AddSeconds(i), i - 50), isNew: true); + } var result = sigmoid.Update(series); @@ -232,7 +234,9 @@ public class SigmoidTests var series = new TSeries(); for (int i = 0; i < 100; i++) + { series.Add(new TValue(DateTime.UtcNow.AddSeconds(i), i - 50), isNew: true); + } var result = Sigmoid.Calculate(series); @@ -274,7 +278,9 @@ public class SigmoidTests double[] source = new double[100]; var rng = new Random(42); for (int i = 0; i < source.Length; i++) + { source[i] = rng.NextDouble() * 200 - 100; + } double[] spanOutput = new double[source.Length]; Sigmoid.Calculate(source.AsSpan(), spanOutput.AsSpan()); @@ -282,10 +288,14 @@ public class SigmoidTests var sigmoid = new Sigmoid(); double[] streamOutput = new double[source.Length]; for (int i = 0; i < source.Length; i++) + { streamOutput[i] = sigmoid.Update(new TValue(DateTime.UtcNow.AddSeconds(i), source[i]), true).Value; + } for (int i = 0; i < source.Length; i++) + { Assert.Equal(streamOutput[i], spanOutput[i], Epsilon); + } } [Fact] @@ -315,7 +325,9 @@ public class SigmoidTests sigmoid.Pub += (_, in _) => eventCount++; for (int i = 0; i < 10; i++) + { source.Add(new TValue(DateTime.UtcNow.AddSeconds(i), i), isNew: true); + } Assert.Equal(10, eventCount); } @@ -351,4 +363,4 @@ public class SigmoidTests Assert.Equal(0.5, result0.Value, Epsilon); Assert.Equal(0.5, result100.Value, Epsilon); } -} \ No newline at end of file +} diff --git a/lib/numerics/sigmoid/Sigmoid.Validation.Tests.cs b/lib/numerics/sigmoid/Sigmoid.Validation.Tests.cs index 8a381f32..19e63d5f 100644 --- a/lib/numerics/sigmoid/Sigmoid.Validation.Tests.cs +++ b/lib/numerics/sigmoid/Sigmoid.Validation.Tests.cs @@ -222,7 +222,9 @@ public class SigmoidValidationTests double[] source = new double[500]; var rng = new Random(42); for (int i = 0; i < source.Length; i++) + { source[i] = rng.NextDouble() * 200 - 50; // Range [-50, 150] + } // Span calculation double[] spanOutput = new double[source.Length]; @@ -236,4 +238,4 @@ public class SigmoidValidationTests Assert.Equal(spanOutput[i], result.Value, Epsilon); } } -} \ No newline at end of file +} diff --git a/lib/numerics/sigmoid/Sigmoid.cs b/lib/numerics/sigmoid/Sigmoid.cs index f9b70307..5d7d4b4e 100644 --- a/lib/numerics/sigmoid/Sigmoid.cs +++ b/lib/numerics/sigmoid/Sigmoid.cs @@ -38,7 +38,9 @@ public sealed class Sigmoid : AbstractBase public Sigmoid(double k = 1.0, double x0 = 0.0) { if (k <= 0) + { throw new ArgumentException("Steepness (k) must be positive", nameof(k)); + } _k = k; _x0 = x0; @@ -65,8 +67,16 @@ public sealed class Sigmoid : AbstractBase { double exponent = -k * (x - x0); // Guard against overflow: exp(>709) overflows, exp(<-709) underflows to 0 - if (exponent > 700) return 0.0; // exp(-700) ≈ 0 - if (exponent < -700) return 1.0; // 1/(1+0) = 1 + if (exponent > 700) + { + return 0.0; // exp(-700) ≈ 0 + } + + if (exponent < -700) + { + return 1.0; // 1/(1+0) = 1 + } + return 1.0 / (1.0 + Math.Exp(exponent)); } @@ -74,9 +84,13 @@ public sealed class Sigmoid : AbstractBase public override TValue Update(TValue input, bool isNew = true) { if (isNew) + { _p_state = _state; + } else + { _state = _p_state; + } double value = input.Value; double result; @@ -134,11 +148,19 @@ public sealed class Sigmoid : AbstractBase public static void Calculate(ReadOnlySpan source, Span output, double k = 1.0, double x0 = 0.0) { if (source.Length == 0) + { throw new ArgumentException("Source cannot be empty", nameof(source)); + } + if (output.Length < source.Length) + { throw new ArgumentException("Output length must be >= source length", nameof(output)); + } + if (k <= 0) + { throw new ArgumentException("Steepness (k) must be positive", nameof(k)); + } double lastValid = 0.5; // Sigmoid(x0) = 0.5 int i = 0; diff --git a/lib/numerics/slope/Slope.Quantower.cs b/lib/numerics/slope/Slope.Quantower.cs index ddf3686d..64cff839 100644 --- a/lib/numerics/slope/Slope.Quantower.cs +++ b/lib/numerics/slope/Slope.Quantower.cs @@ -41,7 +41,10 @@ public class SlopeIndicator : Indicator, IWatchlistIndicator protected override void OnUpdate(UpdateArgs args) { - if (_slope == null || _selector == null) return; + if (_slope == null || _selector == null) + { + return; + } var item = HistoricalData[0, SeekOriginHistory.End]; double value = _selector(item); @@ -60,11 +63,18 @@ public class SlopeIndicator : Indicator, IWatchlistIndicator double slope = _slope.Last.Value; Color color; if (slope > 0) + { color = Color.Green; + } else if (slope < 0) + { color = Color.Red; + } else + { color = Color.Gray; + } + LinesSeries[0].SetMarker(0, new IndicatorLineMarker(color)); } } diff --git a/lib/numerics/slope/Slope.Tests.cs b/lib/numerics/slope/Slope.Tests.cs index 7b460576..124b2033 100644 --- a/lib/numerics/slope/Slope.Tests.cs +++ b/lib/numerics/slope/Slope.Tests.cs @@ -241,10 +241,10 @@ public class SlopeTests { var source = new TSeries(); var slope = new Slope(source); - + source.Add(new TValue(DateTime.UtcNow, 10)); source.Add(new TValue(DateTime.UtcNow, 20)); - + Assert.True(slope.IsHot); Assert.Equal(10, slope.Last.Value); } diff --git a/lib/numerics/slope/Slope.cs b/lib/numerics/slope/Slope.cs index 6003d942..701b432e 100644 --- a/lib/numerics/slope/Slope.cs +++ b/lib/numerics/slope/Slope.cs @@ -98,7 +98,10 @@ public sealed class Slope : AbstractBase public override TSeries Update(TSeries source) { - if (source.Count == 0) return []; + if (source.Count == 0) + { + return []; + } int len = source.Count; ReadOnlySpan sourceValues = source.Values; @@ -155,14 +158,22 @@ public sealed class Slope : AbstractBase public static void Calculate(ReadOnlySpan source, Span output) { if (source.Length != output.Length) + { throw new ArgumentException("Source and output must have the same length", nameof(output)); + } int len = source.Length; - if (len == 0) return; + if (len == 0) + { + return; + } // First element has no previous - set to 0 output[0] = 0.0; - if (len == 1) return; + if (len == 1) + { + return; + } int i = 1; diff --git a/lib/numerics/sqrttrans/Sqrttrans.Quantower.Tests.cs b/lib/numerics/sqrttrans/Sqrttrans.Quantower.Tests.cs index 0693baf3..5864d17f 100644 --- a/lib/numerics/sqrttrans/Sqrttrans.Quantower.Tests.cs +++ b/lib/numerics/sqrttrans/Sqrttrans.Quantower.Tests.cs @@ -137,4 +137,4 @@ public class SqrttransIndicatorTests Assert.Equal(expectedRoots[i], indicator.LinesSeries[0].GetValue(0), 1e-10); } } -} \ No newline at end of file +} diff --git a/lib/numerics/sqrttrans/Sqrttrans.Quantower.cs b/lib/numerics/sqrttrans/Sqrttrans.Quantower.cs index 413a9981..68b7936c 100644 --- a/lib/numerics/sqrttrans/Sqrttrans.Quantower.cs +++ b/lib/numerics/sqrttrans/Sqrttrans.Quantower.cs @@ -40,7 +40,10 @@ public class SqrttransIndicator : Indicator, IWatchlistIndicator protected override void OnUpdate(UpdateArgs args) { - if (_sqrttrans == null || _selector == null) return; + if (_sqrttrans == null || _selector == null) + { + return; + } var item = HistoricalData[0, SeekOriginHistory.End]; double value = _selector(item); @@ -53,4 +56,4 @@ public class SqrttransIndicator : Indicator, IWatchlistIndicator LinesSeries[0].SetValue(_sqrttrans.Last.Value, isHot, ShowColdValues); } -} \ No newline at end of file +} diff --git a/lib/numerics/sqrttrans/Sqrttrans.Tests.cs b/lib/numerics/sqrttrans/Sqrttrans.Tests.cs index 72643fad..eaa9d768 100644 --- a/lib/numerics/sqrttrans/Sqrttrans.Tests.cs +++ b/lib/numerics/sqrttrans/Sqrttrans.Tests.cs @@ -324,4 +324,4 @@ public class SqrttransTests Assert.True(indicator.Last.Value >= 0); } } -} \ No newline at end of file +} diff --git a/lib/numerics/sqrttrans/Sqrttrans.Validation.Tests.cs b/lib/numerics/sqrttrans/Sqrttrans.Validation.Tests.cs index 9ecb57bc..34b29c63 100644 --- a/lib/numerics/sqrttrans/Sqrttrans.Validation.Tests.cs +++ b/lib/numerics/sqrttrans/Sqrttrans.Validation.Tests.cs @@ -236,4 +236,4 @@ public class SqrttransValidationTests Assert.Equal(Math.Sqrt(largeValues[i]), indicator.Last.Value, Tolerance); } } -} \ No newline at end of file +} diff --git a/lib/numerics/sqrttrans/Sqrttrans.cs b/lib/numerics/sqrttrans/Sqrttrans.cs index 4939f2eb..0f721985 100644 --- a/lib/numerics/sqrttrans/Sqrttrans.cs +++ b/lib/numerics/sqrttrans/Sqrttrans.cs @@ -46,9 +46,13 @@ public sealed class Sqrttrans : AbstractBase public override TValue Update(TValue input, bool isNew = true) { if (isNew) + { _p_state = _state; + } else + { _state = _p_state; + } double value = input.Value; double result; @@ -107,9 +111,14 @@ public sealed class Sqrttrans : AbstractBase public static void Calculate(ReadOnlySpan source, Span output) { if (source.Length == 0) + { throw new ArgumentException("Source cannot be empty", nameof(source)); + } + if (output.Length < source.Length) + { throw new ArgumentException("Output length must be >= source length", nameof(output)); + } double lastValid = 0.0; // sqrt(0) = 0 diff --git a/lib/oscillators/ao/Ao.Quantower.cs b/lib/oscillators/ao/Ao.Quantower.cs index e94815c5..0ecff1b5 100644 --- a/lib/oscillators/ao/Ao.Quantower.cs +++ b/lib/oscillators/ao/Ao.Quantower.cs @@ -53,7 +53,9 @@ public sealed class AoIndicator : Indicator, IWatchlistIndicator TValue result = _ao.Update(this.GetInputBar(args), args.IsNewBar()); if (!_ao.IsHot && !ShowColdValues) + { return; + } double prevAo = double.NaN; if (Count > 1) diff --git a/lib/oscillators/ao/Ao.Validation.Tests.cs b/lib/oscillators/ao/Ao.Validation.Tests.cs index 77bcf9c9..150d9734 100644 --- a/lib/oscillators/ao/Ao.Validation.Tests.cs +++ b/lib/oscillators/ao/Ao.Validation.Tests.cs @@ -109,7 +109,10 @@ public sealed class AoValidationTests : IDisposable for (int i = 0; i < _data.Bars.Count; i++) { // Ooples might return 0 for warmup - if (i < 33) continue; // Skip warmup + if (i < 33) + { + continue; // Skip warmup + } Assert.Equal(oValues[i], results[i], ValidationHelper.OoplesTolerance); } diff --git a/lib/oscillators/ao/Ao.cs b/lib/oscillators/ao/Ao.cs index d5392601..5a04f88f 100644 --- a/lib/oscillators/ao/Ao.cs +++ b/lib/oscillators/ao/Ao.cs @@ -60,11 +60,19 @@ public sealed class Ao : ITValuePublisher public Ao(int fastPeriod = 5, int slowPeriod = 34) { if (fastPeriod <= 0) + { throw new ArgumentException("Fast period must be greater than 0", nameof(fastPeriod)); + } + if (slowPeriod <= 0) + { throw new ArgumentException("Slow period must be greater than 0", nameof(slowPeriod)); + } + if (fastPeriod >= slowPeriod) + { throw new ArgumentException("Fast period must be less than slow period", nameof(fastPeriod)); + } _fastPeriod = fastPeriod; _slowPeriod = slowPeriod; @@ -163,7 +171,10 @@ public sealed class Ao : ITValuePublisher /// The AO series public TSeries Update(TBarSeries source) { - if (source.Count == 0) return new TSeries([], []); + if (source.Count == 0) + { + return new TSeries([], []); + } int len = source.Count; var v = new double[len]; @@ -204,10 +215,15 @@ public sealed class Ao : ITValuePublisher public static void Calculate(ReadOnlySpan high, ReadOnlySpan low, Span destination, int fastPeriod = 5, int slowPeriod = 34) { if (high.Length != low.Length || high.Length != destination.Length) + { throw new ArgumentException("High, low, and destination spans must have the same length.", nameof(destination)); + } int len = high.Length; - if (len == 0) return; + if (len == 0) + { + return; + } // Always use pooled buffer to avoid CS8353 stackalloc escape issues // For small sizes, ArrayPool overhead is minimal @@ -243,7 +259,10 @@ public sealed class Ao : ITValuePublisher /// AO series public static TSeries Batch(TBarSeries source, int fastPeriod = 5, int slowPeriod = 34) { - if (source.Count == 0) return new TSeries([], []); + if (source.Count == 0) + { + return new TSeries([], []); + } int len = source.Count; var v = new double[len]; @@ -264,4 +283,4 @@ public sealed class Ao : ITValuePublisher return new TSeries(tList, vList); } -} \ No newline at end of file +} diff --git a/lib/oscillators/apo/Apo.cs b/lib/oscillators/apo/Apo.cs index 3336cb6f..2a46386f 100644 --- a/lib/oscillators/apo/Apo.cs +++ b/lib/oscillators/apo/Apo.cs @@ -58,11 +58,19 @@ public sealed class Apo : ITValuePublisher public Apo(int fastPeriod = 12, int slowPeriod = 26) { if (fastPeriod <= 0) + { throw new ArgumentException("Fast period must be greater than 0", nameof(fastPeriod)); + } + if (slowPeriod <= 0) + { throw new ArgumentException("Slow period must be greater than 0", nameof(slowPeriod)); + } + if (fastPeriod >= slowPeriod) + { throw new ArgumentException("Fast period must be less than slow period", nameof(fastPeriod)); + } _emaFast = new Ema(fastPeriod); _emaSlow = new Ema(slowPeriod); @@ -174,7 +182,9 @@ public sealed class Apo : ITValuePublisher public static void Calculate(ReadOnlySpan source, Span output, int fastPeriod = 12, int slowPeriod = 26) { if (source.Length != output.Length) + { throw new ArgumentException("Source and output spans must be of the same length.", nameof(output)); + } Span fastEma = source.Length <= 1024 ? stackalloc double[source.Length] : new double[source.Length]; Span slowEma = source.Length <= 1024 ? stackalloc double[source.Length] : new double[source.Length]; diff --git a/lib/oscillators/ultosc/Ultosc.Tests.cs b/lib/oscillators/ultosc/Ultosc.Tests.cs index 69f3b4ca..9f80c75e 100644 --- a/lib/oscillators/ultosc/Ultosc.Tests.cs +++ b/lib/oscillators/ultosc/Ultosc.Tests.cs @@ -217,7 +217,10 @@ public class UltoscTests var gbm = new GBM(); var bars = gbm.Fetch(50, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1)); - foreach (var bar in bars) ultosc.Update(bar); + foreach (var bar in bars) + { + ultosc.Update(bar); + } double lastVal = ultosc.Last.Value; Assert.NotEqual(0, lastVal); diff --git a/lib/oscillators/ultosc/Ultosc.cs b/lib/oscillators/ultosc/Ultosc.cs index 9f02b437..83a298e3 100644 --- a/lib/oscillators/ultosc/Ultosc.cs +++ b/lib/oscillators/ultosc/Ultosc.cs @@ -64,15 +64,29 @@ public sealed class Ultosc : AbstractBase public Ultosc(int period1 = 7, int period2 = 14, int period3 = 28) { if (period1 <= 0) + { throw new ArgumentException("Period1 must be greater than 0", nameof(period1)); + } + if (period2 <= 0) + { throw new ArgumentException("Period2 must be greater than 0", nameof(period2)); + } + if (period3 <= 0) + { throw new ArgumentException("Period3 must be greater than 0", nameof(period3)); + } + if (period1 >= period2) + { throw new ArgumentException("Period1 must be less than Period2", nameof(period1)); + } + if (period2 >= period3) + { throw new ArgumentException("Period2 must be less than Period3", nameof(period2)); + } _period1 = period1; _period2 = period2; @@ -212,7 +226,10 @@ public sealed class Ultosc : AbstractBase public TSeries Update(TBarSeries source) { - if (source.Count == 0) return []; + if (source.Count == 0) + { + return []; + } int len = source.Count; var t = new List(len); @@ -243,7 +260,10 @@ public sealed class Ultosc : AbstractBase { // Cannot properly calculate Ultimate Oscillator from single-value series // Return series of neutral values - if (source.Count == 0) return []; + if (source.Count == 0) + { + return []; + } var t = new List(source.Count); var v = new List(source.Count); @@ -281,19 +301,39 @@ public sealed class Ultosc : AbstractBase { int len = high.Length; if (len != low.Length || len != close.Length || len != output.Length) + { throw new ArgumentException("All arrays must have the same length", nameof(output)); - if (period1 <= 0) - throw new ArgumentException("Period1 must be greater than 0", nameof(period1)); - if (period2 <= 0) - throw new ArgumentException("Period2 must be greater than 0", nameof(period2)); - if (period3 <= 0) - throw new ArgumentException("Period3 must be greater than 0", nameof(period3)); - if (period1 >= period2) - throw new ArgumentException("Period1 must be less than Period2", nameof(period1)); - if (period2 >= period3) - throw new ArgumentException("Period2 must be less than Period3", nameof(period2)); + } - if (len == 0) return; + if (period1 <= 0) + { + throw new ArgumentException("Period1 must be greater than 0", nameof(period1)); + } + + if (period2 <= 0) + { + throw new ArgumentException("Period2 must be greater than 0", nameof(period2)); + } + + if (period3 <= 0) + { + throw new ArgumentException("Period3 must be greater than 0", nameof(period3)); + } + + if (period1 >= period2) + { + throw new ArgumentException("Period1 must be less than Period2", nameof(period1)); + } + + if (period2 >= period3) + { + throw new ArgumentException("Period2 must be less than Period3", nameof(period2)); + } + + if (len == 0) + { + return; + } // Allocate buffers for BP and TR double[] bpArray = System.Buffers.ArrayPool.Shared.Rent(len); @@ -386,4 +426,4 @@ public sealed class Ultosc : AbstractBase _p_index = 0; Last = default; } -} \ No newline at end of file +} diff --git a/lib/statistics/beta/Beta.cs b/lib/statistics/beta/Beta.cs index 6b7ca428..4545c150 100644 --- a/lib/statistics/beta/Beta.cs +++ b/lib/statistics/beta/Beta.cs @@ -272,4 +272,4 @@ public sealed class Beta : AbstractBase _sumRm2 = FusedMultiplyAdd(rm, rm, _sumRm2); } } -} \ No newline at end of file +} diff --git a/lib/statistics/cma/Cma.Quantower.Tests.cs b/lib/statistics/cma/Cma.Quantower.Tests.cs index 9f0ccd43..4a029d81 100644 --- a/lib/statistics/cma/Cma.Quantower.Tests.cs +++ b/lib/statistics/cma/Cma.Quantower.Tests.cs @@ -166,4 +166,4 @@ public class CmaIndicatorTests indicator.ProcessUpdate(new UpdateArgs(UpdateReason.NewBar)); Assert.Equal(20.0, indicator.LinesSeries[0].GetValue(0), 1e-10); // CMA = (10+20+30)/3 = 20 } -} \ No newline at end of file +} diff --git a/lib/statistics/cma/Cma.Tests.cs b/lib/statistics/cma/Cma.Tests.cs index 6c3a21ed..4d5a8da3 100644 --- a/lib/statistics/cma/Cma.Tests.cs +++ b/lib/statistics/cma/Cma.Tests.cs @@ -401,7 +401,9 @@ public class CmaTests var gbm = new GBM(startPrice: 100, mu: 0.05, sigma: 0.2, seed: 42); for (int i = 0; i < source.Length; i++) + { source[i] = gbm.Next().Close; + } // Warm up Cma.Batch(source.AsSpan(), output.AsSpan()); @@ -520,7 +522,10 @@ public class CmaTests public void Calculate_ReturnsCorrectResultsAndHotIndicator() { var series = new TSeries(); - for (int i = 1; i <= 10; i++) series.Add(DateTime.UtcNow, i * 10); + for (int i = 1; i <= 10; i++) + { + series.Add(DateTime.UtcNow, i * 10); + } // 10, 20, 30, 40, 50, 60, 70, 80, 90, 100 var (results, indicator) = Cma.Calculate(series); diff --git a/lib/statistics/cma/Cma.cs b/lib/statistics/cma/Cma.cs index 8894548d..289fb081 100644 --- a/lib/statistics/cma/Cma.cs +++ b/lib/statistics/cma/Cma.cs @@ -96,7 +96,10 @@ public sealed class Cma : AbstractBase /// Time interval between values (not used for CMA) public override void Prime(ReadOnlySpan source, TimeSpan? step = null) { - if (source.Length == 0) return; + if (source.Length == 0) + { + return; + } // Reset state _state = default; @@ -162,7 +165,10 @@ public sealed class Cma : AbstractBase public override TSeries Update(TSeries source) { - if (source.Count == 0) return []; + if (source.Count == 0) + { + return []; + } int len = source.Count; var t = new List(len); @@ -207,10 +213,15 @@ public sealed class Cma : AbstractBase public static void Batch(ReadOnlySpan source, Span output) { if (source.Length != output.Length) + { throw new ArgumentException("Source and output must have the same length", nameof(output)); + } int len = source.Length; - if (len == 0) return; + if (len == 0) + { + return; + } double mean = 0; double lastValid = double.NaN; @@ -230,9 +241,13 @@ public sealed class Cma : AbstractBase { double val = source[i]; if (double.IsFinite(val)) + { lastValid = val; + } else + { val = lastValid; + } // M_n = M_(n-1) + alpha * delta using FMA for single-rounding precision double alpha = 1.0 / (i + 1); @@ -264,4 +279,4 @@ public sealed class Cma : AbstractBase _p_state = default; Last = default; } -} \ No newline at end of file +} diff --git a/lib/statistics/covariance/Covariance.Simd.Tests.cs b/lib/statistics/covariance/Covariance.Simd.Tests.cs index d763aa72..55da1d5b 100644 --- a/lib/statistics/covariance/Covariance.Simd.Tests.cs +++ b/lib/statistics/covariance/Covariance.Simd.Tests.cs @@ -82,8 +82,15 @@ public class CovarianceSimdTests { double x = dataX[i]; double y = dataY[i]; - if (!double.IsFinite(x)) x = 0; - if (!double.IsFinite(y)) y = 0; + if (!double.IsFinite(x)) + { + x = 0; + } + + if (!double.IsFinite(y)) + { + y = 0; + } var res = scalarCov.Update(x, y); Assert.Equal(res.Value, result.Values[i], precision: 9); diff --git a/lib/statistics/covariance/Covariance.cs b/lib/statistics/covariance/Covariance.cs index a6a26532..c243a44c 100644 --- a/lib/statistics/covariance/Covariance.cs +++ b/lib/statistics/covariance/Covariance.cs @@ -182,7 +182,9 @@ public sealed class Covariance : AbstractBase public static TSeries Calculate(TSeries sourceX, TSeries sourceY, int period, bool isPopulation = false) { if (sourceX.Count != sourceY.Count) + { throw new ArgumentException("Source series must have the same length", nameof(sourceY)); + } int len = sourceX.Count; var t = new List(len); @@ -203,12 +205,20 @@ public sealed class Covariance : AbstractBase public static void Batch(ReadOnlySpan sourceX, ReadOnlySpan sourceY, Span output, int period, bool isPopulation = false) { if (sourceX.Length != sourceY.Length || sourceX.Length != output.Length) + { throw new ArgumentException("All spans must have the same length", nameof(output)); + } + if (period < 2) + { throw new ArgumentException("Period must be greater than or equal to 2", nameof(period)); + } int len = sourceX.Length; - if (len == 0) return; + if (len == 0) + { + return; + } // SIMD overhead amortizes well for datasets >= 256 elements const int SimdThreshold = 256; @@ -242,8 +252,15 @@ public sealed class Covariance : AbstractBase { double x = sourceX[i]; double y = sourceY[i]; - if (!double.IsFinite(x)) x = 0; - if (!double.IsFinite(y)) y = 0; + if (!double.IsFinite(x)) + { + x = 0; + } + + if (!double.IsFinite(y)) + { + y = 0; + } sumX += x; sumY += y; @@ -270,8 +287,15 @@ public sealed class Covariance : AbstractBase { double x = sourceX[i]; double y = sourceY[i]; - if (!double.IsFinite(x)) x = 0; - if (!double.IsFinite(y)) y = 0; + if (!double.IsFinite(x)) + { + x = 0; + } + + if (!double.IsFinite(y)) + { + y = 0; + } double oldX = bufferX[bufferIndex]; double oldY = bufferY[bufferIndex]; @@ -283,7 +307,10 @@ public sealed class Covariance : AbstractBase bufferX[bufferIndex] = x; bufferY[bufferIndex] = y; bufferIndex++; - if (bufferIndex >= period) bufferIndex = 0; + if (bufferIndex >= period) + { + bufferIndex = 0; + } double n = period; double numerator = sumXY - (sumX * sumY) / n; @@ -357,7 +384,10 @@ public sealed class Covariance : AbstractBase (double sumX, double sumY, double sumXY) = WarmupCovariance(period, len, isPopulation, ref srcXRef, ref srcYRef, ref outRef); - if (len <= period) return; + if (len <= period) + { + return; + } var vInvN = Vector256.Create(invN); var vInvDenom = Vector256.Create(invDenom); @@ -452,13 +482,27 @@ public sealed class Covariance : AbstractBase { double x = Unsafe.Add(ref srcXRef, i); double y = Unsafe.Add(ref srcYRef, i); - if (!double.IsFinite(x)) x = 0; - if (!double.IsFinite(y)) y = 0; + if (!double.IsFinite(x)) + { + x = 0; + } + + if (!double.IsFinite(y)) + { + y = 0; + } double oldX = Unsafe.Add(ref srcXRef, i - period); double oldY = Unsafe.Add(ref srcYRef, i - period); - if (!double.IsFinite(oldX)) oldX = 0; - if (!double.IsFinite(oldY)) oldY = 0; + if (!double.IsFinite(oldX)) + { + oldX = 0; + } + + if (!double.IsFinite(oldY)) + { + oldY = 0; + } sumX = sumX - oldX + x; sumY = sumY - oldY + y; @@ -468,4 +512,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.cs b/lib/statistics/linreg/LinReg.cs index 11771210..3467c7a0 100644 --- a/lib/statistics/linreg/LinReg.cs +++ b/lib/statistics/linreg/LinReg.cs @@ -76,7 +76,9 @@ public sealed class LinReg : AbstractBase public LinReg(int period, int offset = 0) { if (period <= 0) + { throw new ArgumentException("Period must be greater than 0", nameof(period)); + } _period = period; _offset = offset; @@ -262,7 +264,10 @@ public sealed class LinReg : AbstractBase public override TSeries Update(TSeries source) { - if (source.Count == 0) return new TSeries([], []); + if (source.Count == 0) + { + return new TSeries([], []); + } int len = source.Count; var t = new List(len); @@ -332,12 +337,20 @@ public sealed class LinReg : AbstractBase public static void Calculate(ReadOnlySpan source, Span output, int period, int offset = 0, double initialLastValid = 0) { if (source.Length != output.Length) + { throw new ArgumentException("Source and output must have the same length", nameof(output)); + } + if (period <= 0) + { throw new ArgumentException("Period must be greater than 0", nameof(period)); + } int len = source.Length; - if (len == 0) return; + if (len == 0) + { + return; + } // Stack allocate for typical periods (most < 100) // ArrayPool for large periods to avoid stack overflow @@ -353,87 +366,95 @@ public sealed class LinReg : AbstractBase try { - double sum_y = 0; - double sum_xy = 0; - double lastValid = initialLastValid; - int bufferIndex = 0; - int count = 0; + double sum_y = 0; + double sum_xy = 0; + double lastValid = initialLastValid; + int bufferIndex = 0; + int count = 0; - double full_sum_x = 0.5 * period * (period - 1); - double full_sum_x2 = (period - 1.0) * period * (2.0 * period - 1.0) / 6.0; - double full_denom = period * full_sum_x2 - full_sum_x * full_sum_x; + double full_sum_x = 0.5 * period * (period - 1); + double full_sum_x2 = (period - 1.0) * period * (2.0 * period - 1.0) / 6.0; + double full_denom = period * full_sum_x2 - full_sum_x * full_sum_x; - for (int i = 0; i < len; i++) - { - double val = source[i]; - if (double.IsFinite(val)) - lastValid = val; - else - val = lastValid; - - if (count < period) + for (int i = 0; i < len; i++) { - buffer[count] = val; - sum_y += val; - count++; - - sum_xy = 0; - for (int j = 0; j < count; j++) + double val = source[i]; + if (double.IsFinite(val)) { - sum_xy = Math.FusedMultiplyAdd(count - 1 - j, buffer[j], sum_xy); - } - - if (count <= 1) - { - output[i] = val; + lastValid = val; } else { - double n = count; - double sx = 0.5 * n * (n - 1); - double sx2 = (n - 1.0) * n * (2.0 * n - 1.0) / 6.0; - double denom = n * sx2 - sx * sx; + val = lastValid; + } - if (Math.Abs(denom) < MinDenominator) + if (count < period) + { + buffer[count] = val; + sum_y += val; + count++; + + sum_xy = 0; + for (int j = 0; j < count; j++) + { + sum_xy = Math.FusedMultiplyAdd(count - 1 - j, buffer[j], sum_xy); + } + + if (count <= 1) { output[i] = val; } else { - double m = Math.FusedMultiplyAdd(n, sum_xy, -sx * sum_y) / denom; - double b = Math.FusedMultiplyAdd(-m, sx, sum_y) / n; - output[i] = Math.FusedMultiplyAdd(-m, offset, b); + double n = count; + double sx = 0.5 * n * (n - 1); + double sx2 = (n - 1.0) * n * (2.0 * n - 1.0) / 6.0; + double denom = n * sx2 - sx * sx; + + if (Math.Abs(denom) < MinDenominator) + { + output[i] = val; + } + else + { + double m = Math.FusedMultiplyAdd(n, sum_xy, -sx * sum_y) / denom; + double b = Math.FusedMultiplyAdd(-m, sx, sum_y) / n; + output[i] = Math.FusedMultiplyAdd(-m, offset, b); + } + } + + if (count == period) + { + bufferIndex = 0; } } + else + { + double oldest = buffer[bufferIndex]; + double prev_sum_y = sum_y; - if (count == period) - { - bufferIndex = 0; + sum_xy = sum_xy + prev_sum_y - period * oldest; + sum_y = sum_y - oldest + val; + buffer[bufferIndex] = val; + + bufferIndex++; + if (bufferIndex >= period) + { + bufferIndex = 0; + } + + double m = Math.FusedMultiplyAdd(period, sum_xy, -full_sum_x * sum_y) / full_denom; + double b = Math.FusedMultiplyAdd(-m, full_sum_x, sum_y) / period; + output[i] = Math.FusedMultiplyAdd(-m, offset, b); + } } } - else - { - double oldest = buffer[bufferIndex]; - double prev_sum_y = sum_y; - - sum_xy = sum_xy + prev_sum_y - period * oldest; - sum_y = sum_y - oldest + val; - buffer[bufferIndex] = val; - - bufferIndex++; - if (bufferIndex >= period) - bufferIndex = 0; - - double m = Math.FusedMultiplyAdd(period, sum_xy, -full_sum_x * sum_y) / full_denom; - double b = Math.FusedMultiplyAdd(-m, full_sum_x, sum_y) / period; - output[i] = Math.FusedMultiplyAdd(-m, offset, b); - } - } - } finally { if (rentedBuffer != null) + { ArrayPool.Shared.Return(rentedBuffer); + } } } @@ -448,4 +469,4 @@ public sealed class LinReg : AbstractBase Intercept = 0; RSquared = 0; } -} \ No newline at end of file +} diff --git a/lib/statistics/median/Median.Tests.cs b/lib/statistics/median/Median.Tests.cs index f31ba095..aee7e72e 100644 --- a/lib/statistics/median/Median.Tests.cs +++ b/lib/statistics/median/Median.Tests.cs @@ -264,14 +264,21 @@ public class MedianTests // Arrange int period = 5; double[] data = new double[20]; - for (int i = 0; i < data.Length; i++) data[i] = i; + for (int i = 0; i < data.Length; i++) + { + data[i] = i; + } // Act double[] output = new double[data.Length]; Median.Batch(data, output, period); var series = new TSeries(); - for (int i = 0; i < data.Length; i++) series.Add(new TValue(DateTime.MinValue, data[i])); + for (int i = 0; i < data.Length; i++) + { + series.Add(new TValue(DateTime.MinValue, data[i])); + } + var batchSeries = Median.Batch(series, period); // Assert diff --git a/lib/statistics/median/Median.Validation.Tests.cs b/lib/statistics/median/Median.Validation.Tests.cs index 1e2269c5..ceabf132 100644 --- a/lib/statistics/median/Median.Validation.Tests.cs +++ b/lib/statistics/median/Median.Validation.Tests.cs @@ -86,7 +86,10 @@ public sealed class MedianValidationTests : IDisposable private static double CalculateMedian(List sortedWindow) { int count = sortedWindow.Count; - if (count == 0) return 0; // Or NaN + if (count == 0) + { + return 0; // Or NaN + } int mid = count / 2; if (count % 2 != 0) diff --git a/lib/statistics/median/Median.cs b/lib/statistics/median/Median.cs index b763415b..a2dc527b 100644 --- a/lib/statistics/median/Median.cs +++ b/lib/statistics/median/Median.cs @@ -38,7 +38,9 @@ public sealed class Median : AbstractBase, IDisposable public Median(int period) { if (period <= 0) + { throw new ArgumentException("Period must be greater than 0", nameof(period)); + } _period = period; _buffer = new RingBuffer(period); @@ -76,7 +78,10 @@ public sealed class Median : AbstractBase, IDisposable /// public override void Prime(ReadOnlySpan source, TimeSpan? step = null) { - if (source.Length == 0) return; + if (source.Length == 0) + { + return; + } _buffer.Clear(); int warmupLength = Math.Min(source.Length, WarmupPeriod); @@ -151,7 +156,10 @@ public sealed class Median : AbstractBase, IDisposable public override TSeries Update(TSeries source) { - if (source.Count == 0) return []; + if (source.Count == 0) + { + return []; + } int len = source.Count; var t = new List(len); @@ -178,7 +186,10 @@ public sealed class Median : AbstractBase, IDisposable // validCount = elements in sortedBuffer BEFORE insertion int validCount = _buffer.Count - 1; int index = Array.BinarySearch(_sortedBuffer, 0, validCount, value); - if (index < 0) index = ~index; + if (index < 0) + { + index = ~index; + } if (index < validCount) { @@ -224,12 +235,20 @@ public sealed class Median : AbstractBase, IDisposable 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 <= 0) + { throw new ArgumentException("Period must be greater than 0", nameof(period)); + } int len = source.Length; - if (len == 0) return; + if (len == 0) + { + return; + } // Always use ArrayPool to avoid CS8353 stackalloc escape issues double[] rentedSorted = ArrayPool.Shared.Rent(period); @@ -266,7 +285,10 @@ public sealed class Median : AbstractBase, IDisposable windowIdx = (windowIdx + 1) % period; int newIndex = BinarySearchSpan(sortedBuffer, count, val); - if (newIndex < 0) newIndex = ~newIndex; + if (newIndex < 0) + { + newIndex = ~newIndex; + } if (newIndex < count) { @@ -300,11 +322,18 @@ public sealed class Median : AbstractBase, IDisposable int mid = lo + ((hi - lo) >> 1); int cmp = span[mid].CompareTo(value); if (cmp == 0) + { return mid; + } + if (cmp < 0) + { lo = mid + 1; + } else + { hi = mid - 1; + } } return ~lo; } @@ -325,7 +354,10 @@ public sealed class Median : AbstractBase, IDisposable /// public new void Dispose() { - if (_disposed) return; + if (_disposed) + { + return; + } if (_source != null) { @@ -335,4 +367,4 @@ public sealed class Median : AbstractBase, IDisposable _disposed = true; } -} \ No newline at end of file +} diff --git a/lib/statistics/skew/Skew.Tests.cs b/lib/statistics/skew/Skew.Tests.cs index 08b05737..79df4a00 100644 --- a/lib/statistics/skew/Skew.Tests.cs +++ b/lib/statistics/skew/Skew.Tests.cs @@ -284,7 +284,10 @@ public class SkewTests public void Reset_ClearsState() { var skew = new Skew(5); - for (int i = 0; i < 5; i++) skew.Update(new TValue(DateTime.UtcNow, i)); + for (int i = 0; i < 5; i++) + { + skew.Update(new TValue(DateTime.UtcNow, i)); + } skew.Reset(); Assert.False(skew.IsHot); @@ -380,7 +383,10 @@ public class SkewTests // Create large dataset to trigger SIMD path (>= 256) int count = 1000; var data = new double[count]; - for (int i = 0; i < count; i++) data[i] = (double)i; + for (int i = 0; i < count; i++) + { + data[i] = (double)i; + } var series = new TSeries(new System.Collections.Generic.List(new long[count]), new System.Collections.Generic.List(data)); diff --git a/lib/statistics/skew/Skew.cs b/lib/statistics/skew/Skew.cs index 18a96cff..fe2c19c2 100644 --- a/lib/statistics/skew/Skew.cs +++ b/lib/statistics/skew/Skew.cs @@ -104,7 +104,11 @@ public sealed class Skew : AbstractBase // Calculate 2nd moment (Variance) // m2 = Sum((x-mean)^2) / n = (SumSq - Sum^2/n) / n double m2Numerator = _sumSq - (_sum * _sum) / n; - if (m2Numerator < Epsilon) m2Numerator = 0; + if (m2Numerator < Epsilon) + { + m2Numerator = 0; + } + double m2 = m2Numerator / n; // Calculate 3rd moment @@ -143,7 +147,10 @@ public sealed class Skew : AbstractBase public override TSeries Update(TSeries source) { - if (source.Count == 0) return []; + if (source.Count == 0) + { + return []; + } int len = source.Count; var t = new List(len); @@ -225,12 +232,20 @@ public sealed class Skew : AbstractBase public static void Batch(ReadOnlySpan source, Span output, int period, bool isPopulation = false) { if (source.Length != output.Length) + { throw new ArgumentException("Source and output must have the same length", nameof(output)); + } + if (period < 3) + { throw new ArgumentException("Period must be greater than or equal to 3", nameof(period)); + } int len = source.Length; - if (len == 0) return; + if (len == 0) + { + return; + } // Try SIMD path for large, clean datasets // SIMD overhead amortizes well for datasets >= 256 elements @@ -260,7 +275,10 @@ public sealed class Skew : AbstractBase for (; i < warmupEnd; i++) { double val = source[i]; - if (!double.IsFinite(val)) val = 0; + if (!double.IsFinite(val)) + { + val = 0; + } sum += val; sumSq += val * val; @@ -275,10 +293,16 @@ public sealed class Skew : AbstractBase for (; i < len; i++) { double val = source[i]; - if (!double.IsFinite(val)) val = 0; + if (!double.IsFinite(val)) + { + val = 0; + } double oldVal = source[i - period]; - if (!double.IsFinite(oldVal)) oldVal = 0; + if (!double.IsFinite(oldVal)) + { + oldVal = 0; + } sum = sum - oldVal + val; sumSq = sumSq - (oldVal * oldVal) + (val * val); @@ -297,7 +321,11 @@ public sealed class Skew : AbstractBase for (int k = 0; k < period; k++) { double v = source[startIdx + k]; - if (!double.IsFinite(v)) v = 0; + if (!double.IsFinite(v)) + { + v = 0; + } + recalcSum += v; recalcSumSq += v * v; recalcSumCu += v * v * v; @@ -315,13 +343,20 @@ public sealed class Skew : AbstractBase double mean = sum / n; double m2Numerator = sumSq - (sum * sum) / n; - if (m2Numerator < Epsilon) return 0; + if (m2Numerator < Epsilon) + { + return 0; + } + double m2 = m2Numerator / n; double m3Numerator = sumCu - 3 * mean * sumSq + 2 * n * mean * mean * mean; double m3 = m3Numerator / n; - if (m2 <= Epsilon) return 0; + if (m2 <= Epsilon) + { + return 0; + } double g1 = m3 / (m2 * Math.Sqrt(m2)); @@ -367,7 +402,10 @@ public sealed class Skew : AbstractBase WarmupSkew(period, isPopulation, ref srcRef, ref outRef, out double sum, out double sumSq, out double sumCu); - if (len <= period) return; + if (len <= period) + { + return; + } var vInvN = Vector256.Create(invN); var vN = Vector256.Create(n); @@ -509,4 +547,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.Validation.Tests.cs b/lib/statistics/stddev/StdDev.Validation.Tests.cs index 7205e159..b13dc362 100644 --- a/lib/statistics/stddev/StdDev.Validation.Tests.cs +++ b/lib/statistics/stddev/StdDev.Validation.Tests.cs @@ -19,9 +19,16 @@ public sealed class StdDevValidationTests : IDisposable private void Dispose(bool disposing) { - if (_disposed) return; + if (_disposed) + { + return; + } + _disposed = true; - if (disposing) _testData?.Dispose(); + if (disposing) + { + _testData?.Dispose(); + } } #region Skender Validation diff --git a/lib/statistics/stddev/StdDev.cs b/lib/statistics/stddev/StdDev.cs index e6582226..7dd34db1 100644 --- a/lib/statistics/stddev/StdDev.cs +++ b/lib/statistics/stddev/StdDev.cs @@ -57,7 +57,10 @@ public sealed class StdDev : AbstractBase public override TSeries Update(TSeries source) { - if (source.Count == 0) return []; + if (source.Count == 0) + { + return []; + } int len = source.Count; var t = new List(len); @@ -189,4 +192,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/sum/Sum.Tests.cs b/lib/statistics/sum/Sum.Tests.cs index 404bbae9..5a30de21 100644 --- a/lib/statistics/sum/Sum.Tests.cs +++ b/lib/statistics/sum/Sum.Tests.cs @@ -392,7 +392,9 @@ public class SumTests var gbm = new GBM(startPrice: 100, mu: 0.05, sigma: 0.2, seed: 42); for (int i = 0; i < source.Length; i++) + { source[i] = gbm.Next().Close; + } Sum.Batch(source.AsSpan(), output.AsSpan(), 100); @@ -520,7 +522,9 @@ public class SumTests { var series = new TSeries(); for (int i = 1; i <= 10; i++) + { series.Add(DateTime.UtcNow, i * 10); + } // 10, 20, 30, 40, 50, 60, 70, 80, 90, 100 var (results, indicator) = Sum.Calculate(series, 5); diff --git a/lib/statistics/sum/Sum.Validation.Tests.cs b/lib/statistics/sum/Sum.Validation.Tests.cs index a563a7e3..4f9e2597 100644 --- a/lib/statistics/sum/Sum.Validation.Tests.cs +++ b/lib/statistics/sum/Sum.Validation.Tests.cs @@ -26,9 +26,16 @@ public sealed class SumValidationTests : IDisposable private void Dispose(bool disposing) { - if (_disposed) return; + if (_disposed) + { + return; + } + _disposed = true; - if (disposing) _testData?.Dispose(); + if (disposing) + { + _testData?.Dispose(); + } } [Fact] diff --git a/lib/statistics/sum/Sum.cs b/lib/statistics/sum/Sum.cs index cae44c41..6cf1c820 100644 --- a/lib/statistics/sum/Sum.cs +++ b/lib/statistics/sum/Sum.cs @@ -67,7 +67,9 @@ public sealed class Sum : AbstractBase public Sum(int period) { if (period <= 0) + { throw new ArgumentException("Period must be greater than 0", nameof(period)); + } _period = period; _buffer = new RingBuffer(period); @@ -163,7 +165,10 @@ public sealed class Sum : AbstractBase /// public override void Prime(ReadOnlySpan source, TimeSpan? step = null) { - if (source.Length == 0) return; + if (source.Length == 0) + { + return; + } // Reset state _buffer.Clear(); @@ -284,7 +289,10 @@ public sealed class Sum : AbstractBase public override TSeries Update(TSeries source) { - if (source.Count == 0) return []; + if (source.Count == 0) + { + return []; + } int len = source.Count; var t = new List(len); @@ -325,12 +333,20 @@ public sealed class Sum : AbstractBase 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 <= 0) + { throw new ArgumentException("Period must be greater than 0", nameof(period)); + } int len = source.Length; - if (len == 0) return; + if (len == 0) + { + return; + } CalculateScalarCore(source, output, period); } @@ -378,72 +394,82 @@ public sealed class Sum : AbstractBase int tickCount = 0; // Warmup phase - int warmupEnd = Math.Min(period, len); - for (int i = 0; i < warmupEnd; i++) - { - double val = source[i]; - if (double.IsFinite(val)) - lastValid = val; - else - val = lastValid; + int warmupEnd = Math.Min(period, len); + for (int i = 0; i < warmupEnd; i++) + { + double val = source[i]; + if (double.IsFinite(val)) + { + lastValid = val; + } + else + { + val = lastValid; + } - // Kahan-BabuÅ¡ka add - double y = val - c; - double t = sum + y; - c = t - sum - y; - sum = t; + // Kahan-BabuÅ¡ka add + double y = val - c; + double t = sum + y; + c = t - sum - y; + sum = t; - double z = c - cc; - double tt = sum + z; - cc = tt - sum - z; - sum = tt; + double z = c - cc; + double tt = sum + z; + cc = tt - sum - z; + sum = tt; - buffer[i] = val; - output[i] = sum; - } + buffer[i] = val; + output[i] = sum; + } - // Main phase with sliding window - for (int i = period; i < len; i++) - { - double val = source[i]; - if (double.IsFinite(val)) - lastValid = val; - else - val = lastValid; + // Main phase with sliding window + for (int i = period; i < len; i++) + { + double val = source[i]; + if (double.IsFinite(val)) + { + lastValid = val; + } + else + { + val = lastValid; + } - double oldVal = buffer[bufferIndex]; + double oldVal = buffer[bufferIndex]; - // Kahan-BabuÅ¡ka subtract old value - double yS = -oldVal - c; - double tS = sum + yS; - c = tS - sum - yS; - sum = tS; + // Kahan-BabuÅ¡ka subtract old value + double yS = -oldVal - c; + double tS = sum + yS; + c = tS - sum - yS; + sum = tS; - double zS = c - cc; - double ttS = sum + zS; - cc = ttS - sum - zS; - sum = ttS; + double zS = c - cc; + double ttS = sum + zS; + cc = ttS - sum - zS; + sum = ttS; - // Kahan-BabuÅ¡ka add new value - double yA = val - c; - double tA = sum + yA; - c = tA - sum - yA; - sum = tA; + // Kahan-BabuÅ¡ka add new value + double yA = val - c; + double tA = sum + yA; + c = tA - sum - yA; + sum = tA; - double zA = c - cc; - double ttA = sum + zA; - cc = ttA - sum - zA; - sum = ttA; + double zA = c - cc; + double ttA = sum + zA; + cc = ttA - sum - zA; + sum = ttA; - buffer[bufferIndex] = val; - bufferIndex++; - if (bufferIndex >= period) - bufferIndex = 0; + buffer[bufferIndex] = val; + bufferIndex++; + if (bufferIndex >= period) + { + bufferIndex = 0; + } - output[i] = sum; + output[i] = sum; - // Periodic resync for long sequences - tickCount++; + // Periodic resync for long sequences + tickCount++; if (tickCount >= ResyncInterval) { tickCount = 0; @@ -468,7 +494,10 @@ public sealed class Sum : AbstractBase } finally { - if (bufferArray != null) ArrayPool.Shared.Return(bufferArray); + if (bufferArray != null) + { + ArrayPool.Shared.Return(bufferArray); + } } } @@ -482,4 +511,4 @@ public sealed class Sum : AbstractBase _p_state = default; Last = default; } -} \ No newline at end of file +} diff --git a/lib/statistics/variance/Variance.Quantower.cs b/lib/statistics/variance/Variance.Quantower.cs index 9eee92ba..5801db19 100644 --- a/lib/statistics/variance/Variance.Quantower.cs +++ b/lib/statistics/variance/Variance.Quantower.cs @@ -52,7 +52,9 @@ public sealed class VarianceIndicator : Indicator, IWatchlistIndicator protected override void OnUpdate(UpdateArgs args) { if (args.Reason != UpdateReason.NewBar && args.Reason != UpdateReason.HistoricalBar) + { return; + } var item = this.HistoricalData[this.Count - 1, SeekOriginHistory.Begin]; double value = _priceSelector(item); diff --git a/lib/statistics/variance/Variance.Tests.cs b/lib/statistics/variance/Variance.Tests.cs index 59597ef6..6806f7e8 100644 --- a/lib/statistics/variance/Variance.Tests.cs +++ b/lib/statistics/variance/Variance.Tests.cs @@ -46,20 +46,20 @@ public class VarianceTests { // Use simple known values for easier debugging var variance = new Variance(3); - + // Add 3 values: 1, 2, 3 variance.Update(new TValue(DateTime.UtcNow, 1), isNew: true); variance.Update(new TValue(DateTime.UtcNow, 2), isNew: true); var originalResult = variance.Update(new TValue(DateTime.UtcNow, 3), isNew: true); - + double expectedVariance = originalResult.Value; // Variance of [1,2,3] - + // Now correct the 3rd value to 10 (isNew=false) variance.Update(new TValue(DateTime.UtcNow, 10), isNew: false); - + // Correct back to original value 3 (isNew=false) var restoredResult = variance.Update(new TValue(DateTime.UtcNow, 3), isNew: false); - + // Should match original variance Assert.Equal(expectedVariance, restoredResult.Value, 1e-10); } @@ -351,7 +351,10 @@ public class VarianceTests // Create large dataset to trigger SIMD path (>= 256) const int count = 1000; var data = new double[count]; - for (int i = 0; i < count; i++) data[i] = (double)i; + for (int i = 0; i < count; i++) + { + data[i] = (double)i; + } var series = new TSeries(new System.Collections.Generic.List(new long[count]), new System.Collections.Generic.List(data)); @@ -714,4 +717,4 @@ public class VarianceTests Assert.Equal(0, output[0]); // N=1 Assert.Equal(50, output[1]); // Var([10,20]) = 50 } -} \ No newline at end of file +} diff --git a/lib/statistics/variance/Variance.cs b/lib/statistics/variance/Variance.cs index 8e08f8dd..7fa8b372 100644 --- a/lib/statistics/variance/Variance.cs +++ b/lib/statistics/variance/Variance.cs @@ -100,7 +100,10 @@ public sealed class Variance : AbstractBase double numerator = _sumSq - (_buffer.Sum * _buffer.Sum) / n; // Handle floating point noise - if (numerator < 0) numerator = 0; + if (numerator < 0) + { + numerator = 0; + } double denominator = _isPopulation ? n : (n - 1); variance = numerator / denominator; @@ -113,7 +116,10 @@ public sealed class Variance : AbstractBase public override TSeries Update(TSeries source) { - if (source.Count == 0) return []; + if (source.Count == 0) + { + return []; + } int len = source.Count; var t = new List(len); @@ -181,12 +187,20 @@ public sealed class Variance : AbstractBase public static void Batch(ReadOnlySpan source, Span output, int period, bool isPopulation = false) { if (source.Length != output.Length) + { throw new ArgumentException("Source and output must have the same length", nameof(output)); + } + if (period < 2) + { throw new ArgumentException("Period must be greater than or equal to 2", nameof(period)); + } int len = source.Length; - if (len == 0) return; + if (len == 0) + { + return; + } // Try SIMD path for large, clean datasets const int SimdThreshold = 256; @@ -237,7 +251,10 @@ public sealed class Variance : AbstractBase for (; i < warmupEnd; i++) { double val = source[i]; - if (!double.IsFinite(val)) val = 0; // Fallback + if (!double.IsFinite(val)) + { + val = 0; // Fallback + } sum += val; sumSq = Math.FusedMultiplyAdd(val, val, sumSq); @@ -247,7 +264,11 @@ public sealed class Variance : AbstractBase if (n > 1) { double numerator = sumSq - (sum * sum) / n; - if (numerator < 0) numerator = 0; + if (numerator < 0) + { + numerator = 0; + } + double denominator = isPopulation ? n : (n - 1); output[i] = numerator / denominator; } @@ -262,7 +283,10 @@ public sealed class Variance : AbstractBase for (; i < len; i++) { double val = source[i]; - if (!double.IsFinite(val)) val = 0; // Fallback + if (!double.IsFinite(val)) + { + val = 0; // Fallback + } double oldVal = buffer[bufferIndex]; @@ -272,11 +296,18 @@ public sealed class Variance : AbstractBase buffer[bufferIndex] = val; bufferIndex++; - if (bufferIndex >= period) bufferIndex = 0; + if (bufferIndex >= period) + { + bufferIndex = 0; + } double n = period; double numerator = sumSq - (sum * sum) / n; - if (numerator < 0) numerator = 0; + if (numerator < 0) + { + numerator = 0; + } + double denominator = isPopulation ? n : (n - 1); output[i] = numerator / denominator; @@ -305,7 +336,11 @@ public sealed class Variance : AbstractBase if (n > 1) { double num = sumSq - (sum * sum) / n; - if (num < 0) num = 0; + if (num < 0) + { + num = 0; + } + double den = isPopulation ? n : (n - 1); Unsafe.Add(ref outRef, i) = num / den; } @@ -330,7 +365,10 @@ public sealed class Variance : AbstractBase WarmupVariance(period, isPopulation, ref srcRef, ref outRef, out double sum, out double sumSq); - if (len <= period) return; + if (len <= period) + { + return; + } var vInvN = Vector512.Create(invN); var vInvDenom = Vector512.Create(invDenom); @@ -420,7 +458,11 @@ public sealed class Variance : AbstractBase sumSq = Math.FusedMultiplyAdd(val, val, sumSq); double numerator = sumSq - sum * sum * invN; - if (numerator < 0) numerator = 0; + if (numerator < 0) + { + numerator = 0; + } + Unsafe.Add(ref outRef, i) = numerator * invDenom; } } @@ -439,7 +481,10 @@ public sealed class Variance : AbstractBase WarmupVariance(period, isPopulation, ref srcRef, ref outRef, out double sum, out double sumSq); - if (len <= period) return; + if (len <= period) + { + return; + } var vInvN = Vector128.Create(invN); var vInvDenom = Vector128.Create(invDenom); @@ -517,7 +562,11 @@ public sealed class Variance : AbstractBase sumSq = Math.FusedMultiplyAdd(val, val, sumSq); double numerator = sumSq - sum * sum * invN; - if (numerator < 0) numerator = 0; + if (numerator < 0) + { + numerator = 0; + } + Unsafe.Add(ref outRef, i) = numerator * invDenom; } } @@ -536,7 +585,10 @@ public sealed class Variance : AbstractBase WarmupVariance(period, isPopulation, ref srcRef, ref outRef, out double sum, out double sumSq); - if (len <= period) return; + if (len <= period) + { + return; + } var vInvN = Vector256.Create(invN); var vInvDenom = Vector256.Create(invDenom); @@ -634,8 +686,12 @@ public sealed class Variance : AbstractBase sumSq = Math.FusedMultiplyAdd(val, val, sumSq); double numerator = sumSq - sum * sum * invN; - if (numerator < 0) numerator = 0; + if (numerator < 0) + { + numerator = 0; + } + Unsafe.Add(ref outRef, i) = numerator * invDenom; } } -} \ No newline at end of file +} diff --git a/lib/trends_FIR/alma/Alma.cs b/lib/trends_FIR/alma/Alma.cs index 0bb55b66..881d6aa1 100644 --- a/lib/trends_FIR/alma/Alma.cs +++ b/lib/trends_FIR/alma/Alma.cs @@ -46,11 +46,19 @@ public sealed class Alma : AbstractBase public Alma(int period, double offset = 0.85, double sigma = 6.0) { if (period <= 0) + { throw new ArgumentException("Period must be greater than 0", nameof(period)); + } + if (sigma <= 0) + { throw new ArgumentException("Sigma must be greater than 0", nameof(sigma)); + } + if (offset < 0 || offset > 1) + { throw new ArgumentOutOfRangeException(nameof(offset), "Offset must be between 0 and 1"); + } _period = period; _offset = offset; @@ -161,7 +169,10 @@ public sealed class Alma : AbstractBase public override TSeries Update(TSeries source) { - if (source.Count == 0) return new TSeries([], []); + if (source.Count == 0) + { + return new TSeries([], []); + } int len = source.Count; var t = new List(len); @@ -201,7 +212,10 @@ public sealed class Alma : AbstractBase private double CalculateWeightedSum() { int count = _buffer.Count; - if (count == 0) return 0; + if (count == 0) + { + return 0; + } if (count < _period) { @@ -250,13 +264,24 @@ public sealed class Alma : AbstractBase public static void Calculate(ReadOnlySpan source, Span output, int period, double offset = 0.85, double sigma = 6.0) { if (period <= 0) + { throw new ArgumentException("Period must be greater than 0", nameof(period)); + } + if (sigma <= 0) + { throw new ArgumentException("Sigma must be greater than 0", nameof(sigma)); + } + if (offset < 0 || offset > 1) + { throw new ArgumentOutOfRangeException(nameof(offset), "Offset must be between 0 and 1"); + } + if (source.Length != output.Length) + { throw new ArgumentException("Source and output must have the same length", nameof(output)); + } // Allocation Strategy: Stack for small periods, Pool for large double[]? weightsArray = period > 256 ? ArrayPool.Shared.Rent(period) : null; @@ -352,8 +377,15 @@ public sealed class Alma : AbstractBase } finally { - if (weightsArray != null) ArrayPool.Shared.Return(weightsArray); - if (bufferArray != null) ArrayPool.Shared.Return(bufferArray); + if (weightsArray != null) + { + ArrayPool.Shared.Return(weightsArray); + } + + if (bufferArray != null) + { + ArrayPool.Shared.Return(bufferArray); + } } } diff --git a/lib/trends_FIR/blma/Blma.Tests.cs b/lib/trends_FIR/blma/Blma.Tests.cs index 2d3cdc9e..2a351821 100644 --- a/lib/trends_FIR/blma/Blma.Tests.cs +++ b/lib/trends_FIR/blma/Blma.Tests.cs @@ -170,4 +170,4 @@ public class BlmaTests Assert.Equal(input[1].AsDateTime, timestamps[1]); Assert.Equal(input[2].AsDateTime, timestamps[2]); } -} \ No newline at end of file +} diff --git a/lib/trends_FIR/blma/Blma.cs b/lib/trends_FIR/blma/Blma.cs index 821cc019..9f09c406 100644 --- a/lib/trends_FIR/blma/Blma.cs +++ b/lib/trends_FIR/blma/Blma.cs @@ -308,7 +308,11 @@ public sealed class Blma : AbstractBase { int srcIdx = i - count + 1 + j; double srcVal = source[srcIdx]; - if (!double.IsFinite(srcVal)) srcVal = lastValid; + if (!double.IsFinite(srcVal)) + { + srcVal = lastValid; + } + sum += srcVal * currentWeights[j]; } @@ -317,7 +321,11 @@ public sealed class Blma : AbstractBase { int srcIdx = i - count + 1 + j; double srcVal = source[srcIdx]; - if (!double.IsFinite(srcVal)) srcVal = lastValid; + if (!double.IsFinite(srcVal)) + { + srcVal = lastValid; + } + avg += srcVal; } avg /= count; @@ -334,7 +342,11 @@ public sealed class Blma : AbstractBase { int srcIdx = i - period + 1 + j; double srcVal = source[srcIdx]; - if (!double.IsFinite(srcVal)) srcVal = lastValid; + if (!double.IsFinite(srcVal)) + { + srcVal = lastValid; + } + sum += srcVal * weights[j]; avg += srcVal; } @@ -361,4 +373,4 @@ public sealed class Blma : AbstractBase { Calculate(source, destination, period); } -} \ No newline at end of file +} diff --git a/lib/trends_FIR/bwma/Bwma.Quantower.Tests.cs b/lib/trends_FIR/bwma/Bwma.Quantower.Tests.cs index 0769de2f..edc6b2c4 100644 --- a/lib/trends_FIR/bwma/Bwma.Quantower.Tests.cs +++ b/lib/trends_FIR/bwma/Bwma.Quantower.Tests.cs @@ -214,4 +214,4 @@ public class BwmaIndicatorTests Assert.Contains("Bessel", indicator.Description, StringComparison.Ordinal); } -} \ No newline at end of file +} diff --git a/lib/trends_FIR/bwma/Bwma.Quantower.cs b/lib/trends_FIR/bwma/Bwma.Quantower.cs index 5905365b..d446850b 100644 --- a/lib/trends_FIR/bwma/Bwma.Quantower.cs +++ b/lib/trends_FIR/bwma/Bwma.Quantower.cs @@ -54,7 +54,9 @@ public sealed class BwmaIndicator : Indicator, IWatchlistIndicator protected override void OnUpdate(UpdateArgs args) { if (args.Reason != UpdateReason.NewBar && args.Reason != UpdateReason.HistoricalBar && args.Reason != UpdateReason.NewTick) + { return; + } var item = HistoricalData[Count - 1, SeekOriginHistory.Begin]; TValue result = _ma.Update(new TValue(item.TimeLeft.Ticks, _priceSelector(item)), args.IsNewBar()); @@ -62,4 +64,4 @@ public sealed class BwmaIndicator : Indicator, IWatchlistIndicator _series.SetValue(result.Value, _ma.IsHot, ShowColdValues); _series.SetMarker(0, Color.Transparent); } -} \ No newline at end of file +} diff --git a/lib/trends_FIR/bwma/Bwma.Validation.Tests.cs b/lib/trends_FIR/bwma/Bwma.Validation.Tests.cs index 0337149e..13ab185b 100644 --- a/lib/trends_FIR/bwma/Bwma.Validation.Tests.cs +++ b/lib/trends_FIR/bwma/Bwma.Validation.Tests.cs @@ -23,7 +23,11 @@ public sealed class BwmaValidationTests : IDisposable private void Dispose(bool disposing) { - if (_disposed) return; + if (_disposed) + { + return; + } + _disposed = true; if (disposing) { @@ -345,4 +349,4 @@ public sealed class BwmaValidationTests : IDisposable // The parabolic window emphasizes the center, so result should be > mean (1.8) Assert.True(result.Value > 1.8); } -} \ No newline at end of file +} diff --git a/lib/trends_FIR/bwma/Bwma.cs b/lib/trends_FIR/bwma/Bwma.cs index 3c3ac2f1..0339e0c5 100644 --- a/lib/trends_FIR/bwma/Bwma.cs +++ b/lib/trends_FIR/bwma/Bwma.cs @@ -47,9 +47,14 @@ public sealed class Bwma : AbstractBase public Bwma(int period, int order = 0) { if (period <= 0) + { throw new ArgumentException("Period must be greater than 0", nameof(period)); + } + if (order < 0) + { throw new ArgumentOutOfRangeException(nameof(order), "Order must be non-negative"); + } _period = period; _order = order; @@ -180,7 +185,10 @@ public sealed class Bwma : AbstractBase public override TSeries Update(TSeries source) { - if (source.Count == 0) return new TSeries([], []); + if (source.Count == 0) + { + return new TSeries([], []); + } int len = source.Count; var t = new List(len); @@ -241,13 +249,20 @@ public sealed class Bwma : AbstractBase private double CalculateWeightedSum(double fallbackValue) { int count = _buffer.Count; - if (count == 0) return 0; + if (count == 0) + { + return 0; + } if (count < _period) + { return CalculateWeightedSumWarmup(_buffer.GetSpan(), count, _order, _power, fallbackValue); + } if (_invWeightSum == 0.0) + { return fallbackValue; + } ReadOnlySpan internalBuf = _buffer.InternalBuffer; int head = _buffer.StartIndex; @@ -262,9 +277,20 @@ public sealed class Bwma : AbstractBase [MethodImpl(MethodImplOptions.AggressiveInlining)] private static double CalculateWeightedSumWarmup(ReadOnlySpan window, int p, int order, double power, double fallbackValue) { - if (p <= 0) return 0.0; - if (p == 1) return fallbackValue; - if (p == 2) return fallbackValue; + if (p <= 0) + { + return 0.0; + } + + if (p == 1) + { + return fallbackValue; + } + + if (p == 2) + { + return fallbackValue; + } double scale = 2.0 / (p - 1); double sum = 0.0; @@ -275,7 +301,9 @@ public sealed class Bwma : AbstractBase double x = Math.FusedMultiplyAdd(i, scale, -1.0); double arg = Math.FusedMultiplyAdd(-x, x, 1.0); if (arg <= 0.0) + { continue; + } double w; if (order == 0) @@ -292,7 +320,9 @@ public sealed class Bwma : AbstractBase } if (w == 0.0) + { continue; + } sum = Math.FusedMultiplyAdd(window[i], w, sum); wSum += w; @@ -311,14 +341,25 @@ public sealed class Bwma : AbstractBase public static void Calculate(ReadOnlySpan source, Span output, int period, int order = 0) { if (period <= 0) + { throw new ArgumentException("Period must be greater than 0", nameof(period)); + } + if (order < 0) + { throw new ArgumentOutOfRangeException(nameof(order), "Order must be non-negative"); + } + if (source.Length != output.Length) + { throw new ArgumentException("Source and output must have the same length", nameof(output)); + } int len = source.Length; - if (len == 0) return; + if (len == 0) + { + return; + } double power = order * 0.5 + 0.5; @@ -352,7 +393,10 @@ public sealed class Bwma : AbstractBase } finally { - if (bufferArray != null) ArrayPool.Shared.Return(bufferArray); + if (bufferArray != null) + { + ArrayPool.Shared.Return(bufferArray); + } } return; @@ -390,9 +434,15 @@ public sealed class Bwma : AbstractBase ring[ringIdx] = val; ringIdx++; - if (ringIdx >= period) ringIdx = 0; + if (ringIdx >= period) + { + ringIdx = 0; + } - if (count < period) count++; + if (count < period) + { + count++; + } if (count < period) { @@ -415,8 +465,15 @@ public sealed class Bwma : AbstractBase } finally { - if (weightsArray != null) ArrayPool.Shared.Return(weightsArray); - if (ringArray != null) ArrayPool.Shared.Return(ringArray); + if (weightsArray != null) + { + ArrayPool.Shared.Return(weightsArray); + } + + if (ringArray != null) + { + ArrayPool.Shared.Return(ringArray); + } } } diff --git a/lib/trends_FIR/conv/Conv.cs b/lib/trends_FIR/conv/Conv.cs index e99e6111..68fb7d75 100644 --- a/lib/trends_FIR/conv/Conv.cs +++ b/lib/trends_FIR/conv/Conv.cs @@ -40,7 +40,9 @@ public sealed class Conv : AbstractBase public Conv(double[] kernel) { if (kernel == null || kernel.Length == 0) + { throw new ArgumentException("Kernel must not be empty", nameof(kernel)); + } _period = kernel.Length; _kernel = new double[_period]; @@ -134,7 +136,10 @@ public sealed class Conv : AbstractBase public override TSeries Update(TSeries source) { - if (source.Count == 0) return []; + if (source.Count == 0) + { + return []; + } int len = source.Count; List t = new(len); @@ -209,13 +214,21 @@ public sealed class Conv : AbstractBase public static void Batch(ReadOnlySpan source, Span output, double[] kernel) { if (source.Length != output.Length) + { throw new ArgumentException("Source and output must have the same length", nameof(output)); + } + if (kernel == null || kernel.Length == 0) + { throw new ArgumentException("Kernel must not be empty", nameof(kernel)); + } int len = source.Length; int period = kernel.Length; - if (len == 0) return; + if (len == 0) + { + return; + } // Use stackalloc for small kernels to avoid heap allocation Span window = period <= 256 ? stackalloc double[period] : new double[period]; @@ -240,9 +253,15 @@ public sealed class Conv : AbstractBase window[windowIdx] = val; windowIdx = (windowIdx + 1); - if (windowIdx >= period) windowIdx = 0; + if (windowIdx >= period) + { + windowIdx = 0; + } - if (count < period) count++; + if (count < period) + { + count++; + } double sum = 0; diff --git a/lib/trends_FIR/dwma/Dwma.cs b/lib/trends_FIR/dwma/Dwma.cs index 63d0da18..cca70d18 100644 --- a/lib/trends_FIR/dwma/Dwma.cs +++ b/lib/trends_FIR/dwma/Dwma.cs @@ -33,7 +33,9 @@ public sealed class Dwma : AbstractBase public Dwma(int period) { if (period <= 0) + { throw new ArgumentException("Period must be greater than 0", nameof(period)); + } _period = period; _wma1 = new Wma(period); @@ -61,7 +63,10 @@ public sealed class Dwma : AbstractBase [MethodImpl(MethodImplOptions.AggressiveInlining)] public override TValue Update(TValue input, bool isNew = true) { - if (isNew) _sampleCount++; + if (isNew) + { + _sampleCount++; + } TValue wma1Result = _wma1.Update(input, isNew); Last = _wma2.Update(wma1Result, isNew); @@ -71,7 +76,10 @@ public sealed class Dwma : AbstractBase public override TSeries Update(TSeries source) { - if (source.Count == 0) return []; + if (source.Count == 0) + { + return []; + } int len = source.Count; var t = new List(len); @@ -124,12 +132,20 @@ public sealed class Dwma : AbstractBase public static void Calculate(ReadOnlySpan source, Span output, int period) { if (period <= 0) + { throw new ArgumentException("Period must be greater than 0", nameof(period)); + } + if (source.Length != output.Length) + { throw new ArgumentException("Source and output must have the same length", nameof(output)); + } int len = source.Length; - if (len == 0) return; + if (len == 0) + { + return; + } double[]? tempArray = len > 1024 ? ArrayPool.Shared.Rent(len) : null; Span temp = len <= 1024 @@ -143,7 +159,10 @@ public sealed class Dwma : AbstractBase } finally { - if (tempArray != null) ArrayPool.Shared.Return(tempArray); + if (tempArray != null) + { + ArrayPool.Shared.Return(tempArray); + } } } diff --git a/lib/trends_FIR/gwma/Gwma.Quantower.Tests.cs b/lib/trends_FIR/gwma/Gwma.Quantower.Tests.cs index db24a403..c23bcdfd 100644 --- a/lib/trends_FIR/gwma/Gwma.Quantower.Tests.cs +++ b/lib/trends_FIR/gwma/Gwma.Quantower.Tests.cs @@ -203,4 +203,4 @@ public class GwmaIndicatorTests // Different sigma should produce different results Assert.NotEqual(narrowResult, wideResult); } -} \ No newline at end of file +} diff --git a/lib/trends_FIR/gwma/Gwma.Quantower.cs b/lib/trends_FIR/gwma/Gwma.Quantower.cs index 14b9433d..2e8e2700 100644 --- a/lib/trends_FIR/gwma/Gwma.Quantower.cs +++ b/lib/trends_FIR/gwma/Gwma.Quantower.cs @@ -58,4 +58,4 @@ public class GwmaIndicator : Indicator, IWatchlistIndicator Series.SetValue(result.Value, ma.IsHot, ShowColdValues); } -} \ No newline at end of file +} diff --git a/lib/trends_FIR/gwma/Gwma.Validation.Tests.cs b/lib/trends_FIR/gwma/Gwma.Validation.Tests.cs index 8ec919a2..f1b44912 100644 --- a/lib/trends_FIR/gwma/Gwma.Validation.Tests.cs +++ b/lib/trends_FIR/gwma/Gwma.Validation.Tests.cs @@ -242,4 +242,4 @@ public sealed class GwmaValidationTests : IDisposable Assert.Equal(expected, gwma.Last.Value, 1e-10); } -} \ No newline at end of file +} diff --git a/lib/trends_FIR/gwma/Gwma.cs b/lib/trends_FIR/gwma/Gwma.cs index 8bf5ae56..a872ce4c 100644 --- a/lib/trends_FIR/gwma/Gwma.cs +++ b/lib/trends_FIR/gwma/Gwma.cs @@ -49,11 +49,19 @@ public sealed class Gwma : AbstractBase public Gwma(int period, double sigma = 0.4) { if (period <= 0) + { throw new ArgumentException("Period must be greater than 0", nameof(period)); + } + if (sigma <= 0) + { throw new ArgumentException("Sigma must be greater than 0", nameof(sigma)); + } + if (sigma > 1) + { throw new ArgumentOutOfRangeException(nameof(sigma), "Sigma must be between 0 and 1"); + } _period = period; _sigma = sigma; @@ -159,7 +167,10 @@ public sealed class Gwma : AbstractBase public override TSeries Update(TSeries source) { - if (source.Count == 0) return new TSeries([], []); + if (source.Count == 0) + { + return new TSeries([], []); + } int len = source.Count; var t = new List(len); @@ -218,7 +229,10 @@ public sealed class Gwma : AbstractBase private double CalculateWeightedSum(double fallbackValue) { int count = _buffer.Count; - if (count == 0) return 0; + if (count == 0) + { + return 0; + } if (count < _period) { @@ -226,7 +240,9 @@ public sealed class Gwma : AbstractBase } if (_invWeightSum == 0.0) + { return fallbackValue; + } ReadOnlySpan internalBuf = _buffer.InternalBuffer; int head = _buffer.StartIndex; @@ -242,8 +258,15 @@ public sealed class Gwma : AbstractBase [MethodImpl(MethodImplOptions.AggressiveInlining)] private static double CalculateWeightedSumWarmup(ReadOnlySpan window, int p, double sigma, double fallbackValue) { - if (p <= 0) return 0.0; - if (p == 1) return fallbackValue; + if (p <= 0) + { + return 0.0; + } + + if (p == 1) + { + return fallbackValue; + } double center = (p - 1) * 0.5; double invSigmaP = 1.0 / (sigma * p); @@ -271,16 +294,30 @@ public sealed class Gwma : AbstractBase public static void Calculate(ReadOnlySpan source, Span output, int period, double sigma = 0.4) { if (period <= 0) + { throw new ArgumentException("Period must be greater than 0", nameof(period)); + } + if (sigma <= 0) + { throw new ArgumentException("Sigma must be greater than 0", nameof(sigma)); + } + if (sigma > 1) + { throw new ArgumentOutOfRangeException(nameof(sigma), "Sigma must be between 0 and 1"); + } + if (source.Length != output.Length) + { throw new ArgumentException("Source and output must have the same length", nameof(output)); + } int len = source.Length; - if (len == 0) return; + if (len == 0) + { + return; + } if (period > len) { @@ -316,7 +353,10 @@ public sealed class Gwma : AbstractBase } finally { - if (bufferArray != null) ArrayPool.Shared.Return(bufferArray); + if (bufferArray != null) + { + ArrayPool.Shared.Return(bufferArray); + } } return; @@ -358,9 +398,15 @@ public sealed class Gwma : AbstractBase ring[ringIdx] = val; ringIdx++; - if (ringIdx >= period) ringIdx = 0; + if (ringIdx >= period) + { + ringIdx = 0; + } - if (count < period) count++; + if (count < period) + { + count++; + } if (count < period) { @@ -383,8 +429,15 @@ public sealed class Gwma : AbstractBase } finally { - if (weightsArray != null) ArrayPool.Shared.Return(weightsArray); - if (ringArray != null) ArrayPool.Shared.Return(ringArray); + if (weightsArray != null) + { + ArrayPool.Shared.Return(weightsArray); + } + + if (ringArray != null) + { + ArrayPool.Shared.Return(ringArray); + } } } @@ -395,4 +448,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.Quantower.Tests.cs b/lib/trends_FIR/hamma/Hamma.Quantower.Tests.cs index 50528540..b8a86572 100644 --- a/lib/trends_FIR/hamma/Hamma.Quantower.Tests.cs +++ b/lib/trends_FIR/hamma/Hamma.Quantower.Tests.cs @@ -164,4 +164,4 @@ public class HammaIndicatorTests Assert.Equal(20, indicator.Period); Assert.Equal(0, HammaIndicator.MinHistoryDepths); } -} \ No newline at end of file +} diff --git a/lib/trends_FIR/hamma/Hamma.Quantower.cs b/lib/trends_FIR/hamma/Hamma.Quantower.cs index f9e1d7cd..5c3b5778 100644 --- a/lib/trends_FIR/hamma/Hamma.Quantower.cs +++ b/lib/trends_FIR/hamma/Hamma.Quantower.cs @@ -55,4 +55,4 @@ public class HammaIndicator : Indicator, IWatchlistIndicator Series.SetValue(result.Value, ma.IsHot, ShowColdValues); } -} \ 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 28778e1b..3bbc870b 100644 --- a/lib/trends_FIR/hamma/Hamma.Tests.cs +++ b/lib/trends_FIR/hamma/Hamma.Tests.cs @@ -409,4 +409,4 @@ public class HammaTests Assert.Equal(i * 10.0, result.Value, 1e-9); } } -} \ No newline at end of file +} diff --git a/lib/trends_FIR/hamma/Hamma.Validation.Tests.cs b/lib/trends_FIR/hamma/Hamma.Validation.Tests.cs index 2e76ff86..e478175a 100644 --- a/lib/trends_FIR/hamma/Hamma.Validation.Tests.cs +++ b/lib/trends_FIR/hamma/Hamma.Validation.Tests.cs @@ -203,4 +203,4 @@ public sealed class HammaValidationTests : IDisposable // All edge weights should be equal Assert.Equal(w0, w4, 1e-10); } -} \ No newline at end of file +} diff --git a/lib/trends_FIR/hamma/Hamma.cs b/lib/trends_FIR/hamma/Hamma.cs index 8ef2a0de..d4d125b5 100644 --- a/lib/trends_FIR/hamma/Hamma.cs +++ b/lib/trends_FIR/hamma/Hamma.cs @@ -62,7 +62,9 @@ public sealed class Hamma : AbstractBase public Hamma(int period = 10) { if (period <= 0) + { throw new ArgumentException("Period must be greater than 0", nameof(period)); + } _period = period; _buffer = new RingBuffer(period); @@ -184,7 +186,10 @@ public sealed class Hamma : AbstractBase public override TSeries Update(TSeries source) { - if (source.Count == 0) return new TSeries([], []); + if (source.Count == 0) + { + return new TSeries([], []); + } int len = source.Count; var t = new List(len); @@ -224,7 +229,10 @@ public sealed class Hamma : AbstractBase private double CalculateWeightedSum() { int count = _buffer.Count; - if (count == 0) return 0; + if (count == 0) + { + return 0; + } if (count < _period) { @@ -283,9 +291,14 @@ public sealed class Hamma : AbstractBase public static void Calculate(ReadOnlySpan source, Span output, int period = 10) { if (period <= 0) + { throw new ArgumentException("Period must be greater than 0", nameof(period)); + } + if (source.Length != output.Length) + { throw new ArgumentException("Source and output must have the same length", nameof(output)); + } // Allocation Strategy: Stack for small periods, Pool for large double[]? weightsArray = period > 256 ? ArrayPool.Shared.Rent(period) : null; @@ -381,8 +394,15 @@ public sealed class Hamma : AbstractBase } finally { - if (weightsArray != null) ArrayPool.Shared.Return(weightsArray); - if (bufferArray != null) ArrayPool.Shared.Return(bufferArray); + if (weightsArray != null) + { + ArrayPool.Shared.Return(weightsArray); + } + + if (bufferArray != null) + { + ArrayPool.Shared.Return(bufferArray); + } } } diff --git a/lib/trends_FIR/hanma/Hanma.Quantower.Tests.cs b/lib/trends_FIR/hanma/Hanma.Quantower.Tests.cs index 399f982b..887758c9 100644 --- a/lib/trends_FIR/hanma/Hanma.Quantower.Tests.cs +++ b/lib/trends_FIR/hanma/Hanma.Quantower.Tests.cs @@ -164,4 +164,4 @@ public class HanmaIndicatorTests Assert.Equal(20, indicator.Period); Assert.Equal(0, HanmaIndicator.MinHistoryDepths); } -} \ No newline at end of file +} diff --git a/lib/trends_FIR/hanma/Hanma.Quantower.cs b/lib/trends_FIR/hanma/Hanma.Quantower.cs index 81498704..968789e6 100644 --- a/lib/trends_FIR/hanma/Hanma.Quantower.cs +++ b/lib/trends_FIR/hanma/Hanma.Quantower.cs @@ -55,4 +55,4 @@ public class HanmaIndicator : Indicator, IWatchlistIndicator Series.SetValue(result.Value, ma.IsHot, ShowColdValues); } -} \ 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 1d08d660..a5cd1d3d 100644 --- a/lib/trends_FIR/hanma/Hanma.Tests.cs +++ b/lib/trends_FIR/hanma/Hanma.Tests.cs @@ -444,4 +444,4 @@ public class HanmaTests // Result should be 100.0 (weighted average of middle values only) Assert.Equal(100.0, hanma.Last.Value, 1e-9); } -} \ No newline at end of file +} diff --git a/lib/trends_FIR/hanma/Hanma.Validation.Tests.cs b/lib/trends_FIR/hanma/Hanma.Validation.Tests.cs index a79c508f..ecb505f2 100644 --- a/lib/trends_FIR/hanma/Hanma.Validation.Tests.cs +++ b/lib/trends_FIR/hanma/Hanma.Validation.Tests.cs @@ -172,4 +172,4 @@ public class HanmaValidationTests // Should be different (different window coefficients) Assert.NotEqual(hanmaResults.Last.Value, hammaResults.Last.Value); } -} \ No newline at end of file +} diff --git a/lib/trends_FIR/hanma/Hanma.cs b/lib/trends_FIR/hanma/Hanma.cs index 868310bf..e9d75aa6 100644 --- a/lib/trends_FIR/hanma/Hanma.cs +++ b/lib/trends_FIR/hanma/Hanma.cs @@ -58,7 +58,9 @@ public sealed class Hanma : AbstractBase public Hanma(int period = 10) { if (period <= 0) + { throw new ArgumentException("Period must be greater than 0", nameof(period)); + } _period = period; _buffer = new RingBuffer(period); @@ -178,7 +180,10 @@ public sealed class Hanma : AbstractBase public override TSeries Update(TSeries source) { - if (source.Count == 0) return new TSeries([], []); + if (source.Count == 0) + { + return new TSeries([], []); + } int len = source.Count; var t = new List(len); @@ -218,7 +223,10 @@ public sealed class Hanma : AbstractBase private double CalculateWeightedSum() { int count = _buffer.Count; - if (count == 0) return 0; + if (count == 0) + { + return 0; + } if (count < _period) { @@ -289,9 +297,14 @@ public sealed class Hanma : AbstractBase public static void Calculate(ReadOnlySpan source, Span output, int period = 10) { if (period <= 0) + { throw new ArgumentException("Period must be greater than 0", nameof(period)); + } + if (source.Length != output.Length) + { throw new ArgumentException("Source and output must have the same length", nameof(output)); + } // Allocation Strategy: Stack for small periods, Pool for large double[]? weightsArray = period > 256 ? ArrayPool.Shared.Rent(period) : null; @@ -393,15 +406,22 @@ public sealed class Hanma : AbstractBase if (startIdx + count <= period) { for (int j = 0; j < count; j++) + { avg += buffer[startIdx + j]; + } } else { int p1Len = period - startIdx; for (int j = 0; j < p1Len; j++) + { avg += buffer[startIdx + j]; + } + for (int j = 0; j < count - p1Len; j++) + { avg += buffer[j]; + } } output[i] = avg / count; } @@ -410,8 +430,15 @@ public sealed class Hanma : AbstractBase } finally { - if (weightsArray != null) ArrayPool.Shared.Return(weightsArray); - if (bufferArray != null) ArrayPool.Shared.Return(bufferArray); + if (weightsArray != null) + { + ArrayPool.Shared.Return(weightsArray); + } + + if (bufferArray != null) + { + ArrayPool.Shared.Return(bufferArray); + } } } @@ -422,4 +449,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.cs b/lib/trends_FIR/hma/Hma.cs index db0a8cc9..a5a842e8 100644 --- a/lib/trends_FIR/hma/Hma.cs +++ b/lib/trends_FIR/hma/Hma.cs @@ -33,7 +33,10 @@ public sealed class Hma : AbstractBase public Hma(int period) { - if (period <= 1) throw new ArgumentException("Period must be greater than 1", nameof(period)); + if (period <= 1) + { + throw new ArgumentException("Period must be greater than 1", nameof(period)); + } _period = period; int halfPeriod = period / 2; @@ -56,7 +59,10 @@ public sealed class Hma : AbstractBase [MethodImpl(MethodImplOptions.AggressiveInlining)] public override TValue Update(TValue input, bool isNew = true) { - if (isNew) _sampleCount++; + if (isNew) + { + _sampleCount++; + } // 1. Calculate WMA(n) TValue full = _wmaFull.Update(input, isNew); @@ -76,7 +82,10 @@ public sealed class Hma : AbstractBase public override TSeries Update(TSeries source) { - if (source.Count == 0) return []; + if (source.Count == 0) + { + return []; + } int len = source.Count; var t = new List(len); @@ -147,12 +156,20 @@ public sealed class Hma : AbstractBase public static void Calculate(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 greater than 1", nameof(period)); + } int len = source.Length; - if (len == 0) return; + if (len == 0) + { + return; + } int halfPeriod = period / 2; int sqrtPeriod = (int)Math.Sqrt(period); diff --git a/lib/trends_FIR/hwma/Hwma.Quantower.Tests.cs b/lib/trends_FIR/hwma/Hwma.Quantower.Tests.cs index 5354f6ba..755c960b 100644 --- a/lib/trends_FIR/hwma/Hwma.Quantower.Tests.cs +++ b/lib/trends_FIR/hwma/Hwma.Quantower.Tests.cs @@ -164,4 +164,4 @@ public class HwmaIndicatorTests Assert.Equal(20, indicator.Period); Assert.Equal(0, HwmaIndicator.MinHistoryDepths); } -} \ No newline at end of file +} diff --git a/lib/trends_FIR/hwma/Hwma.Quantower.cs b/lib/trends_FIR/hwma/Hwma.Quantower.cs index 655ca553..466ecff5 100644 --- a/lib/trends_FIR/hwma/Hwma.Quantower.cs +++ b/lib/trends_FIR/hwma/Hwma.Quantower.cs @@ -55,4 +55,4 @@ public class HwmaIndicator : Indicator, IWatchlistIndicator Series.SetValue(result.Value, ma.IsHot, ShowColdValues); } -} \ 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 9ec363e9..52c1ada0 100644 --- a/lib/trends_FIR/hwma/Hwma.Tests.cs +++ b/lib/trends_FIR/hwma/Hwma.Tests.cs @@ -435,4 +435,4 @@ public class HwmaTests Assert.True(double.IsFinite(result.Value)); } } -} \ No newline at end of file +} diff --git a/lib/trends_FIR/hwma/Hwma.cs b/lib/trends_FIR/hwma/Hwma.cs index 83f1cee6..53670b83 100644 --- a/lib/trends_FIR/hwma/Hwma.cs +++ b/lib/trends_FIR/hwma/Hwma.cs @@ -66,7 +66,9 @@ public sealed class Hwma : AbstractBase public Hwma(int period = 10) { if (period <= 0) + { throw new ArgumentException("Period must be greater than 0", nameof(period)); + } _alpha = 2.0 / (period + 1.0); _beta = 1.0 / period; @@ -90,11 +92,19 @@ public sealed class Hwma : AbstractBase public Hwma(double alpha, double beta, double gamma) { if (alpha <= 0 || alpha > 1) + { throw new ArgumentException("Alpha must be between 0 (exclusive) and 1 (inclusive)", nameof(alpha)); + } + if (beta < 0 || beta > 1) + { throw new ArgumentException("Beta must be between 0 and 1", nameof(beta)); + } + if (gamma < 0 || gamma > 1) + { throw new ArgumentException("Gamma must be between 0 and 1", nameof(gamma)); + } int effectivePeriod = (int)(2.0 / alpha - 1.0); // Reverse calculate for display _alpha = alpha; @@ -169,7 +179,11 @@ public sealed class Hwma : AbstractBase { // First value is NaN - return NaN Last = new TValue(input.Time, double.NaN); - if (publish) PubEvent(Last); + if (publish) + { + PubEvent(Last); + } + return Last; } @@ -215,7 +229,10 @@ public sealed class Hwma : AbstractBase public override TSeries Update(TSeries source) { - if (source.Count == 0) return new TSeries([], []); + if (source.Count == 0) + { + return new TSeries([], []); + } int len = source.Count; var t = new List(len); @@ -268,11 +285,19 @@ public sealed class Hwma : AbstractBase public static void Calculate(ReadOnlySpan source, Span output, int period = 10) { if (period <= 0) + { throw new ArgumentException("Period must be greater than 0", nameof(period)); - if (source.Length != output.Length) - throw new ArgumentException("Source and output must have the same length", nameof(output)); + } - if (source.Length == 0) return; + if (source.Length != output.Length) + { + throw new ArgumentException("Source and output must have the same length", nameof(output)); + } + + if (source.Length == 0) + { + return; + } double alpha = 2.0 / (period + 1.0); double beta = 1.0 / period; diff --git a/lib/trends_FIR/lsma/Lsma.cs b/lib/trends_FIR/lsma/Lsma.cs index 1dc9fc7d..e9b7dd05 100644 --- a/lib/trends_FIR/lsma/Lsma.cs +++ b/lib/trends_FIR/lsma/Lsma.cs @@ -62,7 +62,9 @@ public sealed class Lsma : AbstractBase public Lsma(int period, int offset = 0) { if (period <= 0) + { throw new ArgumentException("Period must be greater than 0", nameof(period)); + } _period = period; _offset = offset; @@ -219,7 +221,10 @@ public sealed class Lsma : AbstractBase public override TSeries Update(TSeries source) { - if (source.Count == 0) return new TSeries([], []); + if (source.Count == 0) + { + return new TSeries([], []); + } int len = source.Count; var t = new List(len); @@ -295,12 +300,20 @@ public sealed class Lsma : AbstractBase public static void Calculate(ReadOnlySpan source, Span output, int period, int offset = 0, double initialLastValid = double.NaN) { if (source.Length != output.Length) + { throw new ArgumentException("Source and output must have the same length", nameof(output)); + } + if (period <= 0) + { throw new ArgumentException("Period must be greater than 0", nameof(period)); + } int len = source.Length; - if (len == 0) return; + if (len == 0) + { + return; + } const int StackAllocThreshold = 256; Span buffer = period <= StackAllocThreshold @@ -322,9 +335,13 @@ public sealed class Lsma : AbstractBase { double val = source[i]; if (double.IsFinite(val)) + { lastValid = val; + } else + { val = lastValid; + } if (count < period) { @@ -382,7 +399,9 @@ public sealed class Lsma : AbstractBase bufferIndex++; if (bufferIndex >= period) + { bufferIndex = 0; + } double m = Math.FusedMultiplyAdd(period, sum_xy, -full_sum_x * sum_y) / full_denom; double b = Math.FusedMultiplyAdd(-m, full_sum_x, sum_y) / period; @@ -418,4 +437,4 @@ public sealed class Lsma : AbstractBase } base.Dispose(disposing); } -} \ No newline at end of file +} diff --git a/lib/trends_FIR/pwma/Pwma.cs b/lib/trends_FIR/pwma/Pwma.cs index 3e8a4754..3b51509e 100644 --- a/lib/trends_FIR/pwma/Pwma.cs +++ b/lib/trends_FIR/pwma/Pwma.cs @@ -44,7 +44,10 @@ public sealed class Pwma : AbstractBase public Pwma(int period) { - if (period <= 0) throw new ArgumentException("Period must be greater than 0", nameof(period)); + if (period <= 0) + { + throw new ArgumentException("Period must be greater than 0", nameof(period)); + } _period = period; _divisor = (double)period * ((double)period + 1.0) * (2.0 * (double)period + 1.0) / 6.0; @@ -171,7 +174,10 @@ public sealed class Pwma : AbstractBase public override TSeries Update(TSeries source) { - if (source.Count == 0) return new TSeries([], []); + if (source.Count == 0) + { + return new TSeries([], []); + } int len = source.Count; List t = new(len); @@ -244,12 +250,20 @@ public sealed class Pwma : AbstractBase public static void Calculate(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 <= 0) + { throw new ArgumentException("Period must be greater than 0", nameof(period)); + } int len = source.Length; - if (len == 0) return; + if (len == 0) + { + return; + } CalculateScalarCore(source, output, period); } @@ -273,9 +287,13 @@ public sealed class Pwma : AbstractBase { double val = source[i]; if (double.IsFinite(val)) + { lastValid = val; + } else + { val = lastValid; + } sum += val; wsum = Math.FusedMultiplyAdd(i + 1, val, wsum); @@ -291,9 +309,13 @@ public sealed class Pwma : AbstractBase { double val = source[i]; if (double.IsFinite(val)) + { lastValid = val; + } else + { val = lastValid; + } double oldSum = sum; double oldWSum = wsum; @@ -306,7 +328,9 @@ public sealed class Pwma : AbstractBase buffer[bufferIdx] = val; bufferIdx++; if (bufferIdx >= period) + { bufferIdx = 0; + } tickCount++; if (tickCount >= ResyncInterval) @@ -319,7 +343,10 @@ public sealed class Pwma : AbstractBase for (int k = 0; k < period; k++) { int idx = bufferIdx + k; - if (idx >= period) idx -= period; + if (idx >= period) + { + idx -= period; + } double v = buffer[idx]; recalcSum += v; @@ -342,4 +369,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 b6151915..07d016e4 100644 --- a/lib/trends_FIR/sgma/Sgma.Tests.cs +++ b/lib/trends_FIR/sgma/Sgma.Tests.cs @@ -124,7 +124,9 @@ public class SgmaTests var sgma = new Sgma(5, 0); for (int i = 0; i < 5; i++) + { sgma.Update(new TValue(DateTime.UtcNow, 100.0), isNew: true); + } Assert.Equal(100.0, sgma.Last.Value, 1e-9); @@ -390,8 +392,15 @@ public class SgmaTests public void Sgma_ShapePreservation_HighDegreePreservesPeaks() { double[] prices = new double[20]; - for (int i = 0; i < 10; i++) prices[i] = 100 + i * 5; - for (int i = 10; i < 20; i++) prices[i] = 145 - (i - 10) * 5; + for (int i = 0; i < 10; i++) + { + prices[i] = 100 + i * 5; + } + + for (int i = 10; i < 20; i++) + { + prices[i] = 145 - (i - 10) * 5; + } var sgma2 = new Sgma(5, 2); var sgma4 = new Sgma(5, 4); diff --git a/lib/trends_FIR/sgma/Sgma.cs b/lib/trends_FIR/sgma/Sgma.cs index 812b1cf9..900c4d8d 100644 --- a/lib/trends_FIR/sgma/Sgma.cs +++ b/lib/trends_FIR/sgma/Sgma.cs @@ -58,9 +58,14 @@ public sealed class Sgma : AbstractBase public Sgma(int period = 9, int degree = 2) { if (period < 3) + { throw new ArgumentException("Period must be at least 3", nameof(period)); + } + if (degree < 0 || degree > 4) + { throw new ArgumentException("Degree must be between 0 and 4", nameof(degree)); + } // Ensure period is odd _period = period % 2 == 0 ? period + 1 : period; @@ -115,7 +120,11 @@ public sealed class Sgma : AbstractBase weights[5] = 0.0952; weights[6] = -0.0476; double sum7 = 0.0; - for (int i = 0; i < 7; i++) sum7 += weights[i]; + for (int i = 0; i < 7; i++) + { + sum7 += weights[i]; + } + invWeightSum = Math.Abs(sum7) > double.Epsilon ? 1.0 / sum7 : 0.0; return; } @@ -132,7 +141,11 @@ public sealed class Sgma : AbstractBase weights[7] = 0.0337; weights[8] = -0.0281; double sum9 = 0.0; - for (int i = 0; i < 9; i++) sum9 += weights[i]; + for (int i = 0; i < 9; i++) + { + sum9 += weights[i]; + } + invWeightSum = Math.Abs(sum9) > double.Epsilon ? 1.0 / sum9 : 0.0; return; } @@ -209,7 +222,11 @@ public sealed class Sgma : AbstractBase if (!double.IsFinite(val)) { Last = new TValue(input.Time, double.NaN); - if (publish) PubEvent(Last, isNew); + if (publish) + { + PubEvent(Last, isNew); + } + return Last; } @@ -264,7 +281,10 @@ public sealed class Sgma : AbstractBase public override TSeries Update(TSeries source) { - if (source.Count == 0) return new TSeries([], []); + if (source.Count == 0) + { + return new TSeries([], []); + } int len = source.Count; var t = new List(len); @@ -318,13 +338,24 @@ public sealed class Sgma : AbstractBase public static void Calculate(ReadOnlySpan source, Span output, int period = 9, int degree = 2) { if (period < 3) + { throw new ArgumentException("Period must be at least 3", nameof(period)); - if (degree < 0 || degree > 4) - throw new ArgumentException("Degree must be between 0 and 4", nameof(degree)); - if (source.Length != output.Length) - throw new ArgumentException("Source and output must have the same length", nameof(output)); + } - if (source.Length == 0) return; + if (degree < 0 || degree > 4) + { + throw new ArgumentException("Degree must be between 0 and 4", nameof(degree)); + } + + if (source.Length != output.Length) + { + throw new ArgumentException("Source and output must have the same length", nameof(output)); + } + + if (source.Length == 0) + { + return; + } int usePeriod = period % 2 == 0 ? period + 1 : period; int useDegree = degree >= usePeriod ? 2 : degree; @@ -366,9 +397,15 @@ public sealed class Sgma : AbstractBase ring[ringIdx] = val; ringIdx++; - if (ringIdx >= usePeriod) ringIdx = 0; + if (ringIdx >= usePeriod) + { + ringIdx = 0; + } - if (count < usePeriod) count++; + if (count < usePeriod) + { + count++; + } if (count < usePeriod) { @@ -391,8 +428,15 @@ public sealed class Sgma : AbstractBase } finally { - if (weightsArray != null) ArrayPool.Shared.Return(weightsArray); - if (ringArray != null) ArrayPool.Shared.Return(ringArray); + if (weightsArray != null) + { + ArrayPool.Shared.Return(weightsArray); + } + + if (ringArray != null) + { + ArrayPool.Shared.Return(ringArray); + } } } @@ -400,7 +444,9 @@ public sealed class Sgma : AbstractBase private static double CalculateWeightedSumFull(RingBuffer buffer, double[] weights, double invWeightSum, double fallbackValue) { if (Math.Abs(invWeightSum) < double.Epsilon) + { return fallbackValue; + } ReadOnlySpan internalBuf = buffer.InternalBuffer; int head = buffer.StartIndex; @@ -416,8 +462,15 @@ public sealed class Sgma : AbstractBase [MethodImpl(MethodImplOptions.AggressiveInlining)] private static double CalculateWeightedSumWarmup(ReadOnlySpan window, int p, int degree, double fallbackValue) { - if (p <= 0) return 0.0; - if (p == 1) return fallbackValue; + if (p <= 0) + { + return 0.0; + } + + if (p == 1) + { + return fallbackValue; + } if (degree == 2) { diff --git a/lib/trends_FIR/sinema/Sinema.Quantower.Tests.cs b/lib/trends_FIR/sinema/Sinema.Quantower.Tests.cs index c864e0db..2c3d54cd 100644 --- a/lib/trends_FIR/sinema/Sinema.Quantower.Tests.cs +++ b/lib/trends_FIR/sinema/Sinema.Quantower.Tests.cs @@ -157,4 +157,4 @@ public class SinemaIndicatorTests Assert.Equal(20, indicator.Period); Assert.Equal(0, SinemaIndicator.MinHistoryDepths); } -} \ No newline at end of file +} diff --git a/lib/trends_FIR/sinema/Sinema.Quantower.cs b/lib/trends_FIR/sinema/Sinema.Quantower.cs index eb145e54..288fa0ae 100644 --- a/lib/trends_FIR/sinema/Sinema.Quantower.cs +++ b/lib/trends_FIR/sinema/Sinema.Quantower.cs @@ -51,4 +51,4 @@ public sealed class SinemaIndicator : Indicator, IWatchlistIndicator double value = _sinema.Update(new TValue(item.TimeLeft.Ticks, _priceSelector(item)), isNew).Value; _series.SetValue(value, _sinema.IsHot, ShowColdValues); } -} \ No newline at end of file +} diff --git a/lib/trends_FIR/sinema/Sinema.Tests.cs b/lib/trends_FIR/sinema/Sinema.Tests.cs index 3622fda5..93637315 100644 --- a/lib/trends_FIR/sinema/Sinema.Tests.cs +++ b/lib/trends_FIR/sinema/Sinema.Tests.cs @@ -420,7 +420,9 @@ public class SinemaTests double[] output = new double[10000]; var gbm = new GBM(startPrice: 100, mu: 0.05, sigma: 0.2, seed: 42); for (int i = 0; i < source.Length; i++) + { source[i] = gbm.Next().Close; + } // Warm up Sinema.Batch(source.AsSpan(), output.AsSpan(), 100); @@ -562,7 +564,10 @@ public class SinemaTests public void Calculate_ReturnsCorrectResultsAndHotIndicator() { var series = new TSeries(); - for (int i = 1; i <= 10; i++) series.Add(DateTime.UtcNow, i * 10); + for (int i = 1; i <= 10; i++) + { + series.Add(DateTime.UtcNow, i * 10); + } var (results, indicator) = Sinema.Calculate(series, 5); @@ -579,4 +584,4 @@ public class SinemaTests indicator.Update(new TValue(DateTime.UtcNow, 110)); Assert.True(double.IsFinite(indicator.Last.Value)); } -} \ No newline at end of file +} diff --git a/lib/trends_FIR/sinema/Sinema.Validation.Tests.cs b/lib/trends_FIR/sinema/Sinema.Validation.Tests.cs index fb2e85d0..e09d90b3 100644 --- a/lib/trends_FIR/sinema/Sinema.Validation.Tests.cs +++ b/lib/trends_FIR/sinema/Sinema.Validation.Tests.cs @@ -27,7 +27,11 @@ public sealed class SinemaValidationTests : IDisposable private void Dispose(bool disposing) { - if (_disposed) return; + if (_disposed) + { + return; + } + _disposed = true; if (disposing) { @@ -319,4 +323,4 @@ public sealed class SinemaValidationTests : IDisposable _output.WriteLine($"SINEMA({period}) all modes consistent: {batchResult:F10}"); } -} \ No newline at end of file +} diff --git a/lib/trends_FIR/sinema/Sinema.cs b/lib/trends_FIR/sinema/Sinema.cs index 7ed8744d..f9bcf806 100644 --- a/lib/trends_FIR/sinema/Sinema.cs +++ b/lib/trends_FIR/sinema/Sinema.cs @@ -44,7 +44,9 @@ public sealed class Sinema : AbstractBase public Sinema(int period) { if (period <= 0) + { throw new ArgumentException("Period must be greater than 0", nameof(period)); + } _period = period; _buffer = new RingBuffer(period); @@ -101,7 +103,10 @@ public sealed class Sinema : AbstractBase /// Optional time step (unused) public override void Prime(ReadOnlySpan source, TimeSpan? step = null) { - if (source.Length == 0) return; + if (source.Length == 0) + { + return; + } // Reset state _buffer.Clear(); @@ -165,7 +170,10 @@ public sealed class Sinema : AbstractBase [MethodImpl(MethodImplOptions.AggressiveInlining)] private double CalculateFromBuffer() { - if (_buffer.Count == 0) return double.NaN; + if (_buffer.Count == 0) + { + return double.NaN; + } int count = _buffer.Count; double sum = 0; @@ -222,7 +230,10 @@ public sealed class Sinema : AbstractBase public override TSeries Update(TSeries source) { - if (source.Count == 0) return []; + if (source.Count == 0) + { + return []; + } int len = source.Count; var t = new List(len); @@ -269,12 +280,20 @@ public sealed class Sinema : AbstractBase 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 <= 0) + { throw new ArgumentException("Period must be greater than 0", nameof(period)); + } int len = source.Length; - if (len == 0) return; + if (len == 0) + { + return; + } CalculateScalarCore(source, output, period); } @@ -318,9 +337,13 @@ public sealed class Sinema : AbstractBase { double val = source[i]; if (double.IsFinite(val)) + { lastValid = val; + } else + { val = lastValid; + } buffer[i] = val; @@ -351,14 +374,20 @@ public sealed class Sinema : AbstractBase { double val = source[i]; if (double.IsFinite(val)) + { lastValid = val; + } else + { val = lastValid; + } buffer[bufferIndex] = val; bufferIndex++; if (bufferIndex >= period) + { bufferIndex = 0; + } // Calculate weighted sum using circular buffer double sum = 0; @@ -368,7 +397,9 @@ public sealed class Sinema : AbstractBase sum += buffer[bufIdx] * weights[j]; bufIdx++; if (bufIdx >= period) + { bufIdx = 0; + } } output[i] = sum / fullWeightSum; @@ -377,9 +408,14 @@ public sealed class Sinema : AbstractBase finally { if (rentedBuffer != null) + { ArrayPool.Shared.Return(rentedBuffer); + } + if (rentedWeights != null) + { ArrayPool.Shared.Return(rentedWeights); + } } } @@ -407,4 +443,4 @@ public sealed class Sinema : AbstractBase _p_state = default; Last = default; } -} \ No newline at end of file +} diff --git a/lib/trends_FIR/sma/Sma.Tests.cs b/lib/trends_FIR/sma/Sma.Tests.cs index 24b7a054..42b64320 100644 --- a/lib/trends_FIR/sma/Sma.Tests.cs +++ b/lib/trends_FIR/sma/Sma.Tests.cs @@ -425,7 +425,9 @@ public class SmaTests double[] output = new double[10000]; var gbm = new GBM(startPrice: 100, mu: 0.05, sigma: 0.2, seed: 42); for (int i = 0; i < source.Length; i++) + { source[i] = gbm.Next().Close; + } // Warm up Sma.Batch(source.AsSpan(), output.AsSpan(), 100); @@ -571,7 +573,10 @@ public class SmaTests public void Calculate_ReturnsCorrectResultsAndHotIndicator() { var series = new TSeries(); - for (int i = 1; i <= 10; i++) series.Add(DateTime.UtcNow, i * 10); + for (int i = 1; i <= 10; i++) + { + series.Add(DateTime.UtcNow, i * 10); + } // 10, 20, 30, 40, 50, 60, 70, 80, 90, 100 // SMA(5) diff --git a/lib/trends_FIR/sma/Sma.cs b/lib/trends_FIR/sma/Sma.cs index 9b6659f4..d6692559 100644 --- a/lib/trends_FIR/sma/Sma.cs +++ b/lib/trends_FIR/sma/Sma.cs @@ -45,7 +45,9 @@ public sealed class Sma : AbstractBase public Sma(int period) { if (period <= 0) + { throw new ArgumentException("Period must be greater than 0", nameof(period)); + } _period = period; _buffer = new RingBuffer(period); @@ -92,7 +94,10 @@ public sealed class Sma : AbstractBase /// Historical data (only the last 'period' is actually needed) public override void Prime(ReadOnlySpan source, TimeSpan? step = null) { - if (source.Length == 0) return; + if (source.Length == 0) + { + return; + } // Reset state _buffer.Clear(); @@ -213,7 +218,10 @@ public sealed class Sma : AbstractBase public override TSeries Update(TSeries source) { - if (source.Count == 0) return []; + if (source.Count == 0) + { + return []; + } int len = source.Count; var t = new List(len); @@ -262,12 +270,20 @@ public sealed class Sma : AbstractBase 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 <= 0) + { throw new ArgumentException("Period must be greater than 0", nameof(period)); + } int len = source.Length; - if (len == 0) return; + if (len == 0) + { + return; + } // Try SIMD path for large, clean datasets // Requirements: SIMD support, large enough dataset, no NaN values @@ -345,9 +361,13 @@ public sealed class Sma : AbstractBase { double val = source[i]; if (double.IsFinite(val)) + { lastValid = val; + } else + { val = lastValid; + } sum += val; buffer[i] = val; @@ -359,16 +379,22 @@ public sealed class Sma : AbstractBase { double val = source[i]; if (double.IsFinite(val)) + { lastValid = val; + } else + { val = lastValid; + } sum = Math.FusedMultiplyAdd(-1.0, buffer[bufferIndex], sum + val); buffer[bufferIndex] = val; bufferIndex++; if (bufferIndex >= period) + { bufferIndex = 0; + } output[i] = sum / period; @@ -388,7 +414,9 @@ public sealed class Sma : AbstractBase finally { if (rented != null) + { ArrayPool.Shared.Return(rented); + } } } @@ -412,7 +440,9 @@ public sealed class Sma : AbstractBase } if (len <= period) + { return; + } var vInvPeriod = Vector512.Create(invPeriod); int simdEnd = period + (len - period) / VectorWidth * VectorWidth; @@ -486,7 +516,9 @@ public sealed class Sma : AbstractBase } if (len <= period) + { return; + } var vInvPeriod = Vector256.Create(invPeriod); var vZero = Vector256.Zero; @@ -559,7 +591,9 @@ public sealed class Sma : AbstractBase } if (len <= period) + { return; + } var vInvPeriod = Vector128.Create(invPeriod); int simdEnd = period + (len - period) / VectorWidth * VectorWidth; diff --git a/lib/trends_FIR/trima/Trima.Quantower.cs b/lib/trends_FIR/trima/Trima.Quantower.cs index 108df77a..0ec8fbc9 100644 --- a/lib/trends_FIR/trima/Trima.Quantower.cs +++ b/lib/trends_FIR/trima/Trima.Quantower.cs @@ -51,7 +51,9 @@ public sealed class TrimaIndicator : Indicator, IWatchlistIndicator protected override void OnUpdate(UpdateArgs args) { if (args.Reason != UpdateReason.NewBar && args.Reason != UpdateReason.HistoricalBar && args.Reason != UpdateReason.NewTick) + { return; + } var item = HistoricalData[Count - 1, SeekOriginHistory.Begin]; TValue result = _ma.Update(new TValue(item.TimeLeft.Ticks, _priceSelector(item)), args.IsNewBar()); diff --git a/lib/trends_FIR/trima/Trima.cs b/lib/trends_FIR/trima/Trima.cs index 31efc486..27b723e9 100644 --- a/lib/trends_FIR/trima/Trima.cs +++ b/lib/trends_FIR/trima/Trima.cs @@ -34,7 +34,10 @@ public sealed class Trima : AbstractBase public Trima(int period) { - if (period <= 0) throw new ArgumentException("Period must be greater than 0", nameof(period)); + if (period <= 0) + { + throw new ArgumentException("Period must be greater than 0", nameof(period)); + } _period = period; int p1 = (period + 1) / 2; @@ -81,7 +84,10 @@ public sealed class Trima : AbstractBase public override TSeries Update(TSeries source) { - if (source.Count == 0) return []; + if (source.Count == 0) + { + return []; + } int len = source.Count; var t = new List(len); @@ -143,9 +149,14 @@ public sealed class Trima : AbstractBase 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 <= 0) + { throw new ArgumentException("Period must be greater than 0", nameof(period)); + } int p1 = (period + 1) / 2; int p2 = period / 2 + 1; @@ -163,4 +174,4 @@ public sealed class Trima : AbstractBase ArrayPool.Shared.Return(tempArray); } } -} \ No newline at end of file +} diff --git a/lib/trends_FIR/wma/Wma.Coverage.Tests.cs b/lib/trends_FIR/wma/Wma.Coverage.Tests.cs index 60bbfa60..465cad16 100644 --- a/lib/trends_FIR/wma/Wma.Coverage.Tests.cs +++ b/lib/trends_FIR/wma/Wma.Coverage.Tests.cs @@ -12,7 +12,10 @@ public class WmaCoverageTests const int period = 10; int len = 100; // < 256 double[] source = new double[len]; - for (int i = 0; i < len; i++) source[i] = i; + for (int i = 0; i < len; i++) + { + source[i] = i; + } double[] output = new double[len]; @@ -25,12 +28,19 @@ public class WmaCoverageTests [Fact] public void Cover_Avx2_Explicitly() { - if (!Avx2.IsSupported) return; + if (!Avx2.IsSupported) + { + return; + } int period = 10; int len = 1000; double[] source = new double[len]; - for (int i = 0; i < len; i++) source[i] = i; + for (int i = 0; i < len; i++) + { + source[i] = i; + } + double[] output = new double[len]; // Use reflection to invoke private static CalculateSimdCore @@ -78,7 +88,11 @@ public class WmaCoverageTests int period = 10; int len = 1000; double[] source = new double[len]; - for (int i = 0; i < len; i++) source[i] = i; + for (int i = 0; i < len; i++) + { + source[i] = i; + } + double[] output = new double[len]; InvokePrivateStaticMethod_WithSpans("CalculateScalarCore", source, output, period); diff --git a/lib/trends_FIR/wma/Wma.Quantower.cs b/lib/trends_FIR/wma/Wma.Quantower.cs index 917927b5..6bcde952 100644 --- a/lib/trends_FIR/wma/Wma.Quantower.cs +++ b/lib/trends_FIR/wma/Wma.Quantower.cs @@ -51,7 +51,9 @@ public sealed class WmaIndicator : Indicator, IWatchlistIndicator protected override void OnUpdate(UpdateArgs args) { if (args.Reason != UpdateReason.NewBar && args.Reason != UpdateReason.HistoricalBar && args.Reason != UpdateReason.NewTick) + { return; + } var item = HistoricalData[Count - 1, SeekOriginHistory.Begin]; TValue result = _ma.Update(new TValue(item.TimeLeft.Ticks, _priceSelector(item)), args.IsNewBar()); diff --git a/lib/trends_FIR/wma/Wma.Validation.Tests.cs b/lib/trends_FIR/wma/Wma.Validation.Tests.cs index d6037ba3..42c98475 100644 --- a/lib/trends_FIR/wma/Wma.Validation.Tests.cs +++ b/lib/trends_FIR/wma/Wma.Validation.Tests.cs @@ -27,9 +27,16 @@ public sealed class WmaValidationTests : IDisposable private void Dispose(bool disposing) { - if (_disposed) return; + if (_disposed) + { + return; + } + _disposed = true; - if (disposing) _testData?.Dispose(); + if (disposing) + { + _testData?.Dispose(); + } } [Fact] diff --git a/lib/trends_FIR/wma/Wma.cs b/lib/trends_FIR/wma/Wma.cs index 2db443da..4c46685e 100644 --- a/lib/trends_FIR/wma/Wma.cs +++ b/lib/trends_FIR/wma/Wma.cs @@ -53,7 +53,10 @@ public sealed class Wma : AbstractBase public Wma(int period) { - if (period <= 0) throw new ArgumentException("Period must be greater than 0", nameof(period)); + if (period <= 0) + { + throw new ArgumentException("Period must be greater than 0", nameof(period)); + } _period = period; _divisor = (double)period * (period + 1) * 0.5; @@ -173,7 +176,10 @@ public sealed class Wma : AbstractBase public override TSeries Update(TSeries source) { - if (source.Count == 0) return []; + if (source.Count == 0) + { + return []; + } int len = source.Count; var t = new List(len); @@ -197,7 +203,10 @@ public sealed class Wma : AbstractBase public override void Prime(ReadOnlySpan source, TimeSpan? step = null) { - if (source.Length == 0) return; + if (source.Length == 0) + { + return; + } int len = source.Length; int windowSize = Math.Min(len, _period); @@ -258,12 +267,20 @@ public sealed class Wma : AbstractBase 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 <= 0) + { throw new ArgumentException("Period must be greater than 0", nameof(period)); + } int len = source.Length; - if (len == 0) return; + if (len == 0) + { + return; + } const int simdThreshold = 256; if (Avx512F.IsSupported && len >= simdThreshold && !source.ContainsNonFinite()) @@ -305,9 +322,13 @@ public sealed class Wma : AbstractBase { double val = source[i]; if (double.IsFinite(val)) + { lastValid = val; + } else + { val = lastValid; + } sum += val; wsum = Math.FusedMultiplyAdd(i + 1, val, wsum); @@ -322,9 +343,13 @@ public sealed class Wma : AbstractBase { double val = source[i]; if (double.IsFinite(val)) + { lastValid = val; + } else + { val = lastValid; + } double oldSum = sum; double oldest = buffer[bufferIdx]; @@ -334,7 +359,9 @@ public sealed class Wma : AbstractBase buffer[bufferIdx] = val; bufferIdx++; if (bufferIdx >= period) + { bufferIdx = 0; + } output[i] = wsum / divisor; @@ -349,7 +376,10 @@ public sealed class Wma : AbstractBase for (int k = 0; k < period; k++) { int idx = bufferIdx + k; - if (idx >= period) idx -= period; + if (idx >= period) + { + idx -= period; + } double v = buffer[idx]; recalcSum += v; @@ -386,7 +416,9 @@ public sealed class Wma : AbstractBase } if (len <= period) + { return; + } var vInvDivisor = Vector512.Create(invDivisor); var vPeriod = Vector512.Create((double)period); @@ -501,7 +533,9 @@ public sealed class Wma : AbstractBase } if (len <= period) + { return; + } var vInvDivisor = Vector256.Create(invDivisor); var vPeriod = Vector256.Create((double)period); @@ -704,7 +738,9 @@ public sealed class Wma : AbstractBase } if (len <= period) + { return; + } var vInvDivisor = Vector128.Create(invDivisor); int simdEnd = period + ((len - period) / vectorWidth) * vectorWidth; diff --git a/lib/trends_IIR/dema/Dema.cs b/lib/trends_IIR/dema/Dema.cs index dba1549f..bb6305ca 100644 --- a/lib/trends_IIR/dema/Dema.cs +++ b/lib/trends_IIR/dema/Dema.cs @@ -49,7 +49,10 @@ public sealed class Dema : AbstractBase public Dema(int period) { - if (period <= 0) throw new ArgumentException("Period must be greater than 0", nameof(period)); + if (period <= 0) + { + throw new ArgumentException("Period must be greater than 0", nameof(period)); + } _alpha = 2.0 / (period + 1); _decay = 1.0 - _alpha; @@ -66,7 +69,10 @@ public sealed class Dema : AbstractBase public Dema(double alpha) { - if (alpha <= 0 || alpha > 1) throw new ArgumentException("Alpha must be between 0 and 1", nameof(alpha)); + if (alpha <= 0 || alpha > 1) + { + throw new ArgumentException("Alpha must be between 0 and 1", nameof(alpha)); + } _alpha = alpha; _decay = 1.0 - alpha; @@ -94,9 +100,13 @@ public sealed class Dema : AbstractBase // EMA1 double val = input.Value; if (double.IsFinite(val)) + { _lastValidValue = val; + } else + { val = _lastValidValue; + } if (double.IsNaN(val)) { @@ -118,7 +128,10 @@ public sealed class Dema : AbstractBase public override TSeries Update(TSeries source) { - if (source.Count == 0) return []; + if (source.Count == 0) + { + return []; + } int len = source.Count; List t = new(len); @@ -148,9 +161,13 @@ public sealed class Dema : AbstractBase { double val = sourceValues[i]; if (double.IsFinite(val)) + { lastValid = val; + } else + { val = lastValid; + } if (double.IsNaN(val)) { @@ -197,7 +214,9 @@ public sealed class Dema : AbstractBase state.E *= decay; if (!state.IsHot && state.E <= 0.05) // COVERAGE_THRESHOLD + { state.IsHot = true; + } if (state.E <= 1e-10) // COMPENSATOR_THRESHOLD { @@ -232,7 +251,9 @@ public sealed class Dema : AbstractBase public static void Calculate(ReadOnlySpan source, Span output, int period) { if (period <= 0) + { throw new ArgumentException("Period must be greater than 0", nameof(period)); + } double alpha = 2.0 / (period + 1); Calculate(source, output, alpha); @@ -241,11 +262,19 @@ public sealed class Dema : AbstractBase public static void Calculate(ReadOnlySpan source, Span output, double alpha) { if (source.Length != output.Length) + { throw new ArgumentException("Source and output must have the same length", nameof(output)); - if (alpha <= 0 || alpha > 1) - throw new ArgumentException("Alpha must be between 0 and 1", nameof(alpha)); + } - if (source.Length == 0) return; + if (alpha <= 0 || alpha > 1) + { + throw new ArgumentException("Alpha must be between 0 and 1", nameof(alpha)); + } + + if (source.Length == 0) + { + return; + } double decay = 1.0 - alpha; double lastValid = double.NaN; @@ -264,9 +293,13 @@ public sealed class Dema : AbstractBase { double val = source[i]; if (double.IsFinite(val)) + { lastValid = val; + } else + { val = lastValid; + } if (double.IsNaN(val)) { diff --git a/lib/trends_IIR/dsma/Dsma.Quantower.Tests.cs b/lib/trends_IIR/dsma/Dsma.Quantower.Tests.cs index c659b64c..9c2a4e9c 100644 --- a/lib/trends_IIR/dsma/Dsma.Quantower.Tests.cs +++ b/lib/trends_IIR/dsma/Dsma.Quantower.Tests.cs @@ -199,7 +199,7 @@ public class DsmaIndicatorTests // Process first bar indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar)); - + // Process second bar as new indicator.ProcessUpdate(new UpdateArgs(UpdateReason.NewBar)); double afterNewBar = indicator.LinesSeries[0].GetValue(0); diff --git a/lib/trends_IIR/dsma/Dsma.Tests.cs b/lib/trends_IIR/dsma/Dsma.Tests.cs index 9b9b3ffe..fb90f15a 100644 --- a/lib/trends_IIR/dsma/Dsma.Tests.cs +++ b/lib/trends_IIR/dsma/Dsma.Tests.cs @@ -232,7 +232,7 @@ public class DsmaTests // Get the last bar again after loop lastBar = gbm.Next(isNew: false); - + // Inject NaN var nanResult = dsma.Update(new TValue(lastBar.Time, double.NaN)); @@ -257,7 +257,7 @@ public class DsmaTests // Get the last bar again after loop lastBar = gbm.Next(isNew: false); - + // Inject Infinity var infResult = dsma.Update(new TValue(lastBar.Time, double.PositiveInfinity)); var negInfResult = dsma.Update(new TValue(lastBar.Time, double.NegativeInfinity)); diff --git a/lib/trends_IIR/dsma/Dsma.Validation.Tests.cs b/lib/trends_IIR/dsma/Dsma.Validation.Tests.cs index c10138a3..e817593c 100644 --- a/lib/trends_IIR/dsma/Dsma.Validation.Tests.cs +++ b/lib/trends_IIR/dsma/Dsma.Validation.Tests.cs @@ -58,7 +58,7 @@ public class DsmaValidationTests } // In higher volatility, absolute deviation should generally be larger - Assert.True(highVolDeviation > lowVolDeviation * 2, + Assert.True(highVolDeviation > lowVolDeviation * 2, $"High volatility deviation {highVolDeviation:F2} should be significantly larger than low volatility {lowVolDeviation:F2}"); } @@ -96,7 +96,7 @@ public class DsmaValidationTests double avgHighLag = highScaleLag / count; // Lower scale factor should have higher average lag (smoother, less responsive) - Assert.True(avgLowLag > avgHighLag, + Assert.True(avgLowLag > avgHighLag, $"Low scale lag {avgLowLag:F4} should be greater than high scale lag {avgHighLag:F4}"); } @@ -133,7 +133,7 @@ public class DsmaValidationTests double dsmaVariance = dsmaChanges.Average(); // DSMA should have lower variance than raw price - Assert.True(dsmaVariance < priceVariance, + Assert.True(dsmaVariance < priceVariance, $"DSMA variance {dsmaVariance:F4} should be less than price variance {priceVariance:F4}"); } @@ -199,7 +199,7 @@ public class DsmaValidationTests var gbm = new GBM(startPrice: 100.0, mu: 0.01, sigma: 0.2, seed: 654); var series = new TSeries(); - + for (int i = 0; i < 100; i++) { var bar = gbm.Next(isNew: true); @@ -254,7 +254,7 @@ public class DsmaValidationTests double avgLongLag = longLag / count; // Longer period should have higher average lag (more smoothing) - Assert.True(avgLongLag > avgShortLag, + Assert.True(avgLongLag > avgShortLag, $"Long period lag {avgLongLag:F4} should be greater than short period lag {avgShortLag:F4}"); } @@ -319,8 +319,15 @@ public class DsmaValidationTests for (int i = 1; i < prices.Count; i++) { - if (prices[i] > prices[i - 1]) priceUpCount++; - if (dsmaValues[i] > dsmaValues[i - 1]) dsmaUpCount++; + if (prices[i] > prices[i - 1]) + { + priceUpCount++; + } + + if (dsmaValues[i] > dsmaValues[i - 1]) + { + dsmaUpCount++; + } } // DSMA should have similar directional trend but smoother diff --git a/lib/trends_IIR/dsma/Dsma.cs b/lib/trends_IIR/dsma/Dsma.cs index be20ba43..47482c8c 100644 --- a/lib/trends_IIR/dsma/Dsma.cs +++ b/lib/trends_IIR/dsma/Dsma.cs @@ -89,9 +89,14 @@ public sealed class Dsma : AbstractBase public Dsma(int period, double scaleFactor = 0.5) { if (period < 2) + { throw new ArgumentOutOfRangeException(nameof(period), "Period must be >= 2."); + } + if (scaleFactor < 0.01 || scaleFactor > 0.9) + { throw new ArgumentOutOfRangeException(nameof(scaleFactor), "Scale factor must be between 0.01 and 0.9."); + } WarmupPeriod = period; _periodRecip = 1.0 / period; @@ -146,12 +151,16 @@ public sealed class Dsma : AbstractBase HandleStateSnapshot(isNew); value = HandleInvalidInput(value); if (double.IsNaN(value)) + { return double.NaN; + } _state.Bars++; if (_state.Bars == 1) + { return InitializeFirstBar(value); + } return CalculateDsma(value); } @@ -256,7 +265,10 @@ public sealed class Dsma : AbstractBase /// Time series containing DSMA values public override TSeries Update(TSeries source) { - if (source.Count == 0) return []; + if (source.Count == 0) + { + return []; + } int len = source.Count; var t = new List(len); @@ -325,7 +337,9 @@ public sealed class Dsma : AbstractBase double scaleFactor = 0.5) { if (output.Length < source.Length) + { throw new ArgumentException("Output span is shorter than source span.", nameof(output)); + } var dsma = new Dsma(period, scaleFactor); for (int i = 0; i < source.Length; i++) diff --git a/lib/trends_IIR/ema/Ema.Tests.cs b/lib/trends_IIR/ema/Ema.Tests.cs index 1c9b3c38..a80a5b42 100644 --- a/lib/trends_IIR/ema/Ema.Tests.cs +++ b/lib/trends_IIR/ema/Ema.Tests.cs @@ -444,7 +444,9 @@ public class EmaTests var gbm = new GBM(startPrice: 100, mu: 0.05, sigma: 0.2, seed: 42); for (int i = 0; i < source.Length; i++) + { source[i] = gbm.Next().Close; + } // Warm up Ema.Batch(source.AsSpan(), output.AsSpan(), 100); @@ -525,7 +527,10 @@ public class EmaTests // ... // We can verify against a fresh EMA fed with same data var verifyEma = new Ema(5); - foreach (var val in history) verifyEma.Update(new TValue(DateTime.UtcNow, val)); + foreach (var val in history) + { + verifyEma.Update(new TValue(DateTime.UtcNow, val)); + } Assert.Equal(verifyEma.Last.Value, ema.Last.Value, 1e-10); Assert.Equal(verifyEma.IsHot, ema.IsHot); @@ -545,7 +550,10 @@ public class EmaTests ema.Prime(history); var verifyEma = new Ema(5); - foreach (var val in history) verifyEma.Update(new TValue(DateTime.UtcNow, val)); + foreach (var val in history) + { + verifyEma.Update(new TValue(DateTime.UtcNow, val)); + } Assert.Equal(verifyEma.Last.Value, ema.Last.Value, 1e-10); } @@ -565,7 +573,10 @@ public class EmaTests public void Calculate_ReturnsCorrectResultsAndHotIndicator() { var series = new TSeries(); - for (int i = 1; i <= 20; i++) series.Add(DateTime.UtcNow, i * 10); + for (int i = 1; i <= 20; i++) + { + series.Add(DateTime.UtcNow, i * 10); + } // EMA(5) var (results, indicator) = Ema.Calculate(series, 5); diff --git a/lib/trends_IIR/ema/Ema.cs b/lib/trends_IIR/ema/Ema.cs index adbb2efe..1e184b67 100644 --- a/lib/trends_IIR/ema/Ema.cs +++ b/lib/trends_IIR/ema/Ema.cs @@ -91,7 +91,9 @@ public sealed class Ema : AbstractBase public Ema(double alpha) { if (alpha <= 0 || alpha > 1) + { throw new ArgumentException("Alpha must be greater than 0 and at most 1", nameof(alpha)); + } _alpha = alpha; _decay = 1.0 - alpha; @@ -118,7 +120,10 @@ public sealed class Ema : AbstractBase /// Historical data public override void Prime(ReadOnlySpan source, TimeSpan? step = null) { - if (source.Length == 0) return; + if (source.Length == 0) + { + return; + } // Reset state _state = State.New(); @@ -170,7 +175,9 @@ public sealed class Ema : AbstractBase finally { if (rented != null) + { ArrayPool.Shared.Return(rented); + } } } @@ -215,7 +222,10 @@ public sealed class Ema : AbstractBase [MethodImpl(MethodImplOptions.AggressiveOptimization)] public override TSeries Update(TSeries source) { - if (source.Count == 0) return []; + if (source.Count == 0) + { + return []; + } int len = source.Count; var t = new List(len); @@ -262,7 +272,9 @@ public sealed class Ema : AbstractBase state.E *= decay; if (!state.IsHot && state.E <= COVERAGE_THRESHOLD) + { state.IsHot = true; + } if (state.E <= COMPENSATOR_THRESHOLD) { @@ -300,21 +312,29 @@ public sealed class Ema : AbstractBase { double val = source[i]; if (double.IsFinite(val)) + { lastValidValue = val; + } else + { val = lastValidValue; + } state.Ema = Math.FusedMultiplyAdd(state.Ema, decay, alpha * val); state.E *= decay; if (!state.IsHot && state.E <= COVERAGE_THRESHOLD) + { state.IsHot = true; + } output[i] = state.Ema / (1.0 - state.E); state.TickCount++; } if (state.E <= COMPENSATOR_THRESHOLD) + { state.IsCompensated = true; + } } // Phase 2: Post-compensation (hot path) - optimized with loop unrolling @@ -328,22 +348,54 @@ public sealed class Ema : AbstractBase for (; i < unrollEnd; i += 4) { double v0 = Unsafe.Add(ref srcRef, i); - if (!double.IsFinite(v0)) v0 = lastValidValue; else lastValidValue = v0; + if (!double.IsFinite(v0)) + { + v0 = lastValidValue; + } + else + { + lastValidValue = v0; + } + state.Ema = Math.FusedMultiplyAdd(state.Ema, decay, alpha * v0); Unsafe.Add(ref outRef, i) = state.Ema; double v1 = Unsafe.Add(ref srcRef, i + 1); - if (!double.IsFinite(v1)) v1 = lastValidValue; else lastValidValue = v1; + if (!double.IsFinite(v1)) + { + v1 = lastValidValue; + } + else + { + lastValidValue = v1; + } + state.Ema = Math.FusedMultiplyAdd(state.Ema, decay, alpha * v1); Unsafe.Add(ref outRef, i + 1) = state.Ema; double v2 = Unsafe.Add(ref srcRef, i + 2); - if (!double.IsFinite(v2)) v2 = lastValidValue; else lastValidValue = v2; + if (!double.IsFinite(v2)) + { + v2 = lastValidValue; + } + else + { + lastValidValue = v2; + } + state.Ema = Math.FusedMultiplyAdd(state.Ema, decay, alpha * v2); Unsafe.Add(ref outRef, i + 2) = state.Ema; double v3 = Unsafe.Add(ref srcRef, i + 3); - if (!double.IsFinite(v3)) v3 = lastValidValue; else lastValidValue = v3; + if (!double.IsFinite(v3)) + { + v3 = lastValidValue; + } + else + { + lastValidValue = v3; + } + state.Ema = Math.FusedMultiplyAdd(state.Ema, decay, alpha * v3); Unsafe.Add(ref outRef, i + 3) = state.Ema; @@ -364,7 +416,14 @@ public sealed class Ema : AbstractBase for (; i < len; i++) { double val = Unsafe.Add(ref srcRef, i); - if (!double.IsFinite(val)) val = lastValidValue; else lastValidValue = val; + if (!double.IsFinite(val)) + { + val = lastValidValue; + } + else + { + lastValidValue = val; + } state.Ema = Math.FusedMultiplyAdd(state.Ema, decay, alpha * val); Unsafe.Add(ref outRef, i) = state.Ema; @@ -469,7 +528,9 @@ public sealed class Ema : AbstractBase public static void Batch(ReadOnlySpan source, Span output, int period) { if (period <= 0) + { throw new ArgumentException("Period must be greater than 0", nameof(period)); + } double alpha = 2.0 / (period + 1); Batch(source, output, alpha); @@ -486,11 +547,17 @@ public sealed class Ema : AbstractBase public static void Batch(ReadOnlySpan source, Span output, double alpha) { if (source.Length != output.Length) + { throw new ArgumentException("Source and output must have the same length", nameof(output)); + } + ArgumentOutOfRangeException.ThrowIfLessThanOrEqual(alpha, 0.0); ArgumentOutOfRangeException.ThrowIfGreaterThan(alpha, 1.0); - if (source.Length == 0) return; + if (source.Length == 0) + { + return; + } // For large, clean datasets, use optimized path without NaN handling if (source.Length >= CleanPathThreshold && !source.ContainsNonFinite()) @@ -535,4 +602,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.Validation.Tests.cs b/lib/trends_IIR/frama/Frama.Validation.Tests.cs index 17339ed9..138d4c7b 100644 --- a/lib/trends_IIR/frama/Frama.Validation.Tests.cs +++ b/lib/trends_IIR/frama/Frama.Validation.Tests.cs @@ -110,18 +110,39 @@ public class FramaValidationTests lv = lastLow; } - if (hv > maxFull) maxFull = hv; - if (lv < minFull) minFull = lv; + if (hv > maxFull) + { + maxFull = hv; + } + + if (lv < minFull) + { + minFull = lv; + } if (j >= startRecent) { - if (hv > maxRecent) maxRecent = hv; - if (lv < minRecent) minRecent = lv; + if (hv > maxRecent) + { + maxRecent = hv; + } + + if (lv < minRecent) + { + minRecent = lv; + } } else { - if (hv > maxPrev) maxPrev = hv; - if (lv < minPrev) minPrev = lv; + if (hv > maxPrev) + { + maxPrev = hv; + } + + if (lv < minPrev) + { + minPrev = lv; + } } } @@ -134,8 +155,15 @@ public class FramaValidationTests { double dimen = (Math.Log(n1 + n2) - Math.Log(n3)) / 0.693147180559945309417232121458176568; alpha = Math.Exp(-4.6 * (dimen - 1.0)); - if (alpha < 0.01) alpha = 0.01; - if (alpha > 1.0) alpha = 1.0; + if (alpha < 0.01) + { + alpha = 0.01; + } + + if (alpha > 1.0) + { + alpha = 1.0; + } } double price = (highVal + lowVal) * 0.5; diff --git a/lib/trends_IIR/frama/Frama.cs b/lib/trends_IIR/frama/Frama.cs index fa3425b7..49c30d55 100644 --- a/lib/trends_IIR/frama/Frama.cs +++ b/lib/trends_IIR/frama/Frama.cs @@ -147,8 +147,15 @@ public sealed class Frama : ITValuePublisher { double dimen = (Math.Log(n1 + n2) - Math.Log(n3)) / Log2; alpha = Math.Exp(-4.6 * (dimen - 1.0)); - if (alpha < AlphaFloor) alpha = AlphaFloor; - if (alpha > AlphaCeil) alpha = AlphaCeil; + if (alpha < AlphaFloor) + { + alpha = AlphaFloor; + } + + if (alpha > AlphaCeil) + { + alpha = AlphaCeil; + } } double prev = _state.HasValue && double.IsFinite(_state.Frama) ? _state.Frama : price; @@ -170,7 +177,10 @@ public sealed class Frama : ITValuePublisher public TSeries Update(TBarSeries source) { - if (source.Count == 0) return new TSeries([], []); + if (source.Count == 0) + { + return new TSeries([], []); + } int len = source.Count; var v = new double[len]; @@ -195,7 +205,10 @@ public sealed class Frama : ITValuePublisher public TSeries Update(TSeries source) { - if (source.Count == 0) return []; + if (source.Count == 0) + { + return []; + } int len = source.Count; var t = new List(len); @@ -224,7 +237,9 @@ public sealed class Frama : ITValuePublisher public static void Calculate(ReadOnlySpan high, ReadOnlySpan low, int period, Span output) { if (high.Length != low.Length || high.Length != output.Length) + { throw new ArgumentException("Input spans must have the same length.", nameof(output)); + } ArgumentOutOfRangeException.ThrowIfLessThan(period, 2); @@ -240,7 +255,9 @@ public sealed class Frama : ITValuePublisher public static void Calculate(ReadOnlySpan source, Span output, int period) { if (source.Length != output.Length) + { throw new ArgumentException("Source and output must have the same length.", nameof(output)); + } ArgumentOutOfRangeException.ThrowIfLessThan(period, 2); @@ -254,7 +271,10 @@ public sealed class Frama : ITValuePublisher public static TSeries Batch(TBarSeries source, int period) { - if (source.Count == 0) return new TSeries([], []); + if (source.Count == 0) + { + return new TSeries([], []); + } int len = source.Count; var v = new double[len]; @@ -275,7 +295,9 @@ public sealed class Frama : ITValuePublisher { int count = buffer.Count; if (count == 0 || length <= 0) + { return double.NaN; + } int capacity = buffer.Capacity; int start = buffer.StartIndex; @@ -288,10 +310,15 @@ public sealed class Frama : ITValuePublisher { int idx = start + offset + i; if (idx >= capacity) + { idx -= capacity; + } + double v = data[idx]; if (v > max) + { max = v; + } } return max; @@ -302,7 +329,9 @@ public sealed class Frama : ITValuePublisher { int count = buffer.Count; if (count == 0 || length <= 0) + { return double.NaN; + } int capacity = buffer.Capacity; int start = buffer.StartIndex; @@ -315,12 +344,17 @@ public sealed class Frama : ITValuePublisher { int idx = start + offset + i; if (idx >= capacity) + { idx -= capacity; + } + double v = data[idx]; if (v < min) + { min = v; + } } return min; } -} \ No newline at end of file +} diff --git a/lib/trends_IIR/hema/Hema.Validation.Tests.cs b/lib/trends_IIR/hema/Hema.Validation.Tests.cs index 310420fc..9cdd43a9 100644 --- a/lib/trends_IIR/hema/Hema.Validation.Tests.cs +++ b/lib/trends_IIR/hema/Hema.Validation.Tests.cs @@ -90,9 +90,13 @@ public class HemaValidationTests { double val = source[i]; if (double.IsFinite(val)) + { lastValid = val; + } else + { val = lastValid; + } if (double.IsNaN(val)) { diff --git a/lib/trends_IIR/hema/Hema.cs b/lib/trends_IIR/hema/Hema.cs index 364e93a9..bd706793 100644 --- a/lib/trends_IIR/hema/Hema.cs +++ b/lib/trends_IIR/hema/Hema.cs @@ -119,9 +119,13 @@ public sealed class Hema : AbstractBase double val = input.Value; if (double.IsFinite(val)) + { _lastValidValue = val; + } else + { val = _lastValidValue; + } if (double.IsNaN(val)) { @@ -139,7 +143,10 @@ public sealed class Hema : AbstractBase [MethodImpl(MethodImplOptions.AggressiveOptimization)] public override TSeries Update(TSeries source) { - if (source.Count == 0) return []; + if (source.Count == 0) + { + return []; + } int len = source.Count; List t = new(len); @@ -163,9 +170,13 @@ public sealed class Hema : AbstractBase { double val = sourceValues[i]; if (double.IsFinite(val)) + { lastValid = val; + } else + { val = lastValid; + } if (double.IsNaN(val)) { @@ -214,17 +225,23 @@ public sealed class Hema : AbstractBase double emaFast = state.EmaFastRaw * invFast; double deLag = Math.FusedMultiplyAdd(-_ratio, emaSlow, emaFast) * _invOneMinusRatio; if (!double.IsFinite(deLag)) + { deLag = input; + } state.EmaSmoothRaw = Math.FusedMultiplyAdd(state.EmaSmoothRaw, _betaSmooth, _alphaSmooth * deLag); double maxDecay = Math.Max(state.DecaySlow, Math.Max(state.DecayFast, state.DecaySmooth)); if (!state.IsHot && maxDecay <= CoverageThreshold) + { state.IsHot = true; + } state.Warmup = maxDecay > CompensatorThreshold; if (!state.Warmup) + { state.IsHot = true; + } double result = state.EmaSmoothRaw * invSmooth; if (!double.IsFinite(result)) @@ -238,11 +255,16 @@ public sealed class Hema : AbstractBase double deLagFast = Math.FusedMultiplyAdd(-_ratio, state.EmaSlowRaw, state.EmaFastRaw) * _invOneMinusRatio; if (!double.IsFinite(deLagFast)) + { deLagFast = input; + } + state.EmaSmoothRaw = Math.FusedMultiplyAdd(state.EmaSmoothRaw, _betaSmooth, _alphaSmooth * deLagFast); if (!state.IsHot) + { state.IsHot = true; + } double fastResult = state.EmaSmoothRaw; if (!double.IsFinite(fastResult)) @@ -263,10 +285,16 @@ public sealed class Hema : AbstractBase public static void Calculate(ReadOnlySpan source, Span output, int period) { if (source.Length != output.Length) + { throw new ArgumentException("Source and output must have the same length", nameof(output)); + } + ArgumentOutOfRangeException.ThrowIfNegativeOrZero(period); - if (source.Length == 0) return; + if (source.Length == 0) + { + return; + } double n = Math.Max((double)period, 2.0); double alphaSlow = AlphaFromHalfLife(n); @@ -296,9 +324,13 @@ public sealed class Hema : AbstractBase { double val = source[i]; if (double.IsFinite(val)) + { lastValid = val; + } else + { val = lastValid; + } if (double.IsNaN(val)) { @@ -323,7 +355,9 @@ public sealed class Hema : AbstractBase double emaFast = emaFastRaw * invFast; double deLag = Math.FusedMultiplyAdd(-ratio, emaSlow, emaFast) * invOneMinusRatio; if (!double.IsFinite(deLag)) + { deLag = val; + } emaSmoothRaw = Math.FusedMultiplyAdd(emaSmoothRaw, betaSmooth, alphaSmooth * deLag); double result = emaSmoothRaw * invSmooth; @@ -348,7 +382,10 @@ public sealed class Hema : AbstractBase { double deLag = Math.FusedMultiplyAdd(-ratio, emaSlowRaw, emaFastRaw) * invOneMinusRatio; if (!double.IsFinite(deLag)) + { deLag = val; + } + emaSmoothRaw = Math.FusedMultiplyAdd(emaSmoothRaw, betaSmooth, alphaSmooth * deLag); double result = emaSmoothRaw; if (!double.IsFinite(result)) @@ -424,12 +461,16 @@ public sealed class Hema : AbstractBase { double maxDecay = Math.Max(_betaSlow, Math.Max(_betaFast, _betaSmooth)); if (maxDecay <= 0) + { return 1; + } double steps = Math.Log(CoverageThreshold) / Math.Log(maxDecay); if (double.IsNaN(steps) || double.IsInfinity(steps) || steps <= 0) + { return 1; + } return (int)Math.Ceiling(steps); } -} \ No newline at end of file +} diff --git a/lib/trends_IIR/htit/Htit.cs b/lib/trends_IIR/htit/Htit.cs index e9b85055..38e852ea 100644 --- a/lib/trends_IIR/htit/Htit.cs +++ b/lib/trends_IIR/htit/Htit.cs @@ -195,11 +195,25 @@ public sealed class Htit : AbstractBase { double cap = 1.5 * prevPeriod; double floor = 0.67 * prevPeriod; - if (period > cap) period = cap; - if (period < floor) period = floor; + if (period > cap) + { + period = cap; + } + + if (period < floor) + { + period = floor; + } + } + if (period < 6) + { + period = 6; + } + + if (period > 50) + { + period = 50; } - if (period < 6) period = 6; - if (period > 50) period = 50; // Smooth the period (using FMA) _state.Period = Math.FusedMultiplyAdd(0.2, period, 0.8 * prevPeriod); @@ -254,7 +268,10 @@ public sealed class Htit : AbstractBase /// Output time series with HTIT values public override TSeries Update(TSeries source) { - if (source.Count == 0) return new TSeries([], []); + if (source.Count == 0) + { + return new TSeries([], []); + } int len = source.Count; var v = new List(len); @@ -293,9 +310,14 @@ public sealed class Htit : AbstractBase public static void Calculate(ReadOnlySpan source, Span output) { if (source.Length != output.Length) + { throw new ArgumentException("Source and output must have the same length", nameof(output)); + } - if (source.Length == 0) return; + if (source.Length == 0) + { + return; + } // Stack allocate buffers // priceBuffer needs to be larger for IT calculation (up to 50 bars) @@ -416,11 +438,25 @@ public sealed class Htit : AbstractBase { double cap = 1.5 * p_period; double floor = 0.67 * p_period; - if (newPeriod > cap) newPeriod = cap; - if (newPeriod < floor) newPeriod = floor; + if (newPeriod > cap) + { + newPeriod = cap; + } + + if (newPeriod < floor) + { + newPeriod = floor; + } + } + if (newPeriod < 6) + { + newPeriod = 6; + } + + if (newPeriod > 50) + { + newPeriod = 50; } - if (newPeriod < 6) newPeriod = 6; - if (newPeriod > 50) newPeriod = 50; period = Math.FusedMultiplyAdd(0.2, newPeriod, 0.8 * p_period); smoothPeriod = Math.FusedMultiplyAdd(0.33, period, 0.67 * p_smoothPeriod); @@ -475,4 +511,4 @@ public sealed class Htit : AbstractBase } } } -} \ 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 d99117ea..88f66b70 100644 --- a/lib/trends_IIR/jma/Jma.Tests.cs +++ b/lib/trends_IIR/jma/Jma.Tests.cs @@ -310,4 +310,4 @@ public class JmaTests Assert.True(double.IsFinite(restoredResult.Value)); Assert.True(jma.IsHot); } -} \ No newline at end of file +} diff --git a/lib/trends_IIR/jma/Jma.ZeroDiv.Tests.cs b/lib/trends_IIR/jma/Jma.ZeroDiv.Tests.cs index 14fffedb..29facaa7 100644 --- a/lib/trends_IIR/jma/Jma.ZeroDiv.Tests.cs +++ b/lib/trends_IIR/jma/Jma.ZeroDiv.Tests.cs @@ -39,4 +39,4 @@ public class JmaZeroDivTests // With clamping, adapt is slightly non-zero (approx 1e-12), so result is very close to 200. Assert.Equal(200, result.Value, precision: 7); } -} \ No newline at end of file +} diff --git a/lib/trends_IIR/jma/Jma.cs b/lib/trends_IIR/jma/Jma.cs index 57b8f525..ab665396 100644 --- a/lib/trends_IIR/jma/Jma.cs +++ b/lib/trends_IIR/jma/Jma.cs @@ -63,17 +63,28 @@ public sealed class Jma : AbstractBase public Jma(int period, int phase = 0, double power = 0.45) { if (period < 1) + { throw new ArgumentOutOfRangeException(nameof(period), "Period must be >= 1."); + } + if (!double.IsFinite(power)) + { throw new ArgumentException("Power must be finite.", nameof(power)); + } // --- Phase parameter: maps -100..100 -> 0.5..2.5 (Jurik convention) --- if (phase < -100) + { _phaseParam = 0.5; + } else if (phase > 100) + { _phaseParam = 2.5; + } else + { _phaseParam = (phase * 0.01) + 1.5; + } // --- Length / log / divider parameters (from decompiled JMA) --- // L_raw ~ (period - 1)/2, with a tiny lower bound to avoid log(0) @@ -136,7 +147,10 @@ public sealed class Jma : AbstractBase if (!double.IsFinite(value)) { if (_state.Bars == 0) + { return double.NaN; + } + value = _state.LastPrice; } else @@ -146,7 +160,9 @@ public sealed class Jma : AbstractBase _state.Bars++; if (_state.Bars == 1) + { return InitializeFirstBar(value); + } return CalculateJma(value); } @@ -215,8 +231,16 @@ public sealed class Jma : AbstractBase { double ratio = Math.Max(absValue / refVolatility, 0.0); double d = Math.Pow(ratio, _pExponent); - if (d > _logParam) d = _logParam; - if (d < 1.0) d = 1.0; + if (d > _logParam) + { + d = _logParam; + } + + if (d < 1.0) + { + d = 1.0; + } + return d; } @@ -271,7 +295,10 @@ public sealed class Jma : AbstractBase public override TSeries Update(TSeries source) { - if (source.Count == 0) return []; + if (source.Count == 0) + { + return []; + } int len = source.Count; var t = new List(len); @@ -339,9 +366,14 @@ public sealed class Jma : AbstractBase double power = 0.45) { if (output.Length != source.Length) + { throw new ArgumentException("Source and output must have the same length.", nameof(output)); + } + if (source.Length == 0) + { return; + } var jma = new Jma(period, phase, power); for (int i = 0; i < source.Length; i++) @@ -383,10 +415,17 @@ public sealed class Jma : AbstractBase end = drop + slice - 1; } - if (start < 0) start = 0; - if (end >= count) end = count - 1; + if (start < 0) + { + start = 0; + } + + if (end >= count) + { + end = count - 1; + } 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.cs b/lib/trends_IIR/kama/Kama.cs index a1f24aff..ca3ec036 100644 --- a/lib/trends_IIR/kama/Kama.cs +++ b/lib/trends_IIR/kama/Kama.cs @@ -41,13 +41,24 @@ public sealed class Kama : AbstractBase public Kama(int period = 10, int fastPeriod = 2, int slowPeriod = 30) { if (period <= 0) + { throw new ArgumentException("Period must be greater than 0", nameof(period)); + } + if (fastPeriod <= 0) + { throw new ArgumentException("Fast period must be greater than 0", nameof(fastPeriod)); + } + if (slowPeriod <= 0) + { throw new ArgumentException("Slow period must be greater than 0", nameof(slowPeriod)); + } + if (fastPeriod >= slowPeriod) + { throw new ArgumentException("Fast period must be less than slow period", nameof(fastPeriod)); + } // Buffer needs to hold period + 1 values to calculate Change over 'period' bars // Change = Price[0] - Price[period] @@ -161,7 +172,10 @@ public sealed class Kama : AbstractBase // Avoid division by zero double er = (volatility > 1e-10) ? change / volatility : 0.0; // Cap ER at 1.0 just in case floating point errors push it slightly over - if (er > 1.0) er = 1.0; + if (er > 1.0) + { + er = 1.0; + } // double sc = er * (_fastAlpha - _slowAlpha) + _slowAlpha; // skipcq: S125 double sc = Math.FusedMultiplyAdd(er, _fastAlpha - _slowAlpha, _slowAlpha); @@ -184,7 +198,10 @@ public sealed class Kama : AbstractBase public override TSeries Update(TSeries source) { - if (source.Count == 0) return new TSeries([], []); + if (source.Count == 0) + { + return new TSeries([], []); + } int len = source.Count; var t = new List(len); @@ -227,11 +244,30 @@ public sealed class Kama : AbstractBase public static void Calculate(ReadOnlySpan source, Span output, int period, int fastPeriod = 2, int slowPeriod = 30) { - if (period <= 0) throw new ArgumentException("Period must be greater than 0", nameof(period)); - if (fastPeriod <= 0) throw new ArgumentException("Fast period must be greater than 0", nameof(fastPeriod)); - if (slowPeriod <= 0) throw new ArgumentException("Slow period must be greater than 0", nameof(slowPeriod)); - if (fastPeriod >= slowPeriod) throw new ArgumentException("Fast period must be less than slow period", nameof(fastPeriod)); - if (source.Length != output.Length) throw new ArgumentException("Source and output must have the same length", nameof(output)); + if (period <= 0) + { + throw new ArgumentException("Period must be greater than 0", nameof(period)); + } + + if (fastPeriod <= 0) + { + throw new ArgumentException("Fast period must be greater than 0", nameof(fastPeriod)); + } + + if (slowPeriod <= 0) + { + throw new ArgumentException("Slow period must be greater than 0", nameof(slowPeriod)); + } + + if (fastPeriod >= slowPeriod) + { + throw new ArgumentException("Fast period must be less than slow period", nameof(fastPeriod)); + } + + if (source.Length != output.Length) + { + throw new ArgumentException("Source and output must have the same length", nameof(output)); + } double fastAlpha = 2.0 / (fastPeriod + 1); double slowAlpha = 2.0 / (slowPeriod + 1); @@ -252,9 +288,13 @@ public sealed class Kama : AbstractBase { double val = source[i]; if (double.IsFinite(val)) + { lastValid = val; + } else + { val = lastValid; + } if (double.IsNaN(val)) { @@ -287,7 +327,10 @@ public sealed class Kama : AbstractBase } bufferIdx = (bufferIdx + 1) % bufSize; - if (count < bufSize) count++; + if (count < bufSize) + { + count++; + } if (!kamaInitialized) { @@ -307,7 +350,10 @@ public sealed class Kama : AbstractBase double change = Math.Abs(val - buffer[count == bufSize ? bufferIdx : 0]); double er = (volatilitySum > 1e-10) ? change / volatilitySum : 0.0; - if (er > 1.0) er = 1.0; + if (er > 1.0) + { + er = 1.0; + } // double sc = er * (fastAlpha - slowAlpha) + slowAlpha; // skipcq: S125 double sc = Math.FusedMultiplyAdd(er, fastAlpha - slowAlpha, slowAlpha); diff --git a/lib/trends_IIR/mama/Mama.Tests.cs b/lib/trends_IIR/mama/Mama.Tests.cs index de88d6ce..9a2672d0 100644 --- a/lib/trends_IIR/mama/Mama.Tests.cs +++ b/lib/trends_IIR/mama/Mama.Tests.cs @@ -137,8 +137,15 @@ public class MamaTests // Case 2: Update in chunks var chunk1 = new TSeries(); var chunk2 = new TSeries(); - for (int i = 0; i < 25; i++) chunk1.Add(data[i]); - for (int i = 25; i < 50; i++) chunk2.Add(data[i]); + for (int i = 0; i < 25; i++) + { + chunk1.Add(data[i]); + } + + for (int i = 25; i < 50; i++) + { + chunk2.Add(data[i]); + } mama2.Update(chunk1); var result2 = mama2.Update(chunk2); @@ -239,7 +246,10 @@ public class MamaTests const int count = 100; var data = new double[count]; var gbm = new GBM(startPrice: 100, seed: 42); - for (int i = 0; i < count; i++) data[i] = gbm.Next().Close; + for (int i = 0; i < count; i++) + { + data[i] = gbm.Next().Close; + } var output = new double[count]; Mama.Calculate(data, output); @@ -309,7 +319,10 @@ public class MamaTests { var data = new double[60]; var gbm = new GBM(startPrice: 100, seed: 42); - for (int i = 0; i < 60; i++) data[i] = gbm.Next().Close; + for (int i = 0; i < 60; i++) + { + data[i] = gbm.Next().Close; + } // 1. Prime with all but last value var mamaPrimed = new Mama(); @@ -336,7 +349,10 @@ public class MamaTests int count = 100; var data = new double[count]; var gbm = new GBM(startPrice: 100, seed: 42); - for (int i = 0; i < count; i++) data[i] = gbm.Next().Close; + for (int i = 0; i < count; i++) + { + data[i] = gbm.Next().Close; + } var mamaOutput = new double[count]; var famaOutput = new double[count]; @@ -357,7 +373,10 @@ public class MamaTests int count = 100; var data = new double[count]; var gbm = new GBM(startPrice: 100, seed: 42); - for (int i = 0; i < count; i++) data[i] = gbm.Next().Close; + for (int i = 0; i < count; i++) + { + data[i] = gbm.Next().Close; + } var output1 = new double[count]; var output2 = new double[count]; @@ -394,7 +413,10 @@ public class MamaTests int count = 10; var data = new double[count]; var gbm = new GBM(startPrice: 100, seed: 42); - for (int i = 0; i < count; i++) data[i] = gbm.Next().Close; + for (int i = 0; i < count; i++) + { + data[i] = gbm.Next().Close; + } // Get values from span calculation var mamaOutput = new double[count]; @@ -418,7 +440,10 @@ public class MamaTests int count = 100; var data = new double[count]; var gbm = new GBM(startPrice: 100, seed: 42); - for (int i = 0; i < count; i++) data[i] = gbm.Next().Close; + for (int i = 0; i < count; i++) + { + data[i] = gbm.Next().Close; + } // 1. Streaming Mode (instance method) var mama = new Mama(); diff --git a/lib/trends_IIR/mama/Mama.cs b/lib/trends_IIR/mama/Mama.cs index e6c0081e..5973e4c6 100644 --- a/lib/trends_IIR/mama/Mama.cs +++ b/lib/trends_IIR/mama/Mama.cs @@ -111,8 +111,16 @@ public sealed class Mama : AbstractBase return 0.0; // Return neutral angle for invalid inputs } - while (angle <= -Math.PI) angle += TwoPi; - while (angle > Math.PI) angle -= TwoPi; + while (angle <= -Math.PI) + { + angle += TwoPi; + } + + while (angle > Math.PI) + { + angle -= TwoPi; + } + return angle; } @@ -192,11 +200,25 @@ public sealed class Mama : AbstractBase double periodCap = _p_state.Period * 1.5; double periodFloor = _p_state.Period * 0.67; - if (period > periodCap) period = periodCap; - if (period < periodFloor) period = periodFloor; + if (period > periodCap) + { + period = periodCap; + } - if (period < MinPeriod) period = MinPeriod; - if (period > MaxPeriod) period = MaxPeriod; + if (period < periodFloor) + { + period = periodFloor; + } + + if (period < MinPeriod) + { + period = MinPeriod; + } + + if (period > MaxPeriod) + { + period = MaxPeriod; + } // Smooth Period (using FMA) _state.Period = Math.FusedMultiplyAdd(SmoothCoef, period, SmoothPrev * _p_state.Period); @@ -248,7 +270,10 @@ public sealed class Mama : AbstractBase public override TSeries Update(TSeries source) { - if (source.Count == 0) return new TSeries([], []); + if (source.Count == 0) + { + return new TSeries([], []); + } int len = source.Count; var v = new List(len); @@ -307,7 +332,11 @@ public sealed class Mama : AbstractBase throw new ArgumentOutOfRangeException(nameof(fastLimit), "FastLimit must be > SlowLimit"); } - if (source.Length == 0) return; + if (source.Length == 0) + { + return; + } + if (output.Length < source.Length) { throw new ArgumentOutOfRangeException(nameof(output), "Output buffer must be at least as large as the input buffer."); @@ -427,11 +456,25 @@ public sealed class Mama : AbstractBase double periodCap = p_period * 1.5; double periodFloor = p_period * 0.67; - if (newPeriod > periodCap) newPeriod = periodCap; - if (newPeriod < periodFloor) newPeriod = periodFloor; + if (newPeriod > periodCap) + { + newPeriod = periodCap; + } - if (newPeriod < MinPeriod) newPeriod = MinPeriod; - if (newPeriod > MaxPeriod) newPeriod = MaxPeriod; + if (newPeriod < periodFloor) + { + newPeriod = periodFloor; + } + + if (newPeriod < MinPeriod) + { + newPeriod = MinPeriod; + } + + if (newPeriod > MaxPeriod) + { + newPeriod = MaxPeriod; + } // Smooth Period (using FMA) period = Math.FusedMultiplyAdd(SmoothCoef, newPeriod, SmoothPrev * p_period); diff --git a/lib/trends_IIR/mgdi/Mgdi.cs b/lib/trends_IIR/mgdi/Mgdi.cs index 042ed8cc..cc1c1a2b 100644 --- a/lib/trends_IIR/mgdi/Mgdi.cs +++ b/lib/trends_IIR/mgdi/Mgdi.cs @@ -33,7 +33,11 @@ public sealed class Mgdi : AbstractBase public Mgdi(int period = 14, double k = 0.6) { ArgumentOutOfRangeException.ThrowIfLessThan(period, 1); - if (double.IsNaN(k) || double.IsInfinity(k) || k <= 0) throw new ArgumentOutOfRangeException(nameof(k), "k must be a finite value greater than 0"); + if (double.IsNaN(k) || double.IsInfinity(k) || k <= 0) + { + throw new ArgumentOutOfRangeException(nameof(k), "k must be a finite value greater than 0"); + } + _period = period; _k = k; Name = $"Mgdi({period},{k})"; @@ -123,7 +127,10 @@ public sealed class Mgdi : AbstractBase public override TSeries Update(TSeries source) { - if (source.Count == 0) return new TSeries([], []); + if (source.Count == 0) + { + return new TSeries([], []); + } int len = source.Count; var t = new List(len); @@ -167,12 +174,20 @@ public sealed class Mgdi : AbstractBase public static void Calculate(ReadOnlySpan source, Span output, int period = 14, double k = 0.6) { ArgumentOutOfRangeException.ThrowIfLessThan(period, 1); - if (double.IsNaN(k) || double.IsInfinity(k) || k <= 0) throw new ArgumentOutOfRangeException(nameof(k), "k must be a finite value greater than 0"); + if (double.IsNaN(k) || double.IsInfinity(k) || k <= 0) + { + throw new ArgumentOutOfRangeException(nameof(k), "k must be a finite value greater than 0"); + } if (source.Length != output.Length) + { throw new ArgumentException("Source and output must have the same length", nameof(output)); + } - if (source.Length == 0) return; + if (source.Length == 0) + { + return; + } double lastMgdi = 0; double lastValid = 0; @@ -225,4 +240,4 @@ public sealed class Mgdi : AbstractBase { Init(); } -} \ No newline at end of file +} diff --git a/lib/trends_IIR/mma/Mma.Validation.Tests.cs b/lib/trends_IIR/mma/Mma.Validation.Tests.cs index af4e8ee8..4419f844 100644 --- a/lib/trends_IIR/mma/Mma.Validation.Tests.cs +++ b/lib/trends_IIR/mma/Mma.Validation.Tests.cs @@ -68,9 +68,13 @@ public class MmaValidationTests { double val = source[i]; if (double.IsFinite(val)) + { lastValid = val; + } else + { val = lastValid; + } if (double.IsNaN(val)) { @@ -79,16 +83,22 @@ public class MmaValidationTests } if (count < window) + { count++; + } else + { sum -= buffer[head]; + } buffer[head] = val; sum += val; head++; if (head == window) + { head = 0; + } double sma = sum / count; double weightedSum = ComputeWeightedSum(buffer, head, count); @@ -101,7 +111,9 @@ public class MmaValidationTests { int idx = head - 1; if (idx < 0) + { idx = count - 1; + } double weightedSum = 0.0; for (int i = 0; i < count; i++) @@ -111,7 +123,9 @@ public class MmaValidationTests idx--; if (idx < 0) + { idx = count - 1; + } } return weightedSum; diff --git a/lib/trends_IIR/mma/Mma.cs b/lib/trends_IIR/mma/Mma.cs index a063e460..afb8c9ee 100644 --- a/lib/trends_IIR/mma/Mma.cs +++ b/lib/trends_IIR/mma/Mma.cs @@ -74,9 +74,13 @@ public sealed class Mma : AbstractBase double val = input.Value; if (double.IsFinite(val)) + { _lastValidValue = val; + } else + { val = _lastValidValue; + } if (double.IsNaN(val)) { @@ -97,7 +101,10 @@ public sealed class Mma : AbstractBase [MethodImpl(MethodImplOptions.AggressiveOptimization)] public override TSeries Update(TSeries source) { - if (source.Count == 0) return []; + if (source.Count == 0) + { + return []; + } int len = source.Count; var t = new List(len); @@ -123,9 +130,13 @@ public sealed class Mma : AbstractBase { double val = source.Values[i]; if (double.IsFinite(val)) + { lastValid = val; + } else + { val = lastValid; + } if (double.IsNaN(val)) { @@ -175,10 +186,16 @@ public sealed class Mma : AbstractBase public static void Calculate(ReadOnlySpan source, Span output, int period) { if (source.Length != output.Length) + { throw new ArgumentException("Source and output must have the same length.", nameof(output)); + } + ArgumentOutOfRangeException.ThrowIfLessThan(period, 2); - if (source.Length == 0) return; + if (source.Length == 0) + { + return; + } int window = Math.Min(Math.Max(2, period), MaxPeriod); double sum = 0.0; @@ -196,9 +213,13 @@ public sealed class Mma : AbstractBase { double val = source[i]; if (double.IsFinite(val)) + { lastValid = val; + } else + { val = lastValid; + } if (double.IsNaN(val)) { @@ -207,16 +228,22 @@ public sealed class Mma : AbstractBase } if (count < window) + { count++; + } else + { sum -= buffer[head]; + } buffer[head] = val; sum += val; head++; if (head == window) + { head = 0; + } double sma = sum / count; double weightedSum = ComputeWeightedSum(buffer, head, count); @@ -252,7 +279,9 @@ public sealed class Mma : AbstractBase { int count = _buffer.Count; if (count <= 0) + { return double.NaN; + } double sma = _buffer.Sum / count; double weightedSum = ComputeWeightedSum(_buffer, count); @@ -260,7 +289,9 @@ public sealed class Mma : AbstractBase double result = Math.FusedMultiplyAdd(weightedSum, 6.0 / denom, sma); if (!state.IsHot && count >= _period) + { state.IsHot = true; + } return result; } @@ -274,7 +305,9 @@ public sealed class Mma : AbstractBase int idx = start + count - 1; if (idx >= capacity) + { idx -= capacity; + } double weightedSum = 0.0; for (int i = 0; i < count; i++) @@ -284,7 +317,9 @@ public sealed class Mma : AbstractBase idx--; if (idx < 0) + { idx += capacity; + } } return weightedSum; @@ -295,7 +330,9 @@ public sealed class Mma : AbstractBase { int idx = head - 1; if (idx < 0) + { idx = count - 1; + } double weightedSum = 0.0; for (int i = 0; i < count; i++) @@ -305,9 +342,11 @@ public sealed class Mma : AbstractBase idx--; if (idx < 0) + { idx = count - 1; + } } return weightedSum; } -} \ No newline at end of file +} diff --git a/lib/trends_IIR/qema/Qema.Tests.cs b/lib/trends_IIR/qema/Qema.Tests.cs index 15e00e75..3128885e 100644 --- a/lib/trends_IIR/qema/Qema.Tests.cs +++ b/lib/trends_IIR/qema/Qema.Tests.cs @@ -350,7 +350,9 @@ public class QemaTests var gbm = new GBM(startPrice: 100, mu: 0.05, sigma: 0.2, seed: 42); for (int i = 0; i < source.Length; i++) + { source[i] = gbm.Next().Close; + } // Warm up Qema.Batch(source.AsSpan(), output.AsSpan(), 100); @@ -412,7 +414,10 @@ public class QemaTests // Verify against a fresh QEMA fed with same data var verifyQema = new Qema(5); - foreach (var val in history) verifyQema.Update(new TValue(DateTime.UtcNow, val)); + foreach (var val in history) + { + verifyQema.Update(new TValue(DateTime.UtcNow, val)); + } Assert.Equal(verifyQema.Last.Value, qema.Last.Value, 1e-10); Assert.Equal(verifyQema.IsHot, qema.IsHot); @@ -432,7 +437,10 @@ public class QemaTests qema.Prime(history); var verifyQema = new Qema(5); - foreach (var val in history) verifyQema.Update(new TValue(DateTime.UtcNow, val)); + foreach (var val in history) + { + verifyQema.Update(new TValue(DateTime.UtcNow, val)); + } Assert.Equal(verifyQema.Last.Value, qema.Last.Value, 1e-10); } diff --git a/lib/trends_IIR/qema/Qema.cs b/lib/trends_IIR/qema/Qema.cs index 32c1acec..4d1ba859 100644 --- a/lib/trends_IIR/qema/Qema.cs +++ b/lib/trends_IIR/qema/Qema.cs @@ -127,7 +127,10 @@ public sealed class Qema : AbstractBase /// Optional time step (not used) public override void Prime(ReadOnlySpan source, TimeSpan? step = null) { - if (source.Length == 0) return; + if (source.Length == 0) + { + return; + } // Reset state _state1 = EmaState.New(); @@ -163,9 +166,13 @@ public sealed class Qema : AbstractBase { double val = source[i]; if (double.IsFinite(val)) + { lastValid = val; + } else + { val = lastValid; + } double e1 = ComputeEma(val, _alpha1, _decay1, ref s1); double e2 = ComputeEma(e1, _alpha2, _decay2, ref s2); @@ -200,7 +207,11 @@ public sealed class Qema : AbstractBase [MethodImpl(MethodImplOptions.AggressiveInlining)] private static double GetCompensated(EmaState s) { - if (s.IsCompensated) return s.Ema; + if (s.IsCompensated) + { + return s.Ema; + } + return s.Ema / (1.0 - s.E); } @@ -226,9 +237,13 @@ public sealed class Qema : AbstractBase double val = input.Value; if (double.IsFinite(val)) + { _lastValidValue = val; + } else + { val = _lastValidValue; + } // Cascaded EMAs double e1 = ComputeEma(val, _alpha1, _decay1, ref _state1); @@ -247,7 +262,10 @@ public sealed class Qema : AbstractBase public override TSeries Update(TSeries source) { - if (source.Count == 0) return []; + if (source.Count == 0) + { + return []; + } int len = source.Count; List t = new(len); @@ -273,9 +291,13 @@ public sealed class Qema : AbstractBase { double val = sourceValues[i]; if (double.IsFinite(val)) + { lastValid = val; + } else + { val = lastValid; + } double e1 = ComputeEma(val, _alpha1, _decay1, ref s1); double e2 = ComputeEma(e1, _alpha2, _decay2, ref s2); @@ -314,7 +336,9 @@ public sealed class Qema : AbstractBase state.E *= decay; if (!state.IsHot && state.E <= COVERAGE_THRESHOLD) + { state.IsHot = true; + } if (state.E <= COMPENSATOR_THRESHOLD) { @@ -402,10 +426,16 @@ public sealed class Qema : AbstractBase 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)); + } + ArgumentOutOfRangeException.ThrowIfNegativeOrZero(period); - if (source.Length == 0) return; + if (source.Length == 0) + { + return; + } double alpha1 = Clamp01(2.0 / (period + 1)); double r = Math.Pow(1.0 / alpha1, 0.25); @@ -446,9 +476,13 @@ public sealed class Qema : AbstractBase { double val = source[i]; if (double.IsFinite(val)) + { lastValid = val; + } else + { val = lastValid; + } // EMA1 ema1_val = Math.FusedMultiplyAdd(ema1_val, decay1, alpha1 * val); diff --git a/lib/trends_IIR/rema/Rema.Quantower.Tests.cs b/lib/trends_IIR/rema/Rema.Quantower.Tests.cs index d669d2df..9fe7607c 100644 --- a/lib/trends_IIR/rema/Rema.Quantower.Tests.cs +++ b/lib/trends_IIR/rema/Rema.Quantower.Tests.cs @@ -204,4 +204,4 @@ public class RemaIndicatorTests Assert.NotEqual(result1, result2); } -} \ No newline at end of file +} diff --git a/lib/trends_IIR/rema/Rema.Quantower.cs b/lib/trends_IIR/rema/Rema.Quantower.cs index 60eb3d12..012c55ff 100644 --- a/lib/trends_IIR/rema/Rema.Quantower.cs +++ b/lib/trends_IIR/rema/Rema.Quantower.cs @@ -55,4 +55,4 @@ public class RemaIndicator : Indicator, IWatchlistIndicator TValue result = ma.Update(new TValue(item.TimeLeft.Ticks, _priceSelector(item)), isNew: args.IsNewBar()); Series.SetValue(result.Value, ma.IsHot, ShowColdValues); } -} \ No newline at end of file +} diff --git a/lib/trends_IIR/rema/Rema.Tests.cs b/lib/trends_IIR/rema/Rema.Tests.cs index 63ad8349..8fdcd4e3 100644 --- a/lib/trends_IIR/rema/Rema.Tests.cs +++ b/lib/trends_IIR/rema/Rema.Tests.cs @@ -447,7 +447,9 @@ public class RemaTests var gbm = new GBM(startPrice: 100, mu: 0.05, sigma: 0.2, seed: 42); for (int i = 0; i < source.Length; i++) + { source[i] = gbm.Next().Close; + } // Warm up Rema.Batch(source.AsSpan(), output.AsSpan(), 100); @@ -491,7 +493,10 @@ public class RemaTests // Verify against a fresh REMA fed with same data var verifyRema = new Rema(5); - foreach (var val in history) verifyRema.Update(new TValue(DateTime.UtcNow, val)); + foreach (var val in history) + { + verifyRema.Update(new TValue(DateTime.UtcNow, val)); + } Assert.Equal(verifyRema.Last.Value, rema.Last.Value, 1e-10); Assert.Equal(verifyRema.IsHot, rema.IsHot); @@ -511,7 +516,10 @@ public class RemaTests rema.Prime(history); var verifyRema = new Rema(5); - foreach (var val in history) verifyRema.Update(new TValue(DateTime.UtcNow, val)); + foreach (var val in history) + { + verifyRema.Update(new TValue(DateTime.UtcNow, val)); + } Assert.Equal(verifyRema.Last.Value, rema.Last.Value, 1e-10); } @@ -531,7 +539,10 @@ public class RemaTests public void Calculate_ReturnsCorrectResultsAndHotIndicator() { var series = new TSeries(); - for (int i = 1; i <= 20; i++) series.Add(DateTime.UtcNow, i * 10); + for (int i = 1; i <= 20; i++) + { + series.Add(DateTime.UtcNow, i * 10); + } var (results, indicator) = Rema.Calculate(series, 5); @@ -640,14 +651,20 @@ public class RemaTests // 3. Streaming Mode var streamingInd = new Rema(period, lambda); for (int i = 0; i < series.Count; i++) + { streamingInd.Update(series[i]); + } + double streamingResult = streamingInd.Last.Value; // 4. Eventing Mode var pubSource = new TSeries(); var eventingInd = new Rema(pubSource, period, lambda); for (int i = 0; i < series.Count; i++) + { pubSource.Add(series[i]); + } + double eventingResult = eventingInd.Last.Value; Assert.Equal(expected, spanResult, precision: 9); diff --git a/lib/trends_IIR/rema/Rema.Validation.Tests.cs b/lib/trends_IIR/rema/Rema.Validation.Tests.cs index a6fb4553..4c91ee9e 100644 --- a/lib/trends_IIR/rema/Rema.Validation.Tests.cs +++ b/lib/trends_IIR/rema/Rema.Validation.Tests.cs @@ -304,7 +304,10 @@ public sealed class RemaValidationTests : IDisposable private static double CalculateDiffVariance(double[] values, int startIdx, int count) { - if (count < 2) return 0; + if (count < 2) + { + return 0; + } // Calculate differences double sumDiff = 0; @@ -319,10 +322,13 @@ public sealed class RemaValidationTests : IDisposable n++; } - if (n < 2) return 0; + if (n < 2) + { + return 0; + } double mean = sumDiff / n; double variance = (sumDiffSq / n) - (mean * mean); return Math.Max(0, variance); // Ensure non-negative due to floating point } -} \ No newline at end of file +} diff --git a/lib/trends_IIR/rema/Rema.cs b/lib/trends_IIR/rema/Rema.cs index 0d949a80..b629dbbf 100644 --- a/lib/trends_IIR/rema/Rema.cs +++ b/lib/trends_IIR/rema/Rema.cs @@ -69,7 +69,9 @@ public sealed class Rema : AbstractBase { ArgumentOutOfRangeException.ThrowIfNegativeOrZero(period); if (lambda < 0.0 || lambda > 1.0) + { throw new ArgumentOutOfRangeException(nameof(lambda), "Lambda must be between 0 and 1"); + } _alpha = 2.0 / (period + 1); _decay = 1.0 - _alpha; @@ -108,7 +110,10 @@ public sealed class Rema : AbstractBase /// public override void Prime(ReadOnlySpan source, TimeSpan? step = null) { - if (source.Length == 0) return; + if (source.Length == 0) + { + return; + } _state = State.New(); _p_state = State.New(); @@ -152,7 +157,9 @@ public sealed class Rema : AbstractBase finally { if (rented != null) + { ArrayPool.Shared.Return(rented); + } } } @@ -196,7 +203,10 @@ public sealed class Rema : AbstractBase [MethodImpl(MethodImplOptions.AggressiveOptimization)] public override TSeries Update(TSeries source) { - if (source.Count == 0) return []; + if (source.Count == 0) + { + return []; + } int len = source.Count; var t = new List(len); @@ -245,7 +255,9 @@ public sealed class Rema : AbstractBase state.E *= decay; if (state.E <= COVERAGE_THRESHOLD) + { state.IsHot = true; + } result = input; } @@ -273,7 +285,9 @@ public sealed class Rema : AbstractBase state.E *= decay; if (!state.IsHot && state.E <= COVERAGE_THRESHOLD) + { state.IsHot = true; + } if (state.E <= COMPENSATOR_THRESHOLD) { @@ -311,9 +325,13 @@ public sealed class Rema : AbstractBase { double val = Unsafe.Add(ref srcRef, i); if (!double.IsFinite(val)) + { val = lastValidValue; + } else + { lastValidValue = val; + } double result; @@ -326,7 +344,9 @@ public sealed class Rema : AbstractBase state.E *= decay; if (state.E <= COVERAGE_THRESHOLD) + { state.IsHot = true; + } result = val; } @@ -345,7 +365,9 @@ public sealed class Rema : AbstractBase state.E *= decay; if (!state.IsHot && state.E <= COVERAGE_THRESHOLD) + { state.IsHot = true; + } if (state.E <= COMPENSATOR_THRESHOLD) { @@ -399,13 +421,24 @@ public sealed class Rema : AbstractBase public static void Batch(ReadOnlySpan source, Span output, int period, double lambda = 0.5) { if (period <= 0) + { throw new ArgumentException("Period must be greater than 0", nameof(period)); - if (lambda < 0.0 || lambda > 1.0) - throw new ArgumentOutOfRangeException(nameof(lambda), "Lambda must be between 0 and 1"); - if (source.Length != output.Length) - throw new ArgumentException("Source and output must have the same length", nameof(output)); + } - if (source.Length == 0) return; + if (lambda < 0.0 || lambda > 1.0) + { + throw new ArgumentOutOfRangeException(nameof(lambda), "Lambda must be between 0 and 1"); + } + + if (source.Length != output.Length) + { + throw new ArgumentException("Source and output must have the same length", nameof(output)); + } + + if (source.Length == 0) + { + return; + } double alpha = 2.0 / (period + 1); diff --git a/lib/trends_IIR/rgma/Rgma.Tests.cs b/lib/trends_IIR/rgma/Rgma.Tests.cs index f250356d..021d5d9e 100644 --- a/lib/trends_IIR/rgma/Rgma.Tests.cs +++ b/lib/trends_IIR/rgma/Rgma.Tests.cs @@ -98,10 +98,14 @@ public class RgmaTests var streamValues = new List(series.Count); for (int i = 0; i < series.Count; i++) + { streamValues.Add(rgma.Update(series[i]).Value); + } for (int i = 0; i < series.Count; i++) + { Assert.Equal(batch[i].Value, streamValues[i], precision: 10); + } } [Fact] @@ -117,7 +121,9 @@ public class RgmaTests TSeries batch = Rgma.Batch(series, period, passes); for (int i = 0; i < values.Length; i++) + { Assert.Equal(batch[i].Value, output[i], precision: 10); + } } private static TSeries BuildSeries(int count, int seed) diff --git a/lib/trends_IIR/rgma/Rgma.Validation.Tests.cs b/lib/trends_IIR/rgma/Rgma.Validation.Tests.cs index 4a2ec981..a522ed72 100644 --- a/lib/trends_IIR/rgma/Rgma.Validation.Tests.cs +++ b/lib/trends_IIR/rgma/Rgma.Validation.Tests.cs @@ -27,12 +27,16 @@ public sealed class RgmaValidationTests : IDisposable private void Dispose(bool disposing) { if (_disposed) + { return; + } _disposed = true; if (disposing) + { _testData?.Dispose(); + } } [Fact] @@ -52,7 +56,9 @@ public sealed class RgmaValidationTests : IDisposable int startIdx = rgmaResult.Count - compareCount; for (int i = startIdx; i < rgmaResult.Count; i++) + { Assert.Equal(emaResult[i].Value, rgmaResult[i].Value, 1e-10); + } } _output.WriteLine("RGMA(passes=1) Batch validated successfully against EMA"); @@ -81,7 +87,9 @@ public sealed class RgmaValidationTests : IDisposable int startIdx = rgmaResults.Count - compareCount; for (int i = startIdx; i < rgmaResults.Count; i++) + { Assert.Equal(emaResults[i], rgmaResults[i], 1e-10); + } } _output.WriteLine("RGMA(passes=1) Streaming validated successfully against EMA"); @@ -105,7 +113,9 @@ public sealed class RgmaValidationTests : IDisposable int startIdx = sourceData.Length - compareCount; for (int i = startIdx; i < sourceData.Length; i++) + { Assert.Equal(emaOutput[i], rgmaOutput[i], 1e-10); + } } _output.WriteLine("RGMA(passes=1) Span validated successfully against EMA"); @@ -131,7 +141,9 @@ public sealed class RgmaValidationTests : IDisposable var rgmaStream = new Rgma(period, passCount); var streaming = new double[_testData.Data.Count]; for (int i = 0; i < _testData.Data.Count; i++) + { streaming[i] = rgmaStream.Update(_testData.Data[i]).Value; + } // Span var spanOutput = new double[sourceData.Length]; diff --git a/lib/trends_IIR/rgma/Rgma.cs b/lib/trends_IIR/rgma/Rgma.cs index 9a9c9ddc..28923f0c 100644 --- a/lib/trends_IIR/rgma/Rgma.cs +++ b/lib/trends_IIR/rgma/Rgma.cs @@ -111,7 +111,10 @@ public sealed class Rgma : AbstractBase /// public override void Prime(ReadOnlySpan source, TimeSpan? step = null) { - if (source.Length == 0) return; + if (source.Length == 0) + { + return; + } _state = State.New(); _p_state = State.New(); @@ -172,9 +175,14 @@ public sealed class Rgma : AbstractBase finally { if (filtersRented != null) + { ArrayPool.Shared.Return(filtersRented); + } + if (rented != null) + { ArrayPool.Shared.Return(rented); + } } } @@ -189,7 +197,9 @@ public sealed class Rgma : AbstractBase if (_passes <= 8) { for (int i = 0; i < _passes; i++) + { _p_filters[i] = _filters[i]; + } } else { @@ -203,7 +213,9 @@ public sealed class Rgma : AbstractBase if (_passes <= 8) { for (int i = 0; i < _passes; i++) + { _filters[i] = _p_filters[i]; + } } else { @@ -222,7 +234,10 @@ public sealed class Rgma : AbstractBase [MethodImpl(MethodImplOptions.AggressiveOptimization)] public override TSeries Update(TSeries source) { - if (source.Count == 0) return []; + if (source.Count == 0) + { + return []; + } int len = source.Count; var t = new List(len); @@ -263,22 +278,31 @@ public sealed class Rgma : AbstractBase state.TickCount = 1; state.E *= decay; if (state.E <= COVERAGE_THRESHOLD) + { state.IsHot = true; + } + return input; } // Stage 0 filters[0] = Math.FusedMultiplyAdd(alpha, input - filters[0], filters[0]); for (int i = 1; i < filters.Length; i++) + { filters[i] = Math.FusedMultiplyAdd(alpha, filters[i - 1] - filters[i], filters[i]); + } state.TickCount++; state.E *= decay; if (!state.IsHot && state.E <= COVERAGE_THRESHOLD) + { state.IsHot = true; + } if (state.TickCount >= ResyncInterval) + { state.TickCount = 0; + } return filters[^1]; } @@ -298,9 +322,13 @@ public sealed class Rgma : AbstractBase { double x = source[i]; if (double.IsFinite(x)) + { lastValid = x; + } else + { x = lastValid; + } double y; if (!state.IsInitialized) @@ -310,22 +338,31 @@ public sealed class Rgma : AbstractBase state.TickCount = 1; state.E *= decay; if (state.E <= COVERAGE_THRESHOLD) + { state.IsHot = true; + } + y = x; } else { filters[0] = Math.FusedMultiplyAdd(alpha, x - filters[0], filters[0]); for (int p = 1; p < filters.Length; p++) + { filters[p] = Math.FusedMultiplyAdd(alpha, filters[p - 1] - filters[p], filters[p]); + } state.TickCount++; state.E *= decay; if (!state.IsHot && state.E <= COVERAGE_THRESHOLD) + { state.IsHot = true; + } if (state.TickCount >= ResyncInterval) + { state.TickCount = 0; + } y = filters[^1]; } @@ -361,13 +398,24 @@ public sealed class Rgma : AbstractBase public static void Batch(ReadOnlySpan source, Span output, int period, int passes = 3) { if (period <= 0) + { throw new ArgumentException("Period must be greater than 0", nameof(period)); - if (passes <= 0) - throw new ArgumentException("Passes must be greater than 0", nameof(passes)); - if (source.Length != output.Length) - throw new ArgumentException("Source and output must have the same length", nameof(output)); + } - if (source.Length == 0) return; + if (passes <= 0) + { + throw new ArgumentException("Passes must be greater than 0", nameof(passes)); + } + + if (source.Length != output.Length) + { + throw new ArgumentException("Source and output must have the same length", nameof(output)); + } + + if (source.Length == 0) + { + return; + } double alpha = 2.0 / (period / Math.Sqrt(passes) + 1.0); double decay = 1.0 - alpha; @@ -405,7 +453,9 @@ public sealed class Rgma : AbstractBase finally { if (rented != null) + { ArrayPool.Shared.Return(rented); + } } } @@ -420,4 +470,4 @@ public sealed class Rgma : AbstractBase Array.Fill(_p_filters, double.NaN); Last = default; } -} \ No newline at end of file +} diff --git a/lib/trends_IIR/rma/Rma.cs b/lib/trends_IIR/rma/Rma.cs index 4cd9efa1..7e69e78a 100644 --- a/lib/trends_IIR/rma/Rma.cs +++ b/lib/trends_IIR/rma/Rma.cs @@ -29,7 +29,9 @@ public sealed class Rma : AbstractBase public Rma(int period) { if (period <= 0) + { throw new ArgumentException("Period must be greater than 0", nameof(period)); + } _ema = new Ema(1.0 / period); Name = $"Rma({period})"; @@ -118,10 +120,14 @@ public sealed class Rma : AbstractBase public static void Batch(ReadOnlySpan source, Span output, int period) { if (period <= 0) + { throw new ArgumentException("Period must be greater than 0", nameof(period)); + } if (output.Length < source.Length) + { throw new ArgumentException("Output span must be at least as long as source span", nameof(output)); + } double alpha = 1.0 / period; Ema.Batch(source, output, alpha); @@ -150,4 +156,4 @@ public sealed class Rma : AbstractBase _ema.Reset(); Last = default; } -} \ No newline at end of file +} diff --git a/lib/trends_IIR/t3/T3.cs b/lib/trends_IIR/t3/T3.cs index 5c9af726..90caf8eb 100644 --- a/lib/trends_IIR/t3/T3.cs +++ b/lib/trends_IIR/t3/T3.cs @@ -61,11 +61,19 @@ public sealed class T3 : AbstractBase public T3(int period, double vfactor = 0.7) { if (period <= 0) + { throw new ArgumentException("Period must be greater than 0", nameof(period)); + } + if (!double.IsFinite(vfactor)) + { throw new ArgumentOutOfRangeException(nameof(vfactor), "Volume factor must be a finite number (not NaN or Infinity)"); + } + if (vfactor <= 0 || vfactor > 1) + { throw new ArgumentOutOfRangeException(nameof(vfactor), "Volume factor must be greater than 0 and typically <= 1"); + } double alpha = 2.0 / (period + 1); double decay = 1.0 - alpha; @@ -136,7 +144,10 @@ public sealed class T3 : AbstractBase /// Historical data public override void Prime(ReadOnlySpan source, TimeSpan? step = null) { - if (source.Length == 0) return; + if (source.Length == 0) + { + return; + } // Reset state _state = State.New(); @@ -188,7 +199,7 @@ public sealed class T3 : AbstractBase // So the state corresponds to "after processing source". // To get the output value corresponding to the last input, we can calculate it from the state. // But T3 formula uses the *updated* EMAs. - // T3 = c1*e6 + c2*e5 + c3*e4 + c4*e3 + // T3 = c1*e6 + c2*e5 + c3*e4 + c4*e3 // The state has the updated EMAs. double result = Math.FusedMultiplyAdd(_params.C4, _state.E3, Math.FusedMultiplyAdd(_params.C3, _state.E4, @@ -235,7 +246,10 @@ public sealed class T3 : AbstractBase public override TSeries Update(TSeries source) { - if (source.Count == 0) return []; + if (source.Count == 0) + { + return []; + } int len = source.Count; var t = new List(len); @@ -298,9 +312,13 @@ public sealed class T3 : AbstractBase { double val = source[i]; if (double.IsFinite(val)) + { lastValidValue = val; + } else + { val = lastValidValue; + } output[i] = Compute(val, p, ref state); } @@ -322,13 +340,24 @@ public sealed class T3 : AbstractBase public static void Batch(ReadOnlySpan source, Span output, int period, double vfactor = 0.7) { if (period <= 0) + { throw new ArgumentException("Period must be greater than 0", nameof(period)); + } + if (source.Length != output.Length) + { throw new ArgumentException("Source and output must have the same length", nameof(output)); + } + if (!double.IsFinite(vfactor)) + { throw new ArgumentOutOfRangeException(nameof(vfactor), "Volume factor must be a finite number (not NaN or Infinity)"); + } + if (vfactor <= 0 || vfactor > 1) + { throw new ArgumentOutOfRangeException(nameof(vfactor), "Volume factor must be greater than 0 and typically <= 1"); + } double alpha = 2.0 / (period + 1); double decay = 1.0 - alpha; @@ -370,4 +399,4 @@ public sealed class T3 : AbstractBase } base.Dispose(disposing); } -} \ No newline at end of file +} diff --git a/lib/trends_IIR/tema/Tema.Quantower.cs b/lib/trends_IIR/tema/Tema.Quantower.cs index 49c1be7c..75adf717 100644 --- a/lib/trends_IIR/tema/Tema.Quantower.cs +++ b/lib/trends_IIR/tema/Tema.Quantower.cs @@ -51,7 +51,9 @@ public sealed class TemaIndicator : Indicator, IWatchlistIndicator protected override void OnUpdate(UpdateArgs args) { if (args.Reason != UpdateReason.NewBar && args.Reason != UpdateReason.HistoricalBar && args.Reason != UpdateReason.NewTick) + { return; + } var item = HistoricalData[Count - 1, SeekOriginHistory.Begin]; TValue result = _ma.Update(new TValue(item.TimeLeft.Ticks, _priceSelector(item)), args.IsNewBar()); diff --git a/lib/trends_IIR/tema/Tema.cs b/lib/trends_IIR/tema/Tema.cs index d5faec5b..554794d9 100644 --- a/lib/trends_IIR/tema/Tema.cs +++ b/lib/trends_IIR/tema/Tema.cs @@ -50,7 +50,10 @@ public sealed class Tema : AbstractBase public Tema(int period) { - if (period <= 0) throw new ArgumentException("Period must be greater than 0", nameof(period)); + if (period <= 0) + { + throw new ArgumentException("Period must be greater than 0", nameof(period)); + } _alpha = 2.0 / (period + 1); _decay = 1.0 - _alpha; @@ -76,7 +79,10 @@ public sealed class Tema : AbstractBase public Tema(double alpha) { - if (alpha <= 0 || alpha >= 1) throw new ArgumentException("Alpha must be strictly between 0 and 1", nameof(alpha)); + if (alpha <= 0 || alpha >= 1) + { + throw new ArgumentException("Alpha must be strictly between 0 and 1", nameof(alpha)); + } _alpha = alpha; _decay = 1.0 - alpha; @@ -93,7 +99,10 @@ public sealed class Tema : AbstractBase /// Historical data public override void Prime(ReadOnlySpan source, TimeSpan? step = null) { - if (source.Length == 0) return; + if (source.Length == 0) + { + return; + } // Reset state _state1 = EmaState.New(); @@ -131,9 +140,13 @@ public sealed class Tema : AbstractBase { double val = source[i]; if (double.IsFinite(val)) + { lastValid = val; + } else + { val = lastValid; + } double e1 = Compute(val, alpha, decay, ref s1); double e2 = Compute(e1, alpha, decay, ref s2); @@ -157,7 +170,11 @@ public sealed class Tema : AbstractBase double GetCompensated(EmaState s) { - if (s.IsCompensated) return s.Ema; + if (s.IsCompensated) + { + return s.Ema; + } + return s.Ema / (1.0 - s.E); } @@ -196,9 +213,13 @@ public sealed class Tema : AbstractBase // EMA1 double val = input.Value; if (double.IsFinite(val)) + { _lastValidValue = val; + } else + { val = _lastValidValue; + } double e1 = Compute(val, _alpha, _decay, ref _state1); @@ -217,7 +238,10 @@ public sealed class Tema : AbstractBase public override TSeries Update(TSeries source) { - if (source.Count == 0) return []; + if (source.Count == 0) + { + return []; + } int len = source.Count; List t = new(len); @@ -243,9 +267,13 @@ public sealed class Tema : AbstractBase { double val = sourceValues[i]; if (double.IsFinite(val)) + { lastValid = val; + } else + { val = lastValid; + } double e1 = Compute(val, alpha, decay, ref s1); double e2 = Compute(e1, alpha, decay, ref s2); @@ -281,7 +309,9 @@ public sealed class Tema : AbstractBase state.E *= decay; if (!state.IsHot && state.E <= 0.05) // COVERAGE_THRESHOLD + { state.IsHot = true; + } if (state.E <= 1e-10) // COMPENSATOR_THRESHOLD { @@ -316,7 +346,9 @@ public sealed class Tema : AbstractBase public static void Batch(ReadOnlySpan source, Span output, int period) { if (period <= 0) + { throw new ArgumentException("Period must be greater than 0", nameof(period)); + } double alpha = 2.0 / (period + 1); Batch(source, output, alpha); @@ -325,11 +357,19 @@ public sealed class Tema : AbstractBase public static void Batch(ReadOnlySpan source, Span output, double alpha) { if (source.Length != output.Length) + { throw new ArgumentException("Source and output must have the same length", nameof(output)); - if (alpha <= 0 || alpha >= 1) - throw new ArgumentException("Alpha must be strictly between 0 and 1", nameof(alpha)); + } - if (source.Length == 0) return; + if (alpha <= 0 || alpha >= 1) + { + throw new ArgumentException("Alpha must be strictly between 0 and 1", nameof(alpha)); + } + + if (source.Length == 0) + { + return; + } double decay = 1.0 - alpha; double lastValid = 0; @@ -363,9 +403,13 @@ public sealed class Tema : AbstractBase { double val = source[i]; if (double.IsFinite(val)) + { lastValid = val; + } else + { val = lastValid; + } // Update EMA1: ema = decay * ema + alpha * input = FMA(decay, ema, alpha * input) ema1_val = Math.FusedMultiplyAdd(decay, ema1_val, alpha * val); @@ -447,4 +491,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.Tests.cs b/lib/trends_IIR/vama/Vama.Tests.cs index 3037dde2..25f24556 100644 --- a/lib/trends_IIR/vama/Vama.Tests.cs +++ b/lib/trends_IIR/vama/Vama.Tests.cs @@ -296,7 +296,10 @@ public class VamaTests // Verify against a fresh VAMA fed with same data var verifyVama = new Vama(); - foreach (var val in history) verifyVama.Update(new TValue(DateTime.UtcNow, val)); + foreach (var val in history) + { + verifyVama.Update(new TValue(DateTime.UtcNow, val)); + } Assert.Equal(verifyVama.Last.Value, vama.Last.Value, 1e-10); @@ -315,7 +318,10 @@ public class VamaTests vama.Prime(history); var verifyVama = new Vama(); - foreach (var val in history) verifyVama.Update(new TValue(DateTime.UtcNow, val)); + foreach (var val in history) + { + verifyVama.Update(new TValue(DateTime.UtcNow, val)); + } Assert.Equal(verifyVama.Last.Value, vama.Last.Value, 1e-10); } diff --git a/lib/trends_IIR/vama/Vama.cs b/lib/trends_IIR/vama/Vama.cs index d5c5a7a0..f3644a51 100644 --- a/lib/trends_IIR/vama/Vama.cs +++ b/lib/trends_IIR/vama/Vama.cs @@ -77,17 +77,34 @@ public sealed class Vama : AbstractBase public Vama(int baseLength = 20, int shortAtrPeriod = 10, int longAtrPeriod = 50, int minLength = 5, int maxLength = 100) { if (baseLength <= 0) + { throw new ArgumentException("Base length must be greater than 0", nameof(baseLength)); + } + if (shortAtrPeriod <= 0) + { throw new ArgumentException("Short ATR period must be greater than 0", nameof(shortAtrPeriod)); + } + if (longAtrPeriod <= 0) + { throw new ArgumentException("Long ATR period must be greater than 0", nameof(longAtrPeriod)); + } + if (minLength <= 0) + { throw new ArgumentException("Min length must be greater than 0", nameof(minLength)); + } + if (maxLength <= 0) + { throw new ArgumentException("Max length must be greater than 0", nameof(maxLength)); + } + if (minLength > maxLength) + { throw new ArgumentException("Min length must be less than or equal to max length", nameof(minLength)); + } _baseLength = baseLength; _minLength = minLength; @@ -167,11 +184,17 @@ public sealed class Vama : AbstractBase shortAtr.Ema = Math.FusedMultiplyAdd(shortAtr.Ema, _shortDecay, _shortAlpha * trueRange); shortAtr.E *= _shortDecay; - if (shortAtr.E <= EPSILON) shortAtr.IsCompensated = true; + if (shortAtr.E <= EPSILON) + { + shortAtr.IsCompensated = true; + } longAtr.Ema = Math.FusedMultiplyAdd(longAtr.Ema, _longDecay, _longAlpha * trueRange); longAtr.E *= _longDecay; - if (longAtr.E <= EPSILON) longAtr.IsCompensated = true; + if (longAtr.E <= EPSILON) + { + longAtr.IsCompensated = true; + } // Compensated ATR values double shortAtrValue = shortAtr.IsCompensated ? shortAtr.Ema : shortAtr.Ema / (1.0 - shortAtr.E); @@ -187,9 +210,13 @@ public sealed class Vama : AbstractBase // Update circular buffer with source value double sourceValue = input.Close; if (!double.IsFinite(sourceValue)) + { sourceValue = _lastValidValue; + } else + { _lastValidValue = sourceValue; + } // Remove oldest value from sum if it was valid double oldest = _buffer[_state.BufferHead]; @@ -271,7 +298,10 @@ public sealed class Vama : AbstractBase /// public TSeries Update(TBarSeries source) { - if (source.Count == 0) return []; + if (source.Count == 0) + { + return []; + } int len = source.Count; var t = new List(len); @@ -298,7 +328,10 @@ public sealed class Vama : AbstractBase /// public override TSeries Update(TSeries source) { - if (source.Count == 0) return []; + if (source.Count == 0) + { + return []; + } int len = source.Count; var t = new List(len); diff --git a/lib/trends_IIR/vidya/Vidya.Quantower.cs b/lib/trends_IIR/vidya/Vidya.Quantower.cs index 0cbaa118..1039228a 100644 --- a/lib/trends_IIR/vidya/Vidya.Quantower.cs +++ b/lib/trends_IIR/vidya/Vidya.Quantower.cs @@ -51,7 +51,9 @@ public sealed class VidyaIndicator : Indicator, IWatchlistIndicator protected override void OnUpdate(UpdateArgs args) { if (args.Reason != UpdateReason.NewBar && args.Reason != UpdateReason.HistoricalBar && args.Reason != UpdateReason.NewTick) + { return; + } var item = HistoricalData[Count - 1, SeekOriginHistory.Begin]; TValue result = _ma.Update(new TValue(item.TimeLeft.Ticks, _priceSelector(item)), args.IsNewBar()); diff --git a/lib/trends_IIR/vidya/Vidya.cs b/lib/trends_IIR/vidya/Vidya.cs index 0d123c0b..f3f6cb9a 100644 --- a/lib/trends_IIR/vidya/Vidya.cs +++ b/lib/trends_IIR/vidya/Vidya.cs @@ -46,7 +46,9 @@ public sealed class Vidya : AbstractBase public Vidya(int period) { if (period <= 0) + { throw new ArgumentException("Period must be greater than 0", nameof(period)); + } _period = period; _alpha = 2.0 / (period + 1); @@ -100,7 +102,11 @@ public sealed class Vidya : AbstractBase double price = input.Value; if (!double.IsFinite(price)) { - if (!_state.IsInitialized) return input; + if (!_state.IsInitialized) + { + return input; + } + price = _state.CurrentClose; } @@ -147,7 +153,10 @@ public sealed class Vidya : AbstractBase public override TSeries Update(TSeries source) { - if (source.Count == 0) return []; + if (source.Count == 0) + { + return []; + } int len = source.Count; var t = new List(len); @@ -197,7 +206,10 @@ public sealed class Vidya : AbstractBase public override void Prime(ReadOnlySpan source, TimeSpan? step = null) { - if (source.Length == 0) return; + if (source.Length == 0) + { + return; + } // Reset state Reset(); @@ -224,7 +236,10 @@ public sealed class Vidya : AbstractBase for (int i = 1; i < source.Length; i++) { double price = source[i]; - if (!double.IsFinite(price)) price = prevClose; + if (!double.IsFinite(price)) + { + price = prevClose; + } double change = price - prevClose; double up = change > 0 ? change : 0; @@ -295,11 +310,19 @@ public sealed class Vidya : AbstractBase public static void Batch(ReadOnlySpan source, Span output, int period) { if (period <= 0) + { throw new ArgumentException("Period must be greater than 0", nameof(period)); - if (source.Length != output.Length) - throw new ArgumentException("Source and output must have the same length", nameof(output)); + } - if (source.Length == 0) return; + if (source.Length != output.Length) + { + throw new ArgumentException("Source and output must have the same length", nameof(output)); + } + + if (source.Length == 0) + { + return; + } double alpha = 2.0 / (period + 1); @@ -346,7 +369,10 @@ public sealed class Vidya : AbstractBase sumDown += down; head++; - if (head >= period) head = 0; + if (head >= period) + { + head = 0; + } double sum = sumUp + sumDown; double vi = 0; diff --git a/lib/trends_IIR/yzvama/Yzvama.Tests.cs b/lib/trends_IIR/yzvama/Yzvama.Tests.cs index 2d0995b1..5f6c96c4 100644 --- a/lib/trends_IIR/yzvama/Yzvama.Tests.cs +++ b/lib/trends_IIR/yzvama/Yzvama.Tests.cs @@ -70,7 +70,9 @@ public class YzvamaTests Assert.False(yzvama.IsHot); for (int i = 0; i < 5; i++) + { yzvama.Update(new TValue(DateTime.UtcNow, 100 + i), isNew: true); + } Assert.True(yzvama.IsHot); } diff --git a/lib/trends_IIR/yzvama/Yzvama.Validation.Tests.cs b/lib/trends_IIR/yzvama/Yzvama.Validation.Tests.cs index 50aa2f19..1d531138 100644 --- a/lib/trends_IIR/yzvama/Yzvama.Validation.Tests.cs +++ b/lib/trends_IIR/yzvama/Yzvama.Validation.Tests.cs @@ -35,7 +35,9 @@ public class YzvamaValidationTests const double constantValue = 42.5; for (int i = 0; i < 300; i++) + { yzvama.Update(new TValue(DateTime.UtcNow, constantValue), isNew: true); + } Assert.Equal(constantValue, yzvama.Last.Value, Tolerance); } diff --git a/lib/trends_IIR/yzvama/Yzvama.cs b/lib/trends_IIR/yzvama/Yzvama.cs index 0a314a07..c4d3c2ec 100644 --- a/lib/trends_IIR/yzvama/Yzvama.cs +++ b/lib/trends_IIR/yzvama/Yzvama.cs @@ -89,17 +89,34 @@ public sealed class Yzvama : AbstractBase public Yzvama(int yzvShortPeriod = 3, int yzvLongPeriod = 50, int percentileLookback = 100, int minLength = 5, int maxLength = 100) { if (yzvShortPeriod <= 0) + { throw new ArgumentException("Short YZV period must be greater than 0", nameof(yzvShortPeriod)); + } + if (yzvLongPeriod <= 0) + { throw new ArgumentException("Long YZV period must be greater than 0", nameof(yzvLongPeriod)); + } + if (percentileLookback <= 0) + { throw new ArgumentException("Percentile lookback must be greater than 0", nameof(percentileLookback)); + } + if (minLength <= 0) + { throw new ArgumentException("Min length must be greater than 0", nameof(minLength)); + } + if (maxLength <= 0) + { throw new ArgumentException("Max length must be greater than 0", nameof(maxLength)); + } + if (minLength > maxLength) + { throw new ArgumentException("Min length must be less than or equal to max length", nameof(minLength)); + } _percentileLookback = percentileLookback; _minLength = minLength; @@ -163,7 +180,9 @@ public sealed class Yzvama : AbstractBase private static double ComputeYangZhangK(int period) { if (period <= 1) + { return 0.34 / (1.34 + 1.0); + } double ratioN = (period + 1.0) / (period - 1.0); return 0.34 / (1.34 + ratioN); @@ -178,9 +197,13 @@ public sealed class Yzvama : AbstractBase { int mid = lo + ((hi - lo) >> 1); if (sorted[mid] < value) + { lo = mid + 1; + } else + { hi = mid; + } } return lo; } @@ -254,9 +277,13 @@ public sealed class Yzvama : AbstractBase // Sanitize source if (!double.IsFinite(sourceValue)) + { sourceValue = double.IsFinite(_lastValidSource) ? _lastValidSource : 0.0; + } else + { _lastValidSource = sourceValue; + } // Compute Yang-Zhang variance components (log returns) double yzvShort = double.NaN; @@ -290,7 +317,10 @@ public sealed class Yzvama : AbstractBase // Update short RMA variance shortVar.Ema = Math.FusedMultiplyAdd(shortVar.Ema, _shortDecay, _shortAlpha * sSqDailyShort); shortVar.E *= _shortDecay; - if (shortVar.E <= EPSILON) shortVar.IsCompensated = true; + if (shortVar.E <= EPSILON) + { + shortVar.IsCompensated = true; + } double shortVarValue = shortVar.IsCompensated ? shortVar.Ema : shortVar.Ema / (1.0 - shortVar.E); yzvShort = shortVarValue >= 0 ? Math.Sqrt(shortVarValue) : double.NaN; @@ -298,7 +328,10 @@ public sealed class Yzvama : AbstractBase // Update long RMA variance (kept for parity with Pine implementation) longVar.Ema = Math.FusedMultiplyAdd(longVar.Ema, _longDecay, _longAlpha * sSqDailyLong); longVar.E *= _longDecay; - if (longVar.E <= EPSILON) longVar.IsCompensated = true; + if (longVar.E <= EPSILON) + { + longVar.IsCompensated = true; + } } } @@ -414,7 +447,10 @@ public sealed class Yzvama : AbstractBase /// public TSeries Update(TBarSeries source) { - if (source.Count == 0) return []; + if (source.Count == 0) + { + return []; + } int len = source.Count; var t = new List(len); @@ -441,7 +477,10 @@ public sealed class Yzvama : AbstractBase /// public override TSeries Update(TSeries source) { - if (source.Count == 0) return []; + if (source.Count == 0) + { + return []; + } int len = source.Count; var t = new List(len); @@ -473,7 +512,9 @@ public sealed class Yzvama : AbstractBase { Reset(); foreach (double val in source) + { Update(new TValue(DateTime.MinValue, val), isNew: true); + } } /// @@ -484,7 +525,9 @@ public sealed class Yzvama : AbstractBase { Reset(); foreach (TValue tv in source) + { Update(tv, isNew: true); + } } /// diff --git a/lib/trends_IIR/zlema/Zlema.Validation.Tests.cs b/lib/trends_IIR/zlema/Zlema.Validation.Tests.cs index c5fd6b57..8e90806a 100644 --- a/lib/trends_IIR/zlema/Zlema.Validation.Tests.cs +++ b/lib/trends_IIR/zlema/Zlema.Validation.Tests.cs @@ -74,9 +74,13 @@ public class ZlemaValidationTests { double val = source[i]; if (double.IsFinite(val)) + { lastValid = val; + } else + { val = lastValid; + } if (double.IsNaN(val)) { @@ -87,7 +91,9 @@ public class ZlemaValidationTests buffer[head] = val; head++; if (head == bufferSize) + { head = 0; + } double lagged = buffer[head]; double signal = Math.FusedMultiplyAdd(2.0, val, -lagged); diff --git a/lib/trends_IIR/zlema/Zlema.cs b/lib/trends_IIR/zlema/Zlema.cs index 44ba8171..e650bf3c 100644 --- a/lib/trends_IIR/zlema/Zlema.cs +++ b/lib/trends_IIR/zlema/Zlema.cs @@ -58,7 +58,9 @@ public sealed class Zlema : AbstractBase public Zlema(double alpha) { if (alpha <= 0.0 || alpha > 1.0 || !double.IsFinite(alpha)) + { throw new ArgumentException("Alpha must be finite and in (0, 1].", nameof(alpha)); + } _alpha = alpha; _beta = 1.0 - _alpha; @@ -97,9 +99,13 @@ public sealed class Zlema : AbstractBase double val = input.Value; if (double.IsFinite(val)) + { _lastValidValue = val; + } else + { val = _lastValidValue; + } if (double.IsNaN(val)) { @@ -123,7 +129,10 @@ public sealed class Zlema : AbstractBase [MethodImpl(MethodImplOptions.AggressiveOptimization)] public override TSeries Update(TSeries source) { - if (source.Count == 0) return []; + if (source.Count == 0) + { + return []; + } int len = source.Count; List t = new(len); @@ -146,9 +155,13 @@ public sealed class Zlema : AbstractBase { double val = source.Values[i]; if (double.IsFinite(val)) + { lastValid = val; + } else + { val = lastValid; + } if (double.IsNaN(val)) { @@ -190,10 +203,16 @@ public sealed class Zlema : AbstractBase public static void Calculate(ReadOnlySpan source, Span output, int period) { if (source.Length != output.Length) + { throw new ArgumentException("Source and output must have the same length.", nameof(output)); + } + ArgumentOutOfRangeException.ThrowIfNegativeOrZero(period); - if (source.Length == 0) return; + if (source.Length == 0) + { + return; + } double alpha = 2.0 / (period + 1); Calculate(source, output, alpha, period); @@ -202,11 +221,19 @@ public sealed class Zlema : AbstractBase public static void Calculate(ReadOnlySpan source, Span output, double alpha) { if (source.Length != output.Length) + { throw new ArgumentException("Source and output must have the same length.", nameof(output)); - if (alpha <= 0.0 || alpha > 1.0 || !double.IsFinite(alpha)) - throw new ArgumentException("Alpha must be finite and in (0, 1].", nameof(alpha)); + } - if (source.Length == 0) return; + if (alpha <= 0.0 || alpha > 1.0 || !double.IsFinite(alpha)) + { + throw new ArgumentException("Alpha must be finite and in (0, 1].", nameof(alpha)); + } + + if (source.Length == 0) + { + return; + } double period = (2.0 / alpha) - 1.0; Calculate(source, output, alpha, period); @@ -231,7 +258,9 @@ public sealed class Zlema : AbstractBase state.E *= _beta; if (!state.IsHot && state.Bars >= _lag + 1 && state.E <= CoverageThreshold) + { state.IsHot = true; + } if (state.E <= CompensatorThreshold) { @@ -246,7 +275,10 @@ public sealed class Zlema : AbstractBase else { if (!state.IsHot && state.Bars >= _lag + 1) + { state.IsHot = true; + } + result = state.ZlemaRaw; } @@ -257,11 +289,15 @@ public sealed class Zlema : AbstractBase private static int EstimateWarmupPeriod(double beta) { if (beta <= 0.0) + { return 1; + } double steps = Math.Log(CoverageThreshold) / Math.Log(beta); if (double.IsNaN(steps) || double.IsInfinity(steps) || steps <= 0.0) + { return 1; + } return (int)Math.Ceiling(steps); } @@ -317,9 +353,13 @@ public sealed class Zlema : AbstractBase { double val = source[i]; if (double.IsFinite(val)) + { lastValid = val; + } else + { val = lastValid; + } if (double.IsNaN(val)) { @@ -330,7 +370,9 @@ public sealed class Zlema : AbstractBase buffer[head] = val; head++; if (head == bufferSize) + { head = 0; + } double lagged = buffer[head]; double signal = Math.FusedMultiplyAdd(2.0, val, -lagged); diff --git a/lib/volatility/adr/Adr.Quantower.Tests.cs b/lib/volatility/adr/Adr.Quantower.Tests.cs index 7791c476..499ea8c0 100644 --- a/lib/volatility/adr/Adr.Quantower.Tests.cs +++ b/lib/volatility/adr/Adr.Quantower.Tests.cs @@ -186,4 +186,4 @@ public class AdrIndicatorTests Assert.Contains("github.com", indicator.SourceCodeLink, StringComparison.Ordinal); Assert.Contains("Adr.Quantower.cs", indicator.SourceCodeLink, StringComparison.Ordinal); } -} \ No newline at end of file +} diff --git a/lib/volatility/adr/Adr.Quantower.cs b/lib/volatility/adr/Adr.Quantower.cs index f557e6d0..d370f683 100644 --- a/lib/volatility/adr/Adr.Quantower.cs +++ b/lib/volatility/adr/Adr.Quantower.cs @@ -55,4 +55,4 @@ public sealed class AdrIndicator : Indicator, IWatchlistIndicator _series.SetValue(result.Value, _adr.IsHot, ShowColdValues); } -} \ No newline at end of file +} diff --git a/lib/volatility/adr/Adr.Tests.cs b/lib/volatility/adr/Adr.Tests.cs index 9afc04ab..5e312878 100644 --- a/lib/volatility/adr/Adr.Tests.cs +++ b/lib/volatility/adr/Adr.Tests.cs @@ -279,7 +279,10 @@ public class AdrTests var gbm = new GBM(); var bars = gbm.Fetch(50, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1)); - foreach (var bar in bars) adr.Update(bar); + foreach (var bar in bars) + { + adr.Update(bar); + } double lastVal = adr.Last.Value; Assert.NotEqual(0, lastVal); @@ -553,4 +556,4 @@ public class AdrTests // Negative range should be treated as 0 Assert.Equal(0.0, result.Value, 1e-10); } -} \ No newline at end of file +} diff --git a/lib/volatility/adr/Adr.Validation.Tests.cs b/lib/volatility/adr/Adr.Validation.Tests.cs index 903b0a76..47778bb2 100644 --- a/lib/volatility/adr/Adr.Validation.Tests.cs +++ b/lib/volatility/adr/Adr.Validation.Tests.cs @@ -306,4 +306,4 @@ public sealed class AdrValidationTests : IDisposable _output.WriteLine($"ADR: {adr.Last.Value:F4}, ATR: {atr.Last.Value:F4}"); _output.WriteLine("ADR and ATR validated: both produce valid results"); } -} \ No newline at end of file +} diff --git a/lib/volatility/adr/Adr.cs b/lib/volatility/adr/Adr.cs index 90464ee8..d346127f 100644 --- a/lib/volatility/adr/Adr.cs +++ b/lib/volatility/adr/Adr.cs @@ -33,7 +33,9 @@ public sealed class Adr : AbstractBase public Adr(int period, AdrMethod method = AdrMethod.Sma) { if (period <= 0) + { throw new ArgumentException("Period must be greater than 0", nameof(period)); + } _ma = method switch { @@ -137,7 +139,10 @@ public sealed class Adr : AbstractBase /// public TSeries Update(TBarSeries source) { - if (source.Count == 0) return []; + if (source.Count == 0) + { + return []; + } // Calculate range series TSeries rangeSeries = CalculateRanges(source); @@ -223,4 +228,4 @@ public enum AdrMethod Ema = 2, /// Weighted Moving Average Wma = 3 -} \ No newline at end of file +} diff --git a/lib/volatility/atr/Atr.Quantower.Tests.cs b/lib/volatility/atr/Atr.Quantower.Tests.cs index 532b6185..254fd23b 100644 --- a/lib/volatility/atr/Atr.Quantower.Tests.cs +++ b/lib/volatility/atr/Atr.Quantower.Tests.cs @@ -148,4 +148,4 @@ public class AtrIndicatorTests Assert.Contains("github.com", indicator.SourceCodeLink, StringComparison.Ordinal); Assert.Contains("Atr.Quantower.cs", indicator.SourceCodeLink, StringComparison.Ordinal); } -} \ No newline at end of file +} diff --git a/lib/volatility/atr/Atr.Tests.cs b/lib/volatility/atr/Atr.Tests.cs index a881f1ab..2d379e61 100644 --- a/lib/volatility/atr/Atr.Tests.cs +++ b/lib/volatility/atr/Atr.Tests.cs @@ -179,7 +179,10 @@ public class AtrTests var gbm = new GBM(); var bars = gbm.Fetch(50, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1)); - foreach (var bar in bars) atr.Update(bar); + foreach (var bar in bars) + { + atr.Update(bar); + } double lastVal = atr.Last.Value; Assert.NotEqual(0, lastVal); diff --git a/lib/volatility/atr/Atr.cs b/lib/volatility/atr/Atr.cs index 1d55d096..dfd46108 100644 --- a/lib/volatility/atr/Atr.cs +++ b/lib/volatility/atr/Atr.cs @@ -34,7 +34,9 @@ public sealed class Atr : AbstractBase public Atr(int period) { if (period <= 0) + { throw new ArgumentException("Period must be greater than 0", nameof(period)); + } _rma = new Rma(period); Name = $"Atr({period})"; @@ -165,7 +167,10 @@ public sealed class Atr : AbstractBase public TSeries Update(TBarSeries source) { - if (source.Count == 0) return []; + if (source.Count == 0) + { + return []; + } // 1. Calculate TR series TSeries trSeries = CalculateTrueRange(source); @@ -193,7 +198,10 @@ public sealed class Atr : AbstractBase var t = new List(source.Count); var v = new List(source.Count); - if (source.Count == 0) return new TSeries(t, v); + if (source.Count == 0) + { + return new TSeries(t, v); + } // First bar TR = H - L t.Add(source[0].Time); @@ -223,4 +231,4 @@ public sealed class Atr : AbstractBase var atr = new Atr(period); return atr.Update(source); } -} \ No newline at end of file +} diff --git a/lib/volatility/atrn/Atrn.Quantower.Tests.cs b/lib/volatility/atrn/Atrn.Quantower.Tests.cs index 1ae18e1a..2d1df9b7 100644 --- a/lib/volatility/atrn/Atrn.Quantower.Tests.cs +++ b/lib/volatility/atrn/Atrn.Quantower.Tests.cs @@ -181,4 +181,4 @@ public class AtrnIndicatorTests Assert.Contains("github.com", indicator.SourceCodeLink, StringComparison.Ordinal); Assert.Contains("Atrn.Quantower.cs", indicator.SourceCodeLink, StringComparison.Ordinal); } -} \ No newline at end of file +} diff --git a/lib/volatility/atrn/Atrn.Quantower.cs b/lib/volatility/atrn/Atrn.Quantower.cs index 4e7e8b33..8a2e0610 100644 --- a/lib/volatility/atrn/Atrn.Quantower.cs +++ b/lib/volatility/atrn/Atrn.Quantower.cs @@ -48,4 +48,4 @@ public sealed class AtrnIndicator : Indicator, IWatchlistIndicator _series.SetValue(result.Value, _atrn.IsHot, ShowColdValues); } -} \ No newline at end of file +} diff --git a/lib/volatility/atrn/Atrn.Validation.Tests.cs b/lib/volatility/atrn/Atrn.Validation.Tests.cs index 0ce37224..2e1c15f4 100644 --- a/lib/volatility/atrn/Atrn.Validation.Tests.cs +++ b/lib/volatility/atrn/Atrn.Validation.Tests.cs @@ -123,8 +123,15 @@ public sealed class AtrnValidationTests : IDisposable for (int j = startIdx; j < atrValues.Count; j++) { - if (atrValues[j] < minAtr) minAtr = atrValues[j]; - if (atrValues[j] > maxAtr) maxAtr = atrValues[j]; + if (atrValues[j] < minAtr) + { + minAtr = atrValues[j]; + } + + if (atrValues[j] > maxAtr) + { + maxAtr = atrValues[j]; + } } double currentAtr = atrValues[^1]; @@ -336,4 +343,4 @@ public sealed class AtrnValidationTests : IDisposable } #endregion -} \ No newline at end of file +} diff --git a/lib/volatility/atrn/Atrn.cs b/lib/volatility/atrn/Atrn.cs index ee9509a7..a76e2ed8 100644 --- a/lib/volatility/atrn/Atrn.cs +++ b/lib/volatility/atrn/Atrn.cs @@ -43,7 +43,9 @@ public sealed class Atrn : AbstractBase public Atrn(int period) { if (period <= 0) + { throw new ArgumentException("Period must be greater than 0", nameof(period)); + } _lookbackWindow = 10 * period; _rma = new Rma(period); @@ -239,7 +241,10 @@ public sealed class Atrn : AbstractBase /// public TSeries Update(TBarSeries source) { - if (source.Count == 0) return []; + if (source.Count == 0) + { + return []; + } var t = new List(source.Count); var v = new List(source.Count); @@ -259,7 +264,10 @@ public sealed class Atrn : AbstractBase /// public override TSeries Update(TSeries source) { - if (source.Count == 0) return []; + if (source.Count == 0) + { + return []; + } var t = new List(source.Count); var v = new List(source.Count); @@ -287,12 +295,18 @@ public sealed class Atrn : AbstractBase private double GetMax() { ReadOnlySpan span = _atrBuffer.GetSpan(); - if (span.IsEmpty) return 0; + if (span.IsEmpty) + { + return 0; + } double max = double.MinValue; for (int i = 0; i < span.Length; i++) { - if (span[i] > max) max = span[i]; + if (span[i] > max) + { + max = span[i]; + } } return max; } @@ -301,12 +315,18 @@ public sealed class Atrn : AbstractBase private double GetMin() { ReadOnlySpan span = _atrBuffer.GetSpan(); - if (span.IsEmpty) return 0; + if (span.IsEmpty) + { + return 0; + } double min = double.MaxValue; for (int i = 0; i < span.Length; i++) { - if (span[i] < min) min = span[i]; + if (span[i] < min) + { + min = span[i]; + } } return min; } diff --git a/lib/volatility/atrp/Atrp.Quantower.Tests.cs b/lib/volatility/atrp/Atrp.Quantower.Tests.cs index ba292193..be784fc0 100644 --- a/lib/volatility/atrp/Atrp.Quantower.Tests.cs +++ b/lib/volatility/atrp/Atrp.Quantower.Tests.cs @@ -155,4 +155,4 @@ public class AtrpIndicatorTests var indicator = new AtrpIndicator(); Assert.Contains("percentage", indicator.Description, StringComparison.OrdinalIgnoreCase); } -} \ No newline at end of file +} diff --git a/lib/volatility/atrp/Atrp.Quantower.cs b/lib/volatility/atrp/Atrp.Quantower.cs index de624a1e..fdb9e266 100644 --- a/lib/volatility/atrp/Atrp.Quantower.cs +++ b/lib/volatility/atrp/Atrp.Quantower.cs @@ -48,4 +48,4 @@ public sealed class AtrpIndicator : Indicator, IWatchlistIndicator _series.SetValue(result.Value, _atrp.IsHot, ShowColdValues); } -} \ No newline at end of file +} diff --git a/lib/volatility/atrp/Atrp.Tests.cs b/lib/volatility/atrp/Atrp.Tests.cs index 846c2dc5..e1448c93 100644 --- a/lib/volatility/atrp/Atrp.Tests.cs +++ b/lib/volatility/atrp/Atrp.Tests.cs @@ -180,7 +180,10 @@ public class AtrpTests var gbm = new GBM(); var bars = gbm.Fetch(50, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1)); - foreach (var bar in bars) atrp.Update(bar); + foreach (var bar in bars) + { + atrp.Update(bar); + } double lastVal = atrp.Last.Value; Assert.NotEqual(0, lastVal); @@ -449,4 +452,4 @@ public class AtrpTests Assert.True(double.IsNaN(result.Value)); } -} \ No newline at end of file +} diff --git a/lib/volatility/atrp/Atrp.Validation.Tests.cs b/lib/volatility/atrp/Atrp.Validation.Tests.cs index 657363ae..270f191a 100644 --- a/lib/volatility/atrp/Atrp.Validation.Tests.cs +++ b/lib/volatility/atrp/Atrp.Validation.Tests.cs @@ -347,4 +347,4 @@ public sealed class AtrpValidationTests : IDisposable } _output.WriteLine("ATRP Batch(TSeries) validated successfully against Ooples ATR"); } -} \ No newline at end of file +} diff --git a/lib/volatility/atrp/Atrp.cs b/lib/volatility/atrp/Atrp.cs index 88b4d054..b741c2c4 100644 --- a/lib/volatility/atrp/Atrp.cs +++ b/lib/volatility/atrp/Atrp.cs @@ -52,7 +52,9 @@ public sealed class Atrp : AbstractBase public Atrp(int period) { if (period <= 0) + { throw new ArgumentException("Period must be greater than 0", nameof(period)); + } _alpha = 1.0 / period; _decay = 1.0 - _alpha; @@ -135,18 +137,45 @@ public sealed class Atrp : AbstractBase public TValue Update(TBar input, bool isNew = true) { if (isNew) + { _p_state = _state; + } else + { _state = _p_state; + } // Get valid values with last-value substitution double high = input.High; double low = input.Low; double close = input.Close; - if (double.IsFinite(high)) _state.LastValidHigh = high; else high = _state.LastValidHigh; - if (double.IsFinite(low)) _state.LastValidLow = low; else low = _state.LastValidLow; - if (double.IsFinite(close)) _state.LastValidClose = close; else close = _state.LastValidClose; + if (double.IsFinite(high)) + { + _state.LastValidHigh = high; + } + else + { + high = _state.LastValidHigh; + } + + if (double.IsFinite(low)) + { + _state.LastValidLow = low; + } + else + { + low = _state.LastValidLow; + } + + if (double.IsFinite(close)) + { + _state.LastValidClose = close; + } + else + { + close = _state.LastValidClose; + } // Handle case where no valid values yet if (double.IsNaN(close)) @@ -212,7 +241,10 @@ public sealed class Atrp : AbstractBase /// public TSeries Update(TBarSeries source) { - if (source.Count == 0) return []; + if (source.Count == 0) + { + return []; + } var t = new List(source.Count); var v = new List(source.Count); @@ -249,4 +281,4 @@ public sealed class Atrp : AbstractBase var atrp = new Atrp(period); return atrp.Update(source); } -} \ No newline at end of file +} diff --git a/lib/volume/adl/Adl.Tests.cs b/lib/volume/adl/Adl.Tests.cs index 52f23552..c5b20650 100644 --- a/lib/volume/adl/Adl.Tests.cs +++ b/lib/volume/adl/Adl.Tests.cs @@ -206,4 +206,4 @@ public class AdlTests Assert.Equal((i + 1) * 10, output[i]); } } -} \ No newline at end of file +} diff --git a/lib/volume/adl/Adl.cs b/lib/volume/adl/Adl.cs index f18e4f04..0581d75e 100644 --- a/lib/volume/adl/Adl.cs +++ b/lib/volume/adl/Adl.cs @@ -129,7 +129,10 @@ public sealed class Adl : ITValuePublisher public static TSeries Calculate(TBarSeries source) { - if (source.Count == 0) return []; + if (source.Count == 0) + { + return []; + } var t = source.Open.Times.ToArray(); // Times are same for all series var v = new double[source.Count]; @@ -143,7 +146,9 @@ public sealed class Adl : ITValuePublisher public static void Calculate(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) + { throw new ArgumentException("All spans must be of the same length", nameof(output)); + } int len = high.Length; int i = 0; @@ -196,4 +201,4 @@ public sealed class Adl : ITValuePublisher output[i] = sum; } } -} \ No newline at end of file +} diff --git a/lib/volume/adosc/Adosc.cs b/lib/volume/adosc/Adosc.cs index 79f327f3..da272917 100644 --- a/lib/volume/adosc/Adosc.cs +++ b/lib/volume/adosc/Adosc.cs @@ -57,11 +57,19 @@ public sealed class Adosc : ITValuePublisher public Adosc(int fastPeriod = 3, int slowPeriod = 10) { if (fastPeriod <= 0) + { throw new ArgumentException("Fast period must be greater than 0", nameof(fastPeriod)); + } + if (slowPeriod <= 0) + { throw new ArgumentException("Slow period must be greater than 0", nameof(slowPeriod)); + } + if (fastPeriod >= slowPeriod) + { throw new ArgumentException("Fast period must be less than slow period", nameof(fastPeriod)); + } _adl = new Adl(); _emaFast = new Ema(fastPeriod); @@ -181,7 +189,10 @@ public sealed class Adosc : ITValuePublisher } int len = high.Length; - if (len == 0) return; + if (len == 0) + { + return; + } // EMA parameters (same formula as Ema.cs: alpha = 2 / (period + 1)) double alphaFast = 2.0 / (fastPeriod + 1); diff --git a/lib/volume/cmf/Cmf.cs b/lib/volume/cmf/Cmf.cs index c901ae0a..0e71394c 100644 --- a/lib/volume/cmf/Cmf.cs +++ b/lib/volume/cmf/Cmf.cs @@ -67,7 +67,9 @@ public sealed class Cmf : ITValuePublisher public Cmf(int period = 20) { if (period < 1) + { throw new ArgumentException("Period must be >= 1", nameof(period)); + } _period = period; _mfvBuffer = new RingBuffer(period); @@ -183,7 +185,10 @@ public sealed class Cmf : ITValuePublisher public static TSeries Calculate(TBarSeries source, int period = 20) { - if (source.Count == 0) return []; + if (source.Count == 0) + { + return []; + } var t = source.Open.Times.ToArray(); var v = new double[source.Count]; @@ -197,15 +202,29 @@ public sealed class Cmf : ITValuePublisher public static void Calculate(ReadOnlySpan high, ReadOnlySpan low, ReadOnlySpan close, ReadOnlySpan volume, Span output, int period = 20) { if (high.Length != low.Length) + { throw new ArgumentException("High and Low spans must be of the same length", nameof(low)); + } + if (high.Length != close.Length) + { throw new ArgumentException("High and Close spans must be of the same length", nameof(close)); + } + if (high.Length != volume.Length) + { throw new ArgumentException("High and Volume spans must be of the same length", nameof(volume)); + } + if (high.Length != output.Length) + { throw new ArgumentException("Output span must be of the same length as input", nameof(output)); + } + if (period < 1) + { throw new ArgumentException("Period must be >= 1", nameof(period)); + } int len = high.Length; diff --git a/quantower/IndicatorExtensions.Tests.cs b/quantower/IndicatorExtensions.Tests.cs index 487d3dd2..76884dd7 100644 --- a/quantower/IndicatorExtensions.Tests.cs +++ b/quantower/IndicatorExtensions.Tests.cs @@ -86,8 +86,15 @@ public class IndicatorExtensionsTests // Test GetSmoothCurvePoints var series = new LineSeries("Test", Color.Blue, 1, LineStyle.Solid); - for (int i = 0; i < 20; i++) series.AddValue(); - for (int i = 0; i < 20; i++) series.SetValue(100 + i, i); + for (int i = 0; i < 20; i++) + { + series.AddValue(); + } + + for (int i = 0; i < 20; i++) + { + series.SetValue(100 + i, i); + } var points = IndicatorExtensions.GetSmoothCurvePoints(indicator, converter, clientRect, series); Assert.NotEmpty(points); @@ -152,8 +159,15 @@ public class IndicatorExtensionsTests foreach (LineStyle style in Enum.GetValues()) { var series = new LineSeries("Test", Color.Blue, 1, style); - for (int i = 0; i < 20; i++) series.AddValue(); - for (int i = 0; i < 20; i++) series.SetValue(100 + i, i); + for (int i = 0; i < 20; i++) + { + series.AddValue(); + } + + for (int i = 0; i < 20; i++) + { + series.SetValue(100 + i, i); + } // Test with warmup and cold values indicator.PaintSmoothCurve(args, series, warmupPeriod: 5, showColdValues: true); diff --git a/quantower/IndicatorExtensions.cs b/quantower/IndicatorExtensions.cs index 81dfa3c0..adc5aa43 100644 --- a/quantower/IndicatorExtensions.cs +++ b/quantower/IndicatorExtensions.cs @@ -97,7 +97,10 @@ public static class IndicatorExtensions ArgumentNullException.ThrowIfNull(indicator); ArgumentNullException.ThrowIfNull(converter); var data = indicator.HistoricalData; - if (data == null) return Array.Empty(); + if (data == null) + { + return Array.Empty(); + } var lastTime = data.Time(data.Count - 1); var firstTime = data.Time(0); @@ -113,7 +116,10 @@ public static class IndicatorExtensions int rightIndex = (int)data.GetIndexByTime(rightTime.Ticks); int count = leftIndex - rightIndex; - if (count <= 0) return Array.Empty(); + if (count <= 0) + { + return Array.Empty(); + } var allPoints = new Point[count]; @@ -131,7 +137,9 @@ public static class IndicatorExtensions public static void PaintSmoothCurve(this Indicator indicator, PaintChartEventArgs args, LineSeries series, int warmupPeriod, bool showColdValues = true, double tension = 0.5) { if (!series.Visible || indicator.CurrentChart == null) + { return; + } Graphics gr = args.Graphics; gr.SmoothingMode = SmoothingMode.AntiAlias; @@ -142,7 +150,10 @@ public static class IndicatorExtensions gr.SetClip(clientRect); var data = indicator.HistoricalData; - if (data == null) return; + if (data == null) + { + return; + } var lastTime = data.Time(data.Count - 1); var firstTime = data.Time(0); @@ -158,7 +169,10 @@ public static class IndicatorExtensions int rightIndex = (int)data.GetIndexByTime(rightTime.Ticks); int count = leftIndex - rightIndex; - if (count <= 0) return; + if (count <= 0) + { + return; + } // Use ArrayPool to avoid allocations Point[] allPoints = System.Buffers.ArrayPool.Shared.Rent(count); @@ -191,7 +205,7 @@ public static class IndicatorExtensions if (showColdValues) { int coldStart = Math.Max(0, hotCount); - int coldSegments = count - coldStart - 1; + int coldSegments = count - coldStart - 1; if (coldSegments > 0) { diff --git a/quantower/Mocks/TradingPlatformMocks.cs b/quantower/Mocks/TradingPlatformMocks.cs index 722b8ee3..2284dbef 100644 --- a/quantower/Mocks/TradingPlatformMocks.cs +++ b/quantower/Mocks/TradingPlatformMocks.cs @@ -197,7 +197,9 @@ public class HistoricalData for (int i = 0; i < _items.Count; i++) { if (_items[i].TicksLeft == ticks) + { return Count - 1 - i; + } } return -1; } @@ -281,14 +283,18 @@ public class LineSeries(string name, Color color, int width, LineStyle style) public double GetValue(int offset = 0, SeekOriginHistory origin = SeekOriginHistory.End) { if (_values.Count == 0) + { return double.NaN; + } int index = origin == SeekOriginHistory.End ? _values.Count - 1 - offset : offset; if (index < 0 || index >= _values.Count) + { return double.NaN; + } return _values[index]; } @@ -307,7 +313,9 @@ public class LineSeries(string name, Color color, int width, LineStyle style) EnsureMarkerCapacity(offset + 1); int index = _markers.Count - 1 - offset; if (index >= 0 && index < _markers.Count) + { _markers[index] = color; + } } public void SetMarker(int offset, IndicatorLineMarker marker) @@ -324,13 +332,17 @@ public class LineSeries(string name, Color color, int width, LineStyle style) private void EnsureCapacity(int count) { while (_values.Count < count) + { _values.Add(double.NaN); + } } private void EnsureMarkerCapacity(int count) { while (_markers.Count < count) + { _markers.Add(Color.Transparent); + } } public int Count => _values.Count; diff --git a/quantower/Trends/Sgma.Quantower.Tests.cs b/quantower/Trends/Sgma.Quantower.Tests.cs index f803342d..e7395694 100644 --- a/quantower/Trends/Sgma.Quantower.Tests.cs +++ b/quantower/Trends/Sgma.Quantower.Tests.cs @@ -274,4 +274,4 @@ public class SgmaIndicatorTests Assert.True(double.IsFinite(indicatorLow.LinesSeries[0].GetValue(0))); Assert.True(double.IsFinite(indicatorHigh.LinesSeries[0].GetValue(0))); } -} \ No newline at end of file +} diff --git a/quantower/Trends/Sgma.Quantower.cs b/quantower/Trends/Sgma.Quantower.cs index fea646b8..7c96a4ff 100644 --- a/quantower/Trends/Sgma.Quantower.cs +++ b/quantower/Trends/Sgma.Quantower.cs @@ -49,7 +49,10 @@ public sealed class SgmaIndicator : Indicator, IWatchlistIndicator protected override void OnUpdate(UpdateArgs args) { - if (_sgma == null || _selector == null) return; + if (_sgma == null || _selector == null) + { + return; + } var item = HistoricalData[0, SeekOriginHistory.End]; double value = _selector(item); @@ -61,4 +64,4 @@ public sealed class SgmaIndicator : Indicator, IWatchlistIndicator bool isHot = _sgma.IsHot; LinesSeries[0].SetValue(result.Value, isHot, ShowColdValues); } -} \ No newline at end of file +}