mirror of
https://github.com/mihakralj/QuanTAlib.git
synced 2026-08-05 12:37:43 +00:00
Test completeness
This commit is contained in:
@@ -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.
|
||||
|
||||
|
||||
@@ -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
@@ -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
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
{
|
||||
|
||||
@@ -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
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user