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:
Miha Kralj
2026-03-12 12:34:16 -07:00
parent 8937b0c0fa
commit 060649192f
1149 changed files with 1780 additions and 3316 deletions
@@ -0,0 +1,287 @@
using TradingPlatform.BusinessLayer;
namespace QuanTAlib.Tests;
public class TwapIndicatorTests
{
[Fact]
public void TwapIndicator_Constructor_SetsDefaults()
{
var indicator = new TwapIndicator();
Assert.Equal("TWAP - Time Weighted Average Price", indicator.Name);
Assert.False(indicator.SeparateWindow);
Assert.True(indicator.OnBackGround);
Assert.Equal(1, indicator.MinHistoryDepths);
Assert.Equal(0, indicator.Period);
}
[Fact]
public void TwapIndicator_ShortName_IsConstant()
{
var indicator = new TwapIndicator();
Assert.Equal("TWAP", indicator.ShortName);
}
[Fact]
public void TwapIndicator_MinHistoryDepths_EqualsOne()
{
var indicator = new TwapIndicator();
Assert.Equal(1, indicator.MinHistoryDepths);
Assert.Equal(1, ((IWatchlistIndicator)indicator).MinHistoryDepths);
}
[Fact]
public void TwapIndicator_Period_CanBeSet()
{
var indicator = new TwapIndicator { Period = 100 };
Assert.Equal(100, indicator.Period);
}
[Fact]
public void TwapIndicator_Initialize_CreatesInternalTwap()
{
var indicator = new TwapIndicator();
// Initialize should not throw
indicator.Initialize();
// After init, line series should exist
Assert.Single(indicator.LinesSeries);
}
[Fact]
public void TwapIndicator_ProcessUpdate_HistoricalBar_ComputesValue()
{
var indicator = new TwapIndicator { Period = 0 };
indicator.Initialize();
// Add historical data
var now = DateTime.UtcNow;
for (int i = 0; i < 30; i++)
{
double close = 100 + i * 0.5;
indicator.HistoricalData.AddBar(now.AddMinutes(i), close - 2, close + 2, close - 3, close, 100000);
// Process update for each bar to simulate history loading
var args = new UpdateArgs(UpdateReason.HistoricalBar);
indicator.ProcessUpdate(args);
}
// Line series should have a value
double val = indicator.LinesSeries[0].GetValue(0);
Assert.True(double.IsFinite(val));
}
[Fact]
public void TwapIndicator_ProcessUpdate_NewBar_ComputesValue()
{
var indicator = new TwapIndicator { Period = 0 };
indicator.Initialize();
var now = DateTime.UtcNow;
for (int i = 0; i < 30; i++)
{
indicator.HistoricalData.AddBar(now.AddMinutes(i), 100, 110, 90, 105, 100000);
}
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
// Add new bar
indicator.HistoricalData.AddBar(now.AddMinutes(30), 105, 115, 100, 112, 80000);
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.NewBar));
Assert.Equal(2, indicator.LinesSeries[0].Count);
}
[Fact]
public void TwapIndicator_RunningAverage_CorrectCalculation()
{
var indicator = new TwapIndicator { Period = 0 };
indicator.Initialize();
var now = DateTime.UtcNow;
// Bar 1: O=100, H=105, L=95, C=100 -> HLC3 = (105+95+100)/3 = 100
indicator.HistoricalData.AddBar(now, 100, 105, 95, 100, 10000);
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
double firstVal = indicator.LinesSeries[0].GetValue(0);
Assert.Equal(100, firstVal);
// Bar 2: O=100, H=110, L=90, C=105 -> HLC3 = (110+90+105)/3 ≈ 101.67
// TWAP = (100 + 101.67) / 2 ≈ 100.83
indicator.HistoricalData.AddBar(now.AddMinutes(1), 100, 110, 90, 105, 20000);
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.NewBar));
double secondVal = indicator.LinesSeries[0].GetValue(0);
double expectedHlc3Second = (110.0 + 90.0 + 105.0) / 3.0;
double expectedTwap = (100.0 + expectedHlc3Second) / 2.0;
Assert.Equal(expectedTwap, secondVal, 2);
}
[Fact]
public void TwapIndicator_PeriodReset_ResetsAverage()
{
var indicator = new TwapIndicator { Period = 5 };
indicator.Initialize();
var now = DateTime.UtcNow;
// Add 7 bars - reset should occur after bar 5
for (int i = 0; i < 7; i++)
{
double close = 100 + i;
indicator.HistoricalData.AddBar(now.AddMinutes(i), close - 2, close + 2, close - 3, close, 10000);
var args = i == 0
? new UpdateArgs(UpdateReason.HistoricalBar)
: new UpdateArgs(UpdateReason.NewBar);
indicator.ProcessUpdate(args);
}
// After period reset, values should be different than continuous
double val = indicator.LinesSeries[0].GetValue(0);
Assert.True(double.IsFinite(val));
}
[Fact]
public void TwapIndicator_ZeroPeriod_NeverResets()
{
var indicator = new TwapIndicator { Period = 0 };
indicator.Initialize();
var now = DateTime.UtcNow;
double sum = 0;
// Add 20 bars - should never reset
for (int i = 0; i < 20; i++)
{
double close = 100 + i;
double high = close + 2;
double low = close - 3;
double hlc3 = (high + low + close) / 3.0;
sum += hlc3;
indicator.HistoricalData.AddBar(now.AddMinutes(i), close - 2, high, low, close, 10000);
var args = i == 0
? new UpdateArgs(UpdateReason.HistoricalBar)
: new UpdateArgs(UpdateReason.NewBar);
indicator.ProcessUpdate(args);
}
double val = indicator.LinesSeries[0].GetValue(0);
double expectedTwap = sum / 20.0;
Assert.Equal(expectedTwap, val, 1);
}
[Fact]
public void TwapIndicator_DifferentPeriods_ProduceDifferentResults()
{
var now = DateTime.UtcNow;
// Indicator with no reset
var noReset = new TwapIndicator { Period = 0 };
noReset.Initialize();
// Indicator with period=5
var period5 = new TwapIndicator { Period = 5 };
period5.Initialize();
// Add 10 bars to both
for (int i = 0; i < 10; i++)
{
double close = 100 + i * 2;
noReset.HistoricalData.AddBar(now.AddMinutes(i), close - 2, close + 2, close - 3, close, 10000);
period5.HistoricalData.AddBar(now.AddMinutes(i), close - 2, close + 2, close - 3, close, 10000);
var args = i == 0
? new UpdateArgs(UpdateReason.HistoricalBar)
: new UpdateArgs(UpdateReason.NewBar);
noReset.ProcessUpdate(args);
period5.ProcessUpdate(args);
}
double noResetVal = noReset.LinesSeries[0].GetValue(0);
double period5Val = period5.LinesSeries[0].GetValue(0);
// With reset at period 5, the averages should be different
Assert.NotEqual(noResetVal, period5Val, 1);
}
[Fact]
public void TwapIndicator_UsesTypicalPrice_HLC3()
{
var indicator = new TwapIndicator { Period = 0 };
indicator.Initialize();
var now = DateTime.UtcNow;
// Bar with specific OHLC values
double open = 100;
double high = 120;
double low = 80;
double close = 110;
double expectedHlc3 = (high + low + close) / 3.0; // (120 + 80 + 110) / 3 = 103.33
indicator.HistoricalData.AddBar(now, open, high, low, close, 10000);
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
double val = indicator.LinesSeries[0].GetValue(0);
Assert.Equal(expectedHlc3, val, 2);
}
[Fact]
public void TwapIndicator_ValueWithinPriceRange()
{
var indicator = new TwapIndicator { Period = 0 };
indicator.Initialize();
var now = DateTime.UtcNow;
double minLow = double.MaxValue;
double maxHigh = double.MinValue;
// Add bars with varying prices
for (int i = 0; i < 20; i++)
{
double close = 100 + (i % 3 == 0 ? i : -i * 0.5);
double high = close + 5;
double low = close - 5;
minLow = Math.Min(minLow, low);
maxHigh = Math.Max(maxHigh, high);
indicator.HistoricalData.AddBar(now.AddMinutes(i), close - 2, high, low, close, 10000);
var args = i == 0
? new UpdateArgs(UpdateReason.HistoricalBar)
: new UpdateArgs(UpdateReason.NewBar);
indicator.ProcessUpdate(args);
}
double val = indicator.LinesSeries[0].GetValue(0);
Assert.True(val >= minLow && val <= maxHigh,
$"TWAP {val} should be within price range [{minLow}, {maxHigh}]");
}
[Fact]
public void TwapIndicator_MultipleResets_MaintainsCorrectAverage()
{
var indicator = new TwapIndicator { Period = 3 };
indicator.Initialize();
var now = DateTime.UtcNow;
// Add 10 bars - should reset at bar 4 and 7
for (int i = 0; i < 10; i++)
{
double close = 100 + i;
indicator.HistoricalData.AddBar(now.AddMinutes(i), close - 2, close + 2, close - 3, close, 10000);
var args = i == 0
? new UpdateArgs(UpdateReason.HistoricalBar)
: new UpdateArgs(UpdateReason.NewBar);
indicator.ProcessUpdate(args);
double val = indicator.LinesSeries[0].GetValue(0);
Assert.True(double.IsFinite(val), $"Value at bar {i} should be finite");
}
}
}
+424
View File
@@ -0,0 +1,424 @@
using Xunit;
namespace QuanTAlib.Tests;
public class TwapTests
{
private const int DefaultPeriod = 0;
[Fact]
public void Constructor_DefaultParameters_CreatesValidIndicator()
{
var twap = new Twap();
Assert.Equal("Twap(∞)", twap.Name);
Assert.Equal(1, Twap.WarmupPeriod);
Assert.False(twap.IsHot);
}
[Fact]
public void Constructor_CustomPeriod_SetsParameter()
{
var twap = new Twap(period: 10);
Assert.Equal("Twap(10)", twap.Name);
}
[Fact]
public void Constructor_ZeroPeriod_MeansNeverReset()
{
var twap = new Twap(period: 0);
Assert.Equal("Twap(∞)", twap.Name);
}
[Fact]
public void Constructor_NegativePeriod_ThrowsArgumentException()
{
Assert.Throws<ArgumentException>(() => new Twap(period: -1));
}
[Fact]
public void Update_WithTBar_ReturnsValidValue()
{
var twap = new Twap();
var bar = new TBar(DateTime.UtcNow, 100, 110, 90, 105, 1000000);
var result = twap.Update(bar);
Assert.True(double.IsFinite(result.Value));
// First bar: HLC3 = (110 + 90 + 105) / 3 = 101.666...
Assert.Equal((110.0 + 90.0 + 105.0) / 3.0, result.Value, 10);
}
[Fact]
public void Update_WithTValue_ReturnsCurrentValue()
{
var twap = new Twap();
var value = new TValue(DateTime.UtcNow, 100);
var result = twap.Update(value);
Assert.Equal(100, result.Value);
}
[Fact]
public void Update_MultipleValues_CalculatesRunningAverage()
{
var twap = new Twap();
var time = DateTime.UtcNow;
// First value: 100
twap.Update(new TValue(time, 100));
Assert.Equal(100, twap.Last.Value, 10);
// Second value: 200, average = (100 + 200) / 2 = 150
twap.Update(new TValue(time.AddMinutes(1), 200));
Assert.Equal(150, twap.Last.Value, 10);
// Third value: 300, average = (100 + 200 + 300) / 3 = 200
twap.Update(new TValue(time.AddMinutes(2), 300));
Assert.Equal(200, twap.Last.Value, 10);
}
[Fact]
public void Update_WithPeriod_ResetsAtBoundary()
{
var twap = new Twap(period: 3);
var time = DateTime.UtcNow;
// First 3 values: 100, 200, 300
twap.Update(new TValue(time, 100));
twap.Update(new TValue(time.AddMinutes(1), 200));
twap.Update(new TValue(time.AddMinutes(2), 300));
// Average = (100 + 200 + 300) / 3 = 200
Assert.Equal(200, twap.Last.Value, 10);
// Fourth value: 600, resets and starts new session
twap.Update(new TValue(time.AddMinutes(3), 600));
// After reset: Average = 600 / 1 = 600
Assert.Equal(600, twap.Last.Value, 10);
}
[Fact]
public void Update_ZeroPeriod_NeverResets()
{
var twap = new Twap(period: 0);
var time = DateTime.UtcNow;
double sum = 0;
for (int i = 1; i <= 20; i++)
{
sum += i * 10;
twap.Update(new TValue(time.AddMinutes(i), i * 10));
Assert.Equal(sum / i, twap.Last.Value, 10);
}
}
[Fact]
public void Update_IsNewTrue_AdvancesState()
{
var twap = new Twap();
var bar1 = new TBar(DateTime.UtcNow, 100, 110, 90, 105, 1000000);
var result1 = twap.Update(bar1, isNew: true);
var bar2 = new TBar(DateTime.UtcNow.AddMinutes(1), 105, 115, 95, 110, 800000);
var result2 = twap.Update(bar2, isNew: true);
Assert.NotEqual(result1.Time, result2.Time);
}
[Fact]
public void Update_IsNewFalse_UpdatesCurrentBar()
{
var twap = new Twap();
var gbm = new GBM(seed: 42);
// Build up history
for (int i = 0; i < 20; i++)
{
twap.Update(gbm.Next(), isNew: true);
}
// Get a new bar
var bar1 = gbm.Next();
var result1 = twap.Update(bar1, isNew: true);
// Create a correction with different close
var bar2 = new TBar(bar1.Time, bar1.Open, bar1.High, bar1.Low, bar1.Close * 1.1, bar1.Volume);
var result2 = twap.Update(bar2, isNew: false);
Assert.Equal(result1.Time, result2.Time);
Assert.True(double.IsFinite(result2.Value));
}
[Fact]
public void Update_IterativeCorrections_RestoresState()
{
var twap = new Twap();
var gbm = new GBM(seed: 123);
// Build up history
for (int i = 0; i < 20; i++)
{
twap.Update(gbm.Next(), isNew: true);
}
_ = twap.Last.Value;
// New bar
var originalBar = gbm.Next();
twap.Update(originalBar, isNew: true);
// Correction with same values should restore similar state
var correctionBar = originalBar;
var correctedResult = twap.Update(correctionBar, isNew: false);
Assert.True(double.IsFinite(correctedResult.Value));
}
[Fact]
public void Update_WarmupPeriod_IsHotBecomesTrueImmediately()
{
var twap = new Twap();
var time = DateTime.UtcNow;
Assert.False(twap.IsHot);
twap.Update(new TValue(time, 100), isNew: true);
Assert.True(twap.IsHot); // TWAP is valid after first value
}
[Fact]
public void Update_WithNaN_UsesLastValidValue()
{
var twap = new Twap();
var time = DateTime.UtcNow;
// Process some valid values first
for (int i = 0; i < 10; i++)
{
twap.Update(new TValue(time.AddMinutes(i), 100 + i));
}
// Process value with NaN
var nanValue = new TValue(time.AddMinutes(10), double.NaN);
var result = twap.Update(nanValue);
Assert.True(double.IsFinite(result.Value));
}
[Fact]
public void Reset_ClearsState()
{
var twap = new Twap();
var time = DateTime.UtcNow;
for (int i = 0; i < 10; i++)
{
twap.Update(new TValue(time.AddMinutes(i), 100 + i), isNew: true);
}
Assert.True(twap.IsHot);
Assert.True(double.IsFinite(twap.Last.Value));
twap.Reset();
Assert.False(twap.IsHot);
Assert.Equal(default, twap.Last);
}
[Fact]
public void BatchCalculate_MatchesStreaming()
{
var bars = new TBarSeries();
var gbm = new GBM(seed: 42);
for (int i = 0; i < 100; i++)
{
bars.Add(gbm.Next());
}
// Streaming
var twap = new Twap(period: 10);
var streamingValues = new List<double>();
foreach (var bar in bars)
{
streamingValues.Add(twap.Update(bar).Value);
}
// Batch
var batchResult = Twap.Batch(bars, period: 10);
Assert.Equal(bars.Count, batchResult.Count);
for (int i = 0; i < bars.Count; i++)
{
Assert.Equal(streamingValues[i], batchResult[i].Value, 10);
}
}
[Fact]
public void SpanCalculate_MatchesStreaming()
{
var time = DateTime.UtcNow;
var prices = new double[100];
var random = new GBM(seed: 42);
for (int i = 0; i < 100; i++)
{
prices[i] = 100 + random.Next().Close - 100; // Use close price variation
}
// Streaming
var twap = new Twap(period: 10);
var streamingValues = new List<double>();
for (int i = 0; i < prices.Length; i++)
{
streamingValues.Add(twap.Update(new TValue(time.AddMinutes(i), prices[i])).Value);
}
// Span
var output = new double[prices.Length];
Twap.Batch(prices, output, period: 10);
for (int i = 0; i < prices.Length; i++)
{
Assert.Equal(streamingValues[i], output[i], 10);
}
}
[Fact]
public void SpanCalculate_InvalidLengths_ThrowsArgumentException()
{
var price = new double[100];
var output = new double[99]; // Different length
Assert.Throws<ArgumentException>(() => Twap.Batch(price, output));
}
[Fact]
public void SpanCalculate_InvalidPeriod_ThrowsArgumentException()
{
var price = new double[100];
var output = new double[100];
Assert.Throws<ArgumentException>(() => Twap.Batch(price, output, period: -1));
}
[Fact]
public void SpanCalculate_EmptyInput_HandlesGracefully()
{
var price = Array.Empty<double>();
var output = Array.Empty<double>();
Twap.Batch(price, output);
Assert.Empty(output);
}
[Fact]
public void Event_PubFiresOnUpdate()
{
var twap = new Twap();
TValue? receivedValue = null;
bool receivedIsNew = false;
twap.Pub += (object? sender, in TValueEventArgs args) =>
{
receivedValue = args.Value;
receivedIsNew = args.IsNew;
};
var value = new TValue(DateTime.UtcNow, 100);
twap.Update(value, isNew: true);
Assert.NotNull(receivedValue);
Assert.True(receivedIsNew);
}
[Fact]
public void LargeDataset_HandlesWithoutError()
{
var bars = new TBarSeries();
var gbm = new GBM(seed: 42);
for (int i = 0; i < 10000; i++)
{
bars.Add(gbm.Next());
}
var twap = new Twap(period: 100);
foreach (var bar in bars)
{
var result = twap.Update(bar);
Assert.True(double.IsFinite(result.Value));
}
Assert.True(twap.IsHot);
}
[Fact]
public void FormulaVerification_ManualCalculation()
{
// Manual verification of TWAP formula with known values
var twap = new Twap(period: 0); // Never reset
var time = DateTime.UtcNow;
// Value 1: 100, TWAP = 100/1 = 100
twap.Update(new TValue(time, 100));
Assert.Equal(100, twap.Last.Value, 10);
// Value 2: 200, TWAP = (100+200)/2 = 150
twap.Update(new TValue(time.AddMinutes(1), 200));
Assert.Equal(150, twap.Last.Value, 10);
// Value 3: 150, TWAP = (100+200+150)/3 = 150
twap.Update(new TValue(time.AddMinutes(2), 150));
Assert.Equal(150, twap.Last.Value, 10);
// Value 4: 250, TWAP = (100+200+150+250)/4 = 175
twap.Update(new TValue(time.AddMinutes(3), 250));
Assert.Equal(175, twap.Last.Value, 10);
// Value 5: 300, TWAP = (100+200+150+250+300)/5 = 200
twap.Update(new TValue(time.AddMinutes(4), 300));
Assert.Equal(200, twap.Last.Value, 10);
}
[Fact]
public void DifferentPeriods_ProduceDifferentResults()
{
var time = DateTime.UtcNow;
var values = new double[] { 100, 200, 300, 400, 500, 600, 700, 800, 900, 1000 };
// With period = 0 (never reset)
var twap0 = new Twap(period: 0);
foreach (var v in values)
{
twap0.Update(new TValue(time, v));
}
// With period = 5 (reset every 5 bars)
var twap5 = new Twap(period: 5);
foreach (var v in values)
{
twap5.Update(new TValue(time, v));
}
// Results should differ
Assert.NotEqual(twap0.Last.Value, twap5.Last.Value);
// Period 0: average of all 10 values = 550
Assert.Equal(550, twap0.Last.Value, 10);
// Period 5: after reset, average of last 5 values (600,700,800,900,1000) = 800
Assert.Equal(800, twap5.Last.Value, 10);
}
[Fact]
public void Update_UsesTypicalPrice_HLC3()
{
var twap = new Twap();
var time = DateTime.UtcNow;
// Bar with H=110, L=90, C=100
// Typical price = (110 + 90 + 100) / 3 = 100
var bar = new TBar(time, 95, 110, 90, 100, 10000);
var result = twap.Update(bar);
Assert.Equal(100, result.Value, 10);
}
}
@@ -0,0 +1,181 @@
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}]");
}
}
}