mirror of
https://github.com/mihakralj/QuanTAlib.git
synced 2026-08-23 04:58:08 +00:00
volume indicators
This commit is contained in:
@@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
using System.Drawing;
|
||||
using System.Runtime.CompilerServices;
|
||||
using TradingPlatform.BusinessLayer;
|
||||
|
||||
namespace QuanTAlib;
|
||||
|
||||
[SkipLocalsInit]
|
||||
public sealed class TwapIndicator : Indicator, IWatchlistIndicator
|
||||
{
|
||||
[InputParameter("Period", sortIndex: 10, minimum: 0, maximum: 10000, increment: 1)]
|
||||
public int Period { get; set; } = 0;
|
||||
|
||||
[InputParameter("Show cold values", sortIndex: 21)]
|
||||
public bool ShowColdValues { get; set; } = true;
|
||||
|
||||
private Twap _twap = null!;
|
||||
private readonly LineSeries _series;
|
||||
|
||||
#pragma warning disable S2325 // Instance property required by Quantower indicator interface
|
||||
public int MinHistoryDepths => 1;
|
||||
#pragma warning restore S2325
|
||||
int IWatchlistIndicator.MinHistoryDepths => 1;
|
||||
|
||||
public override string ShortName => "TWAP";
|
||||
public override string SourceCodeLink => "https://github.com/mihakralj/QuanTAlib/blob/main/lib/volume/twap/Twap.Quantower.cs";
|
||||
|
||||
public TwapIndicator()
|
||||
{
|
||||
OnBackGround = true;
|
||||
SeparateWindow = false;
|
||||
Name = "TWAP - Time Weighted Average Price";
|
||||
Description = "Time Weighted Average Price gives equal weight to each price point within a session. Resets at specified period intervals (0 = never reset).";
|
||||
|
||||
_series = new LineSeries(name: "TWAP", color: Color.Orange, width: 2, style: LineStyle.Solid);
|
||||
AddLineSeries(_series);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
protected override void OnInit()
|
||||
{
|
||||
_twap = new Twap(Period);
|
||||
base.OnInit();
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
protected override void OnUpdate(UpdateArgs args)
|
||||
{
|
||||
TBar bar = this.GetInputBar(args);
|
||||
TValue result = _twap.Update(bar, args.IsNewBar());
|
||||
|
||||
_series.SetValue(result.Value, _twap.IsHot, ShowColdValues);
|
||||
}
|
||||
}
|
||||
@@ -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.Calculate(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.Calculate(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.Calculate(price, output));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SpanCalculate_InvalidPeriod_ThrowsArgumentException()
|
||||
{
|
||||
var price = new double[100];
|
||||
var output = new double[100];
|
||||
|
||||
Assert.Throws<ArgumentException>(() => Twap.Calculate(price, output, period: -1));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SpanCalculate_EmptyInput_HandlesGracefully()
|
||||
{
|
||||
var price = Array.Empty<double>();
|
||||
var output = Array.Empty<double>();
|
||||
|
||||
Twap.Calculate(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.Calculate(_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.Calculate(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.Calculate(_data.Bars, period);
|
||||
var batchValues = batchResult.Values.ToArray();
|
||||
|
||||
// Span
|
||||
var spanOutput = new double[typicalPrices.Length];
|
||||
Twap.Calculate(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}]");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,294 @@
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace QuanTAlib;
|
||||
|
||||
/// <summary>
|
||||
/// TWAP: Time Weighted Average Price
|
||||
/// A session-based average price that resets at specified intervals.
|
||||
/// Unlike VWAP which weights by volume, TWAP gives equal weight to each price point.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// TWAP Formula:
|
||||
/// On session reset: sumPrices = 0, count = 0
|
||||
/// sumPrices += price
|
||||
/// count += 1
|
||||
/// TWAP = sumPrices / count
|
||||
///
|
||||
/// Key characteristics:
|
||||
/// - Equal weighting of all price points within session
|
||||
/// - Resets at specified period intervals
|
||||
/// - Used as benchmark for algorithmic trading execution
|
||||
/// - Period of 0 means never reset (continuous average from start)
|
||||
///
|
||||
/// Sources:
|
||||
/// PineScript reference: twap.pine
|
||||
/// Algorithmic trading benchmarks
|
||||
/// </remarks>
|
||||
[SkipLocalsInit]
|
||||
public sealed class Twap : ITValuePublisher
|
||||
{
|
||||
private readonly int _period;
|
||||
private const int DefaultPeriod = 0; // 0 = never reset (continuous)
|
||||
|
||||
// State management using record struct for efficiency
|
||||
[StructLayout(LayoutKind.Auto)]
|
||||
private record struct State
|
||||
{
|
||||
public double SumPrices;
|
||||
public int Count;
|
||||
public int Index;
|
||||
public double LastValid;
|
||||
public double Twap;
|
||||
}
|
||||
|
||||
private State _s;
|
||||
private State _ps;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public TValue Last { get; private set; }
|
||||
/// <inheritdoc/>
|
||||
public bool IsHot { get; private set; }
|
||||
/// <inheritdoc/>
|
||||
public static int WarmupPeriod => 1;
|
||||
/// <inheritdoc/>
|
||||
public string Name { get; }
|
||||
/// <inheritdoc/>
|
||||
public event TValuePublishedHandler? Pub;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the TWAP indicator.
|
||||
/// </summary>
|
||||
/// <param name="period">The session period in bars (0 = never reset). Default is 0.</param>
|
||||
/// <exception cref="ArgumentException">Thrown when period is negative.</exception>
|
||||
public Twap(int period = DefaultPeriod)
|
||||
{
|
||||
if (period < 0)
|
||||
{
|
||||
throw new ArgumentException("Period must be non-negative", nameof(period));
|
||||
}
|
||||
_period = period;
|
||||
Name = period == 0 ? "Twap(∞)" : $"Twap({_period})";
|
||||
Reset();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the TWAP indicator with a data source.
|
||||
/// </summary>
|
||||
/// <param name="source">The source indicator providing price data.</param>
|
||||
/// <param name="period">The session period in bars (0 = never reset). Default is 0.</param>
|
||||
public Twap(ITValuePublisher source, int period = DefaultPeriod) : this(period)
|
||||
{
|
||||
source.Pub += Handle;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resets the indicator to its initial state.
|
||||
/// </summary>
|
||||
public void Reset()
|
||||
{
|
||||
_s = new State
|
||||
{
|
||||
SumPrices = 0,
|
||||
Count = 0,
|
||||
Index = 0,
|
||||
LastValid = 0,
|
||||
Twap = 0
|
||||
};
|
||||
_ps = _s;
|
||||
Last = default;
|
||||
IsHot = false;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static double GetFiniteValue(double value, double fallback)
|
||||
{
|
||||
return double.IsFinite(value) ? value : fallback;
|
||||
}
|
||||
|
||||
private void Handle(object? _, in TValueEventArgs args)
|
||||
{
|
||||
Update(args.Value, args.IsNew);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates the TWAP with a new bar.
|
||||
/// </summary>
|
||||
/// <param name="bar">The bar data.</param>
|
||||
/// <param name="isNew">True if this is a new bar, false if updating current bar.</param>
|
||||
/// <returns>The current TWAP value.</returns>
|
||||
public TValue Update(TBar bar, bool isNew = true)
|
||||
{
|
||||
// Use typical price (HLC3) for TWAP
|
||||
double typicalPrice = (bar.High + bar.Low + bar.Close) / 3.0;
|
||||
return Update(new TValue(bar.Time, typicalPrice), isNew);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates the TWAP with a new price value.
|
||||
/// </summary>
|
||||
/// <param name="input">The price value.</param>
|
||||
/// <param name="isNew">True if this is a new value, false if updating current value.</param>
|
||||
/// <returns>The current TWAP value.</returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public TValue Update(TValue input, bool isNew = true)
|
||||
{
|
||||
// State management for bar correction
|
||||
if (isNew)
|
||||
{
|
||||
_ps = _s;
|
||||
}
|
||||
else
|
||||
{
|
||||
_s = _ps;
|
||||
}
|
||||
|
||||
// Local copy for struct promotion
|
||||
var s = _s;
|
||||
|
||||
// Get valid price (substitute NaN/Infinity with last valid)
|
||||
double price = GetFiniteValue(input.Value, s.LastValid);
|
||||
s.LastValid = price;
|
||||
|
||||
// Check for session reset
|
||||
if (isNew)
|
||||
{
|
||||
s.Index++;
|
||||
|
||||
// Reset on period boundary (period > 0 means reset every N bars)
|
||||
if (_period > 0 && s.Index > _period)
|
||||
{
|
||||
s.SumPrices = 0;
|
||||
s.Count = 0;
|
||||
s.Index = 1;
|
||||
}
|
||||
}
|
||||
|
||||
// Accumulate price
|
||||
s.SumPrices += price;
|
||||
s.Count++;
|
||||
|
||||
// Calculate TWAP
|
||||
s.Twap = s.Count > 0 ? s.SumPrices / s.Count : price;
|
||||
|
||||
// Write back state
|
||||
_s = s;
|
||||
|
||||
// Update state tracking
|
||||
IsHot = true; // TWAP is valid after first value
|
||||
|
||||
// Publish result
|
||||
Last = new TValue(input.Time, s.Twap);
|
||||
Pub?.Invoke(this, new TValueEventArgs { Value = Last, IsNew = isNew });
|
||||
|
||||
return Last;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates the TWAP with a series of bars (batch mode).
|
||||
/// </summary>
|
||||
/// <param name="source">The bar series.</param>
|
||||
/// <returns>The result series.</returns>
|
||||
public TSeries Update(TBarSeries source)
|
||||
{
|
||||
var result = new TSeries(source.Count);
|
||||
var prices = new double[source.Count];
|
||||
for (int i = 0; i < source.Count; i++)
|
||||
{
|
||||
TBar bar = source[i];
|
||||
prices[i] = (bar.High + bar.Low + bar.Close) / 3.0;
|
||||
}
|
||||
|
||||
var output = new double[source.Count];
|
||||
Calculate(prices, output, _period);
|
||||
|
||||
for (int i = 0; i < source.Count; i++)
|
||||
{
|
||||
result.Add(new TValue(source[i].Time, output[i]));
|
||||
}
|
||||
|
||||
// Restore internal state by replaying last values
|
||||
Reset();
|
||||
// For continuous TWAP (_period == 0), replay entire series
|
||||
// For periodic TWAP, replay last _period bars
|
||||
int replayCount = _period == 0 ? source.Count : Math.Min(_period, source.Count);
|
||||
int replayStart = source.Count - replayCount;
|
||||
for (int i = replayStart; i < source.Count; i++)
|
||||
{
|
||||
Update(source[i], isNew: true);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates TWAP for a series of bars (static batch mode).
|
||||
/// </summary>
|
||||
/// <param name="source">The bar series.</param>
|
||||
/// <param name="period">The session period in bars (0 = never reset).</param>
|
||||
/// <returns>The result series.</returns>
|
||||
public static TSeries Calculate(TBarSeries source, int period = DefaultPeriod)
|
||||
{
|
||||
var twap = new Twap(period);
|
||||
var result = new TSeries(source.Count);
|
||||
|
||||
foreach (var bar in source)
|
||||
{
|
||||
result.Add(twap.Update(bar));
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates TWAP for span of prices (high-performance span mode).
|
||||
/// </summary>
|
||||
/// <param name="price">The source price span.</param>
|
||||
/// <param name="output">The output TWAP span.</param>
|
||||
/// <param name="period">The session period in bars (0 = never reset). Default is 0.</param>
|
||||
/// <exception cref="ArgumentException">Thrown when output length doesn't match price length or period is invalid.</exception>
|
||||
public static void Calculate(ReadOnlySpan<double> price, Span<double> output, int period = DefaultPeriod)
|
||||
{
|
||||
if (output.Length != price.Length)
|
||||
{
|
||||
throw new ArgumentException("Output length must match price length", nameof(output));
|
||||
}
|
||||
if (period < 0)
|
||||
{
|
||||
throw new ArgumentException("Period must be non-negative", nameof(period));
|
||||
}
|
||||
if (price.Length == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
double sumPrices = 0;
|
||||
int count = 0;
|
||||
int index = 0;
|
||||
double lastValid = price[0];
|
||||
|
||||
for (int i = 0; i < price.Length; i++)
|
||||
{
|
||||
// Get valid price
|
||||
double p = double.IsFinite(price[i]) ? price[i] : lastValid;
|
||||
lastValid = p;
|
||||
|
||||
index++;
|
||||
|
||||
// Reset on period boundary
|
||||
if (period > 0 && index > period)
|
||||
{
|
||||
sumPrices = 0;
|
||||
count = 0;
|
||||
index = 1;
|
||||
}
|
||||
|
||||
// Accumulate
|
||||
sumPrices += p;
|
||||
count++;
|
||||
|
||||
// Calculate TWAP
|
||||
output[i] = sumPrices / count;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,258 @@
|
||||
# TWAP: Time Weighted Average Price
|
||||
|
||||
> "Equal time, equal weight—the simplest benchmark refuses to let any single moment dominate the conversation." — Anonymous Quant
|
||||
|
||||
Time Weighted Average Price (TWAP) calculates the average price over a period by giving equal weight to each price point, regardless of volume. Unlike VWAP which emphasizes high-volume periods, TWAP treats every moment as equally important. This makes it a pure temporal benchmark—ideal for evaluating execution quality when volume patterns could bias the analysis.
|
||||
|
||||
The elegance of TWAP lies in its simplicity: accumulate prices, count observations, divide. No volume weighting, no complex adjustments. Just a running average that answers the question: "What was the typical price during this period?"
|
||||
|
||||
## Historical Context
|
||||
|
||||
TWAP emerged from the world of algorithmic trading in the 1990s alongside its volume-weighted sibling, VWAP. While VWAP became the dominant benchmark for evaluating trade execution, TWAP filled a crucial niche:
|
||||
|
||||
- Markets with unreliable or absent volume data (forex, some futures)
|
||||
- Situations where volume manipulation could skew benchmarks
|
||||
- Academic studies requiring volume-agnostic price measurements
|
||||
- Low-liquidity instruments where volume spikes create VWAP distortions
|
||||
|
||||
The indicator gained renewed interest with the rise of cryptocurrency trading, where volume data quality varies dramatically across exchanges. A TWAP benchmark remains consistent regardless of reported volume, making it valuable for cross-exchange comparisons.
|
||||
|
||||
TWAP also serves as the basis for TWAP execution algorithms—strategies that break large orders into equal slices executed at regular intervals, aiming to achieve the time-weighted average price while minimizing market impact.
|
||||
|
||||
## Architecture & Physics
|
||||
|
||||
TWAP operates as a simple accumulator with optional periodic resets. The state tracks a running sum of prices and a count of observations.
|
||||
|
||||
### Component Breakdown
|
||||
|
||||
1. **Price Accumulation**: Sum of all prices in the current session
|
||||
2. **Count Tracking**: Number of observations accumulated
|
||||
3. **Period Management**: Optional reset at specified intervals
|
||||
4. **Average Calculation**: Sum divided by count
|
||||
|
||||
### State Requirements
|
||||
|
||||
| Component | Type | Purpose |
|
||||
| :--- | :--- | :--- |
|
||||
| SumPrices | double | Running sum of prices in session |
|
||||
| Count | int | Number of prices accumulated |
|
||||
| Index | int | Bar counter for period resets |
|
||||
| LastValid | double | Fallback for NaN/Infinity handling |
|
||||
| Twap | double | Current TWAP value |
|
||||
|
||||
### Session Reset Behavior
|
||||
|
||||
The period parameter controls session boundaries:
|
||||
|
||||
- **Period = 0**: Never reset; continuous average from start
|
||||
- **Period > 0**: Reset sum and count every N bars
|
||||
|
||||
Session resets are critical for intraday benchmarking where you want fresh TWAP calculations for each trading session rather than a cumulative average across days.
|
||||
|
||||
## Mathematical Foundation
|
||||
|
||||
### Running Average Formula
|
||||
|
||||
$$
|
||||
TWAP_t = \frac{\sum_{i=1}^{n} P_i}{n}
|
||||
$$
|
||||
|
||||
where:
|
||||
|
||||
- $P_i$ = Price at observation $i$
|
||||
- $n$ = Number of observations
|
||||
|
||||
### Incremental Update (Streaming)
|
||||
|
||||
$$
|
||||
Sum_t = Sum_{t-1} + P_t
|
||||
$$
|
||||
|
||||
$$
|
||||
Count_t = Count_{t-1} + 1
|
||||
$$
|
||||
|
||||
$$
|
||||
TWAP_t = \frac{Sum_t}{Count_t}
|
||||
$$
|
||||
|
||||
### With Period Reset
|
||||
|
||||
At bar $t$ where $t \mod period = 1$ (first bar of new session):
|
||||
|
||||
$$
|
||||
Sum_t = P_t
|
||||
$$
|
||||
|
||||
$$
|
||||
Count_t = 1
|
||||
$$
|
||||
|
||||
$$
|
||||
TWAP_t = P_t
|
||||
$$
|
||||
|
||||
### Price Source
|
||||
|
||||
For TBar input, the typical price (HLC3) is used:
|
||||
|
||||
$$
|
||||
P_t = \frac{High_t + Low_t + Close_t}{3}
|
||||
$$
|
||||
|
||||
This provides a better representation of average trading price than using close alone.
|
||||
|
||||
## TWAP vs VWAP Comparison
|
||||
|
||||
| Aspect | TWAP | VWAP |
|
||||
| :--- | :--- | :--- |
|
||||
| Weighting | Equal per observation | Volume-proportional |
|
||||
| Volume data required | No | Yes |
|
||||
| Sensitivity to spikes | Time-based only | Volume and price |
|
||||
| Manipulation resistance | Higher | Lower (volume can be faked) |
|
||||
| Formula | $\frac{\sum P}{n}$ | $\frac{\sum (P \times V)}{\sum V}$ |
|
||||
| Use case | Time-based benchmarks | Volume-based benchmarks |
|
||||
|
||||
### When TWAP > VWAP
|
||||
|
||||
High volume concentrated at lower prices during the session. Interpretation: early buying pressure (accumulation) occurred at cheaper levels.
|
||||
|
||||
### When TWAP < VWAP
|
||||
|
||||
High volume concentrated at higher prices during the session. Interpretation: buying pressure came at elevated prices (late to the move).
|
||||
|
||||
## Performance Profile
|
||||
|
||||
### Operation Count (Streaming Mode)
|
||||
|
||||
| Operation | Count | Notes |
|
||||
| :--- | :---: | :--- |
|
||||
| ADD | 3 | HLC3 calculation + sum update |
|
||||
| DIV | 2 | HLC3 calculation + TWAP |
|
||||
| CMP | 1 | Period boundary check |
|
||||
| INC | 2 | Count and index increments |
|
||||
| **Total** | 8 | Per bar, O(1) |
|
||||
|
||||
TWAP is one of the simplest indicators computationally—no lookback buffer, no complex mathematics.
|
||||
|
||||
### Batch Mode (SIMD)
|
||||
|
||||
| Operation | Vectorizable | Notes |
|
||||
| :--- | :---: | :--- |
|
||||
| HLC3 calculation | ✅ | Fully parallel |
|
||||
| Price accumulation | ❌ | Sequential dependency (running sum) |
|
||||
| Count tracking | ❌ | Sequential increment |
|
||||
| Division | ❌ | Depends on running count |
|
||||
|
||||
The running sum dependency limits SIMD optimization. However, the HLC3 preprocessing step can be vectorized when processing bar data.
|
||||
|
||||
### Memory Footprint
|
||||
|
||||
| Scope | Size |
|
||||
| :--- | :--- |
|
||||
| Per instance | 56 bytes (State record struct × 2) |
|
||||
| Buffer requirements | None (O(1) state) |
|
||||
|
||||
### Quality Metrics
|
||||
|
||||
| Metric | Score | Notes |
|
||||
| :--- | :---: | :--- |
|
||||
| **Accuracy** | 10/10 | Exact arithmetic computation |
|
||||
| **Timeliness** | 8/10 | First bar valid; no warmup |
|
||||
| **Smoothness** | 9/10 | Inherently smoothed by averaging |
|
||||
| **Noise Filtering** | 6/10 | Moderate; better with more observations |
|
||||
| **Memory** | 10/10 | O(1) constant regardless of history |
|
||||
|
||||
## Validation
|
||||
|
||||
| Library | Status | Notes |
|
||||
| :--- | :---: | :--- |
|
||||
| **TA-Lib** | N/A | Not implemented (VWAP variants only) |
|
||||
| **Skender** | N/A | Not implemented |
|
||||
| **Tulip** | N/A | Not implemented |
|
||||
| **Ooples** | N/A | Not implemented |
|
||||
| **PineScript** | ✅ | Reference implementation available |
|
||||
|
||||
TWAP is straightforward enough that validation focuses on internal consistency between streaming, batch, and span modes (verified with 1e-9 tolerance) and formula correctness against manual calculations.
|
||||
|
||||
## Common Pitfalls
|
||||
|
||||
1. **Period Selection**: For intraday trading, set period to match your session length (e.g., 390 for regular US equity session in 1-minute bars). Period = 0 creates a cumulative average that becomes increasingly stable—useful for long-term benchmarks but less responsive for intraday analysis.
|
||||
|
||||
2. **HLC3 vs Close**: TWAP uses typical price (HLC3), not close. This better represents the average traded price within each bar but may differ from close-only implementations in other platforms.
|
||||
|
||||
3. **Initial Value**: The first bar's TWAP equals that bar's typical price. Unlike moving averages, there's no "warmup" period where values are unreliable.
|
||||
|
||||
4. **Comparing Across Sessions**: TWAP values are only meaningful within their session context. Comparing TWAP from yesterday to TWAP from today without considering the reset boundary leads to incorrect conclusions.
|
||||
|
||||
5. **TValue Limitations**: When using `Update(TValue)`, you're providing a single price rather than OHLC data. The implementation uses this price directly. For proper TWAP from bar data, use `Update(TBar)`.
|
||||
|
||||
6. **Cumulative Nature**: With period = 0, TWAP becomes increasingly stable as more observations accumulate. After 1000 bars, a new bar changes TWAP by only ~0.1%. Consider whether you need this stability or session-based freshness.
|
||||
|
||||
7. **Reset Timing**: Period resets occur when the bar count exceeds the period. With period = 5, the 6th bar starts a new session. The reset is on boundary crossing, not modular arithmetic.
|
||||
|
||||
8. **isNew Parameter**: Bar correction (isNew = false) properly restores state including accumulated sum and count. Incorrect implementation causes cumulative drift in TWAP values.
|
||||
|
||||
## Interpretation Guide
|
||||
|
||||
### Execution Quality Analysis
|
||||
|
||||
| Execution Price vs TWAP | Interpretation |
|
||||
| :--- | :--- |
|
||||
| Buy below TWAP | Good execution (bought cheaper than average) |
|
||||
| Buy above TWAP | Poor execution (paid premium) |
|
||||
| Sell above TWAP | Good execution (sold higher than average) |
|
||||
| Sell below TWAP | Poor execution (sold at discount) |
|
||||
|
||||
### Trend Analysis
|
||||
|
||||
| Price Position | Market State |
|
||||
| :--- | :--- |
|
||||
| Price consistently above TWAP | Bullish session; buyers dominating |
|
||||
| Price consistently below TWAP | Bearish session; sellers dominating |
|
||||
| Price oscillating around TWAP | Range-bound; equilibrium |
|
||||
| Price diverging from TWAP | Trend acceleration |
|
||||
|
||||
### TWAP as Support/Resistance
|
||||
|
||||
In intraday trading, TWAP often acts as dynamic support/resistance:
|
||||
|
||||
- Uptrend: TWAP provides support; pullbacks to TWAP are buying opportunities
|
||||
- Downtrend: TWAP provides resistance; rallies to TWAP are selling opportunities
|
||||
- Range: Price reverts to TWAP; fade moves away from it
|
||||
|
||||
### Algorithmic Execution Benchmark
|
||||
|
||||
For TWAP execution algorithms:
|
||||
|
||||
- **Slippage** = Actual Avg Price - TWAP
|
||||
- **Positive slippage** (for buys): Paid more than benchmark
|
||||
- **Negative slippage** (for buys): Paid less than benchmark
|
||||
|
||||
Target: Minimize absolute slippage to achieve the unbiased average price.
|
||||
|
||||
## Parameter Selection Guide
|
||||
|
||||
| Use Case | Period Setting | Rationale |
|
||||
| :--- | :--- | :--- |
|
||||
| Intraday benchmarking | Session length | Fresh TWAP each session |
|
||||
| Multi-day analysis | 0 (continuous) | Cumulative average |
|
||||
| Hourly benchmarks | 60 (for 1-min bars) | Reset every hour |
|
||||
| Weekly analysis | Bars per week | Weekly TWAP cycles |
|
||||
| Custom intervals | As needed | Match your trading horizon |
|
||||
|
||||
### Session Length Examples
|
||||
|
||||
| Market | Bars per Session (1-min) |
|
||||
| :--- | :--- |
|
||||
| US Equities (Regular) | 390 |
|
||||
| US Futures (23-hour) | 1380 |
|
||||
| Forex (24-hour) | 1440 |
|
||||
| Crypto (24-hour) | 1440 |
|
||||
|
||||
## References
|
||||
|
||||
- Almgren, R., & Chriss, N. (2001). "Optimal Execution of Portfolio Transactions." *Journal of Risk*.
|
||||
- Berkowitz, S., Logue, D., & Noser, E. (1988). "The Total Cost of Transactions on the NYSE." *Journal of Finance*.
|
||||
- Kissell, R., & Glantz, M. (2003). *Optimal Trading Strategies*. AMACOM.
|
||||
- TradingView. "PineScript TWAP Implementation." Community Scripts.
|
||||
Reference in New Issue
Block a user