namespace QuanTAlib; /// /// Calculates the maximum value over a specified period, with an optional decay factor. /// Useful for tracking the highest point in a time series with the ability to gradually forget old peaks. /// /// /// The Max indicator is particularly useful in financial analysis for: /// - Identifying resistance levels in price charts. /// - Tracking the highest price over a given period. /// - Implementing trailing stop-loss strategies. /// /// The decay factor allows the indicator to adapt to changing market conditions by /// gradually reducing the influence of older maximum values. /// public class Max : AbstractBase { /// /// The number of data points to consider for the maximum calculation. /// private readonly int Period; /// /// Circular buffer to store the most recent data points. /// private readonly CircularBuffer _buffer; /// /// The half-life decay factor used to gradually forget old peaks. /// private readonly double _halfLife; /// /// The current maximum value. /// private double _currentMax; /// /// The previous maximum value. /// private double _p_currentMax; /// /// The number of periods since a new maximum was set. /// private int _timeSinceNewMax; /// /// The previous value of _timeSinceNewMax. /// private int _p_timeSinceNewMax; /// /// Initializes a new instance of the Max class. /// /// The number of data points to consider. Must be at least 1. /// Half-life decay factor. Set to 0 for no decay, higher for faster forgetting of old peaks. Default is 0. /// /// Thrown when the period is less than 1 or decay is negative. /// public Max(int period, double decay = 0) { if (period < 1) { throw new ArgumentOutOfRangeException(nameof(period), "Period must be greater than or equal to 1."); } if (decay < 0) { throw new ArgumentOutOfRangeException(nameof(decay), "Half-life must be non-negative."); } Period = period; WarmupPeriod = 0; _buffer = new CircularBuffer(period); _halfLife = decay * 0.1; Name = $"Max(period={period}, halfLife={decay:F2})"; Init(); } /// /// Initializes a new instance of the Max class with a data source. /// /// The source object that publishes data. /// The number of data points to consider. /// Half-life decay factor. Default is 0. public Max(object source, int period, double decay = 0) : this(period, decay) { var pubEvent = source.GetType().GetEvent("Pub"); pubEvent?.AddEventHandler(source, new ValueSignal(Sub)); } /// /// Resets the Max indicator to its initial state. /// public override void Init() { base.Init(); _currentMax = double.MinValue; _timeSinceNewMax = 0; } /// /// Manages the state of the indicator. /// /// Indicates if the current data point is new. protected override void ManageState(bool isNew) { if (isNew) { _p_currentMax = _currentMax; _lastValidValue = Input.Value; _index++; _timeSinceNewMax++; _p_timeSinceNewMax = _timeSinceNewMax; } else { _currentMax = _p_currentMax; _timeSinceNewMax = _p_timeSinceNewMax; } } /// /// Performs the max calculation. /// /// /// The current maximum value, potentially adjusted by the decay factor. /// /// /// Uses a decay factor to gradually forget old peaks. The max value is always /// capped by the highest value in the current period. /// protected override double Calculation() { ManageState(Input.IsNew); _buffer.Add(Input.Value, Input.IsNew); if (Input.Value >= _currentMax) { _currentMax = Input.Value; _timeSinceNewMax = 0; } double decayRate = 1 - Math.Exp(-_halfLife * _timeSinceNewMax / Period); _currentMax -= decayRate * (_currentMax - _buffer.Average()); _currentMax = Math.Min(_currentMax, _buffer.Max()); IsHot = true; return _currentMax; } }