mirror of
https://github.com/mihakralj/QuanTAlib.git
synced 2026-07-27 17:27:43 +00:00
1910fdca93
- Remove global.json (SDK pinning unnecessary) - Remove nuget.config, move MyGet source to .csproj RestoreAdditionalProjectSources - Gitignore ndepend/ entirely, move badges to docs/img/ - Update README.md and docs/ndepend.md badge paths - Add NDepend project property to QuanTAlib.slnx - Expand .editorconfig ReSharper/diagnostic suppressions - Use ArgumentOutOfRangeException instead of ArgumentException - Use discard _ for unused event sender parameters - Remove quantalib.code-workspace and sonar-suppressions.json - Add filter signature SVGs
703 lines
22 KiB
C#
703 lines
22 KiB
C#
using System.Runtime.CompilerServices;
|
||
using System.Runtime.InteropServices;
|
||
|
||
namespace QuanTAlib;
|
||
|
||
/// <summary>
|
||
/// APZ: Adaptive Price Zone
|
||
/// </summary>
|
||
/// <remarks>
|
||
/// The Adaptive Price Zone (APZ) is a volatility-based technical indicator developed by
|
||
/// Lee Leibfarth. It uses a double-smoothed exponential moving average (EMA) with a
|
||
/// modified smoothing factor based on sqrt(period) to create adaptive bands around price.
|
||
///
|
||
/// Calculation:
|
||
/// smoothing_period = sqrt(period)
|
||
/// alpha = 2 / (smoothing_period + 1)
|
||
/// EMA1_price = alpha × price + (1 - alpha) × EMA1_price[1]
|
||
/// EMA2_price = alpha × EMA1_price + (1 - alpha) × EMA2_price[1] (middle line)
|
||
/// EMA1_range = alpha × (high - low) + (1 - alpha) × EMA1_range[1]
|
||
/// EMA2_range = alpha × EMA1_range + (1 - alpha) × EMA2_range[1] (adaptive range)
|
||
/// upper = middle + (multiplier × adaptive_range)
|
||
/// lower = middle - (multiplier × adaptive_range)
|
||
///
|
||
/// Key characteristics:
|
||
/// - Uses compound warmup compensation for nested EMAs: compensator = 1/(1-beta²)
|
||
/// - Faster response than standard EMAs due to sqrt(period) smoothing
|
||
/// - Bands adapt to volatility via the high-low range
|
||
/// - O(1) complexity per update
|
||
///
|
||
/// Sources:
|
||
/// Leibfarth, Lee (2006). "Trading With An Adaptive Price Zone," Technical Analysis of
|
||
/// Stocks & Commodities, Volume 24:9.
|
||
/// </remarks>
|
||
[SkipLocalsInit]
|
||
public sealed class Apz : ITValuePublisher
|
||
{
|
||
private readonly int _period;
|
||
private readonly double _multiplier;
|
||
private readonly double _alpha;
|
||
private readonly double _beta;
|
||
private readonly double _betaSquared;
|
||
private readonly TBarPublishedHandler _barHandler;
|
||
|
||
private const double ConvergenceThreshold = 1e-10;
|
||
|
||
[StructLayout(LayoutKind.Auto)]
|
||
private record struct State(
|
||
double Ema1Price,
|
||
double Ema2Price,
|
||
double Ema1Range,
|
||
double Ema2Range,
|
||
double E, // Warmup decay factor
|
||
double LastValidPrice,
|
||
double LastValidHigh,
|
||
double LastValidLow,
|
||
bool IsHot
|
||
)
|
||
{
|
||
public static State New() => new()
|
||
{
|
||
Ema1Price = 0,
|
||
Ema2Price = 0,
|
||
Ema1Range = 0,
|
||
Ema2Range = 0,
|
||
E = 1.0,
|
||
LastValidPrice = double.NaN,
|
||
LastValidHigh = double.NaN,
|
||
LastValidLow = double.NaN,
|
||
IsHot = false,
|
||
};
|
||
}
|
||
|
||
private State _state;
|
||
private State _p_state;
|
||
|
||
/// <summary>
|
||
/// Display name for the indicator.
|
||
/// </summary>
|
||
public string Name { get; }
|
||
|
||
/// <summary>
|
||
/// Number of periods before the indicator is considered "hot" (valid).
|
||
/// </summary>
|
||
public int WarmupPeriod { get; }
|
||
|
||
/// <summary>
|
||
/// Current middle band value (double-smoothed EMA of price).
|
||
/// </summary>
|
||
public TValue Last { get; private set; }
|
||
|
||
/// <summary>
|
||
/// Current upper band value.
|
||
/// </summary>
|
||
public TValue Upper { get; private set; }
|
||
|
||
/// <summary>
|
||
/// Current lower band value.
|
||
/// </summary>
|
||
public TValue Lower { get; private set; }
|
||
|
||
/// <summary>
|
||
/// True if the indicator has converged (warmup decay below threshold).
|
||
/// </summary>
|
||
public bool IsHot => _state.IsHot;
|
||
|
||
/// <summary>
|
||
/// Event triggered when a new TValue is available.
|
||
/// </summary>
|
||
public event TValuePublishedHandler? Pub;
|
||
|
||
/// <summary>
|
||
/// Creates APZ with specified period and multiplier.
|
||
/// </summary>
|
||
/// <param name="period">Lookback period (sqrt applied internally for smoothing, must be > 0)</param>
|
||
/// <param name="multiplier">Multiplier for band width (must be > 0, default: 2.0)</param>
|
||
public Apz(int period, double multiplier = 2.0)
|
||
{
|
||
if (period <= 0)
|
||
{
|
||
throw new ArgumentException("Period must be greater than 0", nameof(period));
|
||
}
|
||
|
||
if (multiplier <= 0)
|
||
{
|
||
throw new ArgumentException("Multiplier must be greater than 0", nameof(multiplier));
|
||
}
|
||
|
||
_period = period;
|
||
_multiplier = multiplier;
|
||
|
||
double smoothPeriod = Math.Sqrt(period);
|
||
_alpha = 2.0 / (smoothPeriod + 1.0);
|
||
_beta = 1.0 - _alpha;
|
||
_betaSquared = _beta * _beta;
|
||
|
||
Name = $"Apz({period},{multiplier:F2})";
|
||
// Warmup is based on EMA convergence - use period as approximation
|
||
WarmupPeriod = period;
|
||
_state = State.New();
|
||
_p_state = _state;
|
||
_barHandler = HandleBar;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Creates APZ with TBarSeries source.
|
||
/// </summary>
|
||
public Apz(TBarSeries source, int period, double multiplier = 2.0) : this(period, multiplier)
|
||
{
|
||
Prime(source);
|
||
source.Pub += _barHandler;
|
||
}
|
||
|
||
private void HandleBar(object? sender, in TBarEventArgs e) => Update(e.Value, e.IsNew);
|
||
|
||
/// <summary>
|
||
/// Helper to invoke the Pub event.
|
||
/// </summary>
|
||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||
private void PubEvent(TValue value, bool isNew = true)
|
||
{
|
||
Pub?.Invoke(this, new TValueEventArgs { Value = value, IsNew = isNew });
|
||
}
|
||
|
||
/// <summary>
|
||
/// Gets valid input values, using last-value substitution for non-finite inputs.
|
||
/// </summary>
|
||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||
private (double price, double high, double low) GetValidValues(double price, double high, double low)
|
||
{
|
||
if (double.IsFinite(price))
|
||
{
|
||
_state.LastValidPrice = price;
|
||
}
|
||
else
|
||
{
|
||
price = _state.LastValidPrice;
|
||
}
|
||
|
||
if (double.IsFinite(high))
|
||
{
|
||
_state.LastValidHigh = high;
|
||
}
|
||
else
|
||
{
|
||
high = _state.LastValidHigh;
|
||
}
|
||
|
||
if (double.IsFinite(low))
|
||
{
|
||
_state.LastValidLow = low;
|
||
}
|
||
else
|
||
{
|
||
low = _state.LastValidLow;
|
||
}
|
||
|
||
return (price, high, low);
|
||
}
|
||
|
||
/// <summary>
|
||
/// Core calculation with warmup compensation.
|
||
/// </summary>
|
||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||
private (double middle, double upper, double lower) Compute(double price, double range)
|
||
{
|
||
// Double-smoothed EMA for price
|
||
_state.Ema1Price = Math.FusedMultiplyAdd(_state.Ema1Price, _beta, _alpha * price);
|
||
_state.Ema2Price = Math.FusedMultiplyAdd(_state.Ema2Price, _beta, _alpha * _state.Ema1Price);
|
||
|
||
// Double-smoothed EMA for range
|
||
_state.Ema1Range = Math.FusedMultiplyAdd(_state.Ema1Range, _beta, _alpha * range);
|
||
_state.Ema2Range = Math.FusedMultiplyAdd(_state.Ema2Range, _beta, _alpha * _state.Ema1Range);
|
||
|
||
double middle = _state.Ema2Price;
|
||
double adaptiveRange = _state.Ema2Range;
|
||
|
||
// Apply compound warmup compensation
|
||
if (!_state.IsHot)
|
||
{
|
||
_state.E *= _betaSquared;
|
||
double compensator = 1.0 / (1.0 - _state.E);
|
||
middle *= compensator;
|
||
adaptiveRange *= compensator;
|
||
|
||
if (_state.E <= ConvergenceThreshold)
|
||
{
|
||
_state.IsHot = true;
|
||
}
|
||
}
|
||
|
||
double bandWidth = _multiplier * adaptiveRange;
|
||
return (middle, middle + bandWidth, middle - bandWidth);
|
||
}
|
||
|
||
/// <summary>
|
||
/// Updates the indicator with a TBar input.
|
||
/// </summary>
|
||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||
public TValue Update(TBar input, bool isNew = true)
|
||
{
|
||
if (isNew)
|
||
{
|
||
_p_state = _state;
|
||
}
|
||
else
|
||
{
|
||
_state = _p_state;
|
||
}
|
||
|
||
var (price, high, low) = GetValidValues(input.Close, input.High, input.Low);
|
||
|
||
// Handle first value initialization
|
||
if (double.IsNaN(_state.LastValidPrice))
|
||
{
|
||
Last = new TValue(input.Time, double.NaN);
|
||
Upper = new TValue(input.Time, double.NaN);
|
||
Lower = new TValue(input.Time, double.NaN);
|
||
PubEvent(Last, isNew);
|
||
return Last;
|
||
}
|
||
|
||
double range = high - low;
|
||
if (range < 0)
|
||
{
|
||
range = 0; // Safety check
|
||
}
|
||
|
||
var (middle, upper, lower) = Compute(price, range);
|
||
|
||
Last = new TValue(input.Time, middle);
|
||
Upper = new TValue(input.Time, upper);
|
||
Lower = new TValue(input.Time, lower);
|
||
|
||
PubEvent(Last, isNew);
|
||
return Last;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Updates the indicator with a TBarSeries.
|
||
/// </summary>
|
||
public (TSeries Middle, TSeries Upper, TSeries Lower) Update(TBarSeries source)
|
||
{
|
||
if (source.Count == 0)
|
||
{
|
||
return (new TSeries([], []), new TSeries([], []), new TSeries([], []));
|
||
}
|
||
|
||
int len = source.Count;
|
||
var tMiddle = new List<long>(len);
|
||
var vMiddle = new List<double>(len);
|
||
var tUpper = new List<long>(len);
|
||
var vUpper = new List<double>(len);
|
||
var tLower = new List<long>(len);
|
||
var vLower = new List<double>(len);
|
||
|
||
CollectionsMarshal.SetCount(tMiddle, len);
|
||
CollectionsMarshal.SetCount(vMiddle, len);
|
||
CollectionsMarshal.SetCount(tUpper, len);
|
||
CollectionsMarshal.SetCount(vUpper, len);
|
||
CollectionsMarshal.SetCount(tLower, len);
|
||
CollectionsMarshal.SetCount(vLower, len);
|
||
|
||
var tSpan = CollectionsMarshal.AsSpan(tMiddle);
|
||
var vMiddleSpan = CollectionsMarshal.AsSpan(vMiddle);
|
||
var vUpperSpan = CollectionsMarshal.AsSpan(vUpper);
|
||
var vLowerSpan = CollectionsMarshal.AsSpan(vLower);
|
||
|
||
// Use batch calculation and capture final state for continued streaming
|
||
var finalState = BatchWithState(source.High.Values, source.Low.Values, source.Close.Values,
|
||
new BatchOutputs(vMiddleSpan, vUpperSpan, vLowerSpan), _period, _multiplier);
|
||
|
||
source.Times.CopyTo(tSpan);
|
||
tSpan.CopyTo(CollectionsMarshal.AsSpan(tUpper));
|
||
tSpan.CopyTo(CollectionsMarshal.AsSpan(tLower));
|
||
|
||
// Restore state from batch calculation (no re-processing)
|
||
_state = new State(
|
||
Ema1Price: finalState.Ema1Price,
|
||
Ema2Price: finalState.Ema2Price,
|
||
Ema1Range: finalState.Ema1Range,
|
||
Ema2Range: finalState.Ema2Range,
|
||
E: finalState.E,
|
||
LastValidPrice: finalState.LastValidPrice,
|
||
LastValidHigh: finalState.LastValidHigh,
|
||
LastValidLow: finalState.LastValidLow,
|
||
IsHot: finalState.IsHot
|
||
);
|
||
_p_state = _state;
|
||
|
||
// Update Last/Upper/Lower from final computed values
|
||
if (len > 0)
|
||
{
|
||
var lastTime = source.Times[len - 1];
|
||
Last = new TValue(new DateTime(lastTime, DateTimeKind.Utc), vMiddleSpan[len - 1]);
|
||
Upper = new TValue(new DateTime(lastTime, DateTimeKind.Utc), vUpperSpan[len - 1]);
|
||
Lower = new TValue(new DateTime(lastTime, DateTimeKind.Utc), vLowerSpan[len - 1]);
|
||
}
|
||
|
||
return (new TSeries(tMiddle, vMiddle), new TSeries(tUpper, vUpper), new TSeries(tLower, vLower));
|
||
}
|
||
|
||
/// <summary>
|
||
/// Initializes the indicator state using the provided TBarSeries history.
|
||
/// </summary>
|
||
public void Prime(TBarSeries source)
|
||
{
|
||
if (source.Count == 0)
|
||
{
|
||
return;
|
||
}
|
||
|
||
// Reset state
|
||
_state = State.New();
|
||
_p_state = _state;
|
||
|
||
// Use all available data for priming to ensure proper convergence
|
||
const int startIndex = 0;
|
||
|
||
// Find first valid values in the data
|
||
if (double.IsNaN(_state.LastValidPrice))
|
||
{
|
||
for (int i = startIndex; i < source.Count; i++)
|
||
{
|
||
var bar = source[i];
|
||
if (double.IsFinite(bar.Close))
|
||
{
|
||
_state.LastValidPrice = bar.Close;
|
||
_state.LastValidHigh = bar.High;
|
||
_state.LastValidLow = bar.Low;
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
|
||
// Feed the warmup data
|
||
for (int i = startIndex; i < source.Count; i++)
|
||
{
|
||
var bar = source[i];
|
||
var (price, high, low) = GetValidValues(bar.Close, bar.High, bar.Low);
|
||
|
||
if (double.IsFinite(price))
|
||
{
|
||
double range = Math.Max(0, high - low);
|
||
var (middle, upper, lower) = Compute(price, range);
|
||
Last = new TValue(bar.Time, middle);
|
||
Upper = new TValue(bar.Time, upper);
|
||
Lower = new TValue(bar.Time, lower);
|
||
}
|
||
}
|
||
|
||
_p_state = _state;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Resets the indicator state.
|
||
/// </summary>
|
||
public void Reset()
|
||
{
|
||
_state = State.New();
|
||
_p_state = _state;
|
||
Last = default;
|
||
Upper = default;
|
||
Lower = default;
|
||
}
|
||
|
||
/////////////////////////////////////////////////////////////////////////////////////////////////
|
||
// Static Batch Methods
|
||
/////////////////////////////////////////////////////////////////////////////////////////////////
|
||
|
||
/// <summary>
|
||
/// Output buffers for batch APZ calculation.
|
||
/// </summary>
|
||
[StructLayout(LayoutKind.Auto)]
|
||
#pragma warning disable S1104 // Fields should not have public accessibility
|
||
public ref struct BatchOutputs
|
||
{
|
||
/// <summary>Output middle band (double-smoothed EMA of price)</summary>
|
||
public Span<double> Middle;
|
||
/// <summary>Output upper band</summary>
|
||
public Span<double> Upper;
|
||
/// <summary>Output lower band</summary>
|
||
public Span<double> Lower;
|
||
#pragma warning restore S1104
|
||
|
||
/// <summary>
|
||
/// Creates a new BatchOutputs instance.
|
||
/// </summary>
|
||
public BatchOutputs(Span<double> middle, Span<double> upper, Span<double> lower)
|
||
{
|
||
Middle = middle;
|
||
Upper = upper;
|
||
Lower = lower;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// Internal state for scalar calculation.
|
||
/// </summary>
|
||
[StructLayout(LayoutKind.Auto)]
|
||
private ref struct ScalarState
|
||
{
|
||
internal double Ema1Price;
|
||
internal double Ema2Price;
|
||
internal double Ema1Range;
|
||
internal double Ema2Range;
|
||
internal double E;
|
||
internal double LastValidPrice;
|
||
internal double LastValidHigh;
|
||
internal double LastValidLow;
|
||
internal bool IsHot;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Calculates APZ for the entire TBarSeries using a new instance.
|
||
/// </summary>
|
||
public static (TSeries Middle, TSeries Upper, TSeries Lower) Batch(TBarSeries source, int period, double multiplier = 2.0)
|
||
{
|
||
var apz = new Apz(period, multiplier);
|
||
return apz.Update(source);
|
||
}
|
||
|
||
/// <summary>
|
||
/// Calculates APZ in-place using spans for maximum performance.
|
||
/// Zero-allocation method.
|
||
/// </summary>
|
||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||
public static void Batch(
|
||
ReadOnlySpan<double> high,
|
||
ReadOnlySpan<double> low,
|
||
ReadOnlySpan<double> close,
|
||
BatchOutputs outputs,
|
||
int period,
|
||
double multiplier = 2.0)
|
||
{
|
||
int len = close.Length;
|
||
if (high.Length != len || low.Length != len)
|
||
{
|
||
throw new ArgumentException("Input spans must have the same length", nameof(high));
|
||
}
|
||
|
||
if (outputs.Middle.Length < len || outputs.Upper.Length < len || outputs.Lower.Length < len)
|
||
{
|
||
throw new ArgumentException("Output buffers must be at least as long as input", nameof(outputs));
|
||
}
|
||
|
||
if (period <= 0)
|
||
{
|
||
throw new ArgumentException("Period must be greater than 0", nameof(period));
|
||
}
|
||
|
||
if (multiplier <= 0)
|
||
{
|
||
throw new ArgumentException("Multiplier must be greater than 0", nameof(multiplier));
|
||
}
|
||
|
||
if (len == 0)
|
||
{
|
||
return;
|
||
}
|
||
|
||
CalculateScalarCore(high, low, close, outputs, period, multiplier);
|
||
}
|
||
|
||
/// <summary>
|
||
/// Batch calculation that returns final state for continued streaming.
|
||
/// </summary>
|
||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||
private static ScalarState BatchWithState(
|
||
ReadOnlySpan<double> high,
|
||
ReadOnlySpan<double> low,
|
||
ReadOnlySpan<double> close,
|
||
BatchOutputs outputs,
|
||
int period,
|
||
double multiplier)
|
||
{
|
||
int len = close.Length;
|
||
if (high.Length != len || low.Length != len)
|
||
{
|
||
throw new ArgumentException("Input spans must have the same length", nameof(high));
|
||
}
|
||
|
||
if (outputs.Middle.Length < len || outputs.Upper.Length < len || outputs.Lower.Length < len)
|
||
{
|
||
throw new ArgumentException("Output buffers must be at least as long as input", nameof(outputs));
|
||
}
|
||
|
||
if (period <= 0)
|
||
{
|
||
throw new ArgumentException("Period must be greater than 0", nameof(period));
|
||
}
|
||
|
||
if (multiplier <= 0)
|
||
{
|
||
throw new ArgumentException("Multiplier must be greater than 0", nameof(multiplier));
|
||
}
|
||
|
||
if (len == 0)
|
||
{
|
||
return new ScalarState();
|
||
}
|
||
|
||
return CalculateScalarCoreWithState(high, low, close, outputs, period, multiplier);
|
||
}
|
||
|
||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||
private static void CalculateScalarCore(
|
||
ReadOnlySpan<double> high,
|
||
ReadOnlySpan<double> low,
|
||
ReadOnlySpan<double> close,
|
||
BatchOutputs outputs,
|
||
int period,
|
||
double multiplier)
|
||
{
|
||
_ = CalculateScalarCoreWithState(high, low, close, outputs, period, multiplier);
|
||
}
|
||
|
||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||
private static ScalarState CalculateScalarCoreWithState(
|
||
ReadOnlySpan<double> high,
|
||
ReadOnlySpan<double> low,
|
||
ReadOnlySpan<double> close,
|
||
BatchOutputs outputs,
|
||
int period,
|
||
double multiplier)
|
||
{
|
||
int len = close.Length;
|
||
|
||
double smoothPeriod = Math.Sqrt(period);
|
||
double alpha = 2.0 / (smoothPeriod + 1.0);
|
||
double beta = 1.0 - alpha;
|
||
double betaSquared = beta * beta;
|
||
|
||
Span<double> middle = outputs.Middle;
|
||
Span<double> upper = outputs.Upper;
|
||
Span<double> lower = outputs.Lower;
|
||
|
||
var state = new ScalarState
|
||
{
|
||
Ema1Price = 0,
|
||
Ema2Price = 0,
|
||
Ema1Range = 0,
|
||
Ema2Range = 0,
|
||
E = 1.0,
|
||
LastValidPrice = double.NaN,
|
||
LastValidHigh = double.NaN,
|
||
LastValidLow = double.NaN,
|
||
IsHot = false,
|
||
};
|
||
|
||
// Seed first valid values
|
||
SeedFirstValidValues(high, low, close, ref state);
|
||
|
||
for (int i = 0; i < len; i++)
|
||
{
|
||
double price = close[i];
|
||
double h = high[i];
|
||
double l = low[i];
|
||
|
||
// Get valid values
|
||
if (double.IsFinite(price))
|
||
{
|
||
state.LastValidPrice = price;
|
||
}
|
||
else
|
||
{
|
||
price = state.LastValidPrice;
|
||
}
|
||
|
||
if (double.IsFinite(h))
|
||
{
|
||
state.LastValidHigh = h;
|
||
}
|
||
else
|
||
{
|
||
h = state.LastValidHigh;
|
||
}
|
||
|
||
if (double.IsFinite(l))
|
||
{
|
||
state.LastValidLow = l;
|
||
}
|
||
else
|
||
{
|
||
l = state.LastValidLow;
|
||
}
|
||
|
||
// Handle first valid value
|
||
if (double.IsNaN(price))
|
||
{
|
||
middle[i] = double.NaN;
|
||
upper[i] = double.NaN;
|
||
lower[i] = double.NaN;
|
||
continue;
|
||
}
|
||
|
||
double range = Math.Max(0, h - l);
|
||
|
||
// Double-smoothed EMA for price
|
||
state.Ema1Price = Math.FusedMultiplyAdd(state.Ema1Price, beta, alpha * price);
|
||
state.Ema2Price = Math.FusedMultiplyAdd(state.Ema2Price, beta, alpha * state.Ema1Price);
|
||
|
||
// Double-smoothed EMA for range
|
||
state.Ema1Range = Math.FusedMultiplyAdd(state.Ema1Range, beta, alpha * range);
|
||
state.Ema2Range = Math.FusedMultiplyAdd(state.Ema2Range, beta, alpha * state.Ema1Range);
|
||
|
||
double mid = state.Ema2Price;
|
||
double adaptiveRange = state.Ema2Range;
|
||
|
||
// Apply compound warmup compensation
|
||
if (!state.IsHot)
|
||
{
|
||
state.E *= betaSquared;
|
||
double compensator = 1.0 / (1.0 - state.E);
|
||
mid *= compensator;
|
||
adaptiveRange *= compensator;
|
||
|
||
if (state.E <= ConvergenceThreshold)
|
||
{
|
||
state.IsHot = true;
|
||
}
|
||
}
|
||
|
||
double bandWidth = multiplier * adaptiveRange;
|
||
middle[i] = mid;
|
||
upper[i] = mid + bandWidth;
|
||
lower[i] = mid - bandWidth;
|
||
}
|
||
|
||
return state;
|
||
}
|
||
|
||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||
private static void SeedFirstValidValues(
|
||
ReadOnlySpan<double> high,
|
||
ReadOnlySpan<double> low,
|
||
ReadOnlySpan<double> close,
|
||
ref ScalarState state)
|
||
{
|
||
int len = close.Length;
|
||
for (int k = 0; k < len; k++)
|
||
{
|
||
// Check all three values are finite before assigning state
|
||
if (double.IsFinite(close[k]) && double.IsFinite(high[k]) && double.IsFinite(low[k]))
|
||
{
|
||
state.LastValidPrice = close[k];
|
||
state.LastValidHigh = high[k];
|
||
state.LastValidLow = low[k];
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// Runs a high-performance batch calculation and returns a "Hot" APZ instance.
|
||
/// </summary>
|
||
public static ((TSeries Middle, TSeries Upper, TSeries Lower) Results, Apz Indicator) Calculate(TBarSeries source, int period, double multiplier = 2.0)
|
||
{
|
||
var apz = new Apz(period, multiplier);
|
||
var results = apz.Update(source);
|
||
return (results, apz);
|
||
}
|
||
}
|