// ASI: Accumulation Swing Index // Welles Wilder's cumulative swing index from "New Concepts in Technical Trading Systems" (1978). // Measures the true strength of price swings by accounting for open, high, low, close // across consecutive bars. The cumulative sum separates genuine breakouts from noise. using System.Runtime.CompilerServices; using System.Runtime.InteropServices; namespace QuanTAlib; /// /// ASI: Accumulation Swing Index /// /// /// Welles Wilder's cumulative swing index that measures genuine directional price movement. /// Each bar produces a Swing Index (SI) value based on the relationship between current /// and previous OHLC prices, scaled by the limit move parameter T: /// /// /// K = max(|H - C1|, |L - C1|) /// R = largest of: |H-C1| - 0.5|L-C1| + 0.25|C1-O1| /// |L-C1| - 0.5|H-C1| + 0.25|C1-O1| /// |H-L| + 0.25|C1-O1| /// SI = 50 * ((C-C1) + 0.5*(C-O) + 0.25*(C1-O1)) / R * (K/T) /// ASI = cumulative sum of SI /// /// /// The first bar produces 0 (no previous bar available). IsHot after bar 2. /// Guard: R=0 produces SI=0. /// /// References: /// Wilder, J.W. (1978). New Concepts in Technical Trading Systems. Trend Research. /// /// Detailed documentation [SkipLocalsInit] public sealed class Asi : ITValuePublisher { [StructLayout(LayoutKind.Auto)] private record struct State( double PrevClose, double PrevOpen, double Asi, double LastValidClose, double LastValidOpen, int Count); private State _s; private State _ps; private readonly double _limitMove; private readonly TBarPublishedHandler _barHandler; private readonly TValuePublishedHandler _handler; /// Display name for the indicator. public string Name { get; } /// Bars required for the first valid output (2 bars needed — first is always 0). #pragma warning disable S2325 // Instance property required by ITValuePublisher convention; cannot be static public int WarmupPeriod => 2; #pragma warning restore S2325 /// True once at least 2 bars have been processed. public bool IsHot => _s.Count >= 2; /// Current ASI value. public TValue Last { get; private set; } /// Event fired after each Update call. public event TValuePublishedHandler? Pub; /// /// Creates ASI with the given limit move value. /// /// Maximum daily price change (T). Typically 3.0 for stocks. public Asi(double limitMove = 3.0) { if (limitMove <= 0) { throw new ArgumentException("LimitMove must be greater than 0", nameof(limitMove)); } _limitMove = limitMove; Name = $"Asi({limitMove})"; _s = new State(double.NaN, double.NaN, 0.0, double.NaN, double.NaN, 0); _ps = _s; _barHandler = HandleBar; _handler = Handle; } /// /// Creates ASI chained to a TBarSeries source. /// public Asi(TBarSeries source, double limitMove = 3.0) : this(limitMove) { Prime(source); source.Pub += _barHandler; } /// /// Creates ASI chained to an ITValuePublisher source (uses close price only). /// public Asi(ITValuePublisher source, double limitMove = 3.0) : this(limitMove) { source.Pub += _handler; } private void HandleBar(object? sender, in TBarEventArgs e) => Update(e.Value, e.IsNew); private void Handle(object? sender, in TValueEventArgs e) => Update(new TBar(e.Value.Time, e.Value.Value, e.Value.Value, e.Value.Value, e.Value.Value, 0), e.IsNew); /// Resets all state to initial conditions. [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Reset() { _s = new State(double.NaN, double.NaN, 0.0, double.NaN, double.NaN, 0); _ps = _s; Last = default; } /// /// Updates ASI with a new OHLC bar. /// /// OHLCV bar data /// True to advance state; false to rewrite the latest bar /// Current ASI value as TValue [MethodImpl(MethodImplOptions.AggressiveInlining)] public TValue Update(TBar input, bool isNew = true) { var s = _s; if (isNew) { _ps = s; s.Count++; } else { s = _ps; } // Sanitize inputs — use last-valid on NaN/Infinity double rawClose = input.Close; double rawOpen = input.Open; double rawHigh = input.High; double rawLow = input.Low; double close; if (double.IsFinite(rawClose)) { close = rawClose; } else if (double.IsFinite(s.LastValidClose)) { close = s.LastValidClose; } else { close = 0.0; } double open; if (double.IsFinite(rawOpen)) { open = rawOpen; } else if (double.IsFinite(s.LastValidOpen)) { open = s.LastValidOpen; } else { open = close; } double high = double.IsFinite(rawHigh) ? rawHigh : close; double low = double.IsFinite(rawLow) ? rawLow : close; if (double.IsFinite(rawClose)) { s.LastValidClose = rawClose; } if (double.IsFinite(rawOpen)) { s.LastValidOpen = rawOpen; } double si = 0.0; // First bar: no previous close available — SI = 0 if (double.IsFinite(s.PrevClose)) { double prevClose = s.PrevClose; double prevOpen = double.IsFinite(s.PrevOpen) ? s.PrevOpen : prevClose; double absHC = Math.Abs(high - prevClose); double absLC = Math.Abs(low - prevClose); double absHL = Math.Abs(high - low); double absC1O1 = Math.Abs(prevClose - prevOpen); double K = Math.Max(absHC, absLC); double R; if (absHC >= absLC && absHC >= absHL) { R = Math.FusedMultiplyAdd(-0.5, absLC, absHC) + 0.25 * absC1O1; } else if (absLC >= absHC && absLC >= absHL) { R = Math.FusedMultiplyAdd(-0.5, absHC, absLC) + 0.25 * absC1O1; } else { R = absHL + 0.25 * absC1O1; } if (R > 0.0) { // SI = 50 * [(C-C1) + 0.5*(C-O) + 0.25*(C1-O1)] / R * (K/T) double numerator = Math.FusedMultiplyAdd(0.5, close - open, close - prevClose) + 0.25 * (prevClose - prevOpen); si = 50.0 * numerator / R * (K / _limitMove); } } s.Asi += si; s.PrevClose = close; s.PrevOpen = open; _s = s; Last = new TValue(input.Time, s.Asi); Pub?.Invoke(this, new TValueEventArgs { Value = Last, IsNew = isNew }); return Last; } /// /// Updates ASI as TValue (uses value as close; open=high=low=close). /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public TValue Update(TValue input, bool isNew = true) => Update(new TBar(input.Time, input.Value, input.Value, input.Value, input.Value, 0), isNew); /// /// Batch-computes ASI over a TBarSeries. /// public TSeries Update(TBarSeries source) { int len = source.Count; if (len == 0) { return []; } var t = new List(len); var v = new List(len); CollectionsMarshal.SetCount(t, len); CollectionsMarshal.SetCount(v, len); var tSpan = CollectionsMarshal.AsSpan(t); var vSpan = CollectionsMarshal.AsSpan(v); Batch( source.Open.Values, source.High.Values, source.Low.Values, source.Close.Values, vSpan, _limitMove); source.Open.Times.CopyTo(tSpan); // Restore streaming state by replaying the last bar Reset(); for (int i = 0; i < len; i++) { Update(source[i], isNew: true); } Last = new TValue(tSpan[len - 1], vSpan[len - 1]); return new TSeries(t, v); } /// /// Static factory: computes ASI over a TBarSeries and returns the result series + indicator. /// public static (TSeries Results, Asi Indicator) Calculate(TBarSeries source, double limitMove = 3.0) { var indicator = new Asi(limitMove); TSeries results = indicator.Update(source); return (results, indicator); } /// /// Batch-computes ASI over raw OHLC spans. Zero-allocation for small inputs (stackalloc) or /// direct scalar computation since ASI has no rolling window — it is purely cumulative. /// /// Source open prices /// Source high prices /// Source low prices /// Source close prices /// Destination span for ASI values /// Limit move value T (must be > 0) public static void Batch( ReadOnlySpan open, ReadOnlySpan high, ReadOnlySpan low, ReadOnlySpan close, Span output, double limitMove = 3.0) { if (limitMove <= 0) { throw new ArgumentException("LimitMove must be greater than 0", nameof(limitMove)); } int len = open.Length; if (high.Length != len) { throw new ArgumentException("High length must match open length", nameof(high)); } if (low.Length != len) { throw new ArgumentException("Low length must match open length", nameof(low)); } if (close.Length != len) { throw new ArgumentException("Close length must match open length", nameof(close)); } if (output.Length != len) { throw new ArgumentException("Output length must match input length", nameof(output)); } if (len == 0) { return; } double asi = 0.0; double prevClose = double.NaN; double prevOpen = double.NaN; for (int i = 0; i < len; i++) { double o; if (double.IsFinite(open[i])) { o = open[i]; } else if (double.IsFinite(prevClose)) { o = prevClose; } else { o = 0.0; } double h = double.IsFinite(high[i]) ? high[i] : o; double l = double.IsFinite(low[i]) ? low[i] : o; double c; if (double.IsFinite(close[i])) { c = close[i]; } else if (double.IsFinite(prevClose)) { c = prevClose; } else { c = 0.0; } double si = 0.0; if (double.IsFinite(prevClose)) { double pc = prevClose; double po = double.IsFinite(prevOpen) ? prevOpen : pc; double absHC = Math.Abs(h - pc); double absLC = Math.Abs(l - pc); double absHL = Math.Abs(h - l); double absC1O1 = Math.Abs(pc - po); double K = Math.Max(absHC, absLC); double R; if (absHC >= absLC && absHC >= absHL) { R = Math.FusedMultiplyAdd(-0.5, absLC, absHC) + 0.25 * absC1O1; } else if (absLC >= absHC && absLC >= absHL) { R = Math.FusedMultiplyAdd(-0.5, absHC, absLC) + 0.25 * absC1O1; } else { R = absHL + 0.25 * absC1O1; } if (R > 0.0) { double numerator = Math.FusedMultiplyAdd(0.5, c - o, c - pc) + 0.25 * (pc - po); si = 50.0 * numerator / R * (K / limitMove); } } asi += si; output[i] = asi; prevClose = double.IsFinite(close[i]) ? close[i] : prevClose; prevOpen = double.IsFinite(open[i]) ? open[i] : prevOpen; } } /// Primes the indicator by replaying historical data without firing events. public void Prime(TBarSeries source) { foreach (var bar in source) { Update(bar, isNew: true); } } }