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); } }