Files
QuanTAlib/lib/volume/twap/tests/Twap.Validation.Tests.cs
Miha Kralj 060649192f 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
2026-03-12 12:34:16 -07:00

182 lines
5.5 KiB
C#

namespace QuanTAlib.Tests;
public class TwapValidationTests
{
private readonly ValidationTestData _data;
public TwapValidationTests()
{
_data = new ValidationTestData();
}
// Note: TWAP (Time Weighted Average Price) is not available in TA-Lib, Skender, Tulip, or Ooples.
// Validation tests focus on internal consistency between streaming, batch, and span modes.
[Fact]
public void Twap_Streaming_Matches_Batch()
{
const int period = 20;
// Streaming
var twap = new Twap(period);
var streamingValues = new List<double>();
foreach (var bar in _data.Bars)
{
streamingValues.Add(twap.Update(bar).Value);
}
// Batch
var batchResult = Twap.Batch(_data.Bars, period);
var batchValues = batchResult.Values.ToArray();
ValidationHelper.VerifyData(streamingValues.ToArray(), batchValues, 0, 100, 1e-9);
}
[Fact]
public void Twap_Span_Matches_Streaming()
{
const int period = 20;
// Extract typical prices from bars
var typicalPrices = new double[_data.Bars.Count];
for (int i = 0; i < _data.Bars.Count; i++)
{
var bar = _data.Bars[i];
typicalPrices[i] = (bar.High + bar.Low + bar.Close) / 3.0;
}
// Streaming (using TValue with typical price)
var twap = new Twap(period);
var streamingValues = new List<double>();
for (int i = 0; i < typicalPrices.Length; i++)
{
streamingValues.Add(twap.Update(new TValue(DateTime.UtcNow.AddMinutes(i), typicalPrices[i])).Value);
}
// Span
var spanOutput = new double[typicalPrices.Length];
Twap.Batch(typicalPrices, spanOutput, period);
ValidationHelper.VerifyData(streamingValues.ToArray(), spanOutput, 0, 100, 1e-9);
}
[Fact]
public void Twap_Different_Periods_Produce_Different_Results()
{
const int period1 = 10;
const int period2 = 50;
var twap1 = new Twap(period1);
var twap2 = new Twap(period2);
var values1 = new List<double>();
var values2 = new List<double>();
foreach (var bar in _data.Bars)
{
values1.Add(twap1.Update(bar).Value);
values2.Add(twap2.Update(bar).Value);
}
// With different periods, we expect different results at reset boundaries
bool foundDifference = false;
for (int i = 50; i < values1.Count; i++)
{
if (Math.Abs(values1[i] - values2[i]) > 1e-9)
{
foundDifference = true;
break;
}
}
Assert.True(foundDifference, "Different periods should produce different results");
}
[Fact]
public void Twap_ZeroPeriod_Matches_RunningAverage()
{
// With period = 0, TWAP should be a simple running average of all values
var twap = new Twap(period: 0);
double sum = 0;
int count = 0;
foreach (var bar in _data.Bars)
{
double typicalPrice = (bar.High + bar.Low + bar.Close) / 3.0;
sum += typicalPrice;
count++;
var result = twap.Update(bar);
double expectedAverage = sum / count;
Assert.Equal(expectedAverage, result.Value, 9);
}
}
[Fact]
public void Twap_AllModes_Match_With_Different_Periods()
{
int[] periods = { 5, 10, 20, 50, 100 };
foreach (var period in periods)
{
// Extract typical prices
var typicalPrices = new double[_data.Bars.Count];
for (int i = 0; i < _data.Bars.Count; i++)
{
var bar = _data.Bars[i];
typicalPrices[i] = (bar.High + bar.Low + bar.Close) / 3.0;
}
// Streaming
var twap = new Twap(period);
var streamingValues = new List<double>();
for (int i = 0; i < typicalPrices.Length; i++)
{
streamingValues.Add(twap.Update(new TValue(DateTime.UtcNow.AddMinutes(i), typicalPrices[i])).Value);
}
// Batch
var batchResult = Twap.Batch(_data.Bars, period);
var batchValues = batchResult.Values.ToArray();
// Span
var spanOutput = new double[typicalPrices.Length];
Twap.Batch(typicalPrices, spanOutput, period);
// Verify all modes match
ValidationHelper.VerifyData(streamingValues.ToArray(), batchValues, 0, 100, 1e-9);
ValidationHelper.VerifyData(streamingValues.ToArray(), spanOutput, 0, 100, 1e-9);
}
}
[Fact]
public void Twap_Values_Are_Bounded()
{
const int period = 20;
var twap = new Twap(period);
var values = new List<double>();
foreach (var bar in _data.Bars)
{
values.Add(twap.Update(bar).Value);
}
// All values should be finite
Assert.True(values.All(v => double.IsFinite(v)), "All TWAP values should be finite");
// TWAP should be within the price range
double minPrice = _data.Bars.Min(b => b.Low);
double maxPrice = _data.Bars.Max(b => b.High);
// After warmup, TWAP should be bounded by price range
foreach (var v in values.Skip(period))
{
Assert.True(v >= minPrice * 0.9 && v <= maxPrice * 1.1,
$"TWAP {v} should be within reasonable bounds of price range [{minPrice}, {maxPrice}]");
}
}
}