# Recommended Test Pattern for Indicators This document outlines the standard set of unit tests that every indicator in QuanTAlib should implement to ensure correctness, consistency, and robustness. ## 1. Standard Unit Tests (`[Name].Tests.cs`) These tests verify the internal logic, state management, and API contract of the indicator. ### Constructor & Validation - **`Constructor_ValidatesInput`**: Verify that invalid parameters (e.g., `period <= 0`) throw `ArgumentException`. - **`Constructor_ValidatesOptionalArgs`**: If applicable, verify other parameters (e.g., `alpha`, `sigma`). ### Basic Functionality - **`Calc_ReturnsValue`**: Verify `Update` returns a valid `TValue` and updates the `Last` property. - **`FirstValue_ReturnsExpected`**: Verify the first output value (often the input itself for averages). - **`Properties_Accessible`**: Verify `Last`, `IsHot`, `Name`, etc., are accessible and initialized correctly. ### State Management & Bar Correction - **`Calc_IsNew_AcceptsParameter`**: Verify that `isNew: true` advances the state. - **`Calc_IsNew_False_UpdatesValue`**: Verify that `isNew: false` updates the current value without advancing state (intra-bar update). - **`IterativeCorrections_RestoreToOriginalState`**: Critical test. 1. Feed $N$ values. 2. Remember state. 3. Feed $M$ updates with `isNew: false`. 4. Feed the original $N$-th value again with `isNew: false`. 5. Verify state matches the remembered state. - **`Reset_ClearsState`**: Verify `Reset()` clears all internal state and the indicator behaves like a new instance. ### Warmup & Convergence - **`IsHot_BecomesTrueWhenBufferFull`**: Verify `IsHot` becomes true after the expected number of periods. - **`IsHot_IsPeriodDependent`**: If applicable, verify warmup time scales with period. ### Robustness (NaN/Infinity) - **`NaN_Input_UsesLastValidValue`**: Verify that `NaN` input does not crash and typically carries forward the last valid value. - **`Infinity_Input_UsesLastValidValue`**: Verify handling of `PositiveInfinity` and `NegativeInfinity`. - **`MultipleNaN_ContinuesWithLastValid`**: Verify behavior with consecutive invalid inputs. - **`BatchCalc_HandlesNaN`**: Verify batch processing handles `NaN` correctly. ### Consistency - **`BatchCalc_MatchesIterativeCalc`**: Verify that `Update(TSeries)` produces the same results as a loop of `Update(TValue)`. - **`AllModes_ProduceSameResult`**: **Crucial**. Verify that all 4 usage modes produce identical results: 1. **Batch**: `Indicator.Calculate(TSeries)` 2. **Span**: `Indicator.Calculate(ReadOnlySpan, Span)` 3. **Streaming**: `new Indicator().Update(TValue)` 4. **Eventing**: `new Indicator(source).Update()` ### Span API (High Performance) - **`SpanCalc_ValidatesInput`**: Verify input/output buffer length checks. - **`SpanCalc_MatchesTSeriesCalc`**: Verify Span API output matches TSeries API output. - **`SpanCalc_ZeroAllocation`**: Verify the method runs without obvious errors on large datasets (allocation verified via benchmarks, but this ensures no OOM or stack overflow). - **`SpanCalc_HandlesNaN`**: Verify Span API handles invalid inputs safely. ## 2. Validation Tests (`[Name].Validation.Tests.cs`) These tests compare the indicator's output against established external libraries to ensure mathematical accuracy. - **Compare against Skender.Stock.Indicators**: Primary validation target. - **Compare against TA-Lib**: Secondary validation target. - **Compare against Python (pandas-ta/talib)**: If C# libs are unavailable. - **Tolerance**: Typically `1e-6` to `1e-9`. ## 3. Example Test Template ```csharp [Fact] public void AllModes_ProduceSameResult() { // Arrange int period = 10; var gbm = new GBM(startPrice: 100, mu: 0.05, sigma: 0.2, seed: 123); var bars = gbm.Fetch(1000, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1)); var series = bars.Close; // 1. Batch Mode var batchSeries = MyIndicator.Calculate(series, period); double expected = batchSeries.Last.Value; // 2. Span Mode var tValues = series.Values.ToArray(); var spanInput = new ReadOnlySpan(tValues); var spanOutput = new double[tValues.Length]; MyIndicator.Calculate(spanInput, spanOutput, period); double spanResult = spanOutput[^1]; // 3. Streaming Mode var streamingInd = new MyIndicator(period); 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 MyIndicator(pubSource, period); for (int i = 0; i < series.Count; i++) { pubSource.Add(series[i]); } double eventingResult = eventingInd.Last.Value; // Assert Assert.Equal(expected, spanResult, precision: 9); Assert.Equal(expected, streamingResult, precision: 9); Assert.Equal(expected, eventingResult, precision: 9); }