Test completeness

This commit is contained in:
Miha Kralj
2025-12-14 21:55:40 -08:00
parent f761cc5712
commit a67ad65fa5
35 changed files with 2437 additions and 1405 deletions
+2 -2
View File
@@ -183,7 +183,7 @@ public TValue Update(TValue input, bool isNew = true)
* **Format**: Markdown.
* **Content**: Title, Description, Parameters, Formula (LaTeX), C# Usage Examples.
* **Index**: Add the new indicator to the category index (e.g., `lib/trends/_index.md`).
* **Index**: Add the new indicator to the category index (e.g., `lib/trends/_index.md`) AND the main index (`lib/_index.md`).
* **Linting**: Ensure that markdownlint shows no issues for the file.
* **MD030:** Ensure exactly one space after list markers.
* **MD032:** Ensure lists are surrounded by blank lines.
@@ -209,7 +209,7 @@ When creating a new indicator, you are **DONE** only when:
* [ ] Static `Calculate(Span)` is implemented.
* [ ] Unit tests pass (including edge cases).
* [ ] Validation tests pass against external libs.
* [ ] Documentation is complete and linked in `_index.md`.
* [ ] Documentation is complete and linked in both `_index.md` files.
* [ ] Quantower adapter and tests are implemented.
* [ ] CodeRabbit review issues are resolved.
+113
View File
@@ -0,0 +1,113 @@
# 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<double>(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);
}
+1 -1
View File
@@ -15,7 +15,7 @@
| ALLIGATOR | Williams Alligator | Trends |
| [ALMA](trends/alma/Alma.md) | Arnaud Legoux MA | Trends |
| AMAT | Archer Moving Averages Trends | Trends |
| AO | Awesome Oscillator | Momentum |
| [AO](momentum/ao/Ao.md) | Awesome Oscillator | Momentum |
| AOBV | Archer On-Balance Volume | Volume |
| APCHANNEL | Andrews' Pitchfork | Channels |
| APO | Absolute Price Oscillator | Momentum |
+138 -99
View File
@@ -1,111 +1,150 @@
using System;
using System.Collections.Generic;
using Xunit;
namespace QuanTAlib.Tests;
namespace QuanTAlib;
public class AdxTests
{
private readonly GBM _gbm = new();
[Fact]
public void BasicCalculation_DoesNotCrash()
{
var adx = new Adx(14);
var gbm = new GBM();
var bars = gbm.Fetch(1000, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
for (int i = 0; i < bars.Count; i++)
{
adx.Update(bars[i]);
}
Assert.True(double.IsFinite(adx.Last.Value));
}
[Fact]
public void Constructor_ThrowsArgumentException_WhenPeriodIsInvalid()
public void IsNew_Consistency()
{
var adx = new Adx(14);
var gbm = new GBM();
var bars = gbm.Fetch(100, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
// Feed first 99
for (int i = 0; i < 99; i++)
{
adx.Update(bars[i]);
}
// Update with 100th point (isNew=true)
adx.Update(bars[99], true);
// Update with modified 100th point (isNew=false)
var modifiedBar = new TBar(bars[99].Time, bars[99].Open, bars[99].High + 1.0, bars[99].Low - 1.0, bars[99].Close, bars[99].Volume);
var val2 = adx.Update(modifiedBar, false);
// Create new instance and feed up to modified
var adx2 = new Adx(14);
for (int i = 0; i < 99; i++)
{
adx2.Update(bars[i]);
}
var val3 = adx2.Update(modifiedBar, true);
Assert.Equal(val3.Value, val2.Value, 1e-9);
Assert.Equal(adx2.DiPlus.Value, adx.DiPlus.Value, 1e-9);
Assert.Equal(adx2.DiMinus.Value, adx.DiMinus.Value, 1e-9);
}
[Fact]
public void Reset_Works()
{
var adx = new Adx(14);
var gbm = new GBM();
var bars = gbm.Fetch(100, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
for (int i = 0; i < bars.Count; i++)
{
adx.Update(bars[i]);
}
adx.Reset();
Assert.Equal(0, adx.Last.Value);
Assert.False(adx.IsHot);
// Feed again
for (int i = 0; i < bars.Count; i++)
{
adx.Update(bars[i]);
}
Assert.True(double.IsFinite(adx.Last.Value));
}
[Fact]
public void TBarSeries_Update_Matches_Streaming()
{
var adx = new Adx(14);
var gbm = new GBM();
var bars = gbm.Fetch(200, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
var streamingResults = new List<double>();
for (int i = 0; i < bars.Count; i++)
{
streamingResults.Add(adx.Update(bars[i]).Value);
}
var adx2 = new Adx(14);
var seriesResults = adx2.Update(bars);
Assert.Equal(streamingResults.Count, seriesResults.Count);
for (int i = 0; i < seriesResults.Count; i++)
{
Assert.Equal(streamingResults[i], seriesResults.Values[i], 1e-9);
}
}
[Fact]
public void StaticCalculate_Matches_Streaming()
{
var gbm = new GBM();
var bars = gbm.Fetch(200, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
var adx = new Adx(14);
var streamingResults = new List<double>();
for (int i = 0; i < bars.Count; i++)
{
streamingResults.Add(adx.Update(bars[i]).Value);
}
var staticResults = Adx.Calculate(bars, 14);
Assert.Equal(streamingResults.Count, staticResults.Count);
for (int i = 0; i < staticResults.Count; i++)
{
Assert.Equal(streamingResults[i], staticResults.Values[i], 1e-9);
}
}
[Fact]
public void Chainability_Works()
{
var adx = new Adx(14);
var gbm = new GBM();
var bars = gbm.Fetch(10, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
// Test TBarSeries chain
var result = adx.Update(bars);
Assert.NotNull(result);
Assert.IsType<TSeries>(result);
// Test TBar chain (returns TValue)
var result2 = adx.Update(bars[0]);
Assert.IsType<TValue>(result2);
}
[Fact]
public void Constructor_InvalidParameters_ThrowsArgumentException()
{
Assert.Throws<ArgumentException>(() => new Adx(0));
Assert.Throws<ArgumentException>(() => new Adx(-1));
}
[Fact]
public void Update_ReturnsValidValues_WhenInputIsValid()
{
var adx = new Adx(14);
var bars = _gbm.Fetch(100, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
foreach (var bar in bars)
{
var result = adx.Update(bar);
Assert.True(double.IsFinite(result.Value));
}
}
[Fact]
public void Update_HandlesIsNewCorrectly()
{
var adx = new Adx(14);
// We need enough bars to warm up ADX (2 * Period)
int count = 2 * 14 + 5;
var bars = _gbm.Fetch(count, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
// Feed all but last bar
for (int i = 0; i < count - 1; i++)
{
adx.Update(bars[i]);
}
// Update with last bar (isNew=true)
var result1 = adx.Update(bars[count - 1], true);
// Update with modified last bar (isNew=false)
var modifiedBar = new TBar(bars[count - 1].Time, bars[count - 1].Open, bars[count - 1].High + 1, bars[count - 1].Low - 1, bars[count - 1].Close, bars[count - 1].Volume);
var result2 = adx.Update(modifiedBar, false);
// The result should change because High/Low changed, affecting TR and DM
Assert.NotEqual(result1.Value, result2.Value);
}
[Fact]
public void Reset_ResetsState()
{
var adx = new Adx(14);
var bars = _gbm.Fetch(100, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1)); // Increased to 100
foreach (var bar in bars)
{
adx.Update(bar);
}
Assert.True(adx.IsHot);
adx.Reset();
Assert.False(adx.IsHot);
Assert.Equal(0, adx.Last.Value);
}
[Fact]
public void IsHot_BecomesTrue_AfterWarmup()
{
var adx = new Adx(14);
var bars = _gbm.Fetch(100, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
int i = 0;
for (; i < bars.Count; i++)
{
adx.Update(bars[i]);
if (adx.IsHot) break;
}
Assert.True(i < bars.Count);
Assert.True(adx.IsHot);
}
[Fact]
public void Update_HandlesNaN_Gracefully()
{
var adx = new Adx(14);
var bar = new TBar(DateTime.UtcNow, double.NaN, double.NaN, double.NaN, double.NaN, 0);
var result = adx.Update(bar);
// Should not throw and return finite value (likely 0 or last valid)
// Since it's the first value, it might be 0.
Assert.True(double.IsFinite(result.Value));
}
[Fact]
public void Update_TValue_ReturnsValidResult()
{
var adx = new Adx(14);
var val = new TValue(DateTime.UtcNow, 100);
var result = adx.Update(val);
Assert.True(double.IsFinite(result.Value));
}
}
+2 -11
View File
@@ -8,7 +8,7 @@ using QuanTAlib.Tests;
namespace QuanTAlib;
public class AdxValidationTests : IDisposable
public sealed class AdxValidationTests : IDisposable
{
private readonly ValidationTestData _data;
@@ -19,16 +19,7 @@ public class AdxValidationTests : IDisposable
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing)
{
if (disposing)
{
_data.Dispose();
}
_data.Dispose();
}
[Fact]
+137 -109
View File
@@ -1,121 +1,149 @@
using System;
using System.Collections.Generic;
using Xunit;
namespace QuanTAlib.Tests;
namespace QuanTAlib;
public class AoTests
{
[Fact]
public void Constructor_ValidatesParameters()
public void BasicCalculation_DoesNotCrash()
{
var ao = new Ao(5, 34);
var gbm = new GBM();
var bars = gbm.Fetch(1000, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
for (int i = 0; i < bars.Count; i++)
{
ao.Update(bars[i]);
}
Assert.True(double.IsFinite(ao.Last.Value));
}
[Fact]
public void IsNew_Consistency()
{
var ao = new Ao(5, 34);
var gbm = new GBM();
var bars = gbm.Fetch(100, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
// Feed first 99
for (int i = 0; i < 99; i++)
{
ao.Update(bars[i]);
}
// Update with 100th point (isNew=true)
ao.Update(bars[99], true);
// Update with modified 100th point (isNew=false)
var modifiedBar = new TBar(bars[99].Time, bars[99].Open, bars[99].High + 1.0, bars[99].Low - 1.0, bars[99].Close, bars[99].Volume);
var val2 = ao.Update(modifiedBar, false);
// Create new instance and feed up to modified
var ao2 = new Ao(5, 34);
for (int i = 0; i < 99; i++)
{
ao2.Update(bars[i]);
}
var val3 = ao2.Update(modifiedBar, true);
Assert.Equal(val3.Value, val2.Value, 1e-9);
}
[Fact]
public void Reset_Works()
{
var ao = new Ao(5, 34);
var gbm = new GBM();
var bars = gbm.Fetch(100, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
for (int i = 0; i < bars.Count; i++)
{
ao.Update(bars[i]);
}
ao.Reset();
Assert.Equal(0, ao.Last.Value);
Assert.False(ao.IsHot);
// Feed again
for (int i = 0; i < bars.Count; i++)
{
ao.Update(bars[i]);
}
Assert.True(double.IsFinite(ao.Last.Value));
}
[Fact]
public void TBarSeries_Update_Matches_Streaming()
{
var ao = new Ao(5, 34);
var gbm = new GBM();
var bars = gbm.Fetch(200, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
var streamingResults = new List<double>();
for (int i = 0; i < bars.Count; i++)
{
streamingResults.Add(ao.Update(bars[i]).Value);
}
var ao2 = new Ao(5, 34);
var seriesResults = ao2.Update(bars);
Assert.Equal(streamingResults.Count, seriesResults.Count);
for (int i = 0; i < seriesResults.Count; i++)
{
Assert.Equal(streamingResults[i], seriesResults.Values[i], 1e-9);
}
}
[Fact]
public void StaticCalculate_Matches_Streaming()
{
var gbm = new GBM();
var bars = gbm.Fetch(200, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
var ao = new Ao(5, 34);
var streamingResults = new List<double>();
for (int i = 0; i < bars.Count; i++)
{
streamingResults.Add(ao.Update(bars[i]).Value);
}
var staticResults = Ao.Calculate(bars, 5, 34);
Assert.Equal(streamingResults.Count, staticResults.Count);
for (int i = 0; i < staticResults.Count; i++)
{
Assert.Equal(streamingResults[i], staticResults.Values[i], 1e-9);
}
}
[Fact]
public void Chainability_Works()
{
var ao = new Ao(5, 34);
var gbm = new GBM();
var bars = gbm.Fetch(10, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
// Test TBarSeries chain
var result = ao.Update(bars);
Assert.NotNull(result);
Assert.IsType<TSeries>(result);
// Test TBar chain (returns TValue)
var result2 = ao.Update(bars[0]);
Assert.IsType<TValue>(result2);
}
[Fact]
public void Constructor_InvalidParameters_ThrowsArgumentException()
{
Assert.Throws<ArgumentException>(() => new Ao(0, 34));
Assert.Throws<ArgumentException>(() => new Ao(5, 0));
Assert.Throws<ArgumentException>(() => new Ao(34, 5)); // Fast >= Slow
}
[Fact]
public void IsHot_BecomesTrueAfterSlowPeriod()
{
var ao = new Ao(2, 5);
// Add 4 values
for (int i = 0; i < 4; i++)
{
ao.Update(new TBar(DateTime.UtcNow, 10, 10, 10, 10, 100));
Assert.False(ao.IsHot);
}
// Add 5th value
ao.Update(new TBar(DateTime.UtcNow, 10, 10, 10, 10, 100));
Assert.True(ao.IsHot);
}
[Fact]
public void Calculation_Correctness()
{
// AO = SMA(Median, 5) - SMA(Median, 34)
// Let's use smaller periods for testing: 2 and 4
var ao = new Ao(2, 4);
// Median prices: 10, 20, 30, 40, 50
// SMA2: -, 15, 25, 35, 45
// SMA4: -, -, -, 25, 35
// AO: -, -, -, 10, 10
var data = new[] { 10.0, 20.0, 30.0, 40.0, 50.0 };
// Sma returns average of available data.
// SMA2(10) = 10
// SMA2(10, 20) = 15
// SMA2(20, 30) = 25
// SMA2(30, 40) = 35
// SMA2(40, 50) = 45
// SMA4(10) = 10
// SMA4(10, 20) = 15
// SMA4(10, 20, 30) = 20
// SMA4(10, 20, 30, 40) = 25
// SMA4(20, 30, 40, 50) = 35
// AO:
// 1: 10 - 10 = 0
// 2: 15 - 15 = 0
// 3: 25 - 20 = 5
// 4: 35 - 25 = 10
// 5: 45 - 35 = 10
for (int i = 0; i < data.Length; i++)
{
var bar = new TBar(DateTime.UtcNow, data[i], data[i], data[i], data[i], 100);
var result = ao.Update(bar);
if (i == 2) Assert.Equal(5.0, result.Value);
if (i >= 3) Assert.Equal(10.0, result.Value);
}
}
[Fact]
public void Update_WithIsNewFalse_UpdatesLastValue()
{
var ao = new Ao(2, 4);
// 1. Add 10
ao.Update(new TBar(DateTime.UtcNow, 10, 10, 10, 10, 100));
// SMA2=10, SMA4=10, AO=0
// 2. Add 20
ao.Update(new TBar(DateTime.UtcNow, 20, 20, 20, 20, 100));
// SMA2=15, SMA4=15, AO=0
// 3. Update last with 30 (instead of 20)
var result = ao.Update(new TBar(DateTime.UtcNow, 30, 30, 30, 30, 100), isNew: false);
// SMA2(10, 30) = 20
// SMA4(10, 30) = 20
// AO = 0
Assert.Equal(0.0, result.Value);
// 4. Add 40
result = ao.Update(new TBar(DateTime.UtcNow, 40, 40, 40, 40, 100));
// SMA2(30, 40) = 35
// SMA4(10, 30, 40) = 26.666...
// AO = 35 - 26.666... = 8.333...
Assert.True(result.Value > 0);
}
[Fact]
public void Reset_ClearsState()
{
var ao = new Ao(2, 4);
ao.Update(new TBar(DateTime.UtcNow, 10, 10, 10, 10, 100));
ao.Update(new TBar(DateTime.UtcNow, 20, 20, 20, 20, 100));
ao.Reset();
Assert.False(ao.IsHot);
Assert.Equal(0, ao.Last.Value);
// Should behave like new
ao.Update(new TBar(DateTime.UtcNow, 10, 10, 10, 10, 100));
Assert.Equal(0, ao.Last.Value);
}
}
+42
View File
@@ -155,4 +155,46 @@ public class CfbTests
Assert.Equal(streamingResults[i], spanResults[i]);
}
}
[Fact]
public void Reset_Works()
{
var cfb = new Cfb();
var gbm = new GBM();
var bars = gbm.Fetch(100, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
for (int i = 0; i < bars.Count; i++)
{
cfb.Update(new TValue(bars.Close.Times[i], bars.Close.Values[i]));
}
Assert.True(cfb.Last.Value >= 1.0);
cfb.Reset();
Assert.Equal(0, cfb.Last.Value);
Assert.Equal(0, cfb.Last.Time);
// Feed again
for (int i = 0; i < bars.Count; i++)
{
cfb.Update(new TValue(bars.Close.Times[i], bars.Close.Values[i]));
}
Assert.True(cfb.Last.Value >= 1.0);
}
[Fact]
public void Chainability_Works()
{
var cfb = new Cfb();
var cfb2 = new Cfb(cfb);
for (int i = 0; i < 100; i++)
{
cfb.Update(new TValue(DateTime.UtcNow, i));
}
Assert.True(cfb2.Last.Value > 0);
}
}
+14
View File
@@ -87,6 +87,20 @@ public sealed class Cfb : ITValuePublisher
source.Pub += (item) => Update(item);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Reset()
{
_prices.Clear();
_volatility.Clear();
Array.Clear(_runningSums);
Array.Clear(_p_runningSums);
_state = default;
_state.PrevCfb = 1.0;
_p_state = default;
_p_state.PrevCfb = 1.0;
Last = default;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public TValue Update(TValue input, bool isNew = true)
{
+38
View File
@@ -110,4 +110,42 @@ public class DmxTests
Assert.Equal(0, result.Value);
}
[Fact]
public void StaticCalculate_Matches_Streaming()
{
var gbm = new GBM();
var bars = gbm.Fetch(200, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
var dmx = new Dmx(14);
var streamingResults = new List<double>();
for (int i = 0; i < bars.Count; i++)
{
streamingResults.Add(dmx.Update(bars[i]).Value);
}
var staticResults = Dmx.Calculate(bars, 14);
Assert.Equal(streamingResults.Count, staticResults.Count);
for (int i = 0; i < streamingResults.Count; i++)
{
Assert.Equal(streamingResults[i], staticResults.Values[i], 1e-9);
}
}
[Fact]
public void Chainability_Works()
{
var dmx = new Dmx(14);
var sma = new Sma(dmx, 10);
var gbm = new GBM();
var bars = gbm.Fetch(100, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
for (int i = 0; i < bars.Count; i++)
{
dmx.Update(bars[i]);
}
Assert.True(sma.Last.Value != 0);
}
}
+6
View File
@@ -129,4 +129,10 @@ public sealed class Dmx : ITValuePublisher
return new TSeries(t, v);
}
public static TSeries Calculate(TBarSeries source, int period = 14)
{
var dmx = new Dmx(period);
return dmx.Update(source);
}
}
+35 -13
View File
@@ -20,7 +20,7 @@ public class RsxTests
}
[Fact]
public void Update_ValidInput_ReturnsValidRsx()
public void BasicCalculation_DoesNotCrash()
{
var rsx = new Rsx(14);
var result = rsx.Update(new TValue(DateTime.UtcNow, 100));
@@ -40,7 +40,7 @@ public class RsxTests
}
[Fact]
public void Update_IsNew_Consistency()
public void IsNew_Consistency()
{
var rsx = new Rsx(14);
var time = DateTime.UtcNow;
@@ -52,15 +52,13 @@ public class RsxTests
rsx.Update(new TValue(time, 105), false);
// Update with isNew=false (same time, original value) - should match val1 if state rollback works
// Note: RSX is highly sensitive to path, so exact match might be tricky if intermediate states drift,
// but for a single step rollback it should be very close.
var val3 = rsx.Update(new TValue(time, 100), false);
Assert.Equal(val1.Value, val3.Value, 1e-9);
}
[Fact]
public void Calculate_Span_Matches_Update()
public void StaticCalculate_Matches_Streaming()
{
int period = 14;
int count = 100;
@@ -68,7 +66,35 @@ public class RsxTests
var series = bars.Close;
var rsx = new Rsx(period);
var resultSeries = rsx.Update(series);
var streamingResults = new List<double>();
for (int i = 0; i < count; i++)
{
streamingResults.Add(rsx.Update(new TValue(series.Times[i], series.Values[i])).Value);
}
var staticResults = Rsx.Calculate(series, period);
Assert.Equal(streamingResults.Count, staticResults.Count);
for (int i = 0; i < count; i++)
{
Assert.Equal(streamingResults[i], staticResults.Values[i], 1e-9);
}
}
[Fact]
public void SpanCalculate_Matches_Streaming()
{
int period = 14;
int count = 100;
var bars = _gbm.Fetch(count, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
var series = bars.Close;
var rsx = new Rsx(period);
var streamingResults = new List<double>();
for (int i = 0; i < count; i++)
{
streamingResults.Add(rsx.Update(new TValue(series.Times[i], series.Values[i])).Value);
}
var spanInput = series.Values.ToArray();
var spanOutput = new double[count];
@@ -76,28 +102,24 @@ public class RsxTests
for (int i = 0; i < count; i++)
{
Assert.Equal(resultSeries.Values[i], spanOutput[i], 1e-9);
Assert.Equal(streamingResults[i], spanOutput[i], 1e-9);
}
}
[Fact]
public void Reset_ClearsState()
public void Reset_Works()
{
var rsx = new Rsx(14);
rsx.Update(new TValue(DateTime.UtcNow, 100));
rsx.Reset();
// After reset, it should behave like a new instance
// RSX initializes with 0 filters.
// If we feed it the same value, it should produce the same initial output.
// However, RSX output depends on change (v8), so first value sets LastF8 but v8=0.
var val1 = rsx.Update(new TValue(DateTime.UtcNow, 100));
Assert.Equal(50.0, val1.Value); // Neutral start
}
[Fact]
public void Chain_Works()
public void Chainability_Works()
{
var rsx = new Rsx(14);
var rsx2 = new Rsx(rsx, 14);
+22 -68
View File
@@ -6,7 +6,7 @@ namespace QuanTAlib.Tests;
public class VelTests
{
[Fact]
public void Vel_Constructor_ValidatesInput()
public void Constructor_InvalidPeriod_ThrowsArgumentException()
{
Assert.Throws<ArgumentException>(() => new Vel(0));
Assert.Throws<ArgumentException>(() => new Vel(-1));
@@ -16,7 +16,7 @@ public class VelTests
}
[Fact]
public void Vel_Calc_ReturnsValue()
public void BasicCalculation_DoesNotCrash()
{
var vel = new Vel(10);
@@ -28,38 +28,25 @@ public class VelTests
}
[Fact]
public void Vel_Calc_IsNew_AcceptsParameter()
public void IsNew_Consistency()
{
var vel = new Vel(10);
var time = DateTime.UtcNow;
vel.Update(new TValue(DateTime.UtcNow, 100), isNew: true);
double value1 = vel.Last.Value;
// Update with isNew=true
var val1 = vel.Update(new TValue(time, 100), true);
vel.Update(new TValue(DateTime.UtcNow, 200), isNew: true);
double value2 = vel.Last.Value;
// Update with isNew=false (same time, different value)
vel.Update(new TValue(time, 105), false);
// Values should change with new bars
Assert.NotEqual(value1, value2);
// Update with isNew=false (same time, original value) - should match val1 if state rollback works
var val3 = vel.Update(new TValue(time, 100), false);
Assert.Equal(val1.Value, val3.Value, 1e-9);
}
[Fact]
public void Vel_Calc_IsNew_False_UpdatesValue()
{
var vel = new Vel(10);
vel.Update(new TValue(DateTime.UtcNow, 100));
vel.Update(new TValue(DateTime.UtcNow, 110), isNew: true);
double beforeUpdate = vel.Last.Value;
vel.Update(new TValue(DateTime.UtcNow, 120), isNew: false);
double afterUpdate = vel.Last.Value;
// Update should change the value
Assert.NotEqual(beforeUpdate, afterUpdate);
}
[Fact]
public void Vel_Reset_ClearsState()
public void Reset_Works()
{
var vel = new Vel(10);
@@ -82,7 +69,7 @@ public class VelTests
}
[Fact]
public void Vel_IsHot_BecomesTrueWhenBufferFull()
public void IsHot_BecomesTrueWhenBufferFull()
{
var vel = new Vel(5);
@@ -99,7 +86,7 @@ public class VelTests
}
[Fact]
public void Vel_CalculatesCorrectValue()
public void CalculatesCorrectValue()
{
var vel = new Vel(3);
@@ -119,7 +106,7 @@ public class VelTests
}
[Fact]
public void Vel_StaticCalculate_Works()
public void StaticCalculate_Matches_Streaming()
{
var series = new TSeries();
series.Add(DateTime.UtcNow.Ticks, 10);
@@ -138,7 +125,7 @@ public class VelTests
}
[Fact]
public void Vel_SpanCalc_MatchesTSeriesCalc()
public void SpanCalculate_Matches_Streaming()
{
var series = new TSeries();
double[] source = new double[100];
@@ -166,45 +153,12 @@ public class VelTests
}
[Fact]
public void Vel_AllModes_ProduceSameResult()
public void Chainability_Works()
{
// 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;
var vel = new Vel(10);
var vel2 = new Vel(vel, 10);
// 1. Batch Mode
var batchSeries = Vel.Calculate(series, period);
double expected = batchSeries.Last.Value;
// 2. Span Mode
var tValues = series.Values.ToArray();
var spanInput = new ReadOnlySpan<double>(tValues);
var spanOutput = new double[tValues.Length];
Vel.Calculate(spanInput, spanOutput, period);
double spanResult = spanOutput[^1];
// 3. Streaming Mode
var streamingInd = new Vel(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 Vel(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: 8);
Assert.Equal(expected, eventingResult, precision: 8);
vel.Update(new TValue(DateTime.UtcNow, 100));
Assert.False(double.IsNaN(vel2.Last.Value));
}
}
+152
View File
@@ -59,6 +59,7 @@ public class AlmaTests
// Streaming
var streamingResults = new TSeries();
Assert.True(series.Count > 0);
foreach (var item in series)
{
streamingResults.Add(almaStreaming.Update(item));
@@ -178,4 +179,155 @@ public class AlmaTests
Assert.Equal(0, alma.Last.Value);
Assert.False(alma.IsHot);
}
[Fact]
public void Alma_FirstValue_ReturnsExpected()
{
var alma = new Alma(10);
TValue result = alma.Update(new TValue(DateTime.UtcNow, 100));
Assert.Equal(100.0, result.Value, 1e-9);
}
[Fact]
public void Alma_Properties_Accessible()
{
var alma = new Alma(10);
Assert.False(alma.IsHot);
Assert.Equal(0, alma.Last.Value);
}
[Fact]
public void Alma_Calc_IsNew_AcceptsParameter()
{
var alma = new Alma(10);
alma.Update(new TValue(DateTime.UtcNow, 100), isNew: true);
Assert.Equal(100, alma.Last.Value);
}
[Fact]
public void Alma_IterativeCorrections_RestoreToOriginalState()
{
var alma = new Alma(10);
var gbm = new GBM(startPrice: 100.0, mu: 0.02, sigma: 0.1);
// Feed 10 new values
TValue tenthInput = default;
for (int i = 0; i < 10; i++)
{
var bar = gbm.Next(isNew: true);
tenthInput = new TValue(bar.Time, bar.Close);
alma.Update(tenthInput, isNew: true);
}
// Remember state after 10 values
double valueAfterTen = alma.Last.Value;
// Generate 9 corrections with isNew=false (different values)
for (int i = 0; i < 9; i++)
{
var bar = gbm.Next(isNew: false);
alma.Update(new TValue(bar.Time, bar.Close), isNew: false);
}
// Feed the remembered 10th input again with isNew=false
TValue finalValue = alma.Update(tenthInput, isNew: false);
// Should match the original state after 10 values
Assert.Equal(valueAfterTen, finalValue.Value, 1e-9);
}
[Fact]
public void Alma_Infinity_Input_UsesLastValidValue()
{
var alma = new Alma(10);
alma.Update(new TValue(DateTime.UtcNow, 100));
alma.Update(new TValue(DateTime.UtcNow, 110));
var resultPosInf = alma.Update(new TValue(DateTime.UtcNow, double.PositiveInfinity));
Assert.True(double.IsFinite(resultPosInf.Value));
var resultNegInf = alma.Update(new TValue(DateTime.UtcNow, double.NegativeInfinity));
Assert.True(double.IsFinite(resultNegInf.Value));
}
[Fact]
public void Alma_MultipleNaN_ContinuesWithLastValid()
{
var alma = new Alma(10);
alma.Update(new TValue(DateTime.UtcNow, 100));
var r1 = alma.Update(new TValue(DateTime.UtcNow, double.NaN));
var r2 = alma.Update(new TValue(DateTime.UtcNow, double.NaN));
Assert.True(double.IsFinite(r1.Value));
Assert.True(double.IsFinite(r2.Value));
}
[Fact]
public void Alma_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 = Alma.Calculate(series, period);
double expected = batchSeries.Last.Value;
// 2. Span Mode
var tValues = series.Values.ToArray();
var spanInput = new ReadOnlySpan<double>(tValues);
var spanOutput = new double[tValues.Length];
Alma.Calculate(spanInput, spanOutput, period);
double spanResult = spanOutput[^1];
// 3. Streaming Mode
var streamingInd = new Alma(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 Alma(pubSource, period);
for (int i = 0; i < series.Count; i++)
{
pubSource.Add(series[i]);
}
double eventingResult = eventingInd.Last.Value;
// Assert
Assert.Equal(expected, spanResult, 1e-9);
Assert.Equal(expected, streamingResult, 1e-9);
Assert.Equal(expected, eventingResult, 1e-9);
}
[Fact]
public void Alma_SpanCalc_ValidatesInput()
{
double[] source = [1, 2, 3, 4, 5];
double[] output = new double[5];
double[] wrongSizeOutput = new double[3];
Assert.Throws<ArgumentException>(() => Alma.Calculate(source.AsSpan(), output.AsSpan(), 0));
Assert.Throws<ArgumentException>(() => Alma.Calculate(source.AsSpan(), wrongSizeOutput.AsSpan(), 3));
}
[Fact]
public void Alma_SpanCalc_HandlesNaN()
{
double[] source = [100, 110, double.NaN, 120, 130];
double[] output = new double[5];
Alma.Calculate(source.AsSpan(), output.AsSpan(), 3);
foreach (var val in output)
{
Assert.True(double.IsFinite(val));
}
}
}
+103
View File
@@ -129,4 +129,107 @@ public class ConvTests
var res = conv.Update(new TValue(DateTime.UtcNow, double.NaN));
Assert.True(double.IsNaN(res.Value));
}
[Fact]
public void IterativeCorrections_RestoreToOriginalState()
{
double[] kernel = [0.5, 1.0];
var conv = new Conv(kernel);
var gbm = new GBM(startPrice: 100.0, mu: 0.02, sigma: 0.1);
// Feed 10 new values
TValue tenthInput = default;
for (int i = 0; i < 10; i++)
{
var bar = gbm.Next(isNew: true);
tenthInput = new TValue(bar.Time, bar.Close);
conv.Update(tenthInput, isNew: true);
}
// Remember state after 10 values
double valueAfterTen = conv.Last.Value;
// Generate 9 corrections with isNew=false (different values)
for (int i = 0; i < 9; i++)
{
var bar = gbm.Next(isNew: false);
conv.Update(new TValue(bar.Time, bar.Close), isNew: false);
}
// Feed the remembered 10th input again with isNew=false
TValue finalValue = conv.Update(tenthInput, isNew: false);
// Should match the original state after 10 values
Assert.Equal(valueAfterTen, finalValue.Value, 1e-9);
}
[Fact]
public void AllModes_ProduceSameResult()
{
// Arrange
double[] kernel = [0.1, 0.2, 0.3, 0.4];
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 = Conv.Calculate(series, kernel);
double expected = batchSeries.Last.Value;
// 2. Span Mode
var tValues = series.Values.ToArray();
var spanInput = new ReadOnlySpan<double>(tValues);
var spanOutput = new double[tValues.Length];
Conv.Calculate(spanInput, spanOutput, kernel);
double spanResult = spanOutput[^1];
// 3. Streaming Mode
var streamingInd = new Conv(kernel);
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 Conv(pubSource, kernel);
for (int i = 0; i < series.Count; i++)
{
pubSource.Add(series[i]);
}
double eventingResult = eventingInd.Last.Value;
// Assert
Assert.Equal(expected, spanResult, 1e-9);
Assert.Equal(expected, streamingResult, 1e-9);
Assert.Equal(expected, eventingResult, 1e-9);
}
[Fact]
public void SpanCalc_ValidatesInput()
{
double[] source = [1, 2, 3, 4, 5];
double[] output = new double[5];
double[] wrongSizeOutput = new double[3];
double[] kernel = [0.5, 0.5];
Assert.Throws<ArgumentException>(() => Conv.Calculate(source.AsSpan(), output.AsSpan(), Array.Empty<double>()));
Assert.Throws<ArgumentException>(() => Conv.Calculate(source.AsSpan(), wrongSizeOutput.AsSpan(), kernel));
}
[Fact]
public void SpanCalc_HandlesNaN()
{
double[] source = [100, 110, double.NaN, 120, 130];
double[] output = new double[5];
double[] kernel = [0.5, 0.5];
Conv.Calculate(source.AsSpan(), output.AsSpan(), kernel);
foreach (var val in output)
{
Assert.True(double.IsFinite(val));
}
}
}
+101
View File
@@ -160,6 +160,107 @@ public class DemaTests
Assert.Equal(val.Value, output[i], 1e-9);
}
}
[Fact]
public void Dema_Constructor_ValidatesInput()
{
Assert.Throws<ArgumentException>(() => new Dema(0));
Assert.Throws<ArgumentException>(() => new Dema(-1));
Assert.Throws<ArgumentException>(() => new Dema(0.0));
Assert.Throws<ArgumentException>(() => new Dema(1.1));
}
[Fact]
public void Dema_Calc_IsNew_AcceptsParameter()
{
var dema = new Dema(10);
dema.Update(new TValue(DateTime.UtcNow, 100), isNew: true);
Assert.Equal(100, dema.Last.Value);
}
[Fact]
public void Dema_Reset_ClearsState()
{
var dema = new Dema(10);
dema.Update(new TValue(DateTime.UtcNow, 100));
dema.Update(new TValue(DateTime.UtcNow, 110));
dema.Reset();
Assert.Equal(0, dema.Last.Value);
Assert.False(dema.IsHot);
}
[Fact]
public void Dema_IterativeCorrections_RestoreToOriginalState()
{
var dema = new Dema(10);
var gbm = new GBM(startPrice: 100.0, mu: 0.02, sigma: 0.1);
// Feed 10 new values
TValue tenthInput = default;
for (int i = 0; i < 10; i++)
{
var bar = gbm.Next(isNew: true);
tenthInput = new TValue(bar.Time, bar.Close);
dema.Update(tenthInput, isNew: true);
}
// Remember state after 10 values
double valueAfterTen = dema.Last.Value;
// Generate 9 corrections with isNew=false (different values)
for (int i = 0; i < 9; i++)
{
var bar = gbm.Next(isNew: false);
dema.Update(new TValue(bar.Time, bar.Close), isNew: false);
}
// Feed the remembered 10th input again with isNew=false
TValue finalValue = dema.Update(tenthInput, isNew: false);
// Should match the original state after 10 values
Assert.Equal(valueAfterTen, finalValue.Value, 1e-9);
}
[Fact]
public void Dema_NaN_Input_UsesLastValidValue()
{
var dema = new Dema(10);
dema.Update(new TValue(DateTime.UtcNow, 100));
dema.Update(new TValue(DateTime.UtcNow, 110));
var resultAfterNaN = dema.Update(new TValue(DateTime.UtcNow, double.NaN));
Assert.True(double.IsFinite(resultAfterNaN.Value));
Assert.NotEqual(0, resultAfterNaN.Value);
}
[Fact]
public void Dema_SpanCalc_ValidatesInput()
{
double[] source = [1, 2, 3, 4, 5];
double[] output = new double[5];
double[] wrongSizeOutput = new double[3];
Assert.Throws<ArgumentException>(() => Dema.Calculate(source.AsSpan(), output.AsSpan(), 0));
Assert.Throws<ArgumentException>(() => Dema.Calculate(source.AsSpan(), wrongSizeOutput.AsSpan(), 3));
}
[Fact]
public void Dema_SpanCalc_HandlesNaN()
{
double[] source = [100, 110, double.NaN, 120, 130];
double[] output = new double[5];
Dema.Calculate(source.AsSpan(), output.AsSpan(), 3);
foreach (var val in output)
{
Assert.True(double.IsFinite(val));
}
}
[Fact]
public void Dema_AllModes_ProduceSameResult()
{
+113
View File
@@ -99,4 +99,117 @@ public class DwmaTests
Assert.Equal(source.Count, staticResult.Count);
Assert.Equal(dwma.Last.Value, staticResult.Last.Value, 8);
}
[Fact]
public void IterativeCorrections_RestoreToOriginalState()
{
var dwma = new Dwma(10);
var gbm = new GBM(startPrice: 100.0, mu: 0.02, sigma: 0.1);
// Feed 10 new values
TValue tenthInput = default;
for (int i = 0; i < 10; i++)
{
var bar = gbm.Next(isNew: true);
tenthInput = new TValue(bar.Time, bar.Close);
dwma.Update(tenthInput, isNew: true);
}
// Remember state after 10 values
double valueAfterTen = dwma.Last.Value;
// Generate 9 corrections with isNew=false (different values)
for (int i = 0; i < 9; i++)
{
var bar = gbm.Next(isNew: false);
dwma.Update(new TValue(bar.Time, bar.Close), isNew: false);
}
// Feed the remembered 10th input again with isNew=false
TValue finalValue = dwma.Update(tenthInput, isNew: false);
// Should match the original state after 10 values
Assert.Equal(valueAfterTen, finalValue.Value, 1e-9);
}
[Fact]
public void NaN_Input_UsesLastValidValue()
{
var dwma = new Dwma(5);
dwma.Update(new TValue(DateTime.UtcNow, 100));
dwma.Update(new TValue(DateTime.UtcNow, 110));
var resultAfterNaN = dwma.Update(new TValue(DateTime.UtcNow, double.NaN));
Assert.True(double.IsFinite(resultAfterNaN.Value));
Assert.NotEqual(0, resultAfterNaN.Value);
}
[Fact]
public void SpanCalc_ValidatesInput()
{
double[] source = [1, 2, 3, 4, 5];
double[] output = new double[5];
double[] wrongSizeOutput = new double[3];
Assert.Throws<ArgumentException>(() => Dwma.Calculate(source.AsSpan(), output.AsSpan(), 0));
Assert.Throws<ArgumentException>(() => Dwma.Calculate(source.AsSpan(), wrongSizeOutput.AsSpan(), 3));
}
[Fact]
public void SpanCalc_HandlesNaN()
{
double[] source = [100, 110, double.NaN, 120, 130];
double[] output = new double[5];
Dwma.Calculate(source.AsSpan(), output.AsSpan(), 3);
foreach (var val in output)
{
Assert.True(double.IsFinite(val));
}
}
[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 = Dwma.Calculate(series, period);
double expected = batchSeries.Last.Value;
// 2. Span Mode
var tValues = series.Values.ToArray();
var spanInput = new ReadOnlySpan<double>(tValues);
var spanOutput = new double[tValues.Length];
Dwma.Calculate(spanInput, spanOutput, period);
double spanResult = spanOutput[^1];
// 3. Streaming Mode
var streamingInd = new Dwma(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 Dwma(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);
}
}
+128 -1
View File
@@ -62,6 +62,7 @@ public class HmaTests
// Streaming
var streamingResults = new TSeries();
Assert.True(series.Count > 0);
foreach (var item in series)
{
streamingResults.Add(hmaStreaming.Update(item));
@@ -71,7 +72,7 @@ public class HmaTests
var batchResults = hmaBatch.Update(series);
Assert.Equal(streamingResults.Count, batchResults.Count);
for (int i = 0; i < streamingResults.Count; i++)
for (int i = 0; i < series.Count; i++)
{
Assert.Equal(streamingResults[i].Value, batchResults[i].Value, 1e-9);
}
@@ -152,4 +153,130 @@ public class HmaTests
Assert.Equal(valueAfterCommit, hma.Last.Value, 1e-9);
}
[Fact]
public void Hma_Reset_ClearsState()
{
var hma = new Hma(10);
hma.Update(new TValue(DateTime.UtcNow, 100));
hma.Update(new TValue(DateTime.UtcNow, 110));
hma.Reset();
Assert.Equal(0, hma.Last.Value);
Assert.False(hma.IsHot);
}
[Fact]
public void Hma_IterativeCorrections_RestoreToOriginalState()
{
var hma = new Hma(10);
var gbm = new GBM(startPrice: 100.0, mu: 0.02, sigma: 0.1);
// Feed 10 new values
TValue tenthInput = default;
for (int i = 0; i < 10; i++)
{
var bar = gbm.Next(isNew: true);
tenthInput = new TValue(bar.Time, bar.Close);
hma.Update(tenthInput, isNew: true);
}
// Remember state after 10 values
double valueAfterTen = hma.Last.Value;
// Generate 9 corrections with isNew=false (different values)
for (int i = 0; i < 9; i++)
{
var bar = gbm.Next(isNew: false);
hma.Update(new TValue(bar.Time, bar.Close), isNew: false);
}
// Feed the remembered 10th input again with isNew=false
TValue finalValue = hma.Update(tenthInput, isNew: false);
// Should match the original state after 10 values
Assert.Equal(valueAfterTen, finalValue.Value, 1e-9);
}
[Fact]
public void Hma_NaN_Input_UsesLastValidValue()
{
var hma = new Hma(5);
hma.Update(new TValue(DateTime.UtcNow, 100));
hma.Update(new TValue(DateTime.UtcNow, 110));
var resultAfterNaN = hma.Update(new TValue(DateTime.UtcNow, double.NaN));
Assert.True(double.IsFinite(resultAfterNaN.Value));
Assert.NotEqual(0, resultAfterNaN.Value);
}
[Fact]
public void Hma_SpanCalc_ValidatesInput()
{
double[] source = [1, 2, 3, 4, 5];
double[] output = new double[5];
double[] wrongSizeOutput = new double[3];
Assert.Throws<ArgumentException>(() => Hma.Calculate(source.AsSpan(), output.AsSpan(), 0));
Assert.Throws<ArgumentException>(() => Hma.Calculate(source.AsSpan(), wrongSizeOutput.AsSpan(), 3));
}
[Fact]
public void Hma_SpanCalc_HandlesNaN()
{
double[] source = [100, 110, double.NaN, 120, 130];
double[] output = new double[5];
Hma.Calculate(source.AsSpan(), output.AsSpan(), 3);
foreach (var val in output)
{
Assert.True(double.IsFinite(val));
}
}
[Fact]
public void Hma_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 = Hma.Calculate(series, period);
double expected = batchSeries.Last.Value;
// 2. Span Mode
var tValues = series.Values.ToArray();
var spanInput = new ReadOnlySpan<double>(tValues);
var spanOutput = new double[tValues.Length];
Hma.Calculate(spanInput, spanOutput, period);
double spanResult = spanOutput[^1];
// 3. Streaming Mode
var streamingInd = new Hma(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 Hma(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);
}
}
+118
View File
@@ -79,4 +79,122 @@ public class HtitTests
Assert.Equal(100.0, htit.Last.Value);
}
[Fact]
public void Htit_Calc_IsNew_AcceptsParameter()
{
var htit = new Htit();
htit.Update(new TValue(DateTime.UtcNow, 100), isNew: true);
Assert.Equal(100, htit.Last.Value);
}
[Fact]
public void Htit_Reset_ClearsState()
{
var htit = new Htit();
htit.Update(new TValue(DateTime.UtcNow, 100));
htit.Update(new TValue(DateTime.UtcNow, 110));
htit.Reset();
Assert.Equal(0, htit.Last.Value);
Assert.False(htit.IsHot);
}
[Fact]
public void Htit_IterativeCorrections_RestoreToOriginalState()
{
var htit = new Htit();
var gbm = new GBM(startPrice: 100.0, mu: 0.02, sigma: 0.1);
// Feed 20 new values (needs > 12 for warmup)
TValue lastInput = default;
for (int i = 0; i < 20; i++)
{
var bar = gbm.Next(isNew: true);
lastInput = new TValue(bar.Time, bar.Close);
htit.Update(lastInput, isNew: true);
}
// Remember state after 20 values
double valueAfterTwenty = htit.Last.Value;
// Generate 9 corrections with isNew=false (different values)
for (int i = 0; i < 9; i++)
{
var bar = gbm.Next(isNew: false);
htit.Update(new TValue(bar.Time, bar.Close), isNew: false);
}
// Feed the remembered 20th input again with isNew=false
TValue finalValue = htit.Update(lastInput, isNew: false);
// Should match the original state after 20 values
Assert.Equal(valueAfterTwenty, finalValue.Value, 1e-9);
}
[Fact]
public void Htit_SpanCalc_ValidatesInput()
{
double[] source = [1, 2, 3, 4, 5];
double[] wrongSizeOutput = new double[3];
Assert.Throws<ArgumentException>(() => Htit.Calculate(source.AsSpan(), wrongSizeOutput.AsSpan()));
}
[Fact]
public void Htit_SpanCalc_HandlesNaN()
{
double[] source = [100, 110, double.NaN, 120, 130];
double[] output = new double[5];
Htit.Calculate(source.AsSpan(), output.AsSpan());
foreach (var val in output)
{
Assert.True(double.IsFinite(val));
}
}
[Fact]
public void Htit_AllModes_ProduceSameResult()
{
// Arrange
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 = Htit.Calculate(series);
double expected = batchSeries.Last.Value;
// 2. Span Mode
var tValues = series.Values.ToArray();
var spanInput = new ReadOnlySpan<double>(tValues);
var spanOutput = new double[tValues.Length];
Htit.Calculate(spanInput, spanOutput);
double spanResult = spanOutput[^1];
// 3. Streaming Mode
var streamingInd = new Htit();
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 Htit(pubSource);
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);
}
}
+24 -9
View File
@@ -28,18 +28,18 @@ public class JmaTests
}
[Fact]
public void Jma_Calc_IsNew_AcceptsParameter()
public void Jma_SpanCalc_ValidatesInput()
{
var jma = new Jma(10);
double[] source = [1, 2, 3, 4, 5];
double[] output = new double[5];
double[] wrongSizeOutput = new double[3];
jma.Update(new TValue(DateTime.UtcNow, 100), isNew: true);
double value1 = jma.Last.Value;
// Period must be > 0
Assert.Throws<ArgumentOutOfRangeException>(() => Jma.Calculate(source.AsSpan(), output.AsSpan(), 0, 0, 1.0));
Assert.Throws<ArgumentOutOfRangeException>(() => Jma.Calculate(source.AsSpan(), output.AsSpan(), -1, 0, 1.0));
jma.Update(new TValue(DateTime.UtcNow, 200), isNew: true);
double value2 = jma.Last.Value;
// Values should change with new bars
Assert.NotEqual(value1, value2);
// Output must be same length as source
Assert.Throws<ArgumentException>(() => Jma.Calculate(source.AsSpan(), wrongSizeOutput.AsSpan(), 3, 0, 1.0));
}
[Fact]
@@ -236,4 +236,19 @@ public class JmaTests
Assert.NotEqual(jmaPhase0.Last.Value, jmaPhase100.Last.Value);
Assert.NotEqual(jmaPhase0.Last.Value, jmaPhaseMinus100.Last.Value);
}
[Fact]
public void Jma_SpanCalc_HandlesNaN()
{
double[] source = [100, 110, double.NaN, 120, 130];
double[] output = new double[5];
Jma.Calculate(source.AsSpan(), output.AsSpan(), 3);
foreach (var val in output)
{
Assert.True(double.IsFinite(val));
}
}
}
+111 -2
View File
@@ -60,6 +60,7 @@ public class KamaTests
// Streaming
var streamingResults = new TSeries();
Assert.True(series.Count > 0);
foreach (var item in series)
{
streamingResults.Add(kamaStreaming.Update(item));
@@ -69,9 +70,9 @@ public class KamaTests
var batchResults = kamaBatch.Update(series);
Assert.Equal(streamingResults.Count, batchResults.Count);
for (int i = 0; i < streamingResults.Count; i++)
foreach (var (stream, batch) in streamingResults.Zip(batchResults))
{
Assert.Equal(streamingResults[i].Value, batchResults[i].Value, 1e-9);
Assert.Equal(stream.Value, batch.Value, 1e-9);
}
}
@@ -168,4 +169,112 @@ public class KamaTests
Assert.Equal(100, kama.Last.Value);
}
[Fact]
public void Kama_Calc_IsNew_AcceptsParameter()
{
var kama = new Kama(10);
kama.Update(new TValue(DateTime.UtcNow, 100), isNew: true);
Assert.Equal(100, kama.Last.Value);
}
[Fact]
public void Kama_IterativeCorrections_RestoreToOriginalState()
{
var kama = new Kama(10);
var gbm = new GBM(startPrice: 100.0, mu: 0.02, sigma: 0.1);
// Feed 20 new values (enough to fill buffer and stabilize)
TValue lastInput = default;
for (int i = 0; i < 20; i++)
{
var bar = gbm.Next(isNew: true);
lastInput = new TValue(bar.Time, bar.Close);
kama.Update(lastInput, isNew: true);
}
// Remember state
double valueAfter = kama.Last.Value;
// Generate 5 corrections with isNew=false (different values)
for (int i = 0; i < 5; i++)
{
var bar = gbm.Next(isNew: false);
kama.Update(new TValue(bar.Time, bar.Close), isNew: false);
}
// Feed the remembered last input again with isNew=false
TValue finalValue = kama.Update(lastInput, isNew: false);
// Should match the original state
Assert.Equal(valueAfter, finalValue.Value, 1e-9);
}
[Fact]
public void Kama_SpanCalc_ValidatesInput()
{
double[] source = [1, 2, 3, 4, 5];
double[] output = new double[5];
double[] wrongSizeOutput = new double[3];
Assert.Throws<ArgumentException>(() => Kama.Calculate(source.AsSpan(), output.AsSpan(), 0));
Assert.Throws<ArgumentException>(() => Kama.Calculate(source.AsSpan(), wrongSizeOutput.AsSpan(), 3));
}
[Fact]
public void Kama_SpanCalc_HandlesNaN()
{
double[] source = [100, 110, double.NaN, 120, 130];
double[] output = new double[5];
Kama.Calculate(source.AsSpan(), output.AsSpan(), 3);
foreach (var val in output)
{
Assert.True(double.IsFinite(val));
}
}
[Fact]
public void Kama_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 = Kama.Calculate(series, period);
double expected = batchSeries.Last.Value;
// 2. Span Mode
var tValues = series.Values.ToArray();
var spanInput = new ReadOnlySpan<double>(tValues);
var spanOutput = new double[tValues.Length];
Kama.Calculate(spanInput, spanOutput, period);
double spanResult = spanOutput[^1];
// 3. Streaming Mode
var streamingInd = new Kama(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 Kama(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);
}
}
+6
View File
@@ -227,6 +227,12 @@ public sealed class Kama : ITValuePublisher
return new TSeries(t, v);
}
public static TSeries Calculate(TSeries source, int period, int fastPeriod = 2, int slowPeriod = 30)
{
var kama = new Kama(period, fastPeriod, slowPeriod);
return kama.Update(source);
}
public static void Calculate(ReadOnlySpan<double> source, Span<double> output, int period, int fastPeriod = 2, int slowPeriod = 30)
{
if (period <= 0) throw new ArgumentException("Period must be greater than 0", nameof(period));
+25
View File
@@ -195,4 +195,29 @@ public class LsmaTests
var result = lsma.Update(new TValue(DateTime.UtcNow, 100));
Assert.Equal(100, result.Value);
}
[Fact]
public void IsHot_BecomesTrueWhenBufferFull()
{
int period = 5;
var lsma = new Lsma(period);
for (int i = 0; i < period; i++)
{
Assert.False(lsma.IsHot);
lsma.Update(new TValue(DateTime.UtcNow, i));
}
Assert.True(lsma.IsHot);
}
[Fact]
public void Chainability_Works()
{
var source = new TSeries();
var lsma = new Lsma(source, 10);
source.Add(new TValue(DateTime.UtcNow, 100));
Assert.Equal(100, lsma.Last.Value);
}
}
+79
View File
@@ -99,4 +99,83 @@ public class MamaTests
Assert.Equal(result1[25 + i].Value, result2[i].Value, 6);
}
}
[Fact]
public void IsHot_BecomesTrueAfterWarmup()
{
var mama = new Mama();
// MAMA needs 6 bars to warmup (Index > 6)
for (int i = 0; i < 6; i++)
{
mama.Update(new TValue(DateTime.UtcNow, 100));
Assert.False(mama.IsHot);
}
mama.Update(new TValue(DateTime.UtcNow, 100));
Assert.True(mama.IsHot);
}
[Fact]
public void Reset_ClearsState()
{
var mama = new Mama();
for (int i = 0; i < 10; i++)
{
mama.Update(new TValue(DateTime.UtcNow, 100));
}
Assert.True(mama.IsHot);
mama.Reset();
Assert.False(mama.IsHot);
Assert.True(double.IsNaN(mama.Last.Value));
}
[Fact]
public void Update_BarCorrection_UpdatesCorrectly()
{
var mama = new Mama();
// Warmup
for (int i = 0; i < 10; i++)
{
mama.Update(new TValue(DateTime.UtcNow, 100));
}
// New bar
var result1 = mama.Update(new TValue(DateTime.UtcNow, 110));
// Update same bar with different value
var result2 = mama.Update(new TValue(DateTime.UtcNow, 120), isNew: false);
Assert.NotEqual(result1.Value, result2.Value);
// Verify internal state by adding next bar
var result3 = mama.Update(new TValue(DateTime.UtcNow, 130));
Assert.True(double.IsFinite(result3.Value));
}
[Fact]
public void Calculate_StaticMethod_MatchesObjectInstance()
{
var source = new TSeries();
var gbm = new GBM(startPrice: 100, seed: 42);
for (int i = 0; i < 50; i++)
{
var bar = gbm.Next();
source.Add(bar.C);
}
var mama = new Mama();
var series1 = mama.Update(source);
var series2 = Mama.Calculate(source);
Assert.Equal(series1.Count, series2.Count);
for (int i = 0; i < source.Count; i++)
{
Assert.Equal(series1[i].Value, series2[i].Value, 1e-9);
}
}
}
+11
View File
@@ -61,6 +61,11 @@ public sealed class Mama : ITValuePublisher
}
public void Init()
{
Reset();
}
public void Reset()
{
_state = default;
_state.Mama = double.NaN;
@@ -214,6 +219,12 @@ public sealed class Mama : ITValuePublisher
return new TSeries(t, v);
}
public static TSeries Calculate(TSeries source, double fastLimit = 0.5, double slowLimit = 0.05)
{
var mama = new Mama(fastLimit, slowLimit);
return mama.Update(source);
}
public static void Calculate(ReadOnlySpan<double> source, Span<double> output, double fastLimit = 0.5, double slowLimit = 0.05)
{
var mama = new Mama(fastLimit, slowLimit);
+50
View File
@@ -96,4 +96,54 @@ public class MgdiTests
Assert.Throws<ArgumentOutOfRangeException>(() => new Mgdi(14, double.NaN));
Assert.Throws<ArgumentOutOfRangeException>(() => new Mgdi(14, double.PositiveInfinity));
}
[Fact]
public void Reset_ClearsState()
{
var mgdi = new Mgdi(14);
for (int i = 0; i < 20; i++)
{
mgdi.Update(new TValue(DateTime.UtcNow, 100));
}
Assert.True(mgdi.IsHot);
mgdi.Reset();
Assert.False(mgdi.IsHot);
Assert.Equal(0, mgdi.Last.Value);
}
[Fact]
public void Update_BarCorrection_UpdatesCorrectly()
{
var mgdi = new Mgdi(14);
// Warmup
for (int i = 0; i < 20; i++)
{
mgdi.Update(new TValue(DateTime.UtcNow, 100));
}
// New bar
var result1 = mgdi.Update(new TValue(DateTime.UtcNow, 110));
// Update same bar with different value
var result2 = mgdi.Update(new TValue(DateTime.UtcNow, 120), isNew: false);
Assert.NotEqual(result1.Value, result2.Value);
// Verify internal state by adding next bar
var result3 = mgdi.Update(new TValue(DateTime.UtcNow, 130));
Assert.True(double.IsFinite(result3.Value));
}
[Fact]
public void Chainability_Works()
{
var source = new TSeries();
var mgdi = new Mgdi(source, 14);
source.Add(new TValue(DateTime.UtcNow, 100));
Assert.Equal(100, mgdi.Last.Value);
}
}
+10
View File
@@ -202,4 +202,14 @@ public class RmaTests
// Result should be finite (not NaN)
Assert.True(double.IsFinite(resultAfterNaN.Value));
}
[Fact]
public void Chainability_Works()
{
var source = new TSeries();
var rma = new Rma(source, 10);
source.Add(new TValue(DateTime.UtcNow, 100));
Assert.Equal(100, rma.Last.Value, 1e-9);
}
}
+10
View File
@@ -506,4 +506,14 @@ public class SmaTests
Assert.Equal(expected, streamingResult, precision: 9);
Assert.Equal(expected, eventingResult, precision: 9);
}
[Fact]
public void Chainability_Works()
{
var source = new TSeries();
var sma = new Sma(source, 10);
source.Add(new TValue(DateTime.UtcNow, 100));
Assert.Equal(100, sma.Last.Value);
}
}
+46
View File
@@ -140,4 +140,50 @@ public class SuperTests
Assert.Throws<ArgumentOutOfRangeException>(() => new Super(10, 0));
Assert.Throws<ArgumentOutOfRangeException>(() => new Super(10, -1.0));
}
[Fact]
public void StaticCalculate_Matches_Streaming()
{
var gbm = new GBM();
var bars = gbm.Fetch(200, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
var super = new Super(10, 3.0);
var streamingResults = new List<double>();
for (int i = 0; i < bars.Count; i++)
{
streamingResults.Add(super.Update(bars[i]).Value);
}
var staticResults = Super.Calculate(bars, 10, 3.0);
Assert.Equal(streamingResults.Count, staticResults.Count);
for (int i = 0; i < staticResults.Count; i++)
{
if (double.IsNaN(streamingResults[i]))
{
Assert.True(double.IsNaN(staticResults.Values[i]));
}
else
{
Assert.Equal(streamingResults[i], staticResults.Values[i], 1e-9);
}
}
}
[Fact]
public void Chainability_Works()
{
var super = new Super(10, 3.0);
var gbm = new GBM();
var bars = gbm.Fetch(10, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
// Test TBarSeries chain
var result = super.Update(bars);
Assert.NotNull(result);
Assert.IsType<TSeries>(result);
// Test TBar chain (returns TValue)
var result2 = super.Update(bars[0]);
Assert.IsType<TValue>(result2);
}
}
+6
View File
@@ -217,4 +217,10 @@ public sealed class Super : ITValuePublisher
return new TSeries(t, v);
}
public static TSeries Calculate(TBarSeries source, int period = 10, double multiplier = 3.0)
{
var indicator = new Super(period, multiplier);
return indicator.Update(source);
}
}
+162 -120
View File
@@ -1,131 +1,173 @@
using Xunit;
using System;
using System.Collections.Generic;
using Xunit;
namespace QuanTAlib.Tests;
namespace QuanTAlib;
public class T3Tests
{
[Fact]
public void T3_Constructor_Period_ValidatesInput()
public void BasicCalculation_DoesNotCrash()
{
var t3 = new T3(5, 0.7);
var gbm = new GBM();
var bars = gbm.Fetch(1000, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
for (int i = 0; i < bars.Count; i++)
{
t3.Update(new TValue(bars[i].Time, bars[i].Close));
}
Assert.True(double.IsFinite(t3.Last.Value));
}
[Fact]
public void IsNew_Consistency()
{
var t3 = new T3(5, 0.7);
var gbm = new GBM();
var bars = gbm.Fetch(100, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
// Feed first 99
for (int i = 0; i < 99; i++)
{
t3.Update(new TValue(bars[i].Time, bars[i].Close));
}
// Update with 100th point (isNew=true)
t3.Update(new TValue(bars[99].Time, bars[99].Close), true);
// Update with modified 100th point (isNew=false)
var val2 = t3.Update(new TValue(bars[99].Time, bars[99].Close + 1.0), false);
// Create new instance and feed up to modified
var t3_2 = new T3(5, 0.7);
for (int i = 0; i < 99; i++)
{
t3_2.Update(new TValue(bars[i].Time, bars[i].Close));
}
var val3 = t3_2.Update(new TValue(bars[99].Time, bars[99].Close + 1.0), true);
Assert.Equal(val3.Value, val2.Value, 1e-9);
}
[Fact]
public void Reset_Works()
{
var t3 = new T3(5, 0.7);
var gbm = new GBM();
var bars = gbm.Fetch(100, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
for (int i = 0; i < bars.Count; i++)
{
t3.Update(new TValue(bars[i].Time, bars[i].Close));
}
t3.Reset();
Assert.Equal(0, t3.Last.Value);
Assert.False(t3.IsHot);
// Feed again
for (int i = 0; i < bars.Count; i++)
{
t3.Update(new TValue(bars[i].Time, bars[i].Close));
}
Assert.True(double.IsFinite(t3.Last.Value));
}
[Fact]
public void TSeries_Update_Matches_Streaming()
{
var t3 = new T3(5, 0.7);
var gbm = new GBM();
var bars = gbm.Fetch(200, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
var series = bars.Close;
var streamingResults = new List<double>();
for (int i = 0; i < series.Count; i++)
{
streamingResults.Add(t3.Update(series[i]).Value);
}
var t3_2 = new T3(5, 0.7);
var seriesResults = t3_2.Update(series);
Assert.Equal(streamingResults.Count, seriesResults.Count);
for (int i = 0; i < seriesResults.Count; i++)
{
Assert.Equal(streamingResults[i], seriesResults.Values[i], 1e-9);
}
}
[Fact]
public void StaticCalculate_Matches_Streaming()
{
var gbm = new GBM();
var bars = gbm.Fetch(200, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
var series = bars.Close;
var t3 = new T3(5, 0.7);
var streamingResults = new List<double>();
for (int i = 0; i < series.Count; i++)
{
streamingResults.Add(t3.Update(series[i]).Value);
}
var staticResults = T3.Calculate(series, 5, 0.7);
Assert.Equal(streamingResults.Count, staticResults.Count);
for (int i = 0; i < staticResults.Count; i++)
{
Assert.Equal(streamingResults[i], staticResults.Values[i], 1e-9);
}
}
[Fact]
public void StaticCalculateSpan_Matches_Streaming()
{
var gbm = new GBM();
var bars = gbm.Fetch(200, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
var series = bars.Close;
var t3 = new T3(5, 0.7);
var streamingResults = new List<double>();
for (int i = 0; i < series.Count; i++)
{
streamingResults.Add(t3.Update(series[i]).Value);
}
var spanResults = new double[series.Count];
T3.Calculate(series.Values, spanResults, 5, 0.7);
for (int i = 0; i < spanResults.Length; i++)
{
Assert.Equal(streamingResults[i], spanResults[i], 1e-9);
}
}
[Fact]
public void Chainability_Works()
{
var t3 = new T3(5, 0.7);
var gbm = new GBM();
var bars = gbm.Fetch(10, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
var series = bars.Close;
// Test TSeries chain
var result = t3.Update(series);
Assert.NotNull(result);
Assert.IsType<TSeries>(result);
// Test TValue chain
var result2 = t3.Update(series[0]);
Assert.IsType<TValue>(result2);
}
[Fact]
public void Constructor_InvalidParameters_ThrowsArgumentException()
{
Assert.Throws<ArgumentException>(() => new T3(0));
Assert.Throws<ArgumentException>(() => new T3(-1));
var t3 = new T3(10);
Assert.NotNull(t3);
}
[Fact]
public void T3_ConstantInput_ConvergesToInput()
{
var t3 = new T3(5, 0.7);
double input = 100.0;
// Feed enough values for T3 to converge (it has 6 cascaded EMAs)
for(int i = 0; i < 100; i++)
{
t3.Update(new TValue(DateTime.UtcNow, input));
}
Assert.Equal(input, t3.Last.Value, 1e-9);
}
[Fact]
public void T3_Parameters_AffectResult()
{
// Different volume factors should produce different results for changing data
var t3_low_v = new T3(10, 0.1);
var t3_high_v = new T3(10, 0.9);
var series = new TSeries();
series.Add(DateTime.UtcNow, 100);
series.Add(DateTime.UtcNow.AddMinutes(1), 110);
series.Add(DateTime.UtcNow.AddMinutes(2), 120);
t3_low_v.Update(series);
t3_high_v.Update(series);
Assert.NotEqual(t3_low_v.Last.Value, t3_high_v.Last.Value);
}
[Fact]
public void T3_Reset_ResetsState()
{
var t3 = new T3(10);
t3.Update(new TValue(DateTime.UtcNow, 100));
t3.Update(new TValue(DateTime.UtcNow, 110));
Assert.True(t3.IsHot);
Assert.NotEqual(0, t3.Last.Value);
t3.Reset();
Assert.False(t3.IsHot);
Assert.Equal(0, t3.Last.Value);
// Should accept new data as if fresh
t3.Update(new TValue(DateTime.UtcNow, 50));
Assert.Equal(50, t3.Last.Value, 1e-9); // First value logic: output = input
}
[Fact]
public void T3_Eventing_Works()
{
var source = new TSeries();
var t3 = new T3(source, 10);
double lastVal = 0;
t3.Pub += (v) => lastVal = v.Value;
source.Add(new TValue(DateTime.UtcNow, 100));
Assert.Equal(100, lastVal, 1e-9);
source.Add(new TValue(DateTime.UtcNow, 110));
Assert.NotEqual(100, lastVal);
Assert.NotEqual(0, lastVal);
}
[Fact]
public void T3_SpanTests()
{
var series = new TSeries();
int count = 100;
for(int i=0; i<count; i++)
series.Add(DateTime.UtcNow.AddMinutes(i), 100 + i);
var t3 = new T3(10);
var resSeries = t3.Update(series);
var resSpan = new double[count];
// Correctly use Span.CopyTo
T3.Calculate(series, 10).Values.CopyTo(resSpan.AsSpan());
// Check last values match
Assert.Equal(resSeries.Last.Value, resSpan[count-1], 1e-9);
}
[Fact]
public void T3_BarCorrection_WithNaN_RestoresPreviousValidValue()
{
var t3 = new T3(10);
var time = DateTime.UtcNow;
// Step 1: Update with valid value
t3.Update(new TValue(time, 100), isNew: true);
// Step 2: Update with another valid value
t3.Update(new TValue(time.AddMinutes(1), 200), isNew: true);
double valAfter200 = t3.Last.Value;
// Step 3: Correct with NaN (should use 100)
t3.Update(new TValue(time.AddMinutes(1), double.NaN), isNew: false);
double valAfterNaN = t3.Last.Value;
// Step 4: Correct with 100 (should match NaN result)
t3.Update(new TValue(time.AddMinutes(1), 100), isNew: false);
double valAfter100 = t3.Last.Value;
Assert.NotEqual(valAfter200, valAfterNaN); // Should not be the same as 200
Assert.Equal(valAfter100, valAfterNaN, 1e-9); // Should be the same as using 100
}
}
+165 -300
View File
@@ -1,310 +1,175 @@
namespace QuanTAlib.Tests;
using System;
using System.Collections.Generic;
using Xunit;
namespace QuanTAlib;
#pragma warning disable S2245 // Random is acceptable for simulation/testing purposes
public class TemaTests
{
[Fact]
public void Tema_Constructor_Period_ValidatesInput()
public void BasicCalculation_DoesNotCrash()
{
var tema = new Tema(10);
var gbm = new GBM();
var bars = gbm.Fetch(1000, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
for (int i = 0; i < bars.Count; i++)
{
tema.Update(new TValue(bars[i].Time, bars[i].Close));
}
Assert.True(double.IsFinite(tema.Last.Value));
}
[Fact]
public void IsNew_Consistency()
{
var tema = new Tema(10);
var gbm = new GBM();
var bars = gbm.Fetch(100, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
// Feed first 99
for (int i = 0; i < 99; i++)
{
tema.Update(new TValue(bars[i].Time, bars[i].Close));
}
// Update with 100th point (isNew=true)
tema.Update(new TValue(bars[99].Time, bars[99].Close), true);
// Update with modified 100th point (isNew=false)
var val2 = tema.Update(new TValue(bars[99].Time, bars[99].Close + 1.0), false);
// Create new instance and feed up to modified
var tema2 = new Tema(10);
for (int i = 0; i < 99; i++)
{
tema2.Update(new TValue(bars[i].Time, bars[i].Close));
}
var val3 = tema2.Update(new TValue(bars[99].Time, bars[99].Close + 1.0), true);
Assert.Equal(val3.Value, val2.Value, 1e-9);
}
[Fact]
public void Reset_Works()
{
var tema = new Tema(10);
var gbm = new GBM();
var bars = gbm.Fetch(100, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
for (int i = 0; i < bars.Count; i++)
{
tema.Update(new TValue(bars[i].Time, bars[i].Close));
}
tema.Reset();
Assert.Equal(0, tema.Last.Value);
Assert.False(tema.IsHot);
// Feed again
for (int i = 0; i < bars.Count; i++)
{
tema.Update(new TValue(bars[i].Time, bars[i].Close));
}
Assert.True(double.IsFinite(tema.Last.Value));
}
[Fact]
public void TSeries_Update_Matches_Streaming()
{
var tema = new Tema(10);
var gbm = new GBM();
var bars = gbm.Fetch(200, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
var series = bars.Close;
var streamingResults = new List<double>();
for (int i = 0; i < series.Count; i++)
{
streamingResults.Add(tema.Update(series[i]).Value);
}
var tema2 = new Tema(10);
var seriesResults = tema2.Update(series);
Assert.Equal(streamingResults.Count, seriesResults.Count);
for (int i = 0; i < seriesResults.Count; i++)
{
Assert.Equal(streamingResults[i], seriesResults.Values[i], 1e-9);
}
}
[Fact]
public void StaticCalculate_Matches_Streaming()
{
var gbm = new GBM();
var bars = gbm.Fetch(200, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
var series = bars.Close;
var tema = new Tema(10);
var streamingResults = new List<double>();
for (int i = 0; i < series.Count; i++)
{
streamingResults.Add(tema.Update(series[i]).Value);
}
var staticResults = Tema.Calculate(series, 10);
Assert.Equal(streamingResults.Count, staticResults.Count);
for (int i = 0; i < staticResults.Count; i++)
{
Assert.Equal(streamingResults[i], staticResults.Values[i], 1e-9);
}
}
[Fact]
public void StaticCalculateSpan_Matches_Streaming()
{
var gbm = new GBM();
var bars = gbm.Fetch(200, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
var series = bars.Close;
var tema = new Tema(10);
var streamingResults = new List<double>();
for (int i = 0; i < series.Count; i++)
{
streamingResults.Add(tema.Update(series[i]).Value);
}
var spanResults = new double[series.Count];
Tema.Calculate(series.Values, spanResults, 10);
for (int i = 0; i < spanResults.Length; i++)
{
Assert.Equal(streamingResults[i], spanResults[i], 1e-9);
}
}
[Fact]
public void Chainability_Works()
{
var tema = new Tema(10);
var gbm = new GBM();
var bars = gbm.Fetch(10, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
var series = bars.Close;
// Test TSeries chain
var result = tema.Update(series);
Assert.NotNull(result);
Assert.IsType<TSeries>(result);
// Test TValue chain
var result2 = tema.Update(series[0]);
Assert.IsType<TValue>(result2);
}
[Fact]
public void Constructor_InvalidParameters_ThrowsArgumentException()
{
Assert.Throws<ArgumentException>(() => new Tema(0));
Assert.Throws<ArgumentException>(() => new Tema(-1));
var tema = new Tema(10);
Assert.NotNull(tema);
}
[Fact]
public void Tema_Constructor_Alpha_ValidatesInput()
{
Assert.Throws<ArgumentException>(() => new Tema(0.0));
Assert.Throws<ArgumentException>(() => new Tema(-0.1));
Assert.Throws<ArgumentException>(() => new Tema(1.1));
var tema = new Tema(0.5);
Assert.NotNull(tema);
}
[Fact]
public void Tema_Calc_ReturnsValue()
{
var tema = new Tema(10);
Assert.Equal(0, tema.Last.Value);
TValue result = tema.Update(new TValue(DateTime.UtcNow, 100));
Assert.True(result.Value > 0);
Assert.Equal(result.Value, tema.Last.Value);
}
[Fact]
public void Tema_Calc_IsNew_AcceptsParameter()
{
var tema = new Tema(10);
tema.Update(new TValue(DateTime.UtcNow, 100), isNew: true);
double value1 = tema.Last.Value;
tema.Update(new TValue(DateTime.UtcNow, 105), isNew: true);
double value2 = tema.Last.Value;
// Values should change with new bars
Assert.NotEqual(value1, value2);
}
[Fact]
public void Tema_Calc_IsNew_False_UpdatesValue()
{
var tema = new Tema(10);
tema.Update(new TValue(DateTime.UtcNow, 100));
tema.Update(new TValue(DateTime.UtcNow, 110), isNew: true);
double beforeUpdate = tema.Last.Value;
tema.Update(new TValue(DateTime.UtcNow, 120), isNew: false);
double afterUpdate = tema.Last.Value;
// Update should change the value
Assert.NotEqual(beforeUpdate, afterUpdate);
}
[Fact]
public void Tema_Reset_ClearsState()
{
var tema = new Tema(10);
tema.Update(new TValue(DateTime.UtcNow, 100));
tema.Update(new TValue(DateTime.UtcNow, 105));
double valueBefore = tema.Last.Value;
tema.Reset();
Assert.Equal(0, tema.Last.Value);
// After reset, should accept new values
tema.Update(new TValue(DateTime.UtcNow, 50));
Assert.NotEqual(0, tema.Last.Value);
Assert.NotEqual(valueBefore, tema.Last.Value);
}
[Fact]
public void Tema_Properties_Accessible()
{
var tema = new Tema(10);
Assert.Equal(0, tema.Last.Value);
Assert.False(tema.IsHot);
tema.Update(new TValue(DateTime.UtcNow, 100));
Assert.NotEqual(0, tema.Last.Value);
}
[Fact]
public void Tema_IsHot_BecomesTrueAfterWarmup()
{
var tema = new Tema(10);
// Initially IsHot should be false
Assert.False(tema.IsHot);
// TEMA needs more warmup than EMA due to triple smoothing
int steps = 0;
while (!tema.IsHot && steps < 1000)
{
tema.Update(new TValue(DateTime.UtcNow, 100));
steps++;
}
Assert.True(tema.IsHot);
Assert.True(steps > 0);
}
[Fact]
public void Tema_PeriodEquivalence_BothConstructorsWork()
{
int period = 20;
double alpha = 2.0 / (period + 1);
var temaPeriod = new Tema(period);
var temaAlpha = new Tema(alpha);
// Both should accept Calc calls and produce same result
TValue result1 = temaPeriod.Update(new TValue(DateTime.UtcNow, 100));
TValue result2 = temaAlpha.Update(new TValue(DateTime.UtcNow, 100));
Assert.Equal(result1.Value, result2.Value, 1e-10);
}
[Fact]
public void Tema_IterativeCorrections_RestoreToOriginalState()
{
var tema = new Tema(10);
var gbm = new GBM(startPrice: 100.0, mu: 0.02, sigma: 0.1);
// Feed 10 new values
TValue tenthInput = default;
for (int i = 0; i < 10; i++)
{
var bar = gbm.Next(isNew: true);
tenthInput = new TValue(bar.Time, bar.Close);
tema.Update(tenthInput, isNew: true);
}
// Remember TEMA state after 10 values
double temaAfterTen = tema.Last.Value;
// Generate 9 corrections with isNew=false (different values)
for (int i = 0; i < 9; i++)
{
var bar = gbm.Next(isNew: false);
tema.Update(new TValue(bar.Time, bar.Close), isNew: false);
}
// Feed the remembered 10th input again with isNew=false
TValue finalTema = tema.Update(tenthInput, isNew: false);
// TEMA should match the original state after 10 values
Assert.Equal(temaAfterTen, finalTema.Value, 1e-10);
}
[Fact]
public void Tema_BatchCalc_MatchesIterativeCalc()
{
var temaIterative = new Tema(10);
var temaBatch = new Tema(10);
var gbm = new GBM(startPrice: 100.0, mu: 0.02, sigma: 0.1);
// Generate data
var series = new TSeries();
for (int i = 0; i < 100; i++)
{
var bar = gbm.Next(isNew: true);
series.Add(bar.Time, bar.Close);
}
Assert.True(series.Count > 0);
// Calculate iteratively
var iterativeResults = new TSeries();
foreach (var item in series)
{
iterativeResults.Add(temaIterative.Update(item));
}
// Calculate batch
var batchResults = temaBatch.Update(series);
// Compare
Assert.Equal(iterativeResults.Count, batchResults.Count);
for (int i = 0; i < iterativeResults.Count; i++)
{
Assert.Equal(iterativeResults[i].Value, batchResults[i].Value, 1e-10);
Assert.Equal(iterativeResults[i].Time, batchResults[i].Time);
}
}
[Fact]
public void Tema_NaN_Input_UsesLastValidValue()
{
var tema = new Tema(10);
// Feed some valid values
tema.Update(new TValue(DateTime.UtcNow, 100));
tema.Update(new TValue(DateTime.UtcNow, 110));
// Feed NaN - should use last valid value (110)
var resultAfterNaN = tema.Update(new TValue(DateTime.UtcNow, double.NaN));
// Result should be finite (not NaN)
Assert.True(double.IsFinite(resultAfterNaN.Value));
Assert.NotEqual(0, resultAfterNaN.Value);
}
[Fact]
public void Tema_SpanCalc_MatchesTSeriesCalc()
{
var series = new TSeries();
double[] source = new double[100];
double[] output = new double[100];
var gbm = new GBM(startPrice: 100.0, mu: 0.02, sigma: 0.1, seed: 42);
for (int i = 0; i < 100; i++)
{
var bar = gbm.Next(isNew: true);
source[i] = bar.Close;
series.Add(bar.Time, bar.Close);
}
// Calculate with TSeries API
var tseriesResult = Tema.Calculate(series, 10);
// Calculate with Span API
Tema.Calculate(source.AsSpan(), output.AsSpan(), 10);
// Compare results
for (int i = 0; i < 100; i++)
{
Assert.Equal(tseriesResult[i].Value, output[i], 1e-9);
}
}
[Fact]
public void Tema_SpanCalc_ZeroAllocation()
{
double[] source = new double[10000];
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
Tema.Calculate(source.AsSpan(), output.AsSpan(), 100);
// This test verifies the method runs without throwing
Assert.True(double.IsFinite(output[^1]));
}
[Fact]
public void Tema_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 = Tema.Calculate(series, period);
double expected = batchSeries.Last.Value;
// 2. Span Mode
var tValues = series.Values.ToArray();
var spanInput = new ReadOnlySpan<double>(tValues);
var spanOutput = new double[tValues.Length];
Tema.Calculate(spanInput, spanOutput, period);
double spanResult = spanOutput[^1];
// 3. Streaming Mode
var streamingInd = new Tema(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 Tema(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);
Assert.Throws<ArgumentException>(() => new Tema(1.0));
}
}
+155 -27
View File
@@ -1,45 +1,173 @@
using System;
using System.Collections.Generic;
using Xunit;
using QuanTAlib;
namespace QuanTAlib.Tests;
namespace QuanTAlib;
public class TrimaTests
{
[Fact]
public void StateRestoration_IsCorrect()
public void BasicCalculation_DoesNotCrash()
{
// Arrange
int period = 4;
var trimaStreaming = new Trima(period);
var trimaBatch = new Trima(period);
var trima = new Trima(10);
var gbm = new GBM();
var bars = gbm.Fetch(1000, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
for (int i = 0; i < bars.Count; i++)
{
trima.Update(new TValue(bars[i].Time, bars[i].Close));
}
Assert.True(double.IsFinite(trima.Last.Value));
}
[Fact]
public void IsNew_Consistency()
{
var trima = new Trima(10);
var gbm = new GBM();
var bars = gbm.Fetch(100, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
// Feed first 99
for (int i = 0; i < 99; i++)
{
trima.Update(new TValue(bars[i].Time, bars[i].Close));
}
// Update with 100th point (isNew=true)
trima.Update(new TValue(bars[99].Time, bars[99].Close), true);
// Update with modified 100th point (isNew=false)
var val2 = trima.Update(new TValue(bars[99].Time, bars[99].Close + 1.0), false);
// Create new instance and feed up to modified
var trima2 = new Trima(10);
for (int i = 0; i < 99; i++)
{
trima2.Update(new TValue(bars[i].Time, bars[i].Close));
}
var val3 = trima2.Update(new TValue(bars[99].Time, bars[99].Close + 1.0), true);
Assert.Equal(val3.Value, val2.Value, 1e-9);
}
[Fact]
public void Reset_Works()
{
var trima = new Trima(10);
var gbm = new GBM();
var bars = gbm.Fetch(100, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
for (int i = 0; i < bars.Count; i++)
{
trima.Update(new TValue(bars[i].Time, bars[i].Close));
}
trima.Reset();
Assert.Equal(0, trima.Last.Value);
Assert.False(trima.IsHot);
// Generate enough data to fill the buffers and have some history
int count = 50;
var data = new TSeries();
for (int i = 0; i < count; i++)
// Feed again
for (int i = 0; i < bars.Count; i++)
{
data.Add(new TValue(DateTime.UtcNow.AddMinutes(i), 100 + i));
trima.Update(new TValue(bars[i].Time, bars[i].Close));
}
Assert.True(double.IsFinite(trima.Last.Value));
}
[Fact]
public void TSeries_Update_Matches_Streaming()
{
var trima = new Trima(10);
var gbm = new GBM();
var bars = gbm.Fetch(200, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
var series = bars.Close;
var streamingResults = new List<double>();
for (int i = 0; i < series.Count; i++)
{
streamingResults.Add(trima.Update(series[i]).Value);
}
// Act
// 1. Feed streaming instance
Assert.True(data.Count > 0);
for (int i = 0; i < data.Count; i++)
var trima2 = new Trima(10);
var seriesResults = trima2.Update(series);
Assert.Equal(streamingResults.Count, seriesResults.Count);
for (int i = 0; i < seriesResults.Count; i++)
{
trimaStreaming.Update(data[i]);
Assert.Equal(streamingResults[i], seriesResults.Values[i], 1e-9);
}
}
[Fact]
public void StaticCalculate_Matches_Streaming()
{
var gbm = new GBM();
var bars = gbm.Fetch(200, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
var series = bars.Close;
var trima = new Trima(10);
var streamingResults = new List<double>();
for (int i = 0; i < series.Count; i++)
{
streamingResults.Add(trima.Update(series[i]).Value);
}
var staticResults = Trima.Calculate(series, 10);
Assert.Equal(streamingResults.Count, staticResults.Count);
for (int i = 0; i < staticResults.Count; i++)
{
Assert.Equal(streamingResults[i], staticResults.Values[i], 1e-9);
}
}
// 2. Feed batch instance with all but the last point first, then the last point
// Actually, the Update(TSeries) method is supposed to handle the whole series and leave the state ready for the NEXT point.
// So let's feed the whole series to batch instance.
trimaBatch.Update(data);
[Fact]
public void StaticCalculateSpan_Matches_Streaming()
{
var gbm = new GBM();
var bars = gbm.Fetch(200, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
var series = bars.Close;
var trima = new Trima(10);
var streamingResults = new List<double>();
for (int i = 0; i < series.Count; i++)
{
streamingResults.Add(trima.Update(series[i]).Value);
}
var spanResults = new double[series.Count];
Trima.Calculate(series.Values, spanResults, 10);
for (int i = 0; i < spanResults.Length; i++)
{
Assert.Equal(streamingResults[i], spanResults[i], 1e-9);
}
}
// 3. Now feed one NEW point to both
var newPoint = new TValue(DateTime.UtcNow.AddMinutes(count), 200);
var resultStreaming = trimaStreaming.Update(newPoint);
var resultBatch = trimaBatch.Update(newPoint);
[Fact]
public void Chainability_Works()
{
var trima = new Trima(10);
var gbm = new GBM();
var bars = gbm.Fetch(10, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
var series = bars.Close;
// Test TSeries chain
var result = trima.Update(series);
Assert.NotNull(result);
Assert.IsType<TSeries>(result);
// Test TValue chain
var result2 = trima.Update(series[0]);
Assert.IsType<TValue>(result2);
}
// Assert
Assert.Equal(resultStreaming.Value, resultBatch.Value, precision: 9);
[Fact]
public void Constructor_InvalidParameters_ThrowsArgumentException()
{
Assert.Throws<ArgumentException>(() => new Trima(0));
Assert.Throws<ArgumentException>(() => new Trima(-1));
}
}
+141 -80
View File
@@ -1,111 +1,172 @@
using QuanTAlib;
using System;
using System.Collections.Generic;
using Xunit;
namespace Trends;
namespace QuanTAlib;
public class VidyaTests
{
[Fact]
public void BasicCalculation()
public void BasicCalculation_DoesNotCrash()
{
// Test with a small dataset
// Period = 2
// Alpha = 2 / (2 + 1) = 0.666...
var vidya = new Vidya(2);
// Bar 1: Price 100
// Init: PrevClose=100, LastVidya=100, Ups=[0,0], Downs=[0,0]
// Output: 100
var v1 = vidya.Update(new TValue(DateTime.UtcNow, 100));
Assert.Equal(100, v1.Value);
// Bar 2: Price 110
// Change = 110 - 100 = 10
// Up=10, Down=0
// Ups=[10,0], Downs=[0,0]
// SumUp=10, SumDown=0, Sum=10
// VI = |10-0|/10 = 1
// DynAlpha = 0.666 * 1 = 0.666
// Vidya = 0.666 * 110 + 0.333 * 100 = 73.33 + 33.33 = 106.66
var v2 = vidya.Update(new TValue(DateTime.UtcNow, 110));
Assert.Equal(106.66666666666667, v2.Value, 5);
// Bar 3: Price 105
// Change = 105 - 110 = -5
// Up=0, Down=5
// Ups=[0,10], Downs=[5,0] (Circular buffer logic)
// SumUp=10, SumDown=5, Sum=15
// VI = |10-5|/15 = 5/15 = 0.333
// DynAlpha = 0.666 * 0.333 = 0.222
// Vidya = 0.222 * 105 + 0.777 * 106.66 = 23.33 + 82.96 = 106.29
var v3 = vidya.Update(new TValue(DateTime.UtcNow, 105));
Assert.Equal(106.29629629629629, v3.Value, 5);
var vidya = new Vidya(10);
var gbm = new GBM();
var bars = gbm.Fetch(1000, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
for (int i = 0; i < bars.Count; i++)
{
vidya.Update(new TValue(bars[i].Time, bars[i].Close));
}
Assert.True(double.IsFinite(vidya.Last.Value));
}
[Fact]
public void IsNewConsistency()
public void IsNew_Consistency()
{
var vidya = new Vidya(5);
var inputs = new double[] { 100, 105, 102, 108, 110, 105 };
// Feed normally
var expected = new List<double>();
foreach (var input in inputs)
var vidya = new Vidya(10);
var gbm = new GBM();
var bars = gbm.Fetch(100, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
// Feed first 99
for (int i = 0; i < 99; i++)
{
expected.Add(vidya.Update(new TValue(DateTime.UtcNow, input)).Value);
vidya.Update(new TValue(bars[i].Time, bars[i].Close));
}
// Feed with updates
// Update with 100th point (isNew=true)
vidya.Update(new TValue(bars[99].Time, bars[99].Close), true);
// Update with modified 100th point (isNew=false)
var val2 = vidya.Update(new TValue(bars[99].Time, bars[99].Close + 1.0), false);
// Create new instance and feed up to modified
var vidya2 = new Vidya(10);
for (int i = 0; i < 99; i++)
{
vidya2.Update(new TValue(bars[i].Time, bars[i].Close));
}
var val3 = vidya2.Update(new TValue(bars[99].Time, bars[99].Close + 1.0), true);
Assert.Equal(val3.Value, val2.Value, 1e-9);
}
[Fact]
public void Reset_Works()
{
var vidya = new Vidya(10);
var gbm = new GBM();
var bars = gbm.Fetch(100, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
for (int i = 0; i < bars.Count; i++)
{
vidya.Update(new TValue(bars[i].Time, bars[i].Close));
}
vidya.Reset();
for (int i = 0; i < inputs.Length; i++)
Assert.Equal(0, vidya.Last.Value);
// Feed again
for (int i = 0; i < bars.Count; i++)
{
// Update with a temporary value first
vidya.Update(new TValue(DateTime.UtcNow, inputs[i] + 1), true);
// Correct it
var corrected = vidya.Update(new TValue(DateTime.UtcNow, inputs[i]), false);
Assert.Equal(expected[i], corrected.Value, 1e-9);
vidya.Update(new TValue(bars[i].Time, bars[i].Close));
}
Assert.True(double.IsFinite(vidya.Last.Value));
}
[Fact]
public void TSeries_Update_Matches_Streaming()
{
var vidya = new Vidya(10);
var gbm = new GBM();
var bars = gbm.Fetch(200, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
var series = bars.Close;
var streamingResults = new List<double>();
for (int i = 0; i < series.Count; i++)
{
streamingResults.Add(vidya.Update(series[i]).Value);
}
var vidya2 = new Vidya(10);
var seriesResults = vidya2.Update(series);
Assert.Equal(streamingResults.Count, seriesResults.Count);
for (int i = 0; i < seriesResults.Count; i++)
{
Assert.Equal(streamingResults[i], seriesResults.Values[i], 1e-9);
}
}
[Fact]
public void StaticCalculate_Matches_Streaming()
{
var gbm = new GBM();
var bars = gbm.Fetch(200, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
var series = bars.Close;
var vidya = new Vidya(10);
var streamingResults = new List<double>();
for (int i = 0; i < series.Count; i++)
{
streamingResults.Add(vidya.Update(series[i]).Value);
}
var staticResults = Vidya.Calculate(series, 10);
Assert.Equal(streamingResults.Count, staticResults.Count);
for (int i = 0; i < staticResults.Count; i++)
{
Assert.Equal(streamingResults[i], staticResults.Values[i], 1e-9);
}
}
[Fact]
public void StaticVsInstance()
public void StaticCalculateSpan_Matches_Streaming()
{
var vidya = new Vidya(5);
var inputs = new double[] { 100, 105, 102, 108, 110, 105, 100, 95, 98, 102 };
var tSeries = new TSeries();
tSeries.Add(inputs);
var gbm = new GBM();
var bars = gbm.Fetch(200, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
var series = bars.Close;
var instanceResult = vidya.Update(tSeries);
var staticResult = new double[inputs.Length];
Vidya.Calculate(inputs, staticResult, 5);
for (int i = 0; i < inputs.Length; i++)
var vidya = new Vidya(10);
var streamingResults = new List<double>();
for (int i = 0; i < series.Count; i++)
{
Assert.Equal(instanceResult.Values[i], staticResult[i], 1e-9);
streamingResults.Add(vidya.Update(series[i]).Value);
}
var spanResults = new double[series.Count];
Vidya.Calculate(series.Values, spanResults, 10);
for (int i = 0; i < spanResults.Length; i++)
{
Assert.Equal(streamingResults[i], spanResults[i], 1e-9);
}
}
[Fact]
public void EdgeCases()
public void Chainability_Works()
{
var vidya = new Vidya(5);
var vidya = new Vidya(10);
var gbm = new GBM();
var bars = gbm.Fetch(10, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
var series = bars.Close;
// Empty
Assert.Empty(vidya.Update(new TSeries()));
// Test TSeries chain
var result = vidya.Update(series);
Assert.NotNull(result);
Assert.IsType<TSeries>(result);
// NaN handling
vidya.Reset();
vidya.Update(new TValue(DateTime.UtcNow, 100));
var v2 = vidya.Update(new TValue(DateTime.UtcNow, double.NaN));
Assert.Equal(100, v2.Value); // Should hold previous value
// Period 1
var vidya1 = new Vidya(1);
var v = vidya1.Update(new TValue(DateTime.UtcNow, 100));
Assert.Equal(100, v.Value);
// Test TValue chain
var result2 = vidya.Update(series[0]);
Assert.IsType<TValue>(result2);
}
[Fact]
public void Constructor_InvalidParameters_ThrowsArgumentException()
{
Assert.Throws<ArgumentException>(() => new Vidya(0));
Assert.Throws<ArgumentException>(() => new Vidya(-1));
}
}
+7
View File
@@ -166,6 +166,13 @@ public sealed class Vidya : ITValuePublisher
return new TSeries(t, v);
}
public static TSeries Calculate(TSeries source, int period)
{
var vidya = new Vidya(period);
return vidya.Update(source);
}
/// <summary>
/// Calculates VIDYA for the entire series.
/// </summary>
+164 -563
View File
@@ -1,572 +1,173 @@
namespace QuanTAlib.Tests;
using System;
using System.Collections.Generic;
using Xunit;
namespace QuanTAlib;
#pragma warning disable S2245 // Random is acceptable for simulation/testing purposes
public class WmaTests
{
[Fact]
public void Wma_Constructor_ValidatesInput()
public void BasicCalculation_DoesNotCrash()
{
var wma = new Wma(10);
var gbm = new GBM();
var bars = gbm.Fetch(1000, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
for (int i = 0; i < bars.Count; i++)
{
wma.Update(new TValue(bars[i].Time, bars[i].Close));
}
Assert.True(double.IsFinite(wma.Last.Value));
}
[Fact]
public void IsNew_Consistency()
{
var wma = new Wma(10);
var gbm = new GBM();
var bars = gbm.Fetch(100, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
// Feed first 99
for (int i = 0; i < 99; i++)
{
wma.Update(new TValue(bars[i].Time, bars[i].Close));
}
// Update with 100th point (isNew=true)
wma.Update(new TValue(bars[99].Time, bars[99].Close), true);
// Update with modified 100th point (isNew=false)
var val2 = wma.Update(new TValue(bars[99].Time, bars[99].Close + 1.0), false);
// Create new instance and feed up to modified
var wma2 = new Wma(10);
for (int i = 0; i < 99; i++)
{
wma2.Update(new TValue(bars[i].Time, bars[i].Close));
}
var val3 = wma2.Update(new TValue(bars[99].Time, bars[99].Close + 1.0), true);
Assert.Equal(val3.Value, val2.Value, 1e-9);
}
[Fact]
public void Reset_Works()
{
var wma = new Wma(10);
var gbm = new GBM();
var bars = gbm.Fetch(100, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
for (int i = 0; i < bars.Count; i++)
{
wma.Update(new TValue(bars[i].Time, bars[i].Close));
}
wma.Reset();
Assert.Equal(0, wma.Last.Value);
Assert.False(wma.IsHot);
// Feed again
for (int i = 0; i < bars.Count; i++)
{
wma.Update(new TValue(bars[i].Time, bars[i].Close));
}
Assert.True(double.IsFinite(wma.Last.Value));
}
[Fact]
public void TSeries_Update_Matches_Streaming()
{
var wma = new Wma(10);
var gbm = new GBM();
var bars = gbm.Fetch(200, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
var series = bars.Close;
var streamingResults = new List<double>();
for (int i = 0; i < series.Count; i++)
{
streamingResults.Add(wma.Update(series[i]).Value);
}
var wma2 = new Wma(10);
var seriesResults = wma2.Update(series);
Assert.Equal(streamingResults.Count, seriesResults.Count);
for (int i = 0; i < seriesResults.Count; i++)
{
Assert.Equal(streamingResults[i], seriesResults.Values[i], 1e-9);
}
}
[Fact]
public void StaticCalculate_Matches_Streaming()
{
var gbm = new GBM();
var bars = gbm.Fetch(200, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
var series = bars.Close;
var wma = new Wma(10);
var streamingResults = new List<double>();
for (int i = 0; i < series.Count; i++)
{
streamingResults.Add(wma.Update(series[i]).Value);
}
var staticResults = Wma.Calculate(series, 10);
Assert.Equal(streamingResults.Count, staticResults.Count);
for (int i = 0; i < staticResults.Count; i++)
{
Assert.Equal(streamingResults[i], staticResults.Values[i], 1e-9);
}
}
[Fact]
public void StaticCalculateSpan_Matches_Streaming()
{
var gbm = new GBM();
var bars = gbm.Fetch(200, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
var series = bars.Close;
var wma = new Wma(10);
var streamingResults = new List<double>();
for (int i = 0; i < series.Count; i++)
{
streamingResults.Add(wma.Update(series[i]).Value);
}
var spanResults = new double[series.Count];
Wma.Calculate(series.Values, spanResults, 10);
for (int i = 0; i < spanResults.Length; i++)
{
Assert.Equal(streamingResults[i], spanResults[i], 1e-9);
}
}
[Fact]
public void Chainability_Works()
{
var wma = new Wma(10);
var gbm = new GBM();
var bars = gbm.Fetch(10, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
var series = bars.Close;
// Test TSeries chain
var result = wma.Update(series);
Assert.NotNull(result);
Assert.IsType<TSeries>(result);
// Test TValue chain
var result2 = wma.Update(series[0]);
Assert.IsType<TValue>(result2);
}
[Fact]
public void Constructor_InvalidParameters_ThrowsArgumentException()
{
Assert.Throws<ArgumentException>(() => new Wma(0));
Assert.Throws<ArgumentException>(() => new Wma(-1));
var wma = new Wma(10);
Assert.NotNull(wma);
}
[Fact]
public void Wma_Calc_ReturnsValue()
{
var wma = new Wma(10);
Assert.Equal(0, wma.Last.Value);
TValue result = wma.Update(new TValue(DateTime.UtcNow, 100));
Assert.True(result.Value > 0);
Assert.Equal(result.Value, wma.Last.Value);
}
[Fact]
public void Wma_FirstValue_ReturnsItself()
{
var wma = new Wma(10);
TValue result = wma.Update(new TValue(DateTime.UtcNow, 100));
Assert.Equal(100.0, result.Value, 1e-10);
}
[Fact]
public void Wma_Calc_IsNew_AcceptsParameter()
{
var wma = new Wma(10);
wma.Update(new TValue(DateTime.UtcNow, 100), isNew: true);
double value1 = wma.Last.Value;
wma.Update(new TValue(DateTime.UtcNow, 200), isNew: true);
double value2 = wma.Last.Value;
// Values should change with new bars
Assert.NotEqual(value1, value2);
}
[Fact]
public void Wma_Calc_IsNew_False_UpdatesValue()
{
var wma = new Wma(10);
wma.Update(new TValue(DateTime.UtcNow, 100));
wma.Update(new TValue(DateTime.UtcNow, 110), isNew: true);
double beforeUpdate = wma.Last.Value;
wma.Update(new TValue(DateTime.UtcNow, 120), isNew: false);
double afterUpdate = wma.Last.Value;
// Update should change the value
Assert.NotEqual(beforeUpdate, afterUpdate);
}
[Fact]
public void Wma_Reset_ClearsState()
{
var wma = new Wma(10);
wma.Update(new TValue(DateTime.UtcNow, 100));
wma.Update(new TValue(DateTime.UtcNow, 105));
double valueBefore = wma.Last.Value;
wma.Reset();
Assert.Equal(0, wma.Last.Value);
// After reset, should accept new values
wma.Update(new TValue(DateTime.UtcNow, 50));
Assert.NotEqual(0, wma.Last.Value);
Assert.NotEqual(valueBefore, wma.Last.Value);
}
[Fact]
public void Wma_Properties_Accessible()
{
var wma = new Wma(10);
Assert.Equal(0, wma.Last.Value);
Assert.False(wma.IsHot);
wma.Update(new TValue(DateTime.UtcNow, 100));
Assert.NotEqual(0, wma.Last.Value);
}
[Fact]
public void Wma_IsHot_BecomesTrueWhenBufferFull()
{
var wma = new Wma(5);
Assert.False(wma.IsHot);
for (int i = 1; i <= 4; i++)
{
wma.Update(new TValue(DateTime.UtcNow, i * 10));
Assert.False(wma.IsHot);
}
wma.Update(new TValue(DateTime.UtcNow, 50));
Assert.True(wma.IsHot);
}
[Fact]
public void Wma_CalculatesCorrectWeightedAverage()
{
var wma = new Wma(5);
wma.Update(new TValue(DateTime.UtcNow, 10));
wma.Update(new TValue(DateTime.UtcNow, 20));
wma.Update(new TValue(DateTime.UtcNow, 30));
wma.Update(new TValue(DateTime.UtcNow, 40));
wma.Update(new TValue(DateTime.UtcNow, 50));
// WMA(5) of 10,20,30,40,50 = (1*10 + 2*20 + 3*30 + 4*40 + 5*50) / 15
// = (10 + 40 + 90 + 160 + 250) / 15 = 550 / 15 = 36.666...
Assert.Equal(550.0 / 15.0, wma.Last.Value, 1e-10);
}
[Fact]
public void Wma_SlidingWindow_Works()
{
var wma = new Wma(3);
wma.Update(new TValue(DateTime.UtcNow, 10));
wma.Update(new TValue(DateTime.UtcNow, 20));
wma.Update(new TValue(DateTime.UtcNow, 30));
// WMA(3) of 10,20,30 = (1*10 + 2*20 + 3*30) / 6 = (10 + 40 + 90) / 6 = 140/6 = 23.333...
Assert.Equal(140.0 / 6.0, wma.Last.Value, 1e-10);
wma.Update(new TValue(DateTime.UtcNow, 40));
// WMA(3) of 20,30,40 = (1*20 + 2*30 + 3*40) / 6 = (20 + 60 + 120) / 6 = 200/6 = 33.333...
Assert.Equal(200.0 / 6.0, wma.Last.Value, 1e-10);
wma.Update(new TValue(DateTime.UtcNow, 50));
// WMA(3) of 30,40,50 = (1*30 + 2*40 + 3*50) / 6 = (30 + 80 + 150) / 6 = 260/6 = 43.333...
Assert.Equal(260.0 / 6.0, wma.Last.Value, 1e-10);
}
[Fact]
public void Wma_IterativeCorrections_RestoreToOriginalState()
{
var wma = new Wma(5);
var gbm = new GBM(startPrice: 100.0, mu: 0.02, sigma: 0.1);
// Feed 10 new values
TValue tenthInput = default;
for (int i = 0; i < 10; i++)
{
var bar = gbm.Next(isNew: true);
tenthInput = new TValue(bar.Time, bar.Close);
wma.Update(tenthInput, isNew: true);
}
// Remember WMA state after 10 values
double wmaAfterTen = wma.Last.Value;
// Generate 9 corrections with isNew=false (different values)
for (int i = 0; i < 9; i++)
{
var bar = gbm.Next(isNew: false);
wma.Update(new TValue(bar.Time, bar.Close), isNew: false);
}
// Feed the remembered 10th input again with isNew=false
TValue finalWma = wma.Update(tenthInput, isNew: false);
// WMA should match the original state after 10 values
Assert.Equal(wmaAfterTen, finalWma.Value, 1e-10);
}
[Fact]
public void Wma_BatchCalc_MatchesIterativeCalc()
{
var wmaIterative = new Wma(10);
var wmaBatch = new Wma(10);
var gbm = new GBM(startPrice: 100.0, mu: 0.02, sigma: 0.1);
// Generate data
var series = new TSeries();
for (int i = 0; i < 100; i++)
{
var bar = gbm.Next(isNew: true);
series.Add(bar.Time, bar.Close);
}
Assert.True(series.Count > 0);
// Calculate iteratively
var iterativeResults = new TSeries();
foreach (var item in series)
{
iterativeResults.Add(wmaIterative.Update(item));
}
// Calculate batch
var batchResults = wmaBatch.Update(series);
// Compare
Assert.Equal(iterativeResults.Count, batchResults.Count);
for (int i = 0; i < iterativeResults.Count; i++)
{
Assert.Equal(iterativeResults[i].Value, batchResults[i].Value, 1e-10);
Assert.Equal(iterativeResults[i].Time, batchResults[i].Time);
}
}
[Fact]
public void Wma_Result_ImplicitConversionToDouble()
{
var wma = new Wma(10);
wma.Update(new TValue(DateTime.UtcNow, 100));
// This should compile and work because TValue has implicit conversion to double
double result = wma.Last.Value;
Assert.Equal(100.0, result, 1e-10);
}
[Fact]
public void Wma_NaN_Input_UsesLastValidValue()
{
var wma = new Wma(5);
// Feed some valid values
wma.Update(new TValue(DateTime.UtcNow, 100));
wma.Update(new TValue(DateTime.UtcNow, 110));
// Feed NaN - should use last valid value (110)
var resultAfterNaN = wma.Update(new TValue(DateTime.UtcNow, double.NaN));
// Result should be finite (not NaN)
Assert.True(double.IsFinite(resultAfterNaN.Value));
Assert.NotEqual(0, resultAfterNaN.Value);
}
[Fact]
public void Wma_Infinity_Input_UsesLastValidValue()
{
var wma = new Wma(5);
// Feed some valid values
wma.Update(new TValue(DateTime.UtcNow, 100));
wma.Update(new TValue(DateTime.UtcNow, 110));
// Feed positive infinity - should use last valid value
var resultAfterPosInf = wma.Update(new TValue(DateTime.UtcNow, double.PositiveInfinity));
Assert.True(double.IsFinite(resultAfterPosInf.Value));
// Feed negative infinity - should use last valid value
var resultAfterNegInf = wma.Update(new TValue(DateTime.UtcNow, double.NegativeInfinity));
Assert.True(double.IsFinite(resultAfterNegInf.Value));
}
[Fact]
public void Wma_MultipleNaN_ContinuesWithLastValid()
{
var wma = new Wma(5);
// Feed valid values
wma.Update(new TValue(DateTime.UtcNow, 100));
wma.Update(new TValue(DateTime.UtcNow, 110));
wma.Update(new TValue(DateTime.UtcNow, 120));
// Feed multiple NaN values
var r1 = wma.Update(new TValue(DateTime.UtcNow, double.NaN));
var r2 = wma.Update(new TValue(DateTime.UtcNow, double.NaN));
var r3 = wma.Update(new TValue(DateTime.UtcNow, double.NaN));
// All results should be finite
Assert.True(double.IsFinite(r1.Value));
Assert.True(double.IsFinite(r2.Value));
Assert.True(double.IsFinite(r3.Value));
}
[Fact]
public void Wma_BatchCalc_HandlesNaN()
{
var wma = new Wma(5);
// Create series with NaN values interspersed
var series = new TSeries();
series.Add(DateTime.UtcNow.Ticks, 100);
series.Add(DateTime.UtcNow.Ticks + 1, 110);
series.Add(DateTime.UtcNow.Ticks + 2, double.NaN);
series.Add(DateTime.UtcNow.Ticks + 3, 120);
series.Add(DateTime.UtcNow.Ticks + 4, double.PositiveInfinity);
series.Add(DateTime.UtcNow.Ticks + 5, 130);
var results = wma.Update(series);
// All results should be finite
foreach (var result in results)
{
Assert.True(double.IsFinite(result.Value), $"Expected finite value but got {result.Value}");
}
}
[Fact]
public void Wma_Reset_ClearsLastValidValue()
{
var wma = new Wma(5);
// Feed values including NaN
wma.Update(new TValue(DateTime.UtcNow, 100));
wma.Update(new TValue(DateTime.UtcNow, double.NaN));
// Reset
wma.Reset();
// After reset, first valid value should establish new baseline
var result = wma.Update(new TValue(DateTime.UtcNow, 50));
Assert.Equal(50.0, result.Value, 1e-10);
}
[Fact]
public void Wma_StaticCalculate_Works()
{
var series = new TSeries();
series.Add(DateTime.UtcNow.Ticks, 10);
series.Add(DateTime.UtcNow.Ticks + 1, 20);
series.Add(DateTime.UtcNow.Ticks + 2, 30);
series.Add(DateTime.UtcNow.Ticks + 3, 40);
series.Add(DateTime.UtcNow.Ticks + 4, 50);
var results = Wma.Calculate(series, 3);
Assert.Equal(5, results.Count);
// WMA(3) for last 3 values [30,40,50]: (1*30 + 2*40 + 3*50) / 6 = 260/6 = 43.333...
Assert.Equal(260.0 / 6.0, results.Last.Value, 1e-10);
}
[Fact]
public void Wma_Period1_ReturnsInputValues()
{
var wma = new Wma(1);
Assert.Equal(100.0, wma.Update(new TValue(DateTime.UtcNow, 100)).Value, 1e-10);
Assert.Equal(200.0, wma.Update(new TValue(DateTime.UtcNow, 200)).Value, 1e-10);
Assert.Equal(150.0, wma.Update(new TValue(DateTime.UtcNow, 150)).Value, 1e-10);
}
[Fact]
public void Wma_MoreWeightOnRecentValues()
{
var wma = new Wma(3);
var sma = new Sma(3);
// Feed same values to both
wma.Update(new TValue(DateTime.UtcNow, 10));
sma.Update(new TValue(DateTime.UtcNow, 10));
wma.Update(new TValue(DateTime.UtcNow, 20));
sma.Update(new TValue(DateTime.UtcNow, 20));
wma.Update(new TValue(DateTime.UtcNow, 100)); // High recent value
sma.Update(new TValue(DateTime.UtcNow, 100));
// WMA should be higher than SMA because it weights the high recent value more
// SMA = (10 + 20 + 100) / 3 = 43.333...
// WMA = (1*10 + 2*20 + 3*100) / 6 = (10 + 40 + 300) / 6 = 58.333...
Assert.True(wma.Last.Value > sma.Last.Value);
Assert.Equal(350.0 / 6.0, wma.Last.Value, 1e-10);
Assert.Equal(130.0 / 3.0, sma.Last.Value, 1e-10);
}
[Fact]
public void Wma_WarmupDivisor_CalculatedCorrectly()
{
var wma = new Wma(5);
// First value: divisor = 1*(1+1)/2 = 1
var r1 = wma.Update(new TValue(DateTime.UtcNow, 100));
Assert.Equal(100.0, r1.Value, 1e-10);
// Second value: divisor = 2*(2+1)/2 = 3, wsum = 1*100 + 2*200 = 500
var r2 = wma.Update(new TValue(DateTime.UtcNow, 200));
Assert.Equal(500.0 / 3.0, r2.Value, 1e-10);
// Third value: divisor = 3*(3+1)/2 = 6, wsum = 1*100 + 2*200 + 3*300 = 1400
var r3 = wma.Update(new TValue(DateTime.UtcNow, 300));
Assert.Equal(1400.0 / 6.0, r3.Value, 1e-10);
}
// ============== Span API Tests ==============
[Fact]
public void Wma_SpanCalc_ValidatesInput()
{
double[] source = [1, 2, 3, 4, 5];
double[] output = new double[5];
double[] wrongSizeOutput = new double[3];
// Period must be > 0
Assert.Throws<ArgumentException>(() => Wma.Calculate(source.AsSpan(), output.AsSpan(), 0));
Assert.Throws<ArgumentException>(() => Wma.Calculate(source.AsSpan(), output.AsSpan(), -1));
// Output must be same length as source
Assert.Throws<ArgumentException>(() => Wma.Calculate(source.AsSpan(), wrongSizeOutput.AsSpan(), 3));
}
[Fact]
public void Wma_SpanCalc_MatchesTSeriesCalc()
{
var series = new TSeries();
double[] source = new double[100];
double[] output = new double[100];
var gbm = new GBM(startPrice: 100.0, mu: 0.02, sigma: 0.1, seed: 42);
for (int i = 0; i < 100; i++)
{
var bar = gbm.Next(isNew: true);
source[i] = bar.Close;
series.Add(bar.Time, bar.Close);
}
// Calculate with TSeries API
var tseriesResult = Wma.Calculate(series, 10);
// Calculate with Span API
Wma.Calculate(source.AsSpan(), output.AsSpan(), 10);
// Compare results
for (int i = 0; i < 100; i++)
{
Assert.Equal(tseriesResult[i].Value, output[i], 1e-10);
}
}
[Fact]
public void Wma_SpanCalc_CalculatesCorrectly()
{
double[] source = [10, 20, 30, 40, 50];
double[] output = new double[5];
Wma.Calculate(source.AsSpan(), output.AsSpan(), 3);
// WMA(3) warmup:
// i=0: 10 (1*10 / 1)
// i=1: (1*10 + 2*20) / 3 = 50/3 = 16.666...
// i=2: (1*10 + 2*20 + 3*30) / 6 = 140/6 = 23.333...
// i=3: sliding: (1*20 + 2*30 + 3*40) / 6 = 200/6 = 33.333...
// i=4: (1*30 + 2*40 + 3*50) / 6 = 260/6 = 43.333...
Assert.Equal(10.0, output[0], 1e-10);
Assert.Equal(50.0 / 3.0, output[1], 1e-10);
Assert.Equal(140.0 / 6.0, output[2], 1e-10);
Assert.Equal(200.0 / 6.0, output[3], 1e-10);
Assert.Equal(260.0 / 6.0, output[4], 1e-10);
}
[Fact]
public void Wma_SpanCalc_ZeroAllocation()
{
double[] source = new double[10000];
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
Wma.Calculate(source.AsSpan(), output.AsSpan(), 100);
// This test verifies the method runs without throwing
Assert.True(double.IsFinite(output[^1]));
}
[Fact]
public void Wma_SpanCalc_HandlesNaN()
{
double[] source = [100, 110, double.NaN, 120, 130];
double[] output = new double[5];
Wma.Calculate(source.AsSpan(), output.AsSpan(), 3);
// All outputs should be finite
foreach (var val in output)
{
Assert.True(double.IsFinite(val), $"Expected finite value but got {val}");
}
}
[Fact]
public void Wma_SpanCalc_Period1_ReturnsInput()
{
double[] source = [10, 20, 30, 40, 50];
double[] output = new double[5];
Wma.Calculate(source.AsSpan(), output.AsSpan(), 1);
for (int i = 0; i < source.Length; i++)
{
Assert.Equal(source[i], output[i], 1e-10);
}
}
[Fact]
public void Wma_SpanCalc_UsesStackallocForSmallPeriods()
{
double[] source = new double[1000];
double[] output = new double[1000];
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;
// Period <= 512 uses stackalloc
Wma.Calculate(source.AsSpan(), output.AsSpan(), 100);
Assert.True(double.IsFinite(output[^1]));
// Period > 512 uses heap allocation
double[] output2 = new double[1000];
Wma.Calculate(source.AsSpan(), output2.AsSpan(), 600);
Assert.True(double.IsFinite(output2[^1]));
}
[Fact]
public void Wma_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 = Wma.Calculate(series, period);
double expected = batchSeries.Last.Value;
// 2. Span Mode
var tValues = series.Values.ToArray();
var spanInput = new ReadOnlySpan<double>(tValues);
var spanOutput = new double[tValues.Length];
Wma.Calculate(spanInput, spanOutput, period);
double spanResult = spanOutput[^1];
// 3. Streaming Mode
var streamingInd = new Wma(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 Wma(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);
}
}