mirror of
https://github.com/mihakralj/QuanTAlib.git
synced 2026-08-23 04:58:08 +00:00
docs: remove C# Implementation Considerations sections, clean up temp scripts, reorganize test files
- Remove 'C# Implementation Considerations' sections from 34 indicator .md files - Delete 29 temp PowerShell scripts (_fix_mojibake.ps1, _hex_scan.ps1, etc.) - Move test files into tests/ subdirectories for consistent project structure - Add trader-focused bullet points to indicator documentation
This commit is contained in:
@@ -0,0 +1,223 @@
|
||||
using Xunit;
|
||||
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
public class ChangeTests
|
||||
{
|
||||
private readonly GBM _gbm;
|
||||
private readonly TSeries _source;
|
||||
|
||||
public ChangeTests()
|
||||
{
|
||||
_gbm = new GBM(startPrice: 100, mu: 0.05, sigma: 0.2, seed: 60000);
|
||||
var bars = _gbm.Fetch(100, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
_source = bars.Close;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Change_Constructor_ThrowsOnInvalidPeriod()
|
||||
{
|
||||
Assert.Throws<ArgumentException>(() => new Change(0));
|
||||
Assert.Throws<ArgumentException>(() => new Change(-1));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Change_Constructor_ValidPeriod()
|
||||
{
|
||||
var indicator = new Change(5);
|
||||
Assert.Equal("Change(5)", indicator.Name);
|
||||
Assert.Equal(6, indicator.WarmupPeriod);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Change_Update_ReturnsValue()
|
||||
{
|
||||
var indicator = new Change(1);
|
||||
var result = indicator.Update(new TValue(DateTime.UtcNow, 100.0));
|
||||
Assert.Equal(0.0, result.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Change_BasicCalculation()
|
||||
{
|
||||
var indicator = new Change(1);
|
||||
var time = DateTime.UtcNow;
|
||||
|
||||
indicator.Update(new TValue(time, 100.0));
|
||||
indicator.Update(new TValue(time.AddMinutes(1), 110.0));
|
||||
|
||||
// (110 - 100) / 100 = 0.1
|
||||
Assert.Equal(0.1, indicator.Last.Value, 1e-10);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Change_NegativeChange()
|
||||
{
|
||||
var indicator = new Change(1);
|
||||
var time = DateTime.UtcNow;
|
||||
|
||||
indicator.Update(new TValue(time, 100.0));
|
||||
indicator.Update(new TValue(time.AddMinutes(1), 90.0));
|
||||
|
||||
// (90 - 100) / 100 = -0.1
|
||||
Assert.Equal(-0.1, indicator.Last.Value, 1e-10);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Change_Period2()
|
||||
{
|
||||
var indicator = new Change(2);
|
||||
var time = DateTime.UtcNow;
|
||||
|
||||
indicator.Update(new TValue(time, 100.0));
|
||||
indicator.Update(new TValue(time.AddMinutes(1), 105.0));
|
||||
indicator.Update(new TValue(time.AddMinutes(2), 120.0));
|
||||
|
||||
// (120 - 100) / 100 = 0.2
|
||||
Assert.Equal(0.2, indicator.Last.Value, 1e-10);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Change_IsHot_WhenWarmedUp()
|
||||
{
|
||||
var indicator = new Change(3);
|
||||
var time = DateTime.UtcNow;
|
||||
|
||||
for (int i = 0; i < 3; i++)
|
||||
{
|
||||
Assert.False(indicator.IsHot);
|
||||
indicator.Update(new TValue(time.AddMinutes(i), 100.0 + i));
|
||||
}
|
||||
|
||||
indicator.Update(new TValue(time.AddMinutes(3), 110.0));
|
||||
Assert.True(indicator.IsHot);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Change_Reset_ClearsState()
|
||||
{
|
||||
var indicator = new Change(1);
|
||||
var time = DateTime.UtcNow;
|
||||
|
||||
indicator.Update(new TValue(time, 100.0));
|
||||
indicator.Update(new TValue(time.AddMinutes(1), 110.0));
|
||||
Assert.True(indicator.IsHot);
|
||||
|
||||
indicator.Reset();
|
||||
Assert.False(indicator.IsHot);
|
||||
Assert.Equal(default, indicator.Last);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Change_IsNew_False_RollsBack()
|
||||
{
|
||||
var indicator = new Change(1);
|
||||
var time = DateTime.UtcNow;
|
||||
|
||||
indicator.Update(new TValue(time, 100.0), true);
|
||||
indicator.Update(new TValue(time.AddMinutes(1), 110.0), true);
|
||||
|
||||
// Update with isNew=false (correction)
|
||||
indicator.Update(new TValue(time.AddMinutes(1), 115.0), false);
|
||||
|
||||
// Should recalculate: (115 - 100) / 100 = 0.15
|
||||
Assert.Equal(0.15, indicator.Last.Value, 1e-10);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Change_NaN_HandledGracefully()
|
||||
{
|
||||
var indicator = new Change(1);
|
||||
var time = DateTime.UtcNow;
|
||||
|
||||
indicator.Update(new TValue(time, 100.0));
|
||||
indicator.Update(new TValue(time.AddMinutes(1), double.NaN));
|
||||
|
||||
// Should use last valid value (100), so (100 - 100) / 100 = 0
|
||||
Assert.True(double.IsFinite(indicator.Last.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Change_ZeroDivision_ReturnsZero()
|
||||
{
|
||||
var indicator = new Change(1);
|
||||
var time = DateTime.UtcNow;
|
||||
|
||||
indicator.Update(new TValue(time, 0.0));
|
||||
indicator.Update(new TValue(time.AddMinutes(1), 100.0));
|
||||
|
||||
// Division by zero returns 0
|
||||
Assert.Equal(0.0, indicator.Last.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Change_Batch_MatchesStreaming()
|
||||
{
|
||||
int period = 5;
|
||||
var batchResult = Change.Batch(_source, period);
|
||||
var indicator = new Change(period);
|
||||
|
||||
for (int i = 0; i < _source.Count; i++)
|
||||
{
|
||||
indicator.Update(_source[i]);
|
||||
}
|
||||
|
||||
// Compare last 10 values between batch and streaming
|
||||
var streamResult = new TSeries();
|
||||
for (int j = 0; j < _source.Count; j++)
|
||||
{
|
||||
streamResult.Add(indicator.Update(_source[j]), true);
|
||||
}
|
||||
|
||||
for (int i = Math.Max(0, _source.Count - 10); i < _source.Count; i++)
|
||||
{
|
||||
Assert.Equal(batchResult[i].Value, streamResult[i].Value, 1e-10);
|
||||
}
|
||||
|
||||
// Ensure final values match
|
||||
Assert.Equal(batchResult[^1].Value, indicator.Last.Value, 1e-10);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Change_Span_MatchesBatch()
|
||||
{
|
||||
int period = 5;
|
||||
var values = _source.Values.ToArray();
|
||||
var output = new double[values.Length];
|
||||
|
||||
Change.Batch(values, output, period);
|
||||
var batchResult = Change.Batch(_source, period);
|
||||
|
||||
for (int i = 0; i < values.Length; i++)
|
||||
{
|
||||
Assert.Equal(batchResult[i].Value, output[i], 1e-10);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Change_Span_ThrowsOnInvalidArgs()
|
||||
{
|
||||
var source = new double[10];
|
||||
var output = new double[5];
|
||||
|
||||
Assert.Throws<ArgumentException>(() => Change.Batch(ReadOnlySpan<double>.Empty, output, 1));
|
||||
Assert.Throws<ArgumentException>(() => Change.Batch(source, output, 1));
|
||||
Assert.Throws<ArgumentException>(() => Change.Batch(source, new double[10], 0));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Change_EventChaining_Works()
|
||||
{
|
||||
var source = new Sma(5);
|
||||
var change = new Change(source, 1);
|
||||
|
||||
var time = DateTime.UtcNow;
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
source.Update(new TValue(time.AddMinutes(i), 100.0 + i));
|
||||
}
|
||||
|
||||
Assert.True(change.IsHot);
|
||||
Assert.NotEqual(0.0, change.Last.Value);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,290 @@
|
||||
using Xunit;
|
||||
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// CHANGE validation tests - validates against direct mathematical computation
|
||||
/// and Tulip's ROC indicator (both return decimal format: 0.1 = 10%)
|
||||
/// </summary>
|
||||
public class ChangeValidationTests
|
||||
{
|
||||
private readonly GBM _gbm = new(sigma: 0.5, mu: 0.05, seed: 60100);
|
||||
private const double Tolerance = 1e-10;
|
||||
|
||||
[Fact]
|
||||
public void Change_Batch_MatchesMathFormula()
|
||||
{
|
||||
var bars = _gbm.Fetch(100, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
var series = bars.Close;
|
||||
int period = 10;
|
||||
|
||||
var result = Change.Batch(series, period);
|
||||
|
||||
for (int i = period; i < series.Count; i++)
|
||||
{
|
||||
double current = series[i].Value;
|
||||
double past = series[i - period].Value;
|
||||
double expected = past != 0.0 ? (current - past) / past : 0.0;
|
||||
Assert.Equal(expected, result[i].Value, Tolerance);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Change_Streaming_MatchesMathFormula()
|
||||
{
|
||||
var bars = _gbm.Fetch(100, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
var series = bars.Close;
|
||||
int period = 5;
|
||||
|
||||
var indicator = new Change(period);
|
||||
var results = new List<double>();
|
||||
ReadOnlySpan<double> values = series.Values;
|
||||
|
||||
for (int i = 0; i < series.Count; i++)
|
||||
{
|
||||
indicator.Update(series[i]);
|
||||
results.Add(indicator.Last.Value);
|
||||
}
|
||||
|
||||
for (int i = period; i < series.Count; i++)
|
||||
{
|
||||
double current = values[i];
|
||||
double past = values[i - period];
|
||||
double expected = past != 0.0 ? (current - past) / past : 0.0;
|
||||
Assert.Equal(expected, results[i], Tolerance);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Change_Span_MatchesMathFormula()
|
||||
{
|
||||
var bars = _gbm.Fetch(100, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
var values = bars.Close.Values.ToArray();
|
||||
var output = new double[values.Length];
|
||||
int period = 10;
|
||||
|
||||
Change.Batch(values, output, period);
|
||||
|
||||
for (int i = period; i < values.Length; i++)
|
||||
{
|
||||
double current = values[i];
|
||||
double past = values[i - period];
|
||||
double expected = past != 0.0 ? (current - past) / past : 0.0;
|
||||
Assert.Equal(expected, output[i], Tolerance);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Change_Validate_Tulip_Batch()
|
||||
{
|
||||
var bars = _gbm.Fetch(100, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
var source = bars.Close;
|
||||
double[] tData = source.Values.ToArray();
|
||||
int period = 10;
|
||||
|
||||
// Calculate QuanTAlib Change
|
||||
var qResult = Change.Batch(source, period);
|
||||
|
||||
// Calculate Tulip ROC (returns percentage)
|
||||
var rocIndicator = Tulip.Indicators.roc;
|
||||
double[][] inputs = [tData];
|
||||
double[] options = [period];
|
||||
int lookback = period;
|
||||
double[][] outputs = [new double[tData.Length - lookback]];
|
||||
|
||||
rocIndicator.Run(inputs, options, outputs);
|
||||
var tResult = outputs[0];
|
||||
|
||||
// Compare (Tulip ROC returns same format as QuanTAlib CHANGE)
|
||||
for (int i = 0; i < tResult.Length; i++)
|
||||
{
|
||||
int qIdx = i + lookback;
|
||||
Assert.Equal(tResult[i], qResult[qIdx].Value, Tolerance);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Change_Validate_Tulip_Streaming()
|
||||
{
|
||||
var bars = _gbm.Fetch(100, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
var source = bars.Close;
|
||||
double[] tData = source.Values.ToArray();
|
||||
int period = 10;
|
||||
|
||||
// Calculate QuanTAlib Change (streaming)
|
||||
var indicator = new Change(period);
|
||||
var qResults = new List<double>();
|
||||
foreach (var item in source)
|
||||
{
|
||||
qResults.Add(indicator.Update(item).Value);
|
||||
}
|
||||
|
||||
// Calculate Tulip ROC
|
||||
var rocIndicator = Tulip.Indicators.roc;
|
||||
double[][] inputs = [tData];
|
||||
double[] options = [period];
|
||||
int lookback = period;
|
||||
double[][] outputs = [new double[tData.Length - lookback]];
|
||||
|
||||
rocIndicator.Run(inputs, options, outputs);
|
||||
var tResult = outputs[0];
|
||||
|
||||
// Compare (Tulip ROC returns same format as QuanTAlib CHANGE)
|
||||
for (int i = 0; i < tResult.Length; i++)
|
||||
{
|
||||
int qIdx = i + lookback;
|
||||
Assert.Equal(tResult[i], qResults[qIdx], Tolerance);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Change_ManualCalculation()
|
||||
{
|
||||
var indicator = new Change(1);
|
||||
var time = DateTime.UtcNow;
|
||||
|
||||
double[] values = [100.0, 105.0, 102.0, 108.0, 104.0];
|
||||
|
||||
for (int i = 0; i < values.Length; i++)
|
||||
{
|
||||
indicator.Update(new TValue(time.AddMinutes(i), values[i]));
|
||||
|
||||
if (i == 0)
|
||||
{
|
||||
Assert.Equal(0.0, indicator.Last.Value);
|
||||
}
|
||||
else
|
||||
{
|
||||
double expectedChange = (values[i] - values[i - 1]) / values[i - 1];
|
||||
Assert.Equal(expectedChange, indicator.Last.Value, Tolerance);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Change_AllModesConsistent()
|
||||
{
|
||||
int count = 50;
|
||||
int period = 5;
|
||||
var gbm = new GBM(startPrice: 100, mu: 0.05, sigma: 0.2, seed: 60103);
|
||||
var bars = gbm.Fetch(count, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
var source = bars.Close;
|
||||
|
||||
// Batch
|
||||
var batchResult = Change.Batch(source, period);
|
||||
|
||||
// Streaming
|
||||
var streamingIndicator = new Change(period);
|
||||
var streamingResults = new double[count];
|
||||
for (int i = 0; i < source.Count; i++)
|
||||
{
|
||||
streamingIndicator.Update(source[i]);
|
||||
streamingResults[i] = streamingIndicator.Last.Value;
|
||||
}
|
||||
|
||||
// Span
|
||||
var values = source.Values.ToArray();
|
||||
var spanOutput = new double[count];
|
||||
Change.Batch(values, spanOutput, period);
|
||||
|
||||
// Event-driven
|
||||
var eventIndicator = new Change(period);
|
||||
var eventResults = new double[count];
|
||||
int eventIdx = 0;
|
||||
eventIndicator.Pub += (object? _, in TValueEventArgs e) => eventResults[eventIdx++] = e.Value.Value;
|
||||
for (int i = 0; i < source.Count; i++)
|
||||
{
|
||||
eventIndicator.Update(source[i]);
|
||||
}
|
||||
|
||||
// Compare all modes
|
||||
for (int i = period; i < count; i++)
|
||||
{
|
||||
Assert.Equal(batchResult[i].Value, streamingResults[i], Tolerance);
|
||||
Assert.Equal(batchResult[i].Value, spanOutput[i], Tolerance);
|
||||
Assert.Equal(batchResult[i].Value, eventResults[i], Tolerance);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Change_DifferentPeriods_MatchTulip()
|
||||
{
|
||||
var bars = _gbm.Fetch(100, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
var source = bars.Close;
|
||||
var values = source.Values.ToArray();
|
||||
|
||||
foreach (int period in new[] { 1, 5, 10, 20 })
|
||||
{
|
||||
var result = Change.Batch(source, period);
|
||||
|
||||
// Calculate Tulip ROC
|
||||
var rocIndicator = Tulip.Indicators.roc;
|
||||
double[][] inputs = [values];
|
||||
double[] options = [period];
|
||||
int lookback = period;
|
||||
double[][] outputs = [new double[values.Length - lookback]];
|
||||
|
||||
rocIndicator.Run(inputs, options, outputs);
|
||||
var tResult = outputs[0];
|
||||
|
||||
// Compare
|
||||
for (int i = 0; i < tResult.Length; i++)
|
||||
{
|
||||
int qIdx = i + lookback;
|
||||
Assert.Equal(tResult[i], result[qIdx].Value, Tolerance);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Change_KnownValues()
|
||||
{
|
||||
// Test with simple known sequence
|
||||
double[] data = [100, 110, 99, 120, 100];
|
||||
int period = 1;
|
||||
|
||||
// Expected: 0, 0.1, -0.1, 0.21212..., -0.16666...
|
||||
double[] expected =
|
||||
[
|
||||
0.0,
|
||||
0.1, // (110-100)/100
|
||||
-0.1, // (99-110)/110
|
||||
120.0 / 99.0 - 1.0, // (120-99)/99
|
||||
100.0 / 120.0 - 1.0 // (100-120)/120
|
||||
];
|
||||
|
||||
var indicator = new Change(period);
|
||||
for (int i = 0; i < data.Length; i++)
|
||||
{
|
||||
var result = indicator.Update(new TValue(DateTime.UtcNow, data[i]));
|
||||
Assert.Equal(expected[i], result.Value, Tolerance);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Change_Period2_KnownValues()
|
||||
{
|
||||
double[] data = [100, 105, 120, 110, 130];
|
||||
int period = 2;
|
||||
|
||||
// Expected changes comparing to 2 bars ago:
|
||||
// [0]: 0 (not enough data)
|
||||
// [1]: 0 (not enough data)
|
||||
// [2]: (120-100)/100 = 0.2
|
||||
// [3]: (110-105)/105 = 0.0476...
|
||||
// [4]: (130-120)/120 = 0.0833...
|
||||
|
||||
var indicator = new Change(period);
|
||||
var results = new double[data.Length];
|
||||
for (int i = 0; i < data.Length; i++)
|
||||
{
|
||||
results[i] = indicator.Update(new TValue(DateTime.UtcNow, data[i])).Value;
|
||||
}
|
||||
|
||||
Assert.Equal(0.0, results[0], Tolerance);
|
||||
Assert.Equal(0.0, results[1], Tolerance);
|
||||
Assert.Equal(0.2, results[2], Tolerance);
|
||||
Assert.Equal((110.0 - 105.0) / 105.0, results[3], Tolerance);
|
||||
Assert.Equal((130.0 - 120.0) / 120.0, results[4], Tolerance);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user