using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
namespace QuanTAlib;
///
/// Computes the Time Weighted Average Price (TWAP) that gives equal weight to each price point
/// within a session, optionally resetting at specified period intervals.
///
///
/// TWAP Formula:
/// SumPrices += Price,
/// Count += 1,
/// TWAP = SumPrices / Count.
///
/// Session resets when period > 0 and index exceeds period; period of 0 means never reset.
/// This implementation is optimized for streaming updates with O(1) per bar using running sums.
/// Non-finite inputs (NaN/±Inf) are sanitized by substituting the last finite value observed.
///
/// For the authoritative algorithm reference, full rationale, and behavioral contracts, see the
/// companion files in the same directory.
///
/// Detailed documentation
/// Reference Pine Script implementation
[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;
public TValue Last { get; private set; }
public bool IsHot { get; private set; }
public static int WarmupPeriod => 1;
public string Name { get; }
public event TValuePublishedHandler? Pub;
///
/// Initializes a new instance of the TWAP indicator.
///
/// The session period in bars (0 = never reset). Default is 0.
/// Thrown when period is negative.
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();
}
///
/// Initializes a new instance of the TWAP indicator with a data source.
///
/// The source indicator providing price data.
/// The session period in bars (0 = never reset). Default is 0.
public Twap(ITValuePublisher source, int period = DefaultPeriod) : this(period)
{
source.Pub += Handle;
}
///
/// Resets the indicator to its initial state.
///
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);
}
///
/// Updates the TWAP with a new bar.
///
/// The bar data.
/// True if this is a new bar, false if updating current bar.
/// The current TWAP value.
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);
}
///
/// Updates the TWAP with a new price value.
///
/// The price value.
/// True if this is a new value, false if updating current value.
/// The current TWAP value.
[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;
}
///
/// Updates the TWAP with a series of bars (batch mode).
///
/// The bar series.
/// The result series.
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];
Batch(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;
}
///
/// Initializes the indicator state using the provided bar series history.
///
/// Historical bar data.
public void Prime(TBarSeries source)
{
Reset();
if (source.Count == 0)
{
return;
}
for (int i = 0; i < source.Count; i++)
{
Update(source[i], isNew: true);
}
}
///
/// Calculates TWAP for a series of bars (static batch mode).
///
/// The bar series.
/// The session period in bars (0 = never reset).
/// The result series.
public static TSeries Batch(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;
}
///
/// Calculates TWAP for span of prices (high-performance span mode).
///
/// The source price span.
/// The output TWAP span.
/// The session period in bars (0 = never reset). Default is 0.
/// Thrown when output length doesn't match price length or period is invalid.
public static void Batch(ReadOnlySpan price, Span 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;
}
}
public static (TSeries Results, Twap Indicator) Calculate(TBarSeries source, int period = DefaultPeriod)
{
var indicator = new Twap(period);
TSeries results = indicator.Update(source);
return (results, indicator);
}
}