mirror of
https://github.com/mihakralj/QuanTAlib.git
synced 2026-08-20 11:38:05 +00:00
feat: add new indicators (Decay, Edecay, MinusDi, MinusDm, PlusDi, PlusDm, Maxindex, Minindex, Sarext) and update pine scripts, core libs, validation tests, and python bindings
This commit is contained in:
@@ -18,12 +18,14 @@ Basic mathematical transforms and utility functions for time series. These build
|
||||
| [FFT](fft/Fft.md) | Fast Fourier Transform | Frequency-domain decomposition via FFT algorithm. |
|
||||
| [GAMMADIST](gammadist/Gammadist.md) | Gamma Distribution | Gamma probability distribution transform. |
|
||||
| [HIGHEST](highest/Highest.md) | Rolling Maximum | Maximum value over lookback window. |
|
||||
| [MAXINDEX](maxindex/Maxindex.md) | Rolling Maximum Index | Position of maximum value in rolling window. |
|
||||
| [IFFT](ifft/Ifft.md) | Inverse Fast Fourier Transform | Frequency-to-time domain reconstruction. |
|
||||
| [JERK](jerk/Jerk.md) | Jerk | Rate of acceleration; third derivative of price. |
|
||||
| [LINEARTRANS](lineartrans/Lineartrans.md) | Linear Transform | y = ax + b scaling transformation. |
|
||||
| [LOGNORMDIST](lognormdist/Lognormdist.md) | Log-normal Distribution | Log-normal probability distribution transform. |
|
||||
| [LOGTRANS](logtrans/Logtrans.md) | Logarithmic Transform | Natural log for percentage-based analysis. |
|
||||
| [LOWEST](lowest/Lowest.md) | Rolling Minimum | Minimum value over lookback window. |
|
||||
| [MININDEX](minindex/Minindex.md) | Rolling Minimum Index | Position of minimum value in rolling window. |
|
||||
| [NORMDIST](normdist/Normdist.md) | Normal Distribution | Gaussian probability distribution transform. |
|
||||
| [NORMALIZE](normalize/Normalize.md) | Min-Max Normalization | Scale to [0,1] range using rolling min/max. |
|
||||
| [POISSONDIST](poissondist/Poissondist.md) | Poisson Distribution | Poisson probability distribution transform. |
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// The MIT License (MIT)
|
||||
// Licensed under the Apache License, Version 2.0
|
||||
// © mihakralj
|
||||
//@version=6
|
||||
indicator("Acceleration (Slope of Slope) (ACCEL)", "ACCEL", overlay=false, precision=8)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// The MIT License (MIT)
|
||||
// Licensed under the Apache License, Version 2.0
|
||||
// © mihakralj
|
||||
//@version=6
|
||||
indicator("Beta Distribution CDF (BETADIST)", "BETADIST", overlay=false, precision=6)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// The MIT License (MIT)
|
||||
// Licensed under the Apache License, Version 2.0
|
||||
// © mihakralj
|
||||
//@version=6
|
||||
indicator("Binomial Distribution CDF (BINOMDIST)", "BINOMDIST", overlay=false, precision=6)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// The MIT License (MIT)
|
||||
// Licensed under the Apache License, Version 2.0
|
||||
// © mihakralj
|
||||
//@version=6
|
||||
indicator("Percentage Change (CHANGE)", "CHANGE", overlay=false, format=format.percent)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// The MIT License (MIT)
|
||||
// Licensed under the Apache License, Version 2.0
|
||||
// © mihakralj
|
||||
//@version=6
|
||||
indicator("Continuous Wavelet Transform (CWT)", "CWT", overlay=false, precision=6)
|
||||
|
||||
@@ -322,6 +322,33 @@ public class DwtValidationTests
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Dwt_Correction_Recomputes()
|
||||
{
|
||||
var ind = new Dwt(levels: 4);
|
||||
var t0 = DateTime.MinValue;
|
||||
|
||||
// Build state well past warmup (WarmupPeriod = 2^4 = 16)
|
||||
for (int i = 0; i < 50; i++)
|
||||
{
|
||||
ind.Update(new TValue(t0.AddSeconds(i), 100.0 + i * 0.5));
|
||||
}
|
||||
|
||||
// Anchor bar
|
||||
var anchorTime = t0.AddSeconds(50);
|
||||
const double anchorPrice = 125.0;
|
||||
ind.Update(new TValue(anchorTime, anchorPrice), isNew: true);
|
||||
double anchorResult = ind.Last.Value;
|
||||
|
||||
// Correction with dramatically different value — DWT uses anchor at lag 0
|
||||
ind.Update(new TValue(anchorTime, anchorPrice * 10), isNew: false);
|
||||
Assert.NotEqual(anchorResult, ind.Last.Value);
|
||||
|
||||
// Correction back to original — must exactly restore
|
||||
ind.Update(new TValue(anchorTime, anchorPrice), isNew: false);
|
||||
Assert.Equal(anchorResult, ind.Last.Value, 1e-9);
|
||||
}
|
||||
|
||||
// ─── Helper ──────────────────────────────────────────────────────────────
|
||||
|
||||
private static double Variance(List<double> vals)
|
||||
|
||||
@@ -313,6 +313,14 @@ public sealed class Dwt : AbstractBase
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Primes the indicator with historical values.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Synthetic timestamps are generated by subtracting <c>step × source.Length</c>
|
||||
/// from <see cref="DateTime.UtcNow"/>. For deterministic or replay-safe pipelines
|
||||
/// use <see cref="Update(TValue, bool)"/> directly with explicit timestamps.
|
||||
/// </remarks>
|
||||
public override void Prime(ReadOnlySpan<double> source, TimeSpan? step = null)
|
||||
{
|
||||
TimeSpan interval = step ?? TimeSpan.FromSeconds(1);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// The MIT License (MIT)
|
||||
// Licensed under the Apache License, Version 2.0
|
||||
// © mihakralj
|
||||
//@version=6
|
||||
indicator("Discrete Wavelet Transform (DWT)", "DWT", overlay=false, precision=6)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// The MIT License (MIT)
|
||||
// Licensed under the Apache License, Version 2.0
|
||||
// © mihakralj
|
||||
//@version=6
|
||||
indicator("Exponential Distribution CDF (EXPDIST)", "EXPDIST", overlay=false, precision=6)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// The MIT License (MIT)
|
||||
// Licensed under the Apache License, Version 2.0
|
||||
// © mihakralj
|
||||
//@version=6
|
||||
indicator("Exponential Transformation (EXP)", "Exptrans", overlay=false)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// The MIT License (MIT)
|
||||
// Licensed under the Apache License, Version 2.0
|
||||
// © mihakralj
|
||||
//@version=6
|
||||
indicator("F-Distribution CDF (FDIST)", "FDIST", overlay=false, precision=6)
|
||||
|
||||
@@ -233,4 +233,32 @@ public class FftValidationTests
|
||||
$"Output at {i} must be finite, got {dst[i]}");
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Fft_Correction_StateRestores()
|
||||
{
|
||||
// The Hanning window maps idx=0 to the newest bar and _hanning[0]=0, so
|
||||
// corrections to the anchor bar carry zero spectral weight. isNew=false
|
||||
// determinism is still correct: restoring the original value reproduces the
|
||||
// original result exactly regardless of any intermediate correction.
|
||||
var ind = new Fft(windowSize: 32, maxPeriod: 16);
|
||||
var t0 = DateTime.MinValue;
|
||||
|
||||
for (int i = 0; i < 50; i++)
|
||||
{
|
||||
ind.Update(new TValue(t0.AddSeconds(i), 100.0 + 10.0 * Math.Sin(2 * Math.PI * i / 8.0)));
|
||||
}
|
||||
|
||||
var anchorTime = t0.AddSeconds(50);
|
||||
const double anchorPrice = 100.0;
|
||||
ind.Update(new TValue(anchorTime, anchorPrice), isNew: true);
|
||||
double anchorResult = ind.Last.Value;
|
||||
|
||||
// Apply an arbitrary correction — spectral output is unchanged due to zero Hanning weight
|
||||
ind.Update(new TValue(anchorTime, anchorPrice * 100), isNew: false);
|
||||
|
||||
// Restoring to original must exactly reproduce the original result
|
||||
ind.Update(new TValue(anchorTime, anchorPrice), isNew: false);
|
||||
Assert.Equal(anchorResult, ind.Last.Value, 1e-9);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -212,6 +212,14 @@ public sealed class Fft : AbstractBase
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Primes the indicator with historical values.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Synthetic timestamps are generated by subtracting <c>step × source.Length</c>
|
||||
/// from <see cref="DateTime.UtcNow"/>. For deterministic or replay-safe pipelines
|
||||
/// use <see cref="Update(TValue, bool)"/> directly with explicit timestamps.
|
||||
/// </remarks>
|
||||
public override void Prime(ReadOnlySpan<double> source, TimeSpan? step = null)
|
||||
{
|
||||
TimeSpan interval = step ?? TimeSpan.FromSeconds(1);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// The MIT License (MIT)
|
||||
// Licensed under the Apache License, Version 2.0
|
||||
// © mihakralj
|
||||
//@version=6
|
||||
indicator("FFT Dominant Cycle (Radix-2 FFT)", "FFT-DC", overlay=false, precision=2)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// The MIT License (MIT)
|
||||
// Licensed under the Apache License, Version 2.0
|
||||
// © mihakralj
|
||||
//@version=6
|
||||
indicator("Gamma Distribution CDF (GAMMADIST)", "GAMMADIST", overlay=false, precision=6)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// The MIT License (MIT)
|
||||
// Licensed under the Apache License, Version 2.0
|
||||
// © mihakralj
|
||||
//@version=6
|
||||
indicator("Highest Value (HIGHEST)", "HIGHEST", overlay=true)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// The MIT License (MIT)
|
||||
// Licensed under the Apache License, Version 2.0
|
||||
// © mihakralj
|
||||
//@version=6
|
||||
indicator("Inverse Fast Fourier Transform (IFFT)", "IFFT", overlay=true, precision=6)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// The MIT License (MIT)
|
||||
// Licensed under the Apache License, Version 2.0
|
||||
// © mihakralj
|
||||
//@version=6
|
||||
indicator("Acceleration, Slope of Slope (JERK)", "JERK", overlay=false, precision=8)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// The MIT License (MIT)
|
||||
// Licensed under the Apache License, Version 2.0
|
||||
// © mihakralj
|
||||
//@version=6
|
||||
indicator("Linear Transformation (LINEAR)", "Lineartrans", overlay=false)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// The MIT License (MIT)
|
||||
// Licensed under the Apache License, Version 2.0
|
||||
// © mihakralj
|
||||
//@version=6
|
||||
indicator("Log-Normal Distribution CDF (LOGNORMDIST)", "LOGNORMDIST", overlay=false, precision=6)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// The MIT License (MIT)
|
||||
// Licensed under the Apache License, Version 2.0
|
||||
// © mihakralj
|
||||
//@version=6
|
||||
indicator("Logarithmic Transformation (LOG)", "Logtrans", overlay=false)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// The MIT License (MIT)
|
||||
// Licensed under the Apache License, Version 2.0
|
||||
// © mihakralj
|
||||
//@version=6
|
||||
indicator("Lowest Value (LOWEST)", "LOWEST", overlay=true)
|
||||
|
||||
@@ -0,0 +1,272 @@
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace QuanTAlib;
|
||||
|
||||
/// <summary>
|
||||
/// MAXINDEX: Rolling Maximum Index
|
||||
/// Returns the position of the maximum value within a rolling window.
|
||||
/// Streaming mode: bars-ago offset (0=current, period-1=oldest).
|
||||
/// Batch span mode: absolute array index (TA-Lib compatible).
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Key properties:
|
||||
/// - Returns the index/position of the highest value, not the value itself
|
||||
/// - Streaming output is "bars-ago" offset for natural streaming consumption
|
||||
/// - Batch(ReadOnlySpan) output is absolute array index matching TA-Lib MAXINDEX
|
||||
/// - Tie-breaking: last occurrence wins (most recent bar, using >= comparison)
|
||||
/// - Can be cross-validated: source[Maxindex.Batch[i]] == Highest.Batch[i]
|
||||
/// </remarks>
|
||||
[SkipLocalsInit]
|
||||
public sealed class Maxindex : AbstractBase
|
||||
{
|
||||
private readonly int _period;
|
||||
private readonly RingBuffer _buffer;
|
||||
private record struct State(double LastValid);
|
||||
private State _state, _p_state;
|
||||
|
||||
public override bool IsHot => _buffer.Count >= _period;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new Maxindex indicator with specified lookback period.
|
||||
/// </summary>
|
||||
/// <param name="period">Lookback window size (must be >= 2)</param>
|
||||
public Maxindex(int period)
|
||||
{
|
||||
if (period < 2)
|
||||
{
|
||||
throw new ArgumentException("Period must be >= 2", nameof(period));
|
||||
}
|
||||
|
||||
_period = period;
|
||||
_buffer = new RingBuffer(period);
|
||||
Name = $"Maxindex({period})";
|
||||
WarmupPeriod = period;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new Maxindex indicator with source for event-based chaining.
|
||||
/// </summary>
|
||||
/// <param name="source">Source indicator for chaining</param>
|
||||
/// <param name="period">Lookback window size</param>
|
||||
public Maxindex(ITValuePublisher source, int period) : this(period)
|
||||
{
|
||||
source.Pub += HandleUpdate;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private void HandleUpdate(object? sender, in TValueEventArgs e) => Update(e.Value, e.IsNew);
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public override TValue Update(TValue input, bool isNew = true)
|
||||
{
|
||||
if (isNew)
|
||||
{
|
||||
_p_state = _state;
|
||||
}
|
||||
else
|
||||
{
|
||||
_state = _p_state;
|
||||
}
|
||||
|
||||
double value = double.IsFinite(input.Value) ? input.Value : _state.LastValid;
|
||||
_state = new State(value);
|
||||
|
||||
_buffer.Add(value, isNew);
|
||||
|
||||
// Scan the ring buffer to find the bars-ago index of the maximum value.
|
||||
// Tie-breaking: >= means last occurrence (most recent) wins.
|
||||
ReadOnlySpan<double> span = _buffer.GetSpan();
|
||||
int len = span.Length;
|
||||
double maxVal = span[0];
|
||||
int maxPos = 0;
|
||||
for (int i = 1; i < len; i++)
|
||||
{
|
||||
if (span[i] >= maxVal)
|
||||
{
|
||||
maxVal = span[i];
|
||||
maxPos = i;
|
||||
}
|
||||
}
|
||||
|
||||
// Convert to bars-ago: newest element is at index (len - 1), oldest at 0.
|
||||
// bars-ago = (len - 1) - maxPos
|
||||
double result = (len - 1) - maxPos;
|
||||
|
||||
Last = new TValue(input.Time, result);
|
||||
PubEvent(Last, isNew);
|
||||
return Last;
|
||||
}
|
||||
|
||||
public override TSeries Update(TSeries source)
|
||||
{
|
||||
var result = new TSeries(source.Count);
|
||||
ReadOnlySpan<double> values = source.Values;
|
||||
ReadOnlySpan<long> times = source.Times;
|
||||
|
||||
for (int i = 0; i < source.Count; i++)
|
||||
{
|
||||
var tv = Update(new TValue(new DateTime(times[i], DateTimeKind.Utc), values[i]), true);
|
||||
result.Add(tv, true);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public override void Prime(ReadOnlySpan<double> source, TimeSpan? step = null)
|
||||
{
|
||||
TimeSpan interval = step ?? TimeSpan.FromSeconds(1);
|
||||
DateTime time = DateTime.UtcNow - (interval * source.Length);
|
||||
|
||||
for (int i = 0; i < source.Length; i++)
|
||||
{
|
||||
Update(input: new TValue(time, source[i]), isNew: true);
|
||||
time += interval;
|
||||
}
|
||||
}
|
||||
|
||||
public static TSeries Batch(TSeries source, int period)
|
||||
{
|
||||
var indicator = new Maxindex(period);
|
||||
return indicator.Update(source);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates rolling maximum index over a span of values.
|
||||
/// Output contains ABSOLUTE array indices (TA-Lib MAXINDEX compatible).
|
||||
/// </summary>
|
||||
public static void Batch(ReadOnlySpan<double> source, Span<double> output, int period)
|
||||
{
|
||||
if (source.Length == 0)
|
||||
{
|
||||
throw new ArgumentException("Source cannot be empty", nameof(source));
|
||||
}
|
||||
|
||||
if (output.Length < source.Length)
|
||||
{
|
||||
throw new ArgumentException("Output length must be >= source length", nameof(output));
|
||||
}
|
||||
|
||||
if (period < 2)
|
||||
{
|
||||
throw new ArgumentException("Period must be >= 2", nameof(period));
|
||||
}
|
||||
|
||||
int len = source.Length;
|
||||
|
||||
// Use monotonic deque algorithm — same as Highest but output index, not value.
|
||||
int[]? rentedDeque = null;
|
||||
double[]? rentedValues = null;
|
||||
|
||||
#pragma warning disable S1121 // Assignments should not be made from within sub-expressions
|
||||
Span<int> deque = period <= 256
|
||||
? stackalloc int[period]
|
||||
: (rentedDeque = System.Buffers.ArrayPool<int>.Shared.Rent(period)).AsSpan(0, period);
|
||||
|
||||
Span<double> values = len <= 256
|
||||
? stackalloc double[len]
|
||||
: (rentedValues = System.Buffers.ArrayPool<double>.Shared.Rent(len)).AsSpan(0, len);
|
||||
#pragma warning restore S1121
|
||||
|
||||
try
|
||||
{
|
||||
// First pass: store corrected values (handle NaN/Infinity)
|
||||
double lastValid = 0.0;
|
||||
for (int i = 0; i < len; i++)
|
||||
{
|
||||
double val = source[i];
|
||||
if (double.IsFinite(val))
|
||||
{
|
||||
lastValid = val;
|
||||
values[i] = val;
|
||||
}
|
||||
else
|
||||
{
|
||||
values[i] = lastValid;
|
||||
}
|
||||
}
|
||||
|
||||
// Second pass: compute rolling max index using monotonic deque.
|
||||
// Circular buffer indexing — branch-based wrapping is faster than modulo.
|
||||
int head = 0; // front of deque (oldest/max)
|
||||
int tail = 0; // back of deque (newest)
|
||||
int count = 0; // number of elements in deque
|
||||
int capacity = deque.Length;
|
||||
|
||||
for (int i = 0; i < len; i++)
|
||||
{
|
||||
double value = values[i];
|
||||
|
||||
// Remove indices outside window from front
|
||||
while (count > 0 && deque[head] <= i - period)
|
||||
{
|
||||
head++;
|
||||
if (head >= capacity)
|
||||
{
|
||||
head -= capacity;
|
||||
}
|
||||
|
||||
count--;
|
||||
}
|
||||
|
||||
// Remove smaller-or-equal values from back (>= tie-breaking: last occurrence wins)
|
||||
while (count > 0)
|
||||
{
|
||||
int backIdx = tail - 1;
|
||||
if (backIdx < 0)
|
||||
{
|
||||
backIdx += capacity;
|
||||
}
|
||||
|
||||
if (values[deque[backIdx]] <= value)
|
||||
{
|
||||
tail = backIdx;
|
||||
count--;
|
||||
}
|
||||
else
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Add current index at tail
|
||||
deque[tail] = i;
|
||||
tail++;
|
||||
if (tail >= capacity)
|
||||
{
|
||||
tail -= capacity;
|
||||
}
|
||||
|
||||
count++;
|
||||
|
||||
// Output the ABSOLUTE index of the maximum (not the value)
|
||||
output[i] = deque[head];
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (rentedDeque != null)
|
||||
{
|
||||
System.Buffers.ArrayPool<int>.Shared.Return(rentedDeque);
|
||||
}
|
||||
|
||||
if (rentedValues != null)
|
||||
{
|
||||
System.Buffers.ArrayPool<double>.Shared.Return(rentedValues);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static (TSeries Results, Maxindex Indicator) Calculate(TSeries source, int period)
|
||||
{
|
||||
var indicator = new Maxindex(period);
|
||||
TSeries results = indicator.Update(source);
|
||||
return (results, indicator);
|
||||
}
|
||||
|
||||
public override void Reset()
|
||||
{
|
||||
_buffer.Clear();
|
||||
_state = default;
|
||||
_p_state = default;
|
||||
Last = default;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,167 @@
|
||||
# MAXINDEX: Rolling Maximum Index
|
||||
|
||||
| Property | Value |
|
||||
| ---------------- | -------------------------------- |
|
||||
| **Category** | Numeric |
|
||||
| **Inputs** | Source (close) |
|
||||
| **Parameters** | `period` (default=14, min=2) |
|
||||
| **Outputs** | Single series (Maxindex) |
|
||||
| **Output range** | Streaming: 0 to period-1 (bars-ago); Batch span: absolute array index |
|
||||
| **Warmup** | `period` bars |
|
||||
|
||||
### TL;DR
|
||||
|
||||
- MAXINDEX finds the position (index) of the maximum value within a rolling lookback window.
|
||||
- Parameterized by `period` (minimum 2).
|
||||
- Streaming mode outputs bars-ago offset (0 = current bar holds the max, period-1 = oldest bar).
|
||||
- Batch span mode outputs absolute array indices (TA-Lib MAXINDEX compatible).
|
||||
- Tie-breaking: last occurrence wins (most recent bar, `>=` comparison).
|
||||
- Requires `period` bars of warmup before first valid output (IsHot = true).
|
||||
- Cross-validation: `source[Maxindex.Batch[i]] == Highest.Batch[i]` for all bars after warmup.
|
||||
|
||||
> "It's not just about the peak — it's about *when* the peak occurred."
|
||||
|
||||
MAXINDEX identifies the position of the maximum value within a rolling window. While HIGHEST tells you the peak *value*, MAXINDEX tells you *where* that peak is relative to the current bar. This is essential for pattern recognition, timing analysis, and detecting how "stale" a high is.
|
||||
|
||||
## Historical Context
|
||||
|
||||
The MAXINDEX function originates from TA-Lib (TA_MAXINDEX), used in quantitative trading systems to identify when the highest price in a lookback window occurred. This timing information is critical for:
|
||||
|
||||
- **Breakout freshness**: A max at position 0 means the breakout is happening *now*; at position period-1, the high is stale and fading.
|
||||
- **Pattern detection**: Identifying head-and-shoulders, double tops, and other formations requires knowing *when* peaks occurred.
|
||||
- **Momentum analysis**: The position of the high within the window indicates whether momentum is building or decaying.
|
||||
|
||||
## Architecture & Physics
|
||||
|
||||
### 1. Streaming Mode — Bars-Ago Offset
|
||||
|
||||
In streaming mode, the output represents how many bars ago the maximum occurred:
|
||||
|
||||
$$
|
||||
\text{Maxindex}_t = t - \arg\max_{t-n+1 \leq k \leq t} V_k
|
||||
$$
|
||||
|
||||
where $n$ is the lookback period. A value of 0 means the current bar is the maximum; a value of $n-1$ means the oldest bar in the window holds the maximum.
|
||||
|
||||
### 2. Batch Span Mode — Absolute Index
|
||||
|
||||
In the `Batch(ReadOnlySpan)` method, output is the absolute array index:
|
||||
|
||||
$$
|
||||
\text{output}[i] = \arg\max_{i-n+1 \leq k \leq i} V_k
|
||||
$$
|
||||
|
||||
This matches TA-Lib's MAXINDEX convention and enables direct array lookup: `source[output[i]]` yields the maximum value.
|
||||
|
||||
### 3. Tie-Breaking
|
||||
|
||||
When multiple values in the window are equal to the maximum, the **most recent** (rightmost) occurrence wins:
|
||||
|
||||
$$
|
||||
\text{Maxindex}_t = \max \{ k : V_k = \max(\text{window}) \}
|
||||
$$
|
||||
|
||||
This is achieved using `>=` comparison, matching TA-Lib behavior.
|
||||
|
||||
### 4. Monotonic Deque (Batch Mode)
|
||||
|
||||
The batch span method uses the same O(n) monotonic deque algorithm as Highest, but outputs the index stored at the deque head rather than the value at that index:
|
||||
|
||||
```
|
||||
// Highest: output[i] = values[deque.PeekHead()] → the VALUE
|
||||
// Maxindex: output[i] = deque.PeekHead() → the INDEX
|
||||
```
|
||||
|
||||
### 5. Bar Correction via Rollback
|
||||
|
||||
When `isNew=false`, the indicator:
|
||||
1. Restores previous state (`_state = _p_state`)
|
||||
2. Replaces the last value in the buffer
|
||||
3. Re-scans the buffer to find the new maximum position
|
||||
|
||||
## Mathematical Foundation
|
||||
|
||||
### Rolling Maximum Index Definition
|
||||
|
||||
$$
|
||||
\text{Maxindex}_t = \arg\max_{t-n+1 \leq k \leq t} V_k
|
||||
$$
|
||||
|
||||
where $n$ is the lookback period and ties are broken in favor of the most recent occurrence.
|
||||
|
||||
### Partial Window Behavior
|
||||
|
||||
Before the window is full:
|
||||
|
||||
$$
|
||||
\text{Maxindex}_t = \arg\max_{0 \leq k \leq t} V_k \quad \text{for } t < n
|
||||
$$
|
||||
|
||||
### Complexity Analysis
|
||||
|
||||
| Operation | Streaming | Batch (Deque) |
|
||||
| :--- | :---: | :---: |
|
||||
| Per-update (worst) | O(n) | O(n) |
|
||||
| Per-update (amortized) | O(n) | O(1) |
|
||||
| Total for N updates | O(N×n) | O(N) |
|
||||
|
||||
Streaming uses a linear scan of the RingBuffer, which is O(period) per bar — acceptable for typical periods (5–30). Batch mode uses the monotonic deque for O(1) amortized.
|
||||
|
||||
## Performance Profile
|
||||
|
||||
### Streaming Mode (Linear Scan)
|
||||
|
||||
| Operation | Count | Cost (cycles) | Subtotal |
|
||||
| :--- | :---: | :---: | :---: |
|
||||
| CMP (scan) | period | 1 | period |
|
||||
| Array access | period | 3 | 3×period |
|
||||
| Index arithmetic | 2 | 1 | 2 |
|
||||
| **Total** | — | — | **~4×period cycles** |
|
||||
|
||||
### Batch Mode (Monotonic Deque)
|
||||
|
||||
| Operation | Count | Cost (cycles) | Subtotal |
|
||||
| :--- | :---: | :---: | :---: |
|
||||
| CMP (expired check) | 1 | 1 | 1 |
|
||||
| CMP (monotonicity) | ~2 avg | 1 | 2 |
|
||||
| Array access | 3 | 3 | 9 |
|
||||
| Index arithmetic | 2 | 1 | 2 |
|
||||
| **Total** | **~8** | — | **~14 cycles** |
|
||||
|
||||
### Quality Metrics
|
||||
|
||||
| Metric | Score | Notes |
|
||||
| :--- | :---: | :--- |
|
||||
| **Accuracy** | 10/10 | Exact index of maximum |
|
||||
| **Timeliness** | 10/10 | Zero lag for index detection |
|
||||
| **Smoothness** | 2/10 | Discrete jumps as window slides |
|
||||
| **Computational Cost** | 8/10 | O(period) streaming, O(1) batch |
|
||||
| **Memory** | 7/10 | O(n) for buffer + deque |
|
||||
|
||||
## Validation
|
||||
|
||||
| Library | Status | Notes |
|
||||
| :--- | :---: | :--- |
|
||||
| **TA-Lib MAXINDEX** | ✅ | Batch span output matches absolute indices |
|
||||
| **Cross-validation** | ✅ | `source[Maxindex[i]] == Highest[i]` for all valid bars |
|
||||
| **Known Values** | ✅ | Manual verification |
|
||||
|
||||
## Common Pitfalls
|
||||
|
||||
1. **Two Output Modes**: Streaming returns bars-ago offset; Batch(ReadOnlySpan) returns absolute array index. Do not mix them up.
|
||||
|
||||
2. **Period Minimum is 2**: Unlike Highest (which accepts period=1), Maxindex requires period >= 2, since the index of a single element is trivially 0.
|
||||
|
||||
3. **Tie-Breaking**: Uses `>=` so the most recent (rightmost) occurrence wins ties. This matches TA-Lib convention.
|
||||
|
||||
4. **Window Boundary Effects**: When the previous max expires from the window, the index can jump abruptly. This is expected behavior.
|
||||
|
||||
5. **Warmup Period**: `IsHot` becomes true after `period` values. Before warmup, returns index within available data.
|
||||
|
||||
6. **Using isNew Incorrectly**: Use `isNew: false` only when correcting the current bar. New bars must use `isNew: true`.
|
||||
|
||||
## References
|
||||
|
||||
- TA-Lib: MAXINDEX function documentation.
|
||||
- Lemire, Daniel. (2006). "Streaming Maximum-Minimum Filter Using No More than Three Comparisons per Element."
|
||||
- Tarjan, Robert E. (1985). "Amortized Computational Complexity." SIAM Journal on Algebraic Discrete Methods.
|
||||
@@ -0,0 +1,272 @@
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace QuanTAlib;
|
||||
|
||||
/// <summary>
|
||||
/// MININDEX: Rolling Minimum Index
|
||||
/// Returns the position of the minimum value within a rolling window.
|
||||
/// Streaming mode: bars-ago offset (0=current, period-1=oldest).
|
||||
/// Batch span mode: absolute array index (TA-Lib compatible).
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Key properties:
|
||||
/// - Returns the index/position of the lowest value, not the value itself
|
||||
/// - Streaming output is "bars-ago" offset for natural streaming consumption
|
||||
/// - Batch(ReadOnlySpan) output is absolute array index matching TA-Lib MININDEX
|
||||
/// - Tie-breaking: last occurrence wins (most recent bar, using <= comparison)
|
||||
/// - Can be cross-validated: source[Minindex.Batch[i]] == Lowest.Batch[i]
|
||||
/// </remarks>
|
||||
[SkipLocalsInit]
|
||||
public sealed class Minindex : AbstractBase
|
||||
{
|
||||
private readonly int _period;
|
||||
private readonly RingBuffer _buffer;
|
||||
private record struct State(double LastValid);
|
||||
private State _state, _p_state;
|
||||
|
||||
public override bool IsHot => _buffer.Count >= _period;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new Minindex indicator with specified lookback period.
|
||||
/// </summary>
|
||||
/// <param name="period">Lookback window size (must be >= 2)</param>
|
||||
public Minindex(int period)
|
||||
{
|
||||
if (period < 2)
|
||||
{
|
||||
throw new ArgumentException("Period must be >= 2", nameof(period));
|
||||
}
|
||||
|
||||
_period = period;
|
||||
_buffer = new RingBuffer(period);
|
||||
Name = $"Minindex({period})";
|
||||
WarmupPeriod = period;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new Minindex indicator with source for event-based chaining.
|
||||
/// </summary>
|
||||
/// <param name="source">Source indicator for chaining</param>
|
||||
/// <param name="period">Lookback window size</param>
|
||||
public Minindex(ITValuePublisher source, int period) : this(period)
|
||||
{
|
||||
source.Pub += HandleUpdate;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private void HandleUpdate(object? sender, in TValueEventArgs e) => Update(e.Value, e.IsNew);
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public override TValue Update(TValue input, bool isNew = true)
|
||||
{
|
||||
if (isNew)
|
||||
{
|
||||
_p_state = _state;
|
||||
}
|
||||
else
|
||||
{
|
||||
_state = _p_state;
|
||||
}
|
||||
|
||||
double value = double.IsFinite(input.Value) ? input.Value : _state.LastValid;
|
||||
_state = new State(value);
|
||||
|
||||
_buffer.Add(value, isNew);
|
||||
|
||||
// Scan the ring buffer to find the bars-ago index of the minimum value.
|
||||
// Tie-breaking: <= means last occurrence (most recent) wins.
|
||||
ReadOnlySpan<double> span = _buffer.GetSpan();
|
||||
int len = span.Length;
|
||||
double minVal = span[0];
|
||||
int minPos = 0;
|
||||
for (int i = 1; i < len; i++)
|
||||
{
|
||||
if (span[i] <= minVal)
|
||||
{
|
||||
minVal = span[i];
|
||||
minPos = i;
|
||||
}
|
||||
}
|
||||
|
||||
// Convert to bars-ago: newest element is at index (len - 1), oldest at 0.
|
||||
// bars-ago = (len - 1) - minPos
|
||||
double result = (len - 1) - minPos;
|
||||
|
||||
Last = new TValue(input.Time, result);
|
||||
PubEvent(Last, isNew);
|
||||
return Last;
|
||||
}
|
||||
|
||||
public override TSeries Update(TSeries source)
|
||||
{
|
||||
var result = new TSeries(source.Count);
|
||||
ReadOnlySpan<double> values = source.Values;
|
||||
ReadOnlySpan<long> times = source.Times;
|
||||
|
||||
for (int i = 0; i < source.Count; i++)
|
||||
{
|
||||
var tv = Update(new TValue(new DateTime(times[i], DateTimeKind.Utc), values[i]), true);
|
||||
result.Add(tv, true);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public override void Prime(ReadOnlySpan<double> source, TimeSpan? step = null)
|
||||
{
|
||||
TimeSpan interval = step ?? TimeSpan.FromSeconds(1);
|
||||
DateTime time = DateTime.UtcNow - (interval * source.Length);
|
||||
|
||||
for (int i = 0; i < source.Length; i++)
|
||||
{
|
||||
Update(input: new TValue(time, source[i]), isNew: true);
|
||||
time += interval;
|
||||
}
|
||||
}
|
||||
|
||||
public static TSeries Batch(TSeries source, int period)
|
||||
{
|
||||
var indicator = new Minindex(period);
|
||||
return indicator.Update(source);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates rolling minimum index over a span of values.
|
||||
/// Output contains ABSOLUTE array indices (TA-Lib MININDEX compatible).
|
||||
/// </summary>
|
||||
public static void Batch(ReadOnlySpan<double> source, Span<double> output, int period)
|
||||
{
|
||||
if (source.Length == 0)
|
||||
{
|
||||
throw new ArgumentException("Source cannot be empty", nameof(source));
|
||||
}
|
||||
|
||||
if (output.Length < source.Length)
|
||||
{
|
||||
throw new ArgumentException("Output length must be >= source length", nameof(output));
|
||||
}
|
||||
|
||||
if (period < 2)
|
||||
{
|
||||
throw new ArgumentException("Period must be >= 2", nameof(period));
|
||||
}
|
||||
|
||||
int len = source.Length;
|
||||
|
||||
// Use monotonic deque algorithm — same as Lowest but output index, not value.
|
||||
int[]? rentedDeque = null;
|
||||
double[]? rentedValues = null;
|
||||
|
||||
#pragma warning disable S1121 // Assignments should not be made from within sub-expressions
|
||||
Span<int> deque = period <= 256
|
||||
? stackalloc int[period]
|
||||
: (rentedDeque = System.Buffers.ArrayPool<int>.Shared.Rent(period)).AsSpan(0, period);
|
||||
|
||||
Span<double> values = len <= 256
|
||||
? stackalloc double[len]
|
||||
: (rentedValues = System.Buffers.ArrayPool<double>.Shared.Rent(len)).AsSpan(0, len);
|
||||
#pragma warning restore S1121
|
||||
|
||||
try
|
||||
{
|
||||
// First pass: store corrected values (handle NaN/Infinity)
|
||||
double lastValid = 0.0;
|
||||
for (int i = 0; i < len; i++)
|
||||
{
|
||||
double val = source[i];
|
||||
if (double.IsFinite(val))
|
||||
{
|
||||
lastValid = val;
|
||||
values[i] = val;
|
||||
}
|
||||
else
|
||||
{
|
||||
values[i] = lastValid;
|
||||
}
|
||||
}
|
||||
|
||||
// Second pass: compute rolling min index using monotonic deque.
|
||||
// Circular buffer indexing — branch-based wrapping is faster than modulo.
|
||||
int head = 0; // front of deque (oldest/min)
|
||||
int tail = 0; // back of deque (newest)
|
||||
int count = 0; // number of elements in deque
|
||||
int capacity = deque.Length;
|
||||
|
||||
for (int i = 0; i < len; i++)
|
||||
{
|
||||
double value = values[i];
|
||||
|
||||
// Remove indices outside window from front
|
||||
while (count > 0 && deque[head] <= i - period)
|
||||
{
|
||||
head++;
|
||||
if (head >= capacity)
|
||||
{
|
||||
head -= capacity;
|
||||
}
|
||||
|
||||
count--;
|
||||
}
|
||||
|
||||
// Remove larger-or-equal values from back (<= tie-breaking: last occurrence wins)
|
||||
while (count > 0)
|
||||
{
|
||||
int backIdx = tail - 1;
|
||||
if (backIdx < 0)
|
||||
{
|
||||
backIdx += capacity;
|
||||
}
|
||||
|
||||
if (values[deque[backIdx]] >= value)
|
||||
{
|
||||
tail = backIdx;
|
||||
count--;
|
||||
}
|
||||
else
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Add current index at tail
|
||||
deque[tail] = i;
|
||||
tail++;
|
||||
if (tail >= capacity)
|
||||
{
|
||||
tail -= capacity;
|
||||
}
|
||||
|
||||
count++;
|
||||
|
||||
// Output the ABSOLUTE index of the minimum (not the value)
|
||||
output[i] = deque[head];
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (rentedDeque != null)
|
||||
{
|
||||
System.Buffers.ArrayPool<int>.Shared.Return(rentedDeque);
|
||||
}
|
||||
|
||||
if (rentedValues != null)
|
||||
{
|
||||
System.Buffers.ArrayPool<double>.Shared.Return(rentedValues);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static (TSeries Results, Minindex Indicator) Calculate(TSeries source, int period)
|
||||
{
|
||||
var indicator = new Minindex(period);
|
||||
TSeries results = indicator.Update(source);
|
||||
return (results, indicator);
|
||||
}
|
||||
|
||||
public override void Reset()
|
||||
{
|
||||
_buffer.Clear();
|
||||
_state = default;
|
||||
_p_state = default;
|
||||
Last = default;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,167 @@
|
||||
# MININDEX: Rolling Minimum Index
|
||||
|
||||
| Property | Value |
|
||||
| ---------------- | -------------------------------- |
|
||||
| **Category** | Numeric |
|
||||
| **Inputs** | Source (close) |
|
||||
| **Parameters** | `period` (default=14, min=2) |
|
||||
| **Outputs** | Single series (Minindex) |
|
||||
| **Output range** | Streaming: 0 to period-1 (bars-ago); Batch span: absolute array index |
|
||||
| **Warmup** | `period` bars |
|
||||
|
||||
### TL;DR
|
||||
|
||||
- MININDEX finds the position (index) of the minimum value within a rolling lookback window.
|
||||
- Parameterized by `period` (minimum 2).
|
||||
- Streaming mode outputs bars-ago offset (0 = current bar holds the min, period-1 = oldest bar).
|
||||
- Batch span mode outputs absolute array indices (TA-Lib MININDEX compatible).
|
||||
- Tie-breaking: last occurrence wins (most recent bar, `<=` comparison).
|
||||
- Requires `period` bars of warmup before first valid output (IsHot = true).
|
||||
- Cross-validation: `source[Minindex.Batch[i]] == Lowest.Batch[i]` for all bars after warmup.
|
||||
|
||||
> "Finding support isn't just about the price — it's about *when* the floor was set."
|
||||
|
||||
MININDEX identifies the position of the minimum value within a rolling window. While LOWEST tells you the trough *value*, MININDEX tells you *where* that trough is relative to the current bar. This is essential for support analysis, timing studies, and detecting how "stale" a low is.
|
||||
|
||||
## Historical Context
|
||||
|
||||
The MININDEX function originates from TA-Lib (TA_MININDEX), used in quantitative trading systems to identify when the lowest price in a lookback window occurred. This timing information is critical for:
|
||||
|
||||
- **Support freshness**: A min at position 0 means support is being tested *now*; at position period-1, the low is stale and potentially irrelevant.
|
||||
- **Pattern detection**: Identifying double bottoms, inverse head-and-shoulders, and other formations requires knowing *when* troughs occurred.
|
||||
- **Exhaustion analysis**: The position of the low within the window indicates whether selling pressure is current or historical.
|
||||
|
||||
## Architecture & Physics
|
||||
|
||||
### 1. Streaming Mode — Bars-Ago Offset
|
||||
|
||||
In streaming mode, the output represents how many bars ago the minimum occurred:
|
||||
|
||||
$$
|
||||
\text{Minindex}_t = t - \arg\min_{t-n+1 \leq k \leq t} V_k
|
||||
$$
|
||||
|
||||
where $n$ is the lookback period. A value of 0 means the current bar is the minimum; a value of $n-1$ means the oldest bar in the window holds the minimum.
|
||||
|
||||
### 2. Batch Span Mode — Absolute Index
|
||||
|
||||
In the `Batch(ReadOnlySpan)` method, output is the absolute array index:
|
||||
|
||||
$$
|
||||
\text{output}[i] = \arg\min_{i-n+1 \leq k \leq i} V_k
|
||||
$$
|
||||
|
||||
This matches TA-Lib's MININDEX convention and enables direct array lookup: `source[output[i]]` yields the minimum value.
|
||||
|
||||
### 3. Tie-Breaking
|
||||
|
||||
When multiple values in the window are equal to the minimum, the **most recent** (rightmost) occurrence wins:
|
||||
|
||||
$$
|
||||
\text{Minindex}_t = \max \{ k : V_k = \min(\text{window}) \}
|
||||
$$
|
||||
|
||||
This is achieved using `<=` comparison, matching TA-Lib behavior.
|
||||
|
||||
### 4. Monotonic Deque (Batch Mode)
|
||||
|
||||
The batch span method uses the same O(n) monotonic deque algorithm as Lowest, but outputs the index stored at the deque head rather than the value at that index:
|
||||
|
||||
```
|
||||
// Lowest: output[i] = values[deque.PeekHead()] → the VALUE
|
||||
// Minindex: output[i] = deque.PeekHead() → the INDEX
|
||||
```
|
||||
|
||||
### 5. Bar Correction via Rollback
|
||||
|
||||
When `isNew=false`, the indicator:
|
||||
1. Restores previous state (`_state = _p_state`)
|
||||
2. Replaces the last value in the buffer
|
||||
3. Re-scans the buffer to find the new minimum position
|
||||
|
||||
## Mathematical Foundation
|
||||
|
||||
### Rolling Minimum Index Definition
|
||||
|
||||
$$
|
||||
\text{Minindex}_t = \arg\min_{t-n+1 \leq k \leq t} V_k
|
||||
$$
|
||||
|
||||
where $n$ is the lookback period and ties are broken in favor of the most recent occurrence.
|
||||
|
||||
### Partial Window Behavior
|
||||
|
||||
Before the window is full:
|
||||
|
||||
$$
|
||||
\text{Minindex}_t = \arg\min_{0 \leq k \leq t} V_k \quad \text{for } t < n
|
||||
$$
|
||||
|
||||
### Complexity Analysis
|
||||
|
||||
| Operation | Streaming | Batch (Deque) |
|
||||
| :--- | :---: | :---: |
|
||||
| Per-update (worst) | O(n) | O(n) |
|
||||
| Per-update (amortized) | O(n) | O(1) |
|
||||
| Total for N updates | O(N×n) | O(N) |
|
||||
|
||||
Streaming uses a linear scan of the RingBuffer, which is O(period) per bar — acceptable for typical periods (5–30). Batch mode uses the monotonic deque for O(1) amortized.
|
||||
|
||||
## Performance Profile
|
||||
|
||||
### Streaming Mode (Linear Scan)
|
||||
|
||||
| Operation | Count | Cost (cycles) | Subtotal |
|
||||
| :--- | :---: | :---: | :---: |
|
||||
| CMP (scan) | period | 1 | period |
|
||||
| Array access | period | 3 | 3×period |
|
||||
| Index arithmetic | 2 | 1 | 2 |
|
||||
| **Total** | — | — | **~4×period cycles** |
|
||||
|
||||
### Batch Mode (Monotonic Deque)
|
||||
|
||||
| Operation | Count | Cost (cycles) | Subtotal |
|
||||
| :--- | :---: | :---: | :---: |
|
||||
| CMP (expired check) | 1 | 1 | 1 |
|
||||
| CMP (monotonicity) | ~2 avg | 1 | 2 |
|
||||
| Array access | 3 | 3 | 9 |
|
||||
| Index arithmetic | 2 | 1 | 2 |
|
||||
| **Total** | **~8** | — | **~14 cycles** |
|
||||
|
||||
### Quality Metrics
|
||||
|
||||
| Metric | Score | Notes |
|
||||
| :--- | :---: | :--- |
|
||||
| **Accuracy** | 10/10 | Exact index of minimum |
|
||||
| **Timeliness** | 10/10 | Zero lag for index detection |
|
||||
| **Smoothness** | 2/10 | Discrete jumps as window slides |
|
||||
| **Computational Cost** | 8/10 | O(period) streaming, O(1) batch |
|
||||
| **Memory** | 7/10 | O(n) for buffer + deque |
|
||||
|
||||
## Validation
|
||||
|
||||
| Library | Status | Notes |
|
||||
| :--- | :---: | :--- |
|
||||
| **TA-Lib MININDEX** | ✅ | Batch span output matches absolute indices |
|
||||
| **Cross-validation** | ✅ | `source[Minindex[i]] == Lowest[i]` for all valid bars |
|
||||
| **Known Values** | ✅ | Manual verification |
|
||||
|
||||
## Common Pitfalls
|
||||
|
||||
1. **Two Output Modes**: Streaming returns bars-ago offset; Batch(ReadOnlySpan) returns absolute array index. Do not mix them up.
|
||||
|
||||
2. **Period Minimum is 2**: Unlike Lowest (which accepts period=1), Minindex requires period >= 2, since the index of a single element is trivially 0.
|
||||
|
||||
3. **Tie-Breaking**: Uses `<=` so the most recent (rightmost) occurrence wins ties. This matches TA-Lib convention.
|
||||
|
||||
4. **Window Boundary Effects**: When the previous min expires from the window, the index can jump abruptly. This is expected behavior.
|
||||
|
||||
5. **Warmup Period**: `IsHot` becomes true after `period` values. Before warmup, returns index within available data.
|
||||
|
||||
6. **Using isNew Incorrectly**: Use `isNew: false` only when correcting the current bar. New bars must use `isNew: true`.
|
||||
|
||||
## References
|
||||
|
||||
- TA-Lib: MININDEX function documentation.
|
||||
- Lemire, Daniel. (2006). "Streaming Maximum-Minimum Filter Using No More than Three Comparisons per Element."
|
||||
- Tarjan, Robert E. (1985). "Amortized Computational Complexity." SIAM Journal on Algebraic Discrete Methods.
|
||||
@@ -1,4 +1,4 @@
|
||||
// The MIT License (MIT)
|
||||
// Licensed under the Apache License, Version 2.0
|
||||
// © mihakralj
|
||||
//@version=6
|
||||
indicator("Min-Max Normalization (NORMALIZE)", "NORMALIZE", overlay=false, precision=6)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// The MIT License (MIT)
|
||||
// Licensed under the Apache License, Version 2.0
|
||||
// © mihakralj
|
||||
//@version=6
|
||||
indicator("Normal Distribution CDF (NORMDIST)", "NORMDIST", overlay=false, precision=6)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// The MIT License (MIT)
|
||||
// Licensed under the Apache License, Version 2.0
|
||||
// © mihakralj
|
||||
//@version=6
|
||||
indicator("Poisson Distribution CDF (POISSONDIST)", "POISSONDIST", overlay=false, precision=6)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// The MIT License (MIT)
|
||||
// Licensed under the Apache License, Version 2.0
|
||||
// © mihakralj
|
||||
//@version=6
|
||||
indicator("Rectified Linear Unit (ReLU)", "ReLU", overlay=false, precision=6)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// The MIT License (MIT)
|
||||
// Licensed under the Apache License, Version 2.0
|
||||
// © mihakralj
|
||||
//@version=6
|
||||
indicator("Logistic Function (SIGMOID)", "SIGMOID", overlay=false, precision=6)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// The MIT License (MIT)
|
||||
// Licensed under the Apache License, Version 2.0
|
||||
// © mihakralj
|
||||
//@version=6
|
||||
indicator("Slope, Linear Regression (SLOPE)", "SLOPE", overlay=false, precision=8)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// The MIT License (MIT)
|
||||
// Licensed under the Apache License, Version 2.0
|
||||
// © mihakralj
|
||||
//@version=6
|
||||
indicator("Square Root Transformation (SQRT)", "Sqrttrans", overlay=false)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// The MIT License (MIT)
|
||||
// Licensed under the Apache License, Version 2.0
|
||||
// © mihakralj
|
||||
//@version=6
|
||||
indicator("Student's t-Distribution CDF (TDIST)", "TDIST", overlay=false, precision=6)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// The MIT License (MIT)
|
||||
// Licensed under the Apache License, Version 2.0
|
||||
// © mihakralj
|
||||
//@version=6
|
||||
indicator("Weibull Distribution CDF (WEIBULLDIST)", "WEIBULLDIST", overlay=false, precision=6)
|
||||
|
||||
Reference in New Issue
Block a user