mirror of
https://github.com/mihakralj/QuanTAlib.git
synced 2026-07-27 17:27:43 +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:
@@ -1,4 +1,4 @@
|
||||
Apache License
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
@@ -48,7 +48,7 @@
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
submitted to the Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
@@ -60,7 +60,7 @@
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
on behalf of whom a Contribution has been received by the Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
@@ -106,7 +106,7 @@
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
within such NOTICE file, excluding any notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
@@ -175,18 +175,7 @@
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
Copyright 2024 Miha Kralj
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
||||
@@ -197,11 +197,15 @@
|
||||
* [CHOP - Choppiness Index](/lib/dynamics/chop/Chop.md)
|
||||
* [DMX - Jurik Directional Movement Index](/lib/dynamics/dmx/Dmx.md)
|
||||
* [DX - Directional Movement Index](/lib/dynamics/dx/Dx.md)
|
||||
* [MINUS_DI - Minus Directional Indicator](/lib/dynamics/minus_di/MinusDi.md)
|
||||
* [MINUS_DM - Minus Directional Movement](/lib/dynamics/minus_dm/MinusDm.md)
|
||||
* [GHLA - Gann High-Low Activator](/lib/dynamics/ghla/Ghla.md)
|
||||
* [HT_TRENDMODE - Ehlers Hilbert Transform Trend vs Cycle Mode](/lib/dynamics/ht_trendmode/HtTrendmode.md)
|
||||
* [ICHIMOKU - Ichimoku Cloud](/lib/dynamics/ichimoku/Ichimoku.md)
|
||||
* [IMPULSE - Elder Impulse System](/lib/dynamics/impulse/Impulse.md)
|
||||
* [PFE - Polarized Fractal Efficiency](/lib/dynamics/pfe/Pfe.md)
|
||||
* [PLUS_DI - Plus Directional Indicator](/lib/dynamics/plus_di/PlusDi.md)
|
||||
* [PLUS_DM - Plus Directional Movement](/lib/dynamics/plus_dm/PlusDm.md)
|
||||
* [QSTICK - Qstick Indicator](/lib/dynamics/qstick/Qstick.md)
|
||||
* [RAVI - Chande Range Action Verification Index](/lib/dynamics/ravi/Ravi.md)
|
||||
* [SUPER - SuperTrend](/lib/dynamics/super/Super.md)
|
||||
@@ -369,12 +373,14 @@
|
||||
* [FFT - Fast Fourier Transform](/lib/numerics/fft/Fft.md)
|
||||
* [GAMMADIST - Gamma Distribution](/lib/numerics/gammadist/Gammadist.md)
|
||||
* [HIGHEST - Rolling Maximum](/lib/numerics/highest/Highest.md)
|
||||
* [MAXINDEX - Index of Highest Value](/lib/numerics/maxindex/Maxindex.md)
|
||||
* [IFFT - Inverse Fast Fourier Transform](/lib/numerics/ifft/Ifft.md)
|
||||
* [JERK - Jerk](/lib/numerics/jerk/Jerk.md)
|
||||
* [LINEARTRANS - Linear Transform](/lib/numerics/lineartrans/Lineartrans.md)
|
||||
* [LOGNORMDIST - Log-Normal Distribution](/lib/numerics/lognormdist/Lognormdist.md)
|
||||
* [LOGTRANS - Logarithmic Transform](/lib/numerics/logtrans/Logtrans.md)
|
||||
* [LOWEST - Rolling Minimum](/lib/numerics/lowest/Lowest.md)
|
||||
* [MININDEX - Index of Lowest Value](/lib/numerics/minindex/Minindex.md)
|
||||
* [NORMDIST - Normal Distribution](/lib/numerics/normdist/Normdist.md)
|
||||
* [NORMALIZE - Min-Max Normalization](/lib/numerics/normalize/Normalize.md)
|
||||
* [POISSONDIST - Poisson Distribution](/lib/numerics/poissondist/Poissondist.md)
|
||||
@@ -447,6 +453,7 @@
|
||||
* [PIVOTFIB - Fibonacci Pivot Points](/lib/reversals/pivotfib/Pivotfib.md)
|
||||
* [PIVOTWOOD - Woodie's Pivot Points](/lib/reversals/pivotwood/Pivotwood.md)
|
||||
* [PSAR - Parabolic Stop And Reverse](/lib/reversals/psar/Psar.md)
|
||||
* [SAREXT - Parabolic SAR Extended](/lib/reversals/sarext/Sarext.md)
|
||||
* [SWINGS - Swing High/Low Detection](/lib/reversals/swings/Swings.md)
|
||||
* [TTM_SCALPER - TTM Scalper Alert](/lib/reversals/ttm_scalper/TtmScalper.md)
|
||||
|
||||
|
||||
@@ -194,12 +194,16 @@ Indicators measuring trend strength, regime, and directional movement quality.
|
||||
| [**CHOP**](../lib/dynamics/chop/Chop.md) | Choppiness Index | ATR sum vs range; trending vs choppy |
|
||||
| [**DMX**](../lib/dynamics/dmx/Dmx.md) | Jurik DMX | Enhanced directional movement |
|
||||
| [**DX**](../lib/dynamics/dx/Dx.md) | Directional Movement Index | Raw directional strength |
|
||||
| [**MINUS_DI**](../lib/dynamics/minusdi/MinusDi.md) | Minus Directional Indicator | Downward DI (0-100) |
|
||||
| [**MINUS_DM**](../lib/dynamics/minusdm/MinusDm.md) | Minus Directional Movement | Smoothed downward DM |
|
||||
| [**GHLA**](../lib/dynamics/ghla/Ghla.md) | Gann High-Low Activator | SMA(High)/SMA(Low) alternating on crossover |
|
||||
| [**HT_TRENDMODE**](../lib/dynamics/ht_trendmode/HtTrendmode.md) | Ehlers Hilbert Transform Trend vs Cycle Mode | Cycle vs trend regime detection |
|
||||
| [**ICHIMOKU**](../lib/dynamics/ichimoku/Ichimoku.md) | Ichimoku Cloud | Multi-component trend system |
|
||||
| [**IMPULSE**](../lib/dynamics/impulse/Impulse.md) | Elder Impulse System | EMA + MACD-H trend/momentum fusion |
|
||||
| [**QSTICK**](../lib/dynamics/qstick/Qstick.md) | Qstick | Average close-open difference |
|
||||
| [**PFE**](../lib/dynamics/pfe/Pfe.md) | Polarized Fractal Efficiency | Fractal path efficiency as trend strength |
|
||||
| [**PLUS_DI**](../lib/dynamics/plusdi/PlusDi.md) | Plus Directional Indicator | Upward DI (0-100) |
|
||||
| [**PLUS_DM**](../lib/dynamics/plusdm/PlusDm.md) | Plus Directional Movement | Smoothed upward DM |
|
||||
| [**RAVI**](../lib/dynamics/ravi/Ravi.md) | Chande Range Action Verification Index | Dual-SMA divergence as trend strength |
|
||||
| [**SUPER**](../lib/dynamics/super/Super.md) | SuperTrend | ATR-based trend bands |
|
||||
| [**TTM_SQUEEZE**](../lib/dynamics/ttm_squeeze/TtmSqueeze.md) | TTM Squeeze | BB inside KC squeeze with momentum |
|
||||
@@ -432,11 +436,13 @@ Mathematical transformations and derivative indicators. Building blocks for anal
|
||||
| [**DWT**](../lib/numerics/dwt/Dwt.md) | Discrete Wavelet Transform | À trous Haar stationary DWT — multi-resolution approximation + detail, WarmupPeriod = 2^levels |
|
||||
| [**EXPTRANS**](../lib/numerics/exptrans/Exptrans.md) | Exponential Transform | e^x for log-space reversal |
|
||||
| [**HIGHEST**](../lib/numerics/highest/Highest.md) | Rolling Maximum | O(1) via monotonic deque |
|
||||
| [**MAXINDEX**](../lib/numerics/maxindex/Maxindex.md) | Rolling Maximum Index | Position of max in window |
|
||||
| [**JERK**](../lib/numerics/jerk/Jerk.md) | Jerk (3rd Derivative) | Change in acceleration |
|
||||
| [**LINEARTRANS**](../lib/numerics/lineartrans/Lineartrans.md) | Linear Transform | y = ax + b scaling |
|
||||
| [**LOGNORMDIST**](../lib/numerics/lognormdist/Lognormdist.md) | Log-Normal Distribution | Log-normal CDF via min-max normalization |
|
||||
| [**LOGTRANS**](../lib/numerics/logtrans/Logtrans.md) | Logarithmic Transform | Natural log for percentage analysis |
|
||||
| [**LOWEST**](../lib/numerics/lowest/Lowest.md) | Rolling Minimum | O(1) via monotonic deque |
|
||||
| [**MININDEX**](../lib/numerics/minindex/Minindex.md) | Rolling Minimum Index | Position of min in window |
|
||||
| [**NORMALIZE**](../lib/numerics/normalize/Normalize.md) | Min-Max Normalization | Scale to [0,1] via rolling min/max |
|
||||
| [**RELU**](../lib/numerics/relu/Relu.md) | Rectified Linear Unit | max(0, x) activation |
|
||||
| [**SIGMOID**](../lib/numerics/sigmoid/Sigmoid.md) | Logistic Function | 1/(1+e^-x) bounded [0,1] |
|
||||
@@ -492,6 +498,7 @@ Reversal indicators identify potential turning points, compute adaptive stop lev
|
||||
| [**PIVOTFIB**](../lib/reversals/pivotfib/Pivotfib.md) | Fibonacci Pivot Points | Fibonacci pivots: 7 levels (PP, R1-R3, S1-S3); ratios 0.382/0.618/1.000 applied to range |
|
||||
| [**PIVOTWOOD**](../lib/reversals/pivotwood/Pivotwood.md) | Woodie's Pivot Points | Close-weighted pivots: 7 levels (PP, R1-R3, S1-S3); PP = (H+L+2C)/4 biased toward close |
|
||||
| [**PSAR**](../lib/reversals/psar/Psar.md) | Parabolic Stop And Reverse | Accelerating trailing stop; SAR dots flip on reversal; Welles Wilder (1978) |
|
||||
| [**SAREXT**](../lib/reversals/sarext/Sarext.md) | Parabolic SAR Extended | Asymmetric AF PSAR; sign-encoded output |
|
||||
| [**SWINGS**](../lib/reversals/swings/Swings.md) | Swing High/Low Detection | Configurable-lookback pattern detector; dual SwingHigh/SwingLow with persistent levels |
|
||||
| [**TTM_SCALPER**](../lib/reversals/ttm_scalper/TtmScalper.md) | TTM Scalper Alert | 3-bar pivot high/low detection for scalping entries; John Carter |
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// The MIT License (MIT)
|
||||
// Licensed under the Apache License, Version 2.0
|
||||
// © mihakralj
|
||||
//@version=6
|
||||
indicator("Aberration (ABERR)", "ABERR", overlay=true)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// The MIT License (MIT)
|
||||
// Licensed under the Apache License, Version 2.0
|
||||
// © mihakralj
|
||||
//@version=6
|
||||
indicator("Acceleration Bands (ACCBANDS)", "ACCBANDS", overlay=true)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// The MIT License (MIT)
|
||||
// Licensed under the Apache License, Version 2.0
|
||||
// © mihakralj
|
||||
//@version=6
|
||||
indicator("Adaptive Price Channel (APCHANNEL)", "APCHANNEL", overlay=true)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// The MIT License (MIT)
|
||||
// Licensed under the Apache License, Version 2.0
|
||||
// © mihakralj
|
||||
//@version=6
|
||||
indicator("Adaptive Price Zone", "APZ", overlay=true)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// The MIT License (MIT)
|
||||
// Licensed under the Apache License, Version 2.0
|
||||
// © mihakralj
|
||||
//@version=6
|
||||
indicator("ATR Bands (ATRBANDS)", "ATRBANDS", overlay=true)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// The MIT License (MIT)
|
||||
// Licensed under the Apache License, Version 2.0
|
||||
// © mihakralj
|
||||
//@version=6
|
||||
indicator("Bollinger Bands (BBANDS)", "BBANDS", overlay=true)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// The MIT License (MIT)
|
||||
// Licensed under the Apache License, Version 2.0
|
||||
// © mihakralj
|
||||
//@version=6
|
||||
indicator("Donchian Channels (DCHANNEL)", "DCHANNEL", overlay=true)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// The MIT License (MIT)
|
||||
// Licensed under the Apache License, Version 2.0
|
||||
// © mihakralj
|
||||
//@version=6
|
||||
indicator("Decay Min-Max Channel (DECAYCHANNEL)", "DECAYCHANNEL", overlay=true)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// The MIT License (MIT)
|
||||
// Licensed under the Apache License, Version 2.0
|
||||
// © mihakralj
|
||||
//@version=6
|
||||
indicator("Fractal Chaos Bands (FCB)", "FCB", overlay=true)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// The MIT License (MIT)
|
||||
// Licensed under the Apache License, Version 2.0
|
||||
// © mihakralj
|
||||
//@version=6
|
||||
indicator("Jurik Adaptive Envelope Bands", "JBANDS", overlay=true)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// The MIT License (MIT)
|
||||
// Licensed under the Apache License, Version 2.0
|
||||
// © mihakralj
|
||||
//@version=6
|
||||
indicator("Keltner Channel (KCHANNEL)", "KCHANNEL", overlay=true)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// The MIT License (MIT)
|
||||
// Licensed under the Apache License, Version 2.0
|
||||
// © mihakralj
|
||||
//@version=6
|
||||
indicator("MA Envelope (MAE)", "MAE", overlay=true)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// The MIT License (MIT)
|
||||
// Licensed under the Apache License, Version 2.0
|
||||
// © mihakralj
|
||||
//@version=6
|
||||
indicator("Min-Max Channel (MMCHANNEL)", "MMCHANNEL", overlay=true)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// The MIT License (MIT)
|
||||
// Licensed under the Apache License, Version 2.0
|
||||
// © mihakralj
|
||||
//@version=6
|
||||
indicator("Price Channel (PCHANNEL)", "PCHANNEL", overlay=true)
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using System.Buffers;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
@@ -206,7 +207,7 @@ public sealed class Regchannel : ITValuePublisher
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
sumY += values[i];
|
||||
sumXY += i * values[i];
|
||||
sumXY = Math.FusedMultiplyAdd((double)i, values[i], sumXY);
|
||||
}
|
||||
|
||||
double n = count;
|
||||
@@ -325,7 +326,11 @@ public sealed class Regchannel : ITValuePublisher
|
||||
|
||||
/// <summary>
|
||||
/// Batch calculation using spans.
|
||||
/// O(period) per bar: sums recomputed from circular buffer each bar for numerical
|
||||
/// consistency with the streaming path, plus closed-form residual variance:
|
||||
/// sumResiduals² = sumY² − intercept·sumY − slope·sumXY, eliminating a second O(period) pass.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveOptimization)]
|
||||
public static void Batch(
|
||||
ReadOnlySpan<double> source,
|
||||
Span<double> middle,
|
||||
@@ -360,24 +365,42 @@ public sealed class Regchannel : ITValuePublisher
|
||||
double sumX2Full = (period - 1.0) * period * (2.0 * period - 1.0) / 6.0;
|
||||
double denomFull = period * sumX2Full - sumXFull * sumXFull;
|
||||
|
||||
// Track last valid value for NaN substitution
|
||||
double lastValid = double.NaN;
|
||||
|
||||
for (int i = 0; i < len; i++)
|
||||
// Circular buffer of NaN-sanitised values for O(1) sliding-window recurrences.
|
||||
const int StackAllocThreshold = 256;
|
||||
double[]? rentedWindow = null;
|
||||
scoped Span<double> window;
|
||||
if (period <= StackAllocThreshold)
|
||||
{
|
||||
// Get valid value with last-valid substitution
|
||||
double currentValue = source[i];
|
||||
if (double.IsFinite(currentValue))
|
||||
{
|
||||
lastValid = currentValue;
|
||||
window = stackalloc double[period];
|
||||
}
|
||||
else
|
||||
{
|
||||
currentValue = lastValid;
|
||||
rentedWindow = ArrayPool<double>.Shared.Rent(period);
|
||||
window = rentedWindow.AsSpan(0, period);
|
||||
}
|
||||
|
||||
// If still NaN (no valid value seen yet), output NaN
|
||||
if (!double.IsFinite(currentValue))
|
||||
try
|
||||
{
|
||||
window.Clear();
|
||||
|
||||
double lastValid = double.NaN;
|
||||
int head = 0, count = 0;
|
||||
|
||||
for (int i = 0; i < len; i++)
|
||||
{
|
||||
// NaN substitution
|
||||
double y = source[i];
|
||||
if (double.IsFinite(y))
|
||||
{
|
||||
lastValid = y;
|
||||
}
|
||||
else
|
||||
{
|
||||
y = lastValid;
|
||||
}
|
||||
|
||||
// No valid value seen yet
|
||||
if (!double.IsFinite(y))
|
||||
{
|
||||
middle[i] = double.NaN;
|
||||
upper[i] = double.NaN;
|
||||
@@ -385,37 +408,32 @@ public sealed class Regchannel : ITValuePublisher
|
||||
continue;
|
||||
}
|
||||
|
||||
int count = Math.Min(i + 1, period);
|
||||
int start = i - count + 1;
|
||||
// Store in circular buffer
|
||||
window[head] = y;
|
||||
head = (head + 1) % period;
|
||||
if (count < period)
|
||||
{
|
||||
count++;
|
||||
}
|
||||
|
||||
if (count <= 1)
|
||||
{
|
||||
middle[i] = currentValue;
|
||||
upper[i] = currentValue;
|
||||
lower[i] = currentValue;
|
||||
middle[i] = y;
|
||||
upper[i] = y;
|
||||
lower[i] = y;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Calculate sums for linear regression with NaN handling
|
||||
double sumY = 0;
|
||||
double sumXY = 0;
|
||||
double lastValidInWindow = double.NaN;
|
||||
|
||||
for (int j = 0; j < count; j++)
|
||||
// Recompute sums fresh from circular buffer (oldest-to-newest).
|
||||
// This matches the streaming Update() path numerically.
|
||||
double sumY = 0, sumXY = 0, sumY2 = 0;
|
||||
int oldestIdx = (head - count + period) % period;
|
||||
for (int k = 0; k < count; k++)
|
||||
{
|
||||
double rawY = source[start + j];
|
||||
double y;
|
||||
if (double.IsFinite(rawY))
|
||||
{
|
||||
lastValidInWindow = rawY;
|
||||
y = rawY;
|
||||
}
|
||||
else
|
||||
{
|
||||
y = double.IsFinite(lastValidInWindow) ? lastValidInWindow : 0.0;
|
||||
}
|
||||
sumY += y;
|
||||
sumXY += j * y;
|
||||
double wk = window[(oldestIdx + k) % period];
|
||||
sumY += wk;
|
||||
sumXY = Math.FusedMultiplyAdd((double)k, wk, sumXY);
|
||||
sumY2 = Math.FusedMultiplyAdd(wk, wk, sumY2);
|
||||
}
|
||||
|
||||
double n = count;
|
||||
@@ -448,27 +466,9 @@ public sealed class Regchannel : ITValuePublisher
|
||||
regression = Math.FusedMultiplyAdd(slope, count - 1, intercept);
|
||||
}
|
||||
|
||||
// Calculate standard deviation of residuals with NaN handling
|
||||
double sumResiduals2 = 0;
|
||||
lastValidInWindow = double.NaN;
|
||||
for (int j = 0; j < count; j++)
|
||||
{
|
||||
double rawY = source[start + j];
|
||||
double y;
|
||||
if (double.IsFinite(rawY))
|
||||
{
|
||||
lastValidInWindow = rawY;
|
||||
y = rawY;
|
||||
}
|
||||
else
|
||||
{
|
||||
y = double.IsFinite(lastValidInWindow) ? lastValidInWindow : 0.0;
|
||||
}
|
||||
double predicted = Math.FusedMultiplyAdd(slope, j, intercept);
|
||||
double residual = y - predicted;
|
||||
sumResiduals2 = Math.FusedMultiplyAdd(residual, residual, sumResiduals2);
|
||||
}
|
||||
|
||||
// Closed-form residual variance (normal-equation identity):
|
||||
// sumResiduals² = sumY² − intercept·sumY − slope·sumXY
|
||||
double sumResiduals2 = Math.Max(0.0, sumY2 - intercept * sumY - slope * sumXY);
|
||||
double stdDev = Math.Sqrt(sumResiduals2 / n);
|
||||
double band = multiplier * stdDev;
|
||||
|
||||
@@ -477,6 +477,14 @@ public sealed class Regchannel : ITValuePublisher
|
||||
lower[i] = regression - band;
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (rentedWindow != null)
|
||||
{
|
||||
ArrayPool<double>.Shared.Return(rentedWindow);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static (TSeries Middle, TSeries Upper, TSeries Lower) Batch(TSeries source, int period = 20, double multiplier = 2.0)
|
||||
{
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// The MIT License (MIT)
|
||||
// Licensed under the Apache License, Version 2.0
|
||||
// © mihakralj
|
||||
//@version=6
|
||||
indicator("Regression Channels (REGCHANNEL)", "REGCHANNEL", overlay=true)
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using System.Buffers;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
@@ -325,7 +326,11 @@ public sealed class Sdchannel : ITValuePublisher
|
||||
|
||||
/// <summary>
|
||||
/// Batch calculation using spans.
|
||||
/// O(period) per bar: sums and residuals recomputed from circular buffer each bar for numerical
|
||||
/// consistency with the streaming path. Uses the same plain arithmetic as Update() for sumXY
|
||||
/// and the same FMA residual loop to ensure streaming/batch agreement within 1e-9.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveOptimization)]
|
||||
public static void Batch(
|
||||
ReadOnlySpan<double> source,
|
||||
Span<double> middle,
|
||||
@@ -360,28 +365,74 @@ public sealed class Sdchannel : ITValuePublisher
|
||||
double sumX2Full = (period - 1.0) * period * (2.0 * period - 1.0) / 6.0;
|
||||
double denomFull = period * sumX2Full - sumXFull * sumXFull;
|
||||
|
||||
// Circular buffer of NaN-sanitised values for O(1) sliding-window recurrences.
|
||||
const int StackAllocThreshold = 256;
|
||||
double[]? rentedWindow = null;
|
||||
scoped Span<double> window;
|
||||
if (period <= StackAllocThreshold)
|
||||
{
|
||||
window = stackalloc double[period];
|
||||
}
|
||||
else
|
||||
{
|
||||
rentedWindow = ArrayPool<double>.Shared.Rent(period);
|
||||
window = rentedWindow.AsSpan(0, period);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
window.Clear();
|
||||
|
||||
double lastValid = double.NaN;
|
||||
int head = 0, count = 0;
|
||||
|
||||
for (int i = 0; i < len; i++)
|
||||
{
|
||||
int count = Math.Min(i + 1, period);
|
||||
int start = i - count + 1;
|
||||
|
||||
if (count <= 1)
|
||||
// NaN substitution matching GetValid() in streaming Update()
|
||||
double y = source[i];
|
||||
if (double.IsFinite(y))
|
||||
{
|
||||
middle[i] = source[i];
|
||||
upper[i] = source[i];
|
||||
lower[i] = source[i];
|
||||
lastValid = y;
|
||||
}
|
||||
else
|
||||
{
|
||||
y = lastValid;
|
||||
}
|
||||
|
||||
// No valid value seen yet
|
||||
if (!double.IsFinite(y))
|
||||
{
|
||||
middle[i] = double.NaN;
|
||||
upper[i] = double.NaN;
|
||||
lower[i] = double.NaN;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Calculate sums for linear regression
|
||||
double sumY = 0;
|
||||
double sumXY = 0;
|
||||
|
||||
for (int j = 0; j < count; j++)
|
||||
// Store in circular buffer
|
||||
window[head] = y;
|
||||
head = (head + 1) % period;
|
||||
if (count < period)
|
||||
{
|
||||
double y = source[start + j];
|
||||
sumY += y;
|
||||
sumXY += j * y;
|
||||
count++;
|
||||
}
|
||||
|
||||
if (count <= 1)
|
||||
{
|
||||
middle[i] = y;
|
||||
upper[i] = y;
|
||||
lower[i] = y;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Recompute sums fresh from circular buffer (oldest-to-newest).
|
||||
// Plain arithmetic for sumXY matches streaming Update() path numerically.
|
||||
double sumY = 0, sumXY = 0;
|
||||
int oldestIdx = (head - count + period) % period;
|
||||
for (int k = 0; k < count; k++)
|
||||
{
|
||||
double wk = window[(oldestIdx + k) % period];
|
||||
sumY += wk;
|
||||
sumXY += (double)k * wk;
|
||||
}
|
||||
|
||||
double n = count;
|
||||
@@ -414,12 +465,13 @@ public sealed class Sdchannel : ITValuePublisher
|
||||
regression = Math.FusedMultiplyAdd(slope, count - 1, intercept);
|
||||
}
|
||||
|
||||
// Calculate standard deviation of residuals
|
||||
// Explicit residual loop matching streaming Update() path numerically.
|
||||
double sumResiduals2 = 0;
|
||||
for (int j = 0; j < count; j++)
|
||||
for (int k = 0; k < count; k++)
|
||||
{
|
||||
double predicted = Math.FusedMultiplyAdd(slope, j, intercept);
|
||||
double residual = source[start + j] - predicted;
|
||||
double wk = window[(oldestIdx + k) % period];
|
||||
double predicted = Math.FusedMultiplyAdd(slope, k, intercept);
|
||||
double residual = wk - predicted;
|
||||
sumResiduals2 = Math.FusedMultiplyAdd(residual, residual, sumResiduals2);
|
||||
}
|
||||
|
||||
@@ -431,6 +483,14 @@ public sealed class Sdchannel : ITValuePublisher
|
||||
lower[i] = regression - band;
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (rentedWindow != null)
|
||||
{
|
||||
ArrayPool<double>.Shared.Return(rentedWindow);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static (TSeries Middle, TSeries Upper, TSeries Lower) Batch(TSeries source, int period = 20, double multiplier = 2.0)
|
||||
{
|
||||
@@ -464,7 +524,9 @@ public sealed class Sdchannel : ITValuePublisher
|
||||
|
||||
public static ((TSeries Middle, TSeries Upper, TSeries Lower) Results, Sdchannel Indicator) Calculate(TSeries source, int period = 20, double multiplier = 2.0)
|
||||
{
|
||||
var indicator = new Sdchannel(source, period, multiplier);
|
||||
// Use parameterless constructor to avoid double-processing: new Sdchannel(source, ...) calls Prime(source),
|
||||
// then Update(source) would call Prime again.
|
||||
var indicator = new Sdchannel(period, multiplier);
|
||||
var results = indicator.Update(source);
|
||||
return (results, indicator);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// The MIT License (MIT)
|
||||
// Licensed under the Apache License, Version 2.0
|
||||
// © mihakralj
|
||||
//@version=6
|
||||
indicator("Standard Deviation Channel (SDCHANNEL)", "SDCHANNEL", overlay=true)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// The MIT License (MIT)
|
||||
// Licensed under the Apache License, Version 2.0
|
||||
// © mihakralj
|
||||
//@version=6
|
||||
indicator("Stoller Average Range Channel (STARCHANNEL)", "STARCHANNEL", overlay=true)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// The MIT License (MIT)
|
||||
// Licensed under the Apache License, Version 2.0
|
||||
// © mihakralj
|
||||
//@version=6
|
||||
indicator("Super Trend Bands (STBANDS)", "STBANDS", overlay=true)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// The MIT License (MIT)
|
||||
// Licensed under the Apache License, Version 2.0
|
||||
// © mihakralj
|
||||
//@version=6
|
||||
// Ultimate Bands logic based on work by John F. Ehlers (c) 2024
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// The MIT License (MIT)
|
||||
// Licensed under the Apache License, Version 2.0
|
||||
// © mihakralj
|
||||
//@version=6
|
||||
// Ultimate Channel logic based on work by John F. Ehlers (c) 2024
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// The MIT License (MIT)
|
||||
// Licensed under the Apache License, Version 2.0
|
||||
// © mihakralj
|
||||
//@version=6
|
||||
indicator("VWAP Bands (VWAPBANDS)", "VWAPBANDS", overlay=true)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// The MIT License (MIT)
|
||||
// Licensed under the Apache License, Version 2.0
|
||||
// © mihakralj
|
||||
//@version=6
|
||||
indicator("VWAP with Standard Deviation Bands", "VWAPSD", overlay=true)
|
||||
|
||||
@@ -12,7 +12,7 @@ public class RingBufferTests
|
||||
Assert.Equal(0, buffer.Count);
|
||||
Assert.False(buffer.IsFull);
|
||||
Assert.Equal(0, buffer.Sum);
|
||||
Assert.Equal(0, buffer.Average);
|
||||
Assert.True(double.IsNaN(buffer.Average));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -192,7 +192,7 @@ public class RingBufferTests
|
||||
|
||||
Assert.Equal(0, buffer.Count);
|
||||
Assert.Equal(0, buffer.Sum);
|
||||
Assert.Equal(0, buffer.Average);
|
||||
Assert.True(double.IsNaN(buffer.Average));
|
||||
Assert.False(buffer.IsFull);
|
||||
}
|
||||
|
||||
@@ -453,11 +453,11 @@ public class RingBufferTests
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Average_EmptyBuffer_ReturnsZero()
|
||||
public void Average_EmptyBuffer_ReturnsNaN()
|
||||
{
|
||||
var buffer = new RingBuffer(5);
|
||||
|
||||
Assert.Equal(0, buffer.Average);
|
||||
Assert.True(double.IsNaN(buffer.Average));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
||||
@@ -141,12 +141,12 @@ public sealed class RingBuffer : IEnumerable<double>
|
||||
|
||||
/// <summary>
|
||||
/// Average of all elements in the buffer.
|
||||
/// Returns 0 if buffer is empty.
|
||||
/// Returns <see langword="double.NaN"/> if buffer is empty.
|
||||
/// </summary>
|
||||
public double Average
|
||||
{
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
get => _count > 0 ? _sum / _count : 0;
|
||||
get => _count > 0 ? _sum / _count : double.NaN;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -90,7 +90,7 @@ public static class SimdExtensions
|
||||
for (int i = 0; i < span.Length; i++)
|
||||
{
|
||||
double diff = span[i] - mean;
|
||||
sumSquares += diff * diff;
|
||||
sumSquares = Math.FusedMultiplyAdd(diff, diff, sumSquares);
|
||||
}
|
||||
return sumSquares / (span.Length - 1);
|
||||
}
|
||||
@@ -438,7 +438,7 @@ public static class SimdExtensions
|
||||
for (; i < span.Length; i++)
|
||||
{
|
||||
double diff = span[i] - m;
|
||||
result += diff * diff;
|
||||
result = Math.FusedMultiplyAdd(diff, diff, result);
|
||||
}
|
||||
|
||||
return result / (span.Length - 1);
|
||||
@@ -682,17 +682,17 @@ public static class SimdExtensions
|
||||
// Unroll scalar loop with 4 accumulators to break dependency chains
|
||||
for (; i <= len - 4; i += 4)
|
||||
{
|
||||
s1 += Unsafe.Add(ref ar, i) * Unsafe.Add(ref br, i);
|
||||
s2 += Unsafe.Add(ref ar, i + 1) * Unsafe.Add(ref br, i + 1);
|
||||
s3 += Unsafe.Add(ref ar, i + 2) * Unsafe.Add(ref br, i + 2);
|
||||
s4 += Unsafe.Add(ref ar, i + 3) * Unsafe.Add(ref br, i + 3);
|
||||
s1 = Math.FusedMultiplyAdd(Unsafe.Add(ref ar, i), Unsafe.Add(ref br, i), s1);
|
||||
s2 = Math.FusedMultiplyAdd(Unsafe.Add(ref ar, i + 1), Unsafe.Add(ref br, i + 1), s2);
|
||||
s3 = Math.FusedMultiplyAdd(Unsafe.Add(ref ar, i + 2), Unsafe.Add(ref br, i + 2), s3);
|
||||
s4 = Math.FusedMultiplyAdd(Unsafe.Add(ref ar, i + 3), Unsafe.Add(ref br, i + 3), s4);
|
||||
}
|
||||
|
||||
double s = s1 + s2 + s3 + s4;
|
||||
|
||||
for (; i < len; i++)
|
||||
{
|
||||
s += Unsafe.Add(ref ar, i) * Unsafe.Add(ref br, i);
|
||||
s = Math.FusedMultiplyAdd(Unsafe.Add(ref ar, i), Unsafe.Add(ref br, i), s);
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
@@ -213,6 +213,12 @@ public class TSeries : IReadOnlyList<TValue>, ITValuePublisher
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void Add(DateTime time, double value, bool isNew = true) => Add(new TValue(time, value), isNew);
|
||||
|
||||
/// <remarks>
|
||||
/// <b>Synthetic timestamps:</b> Each element receives a fabricated timestamp starting at
|
||||
/// <see cref="DateTime.UtcNow"/> (captured once at call time) and incrementing by one minute
|
||||
/// per item. These timestamps do not reflect real market time. Use the typed overloads
|
||||
/// (<see cref="Add(TValue)"/>, <see cref="Add(DateTime,double,bool)"/>) when real timestamps matter.
|
||||
/// </remarks>
|
||||
public void Add(IEnumerable<double> values)
|
||||
{
|
||||
long t = DateTime.UtcNow.Ticks;
|
||||
|
||||
@@ -364,5 +364,33 @@ public class CcorValidationTests
|
||||
"Different thresholds should produce different market state classifications");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Ccor_Correction_Recomputes()
|
||||
{
|
||||
var ind = new Ccor(period: 20);
|
||||
var t0 = new DateTime(946_684_800_000_000_0L, DateTimeKind.Utc);
|
||||
|
||||
// Build state well past warmup
|
||||
for (int i = 0; i < 100; i++)
|
||||
{
|
||||
ind.Update(new TValue(t0.AddMinutes(i),
|
||||
100.0 + 10.0 * Math.Sin(2.0 * Math.PI * i / 20.0)), isNew: true);
|
||||
}
|
||||
|
||||
// Anchor bar
|
||||
var anchorTime = t0.AddMinutes(100);
|
||||
const double anchorPrice = 105.5;
|
||||
ind.Update(new TValue(anchorTime, anchorPrice), isNew: true);
|
||||
double anchorResult = ind.Last.Value;
|
||||
|
||||
// Correction with a dramatically different price — recompute must yield different result
|
||||
ind.Update(new TValue(anchorTime, anchorPrice * 10.0), isNew: false);
|
||||
Assert.NotEqual(anchorResult, ind.Last.Value);
|
||||
|
||||
// Correction back to original price — must exactly restore original result
|
||||
ind.Update(new TValue(anchorTime, anchorPrice), isNew: false);
|
||||
Assert.Equal(anchorResult, ind.Last.Value, Tolerance);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
@@ -231,7 +231,7 @@ public sealed class Ccor : AbstractBase
|
||||
{
|
||||
foreach (double value in source)
|
||||
{
|
||||
Update(new TValue(DateTime.UtcNow, value));
|
||||
Update(new TValue(DateTime.MinValue, value));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// The MIT License (MIT)
|
||||
// Licensed under the Apache License, Version 2.0
|
||||
// © mihakralj
|
||||
//@version=6
|
||||
indicator("Ehlers Correlation Cycle (CCOR)", "CCOR", overlay=false)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// The MIT License (MIT)
|
||||
// Licensed under the Apache License, Version 2.0
|
||||
// © mihakralj
|
||||
//@version=6
|
||||
indicator("Ehlers Cyber Cycle (CCYC)", "CCYC", overlay=false)
|
||||
|
||||
+1
-1
@@ -295,7 +295,7 @@ public sealed class Cg : AbstractBase
|
||||
int idx = (effectiveStart + j) % period;
|
||||
double price = buffer[idx];
|
||||
int weight = j + 1; // 1-based weighting
|
||||
weightedSum += weight * price;
|
||||
weightedSum = Math.FusedMultiplyAdd(weight, price, weightedSum);
|
||||
sum += price;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// The MIT License (MIT)
|
||||
// Licensed under the Apache License, Version 2.0
|
||||
// © mihakralj
|
||||
//@version=6
|
||||
indicator("Ehlers Center of Gravity (CG)", "CG", overlay=false)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// The MIT License (MIT)
|
||||
// Licensed under the Apache License, Version 2.0
|
||||
// © mihakralj
|
||||
//@version=6
|
||||
indicator("Ehlers Detrended Synthetic Price (DSP)", "DSP", overlay=false)
|
||||
|
||||
@@ -418,5 +418,33 @@ public class EacpValidationTests
|
||||
Assert.True(diff > 5, $"Should detect different cycles: {eacp1.DominantCycle} vs {eacp2.DominantCycle}");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Eacp_Correction_Recomputes()
|
||||
{
|
||||
var ind = new Eacp(8, 48, 3, true);
|
||||
var t0 = new DateTime(946_684_800_000_000_0L, DateTimeKind.Utc);
|
||||
|
||||
// Build state well past warmup
|
||||
for (int i = 0; i < 100; i++)
|
||||
{
|
||||
ind.Update(new TValue(t0.AddMinutes(i),
|
||||
100.0 + 10.0 * Math.Sin(2.0 * Math.PI * i / 20.0)), isNew: true);
|
||||
}
|
||||
|
||||
// Anchor bar
|
||||
var anchorTime = t0.AddMinutes(100);
|
||||
const double anchorPrice = 105.5;
|
||||
ind.Update(new TValue(anchorTime, anchorPrice), isNew: true);
|
||||
double anchorResult = ind.Last.Value;
|
||||
|
||||
// Correction with a dramatically different price — recompute must yield different result
|
||||
ind.Update(new TValue(anchorTime, anchorPrice * 10.0), isNew: false);
|
||||
Assert.NotEqual(anchorResult, ind.Last.Value);
|
||||
|
||||
// Correction back to original price — must exactly restore original result
|
||||
ind.Update(new TValue(anchorTime, anchorPrice), isNew: false);
|
||||
Assert.Equal(anchorResult, ind.Last.Value, Tolerance);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
@@ -48,6 +48,7 @@ public sealed class Eacp : AbstractBase
|
||||
private readonly double[] _corr;
|
||||
private readonly double[] _power;
|
||||
private readonly double[] _smooth;
|
||||
private readonly double[] _p_smooth;
|
||||
|
||||
// State for filters and output
|
||||
[StructLayout(LayoutKind.Auto)]
|
||||
@@ -122,6 +123,7 @@ public sealed class Eacp : AbstractBase
|
||||
_corr = new double[size];
|
||||
_power = new double[size];
|
||||
_smooth = new double[size];
|
||||
_p_smooth = new double[size];
|
||||
_filtHistory = new RingBuffer(size + maxPeriod);
|
||||
|
||||
Name = $"Eacp({minPeriod},{maxPeriod})";
|
||||
@@ -156,11 +158,13 @@ public sealed class Eacp : AbstractBase
|
||||
{
|
||||
_ps = _s;
|
||||
_filtHistory.Snapshot();
|
||||
Array.Copy(_smooth, _p_smooth, _smooth.Length);
|
||||
}
|
||||
else
|
||||
{
|
||||
_s = _ps;
|
||||
_filtHistory.Restore();
|
||||
Array.Copy(_p_smooth, _smooth, _smooth.Length);
|
||||
}
|
||||
|
||||
var s = _s;
|
||||
@@ -407,6 +411,7 @@ public sealed class Eacp : AbstractBase
|
||||
Array.Clear(_corr);
|
||||
Array.Clear(_power);
|
||||
Array.Clear(_smooth);
|
||||
Array.Clear(_p_smooth);
|
||||
Last = default;
|
||||
}
|
||||
|
||||
@@ -414,7 +419,7 @@ public sealed class Eacp : AbstractBase
|
||||
{
|
||||
foreach (double value in source)
|
||||
{
|
||||
Update(new TValue(DateTime.UtcNow, value));
|
||||
Update(new TValue(DateTime.MinValue, value));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -459,7 +464,7 @@ public sealed class Eacp : AbstractBase
|
||||
var eacp = new Eacp(minPeriod, maxPeriod, avgLength, enhance);
|
||||
for (int i = 0; i < len; i++)
|
||||
{
|
||||
var result = eacp.Update(new TValue(DateTime.UtcNow, source[i]));
|
||||
var result = eacp.Update(new TValue(DateTime.MinValue, source[i]));
|
||||
output[i] = result.Value;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// The MIT License (MIT)
|
||||
// Licensed under the Apache License, Version 2.0
|
||||
// © mihakralj
|
||||
//@version=6
|
||||
indicator("Ehlers Autocorrelation Periodogram (EACP)","EACP",overlay=false)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// The MIT License (MIT)
|
||||
// Licensed under the Apache License, Version 2.0
|
||||
// © mihakralj
|
||||
//@version=6
|
||||
indicator("Ehlers Even Better Sinewave (EBSW)", "EBSW", overlay=false)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// The MIT License (MIT)
|
||||
// Licensed under the Apache License, Version 2.0
|
||||
// © mihakralj
|
||||
//@version=6
|
||||
indicator("Ehlers Homodyne Discriminator (HOMOD)","HOMOD",overlay=false)
|
||||
|
||||
@@ -92,4 +92,32 @@ public sealed class HtDcperiodValidationTests : IDisposable
|
||||
var q = new HtDcperiod();
|
||||
Assert.Equal(talibLookback, q.WarmupPeriod);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HtDcperiod_Correction_Recomputes()
|
||||
{
|
||||
var ind = new HtDcperiod();
|
||||
var t0 = new DateTime(946_684_800_000_000_0L, DateTimeKind.Utc);
|
||||
|
||||
// Build state well past warmup
|
||||
for (int i = 0; i < 100; i++)
|
||||
{
|
||||
ind.Update(new TValue(t0.AddMinutes(i),
|
||||
100.0 + 10.0 * Math.Sin(2.0 * Math.PI * i / 20.0)), isNew: true);
|
||||
}
|
||||
|
||||
// Anchor bar
|
||||
var anchorTime = t0.AddMinutes(100);
|
||||
const double anchorPrice = 105.5;
|
||||
ind.Update(new TValue(anchorTime, anchorPrice), isNew: true);
|
||||
double anchorResult = ind.Last.Value;
|
||||
|
||||
// Correction with a dramatically different price — recompute must yield different result
|
||||
ind.Update(new TValue(anchorTime, anchorPrice * 10.0), isNew: false);
|
||||
Assert.NotEqual(anchorResult, ind.Last.Value);
|
||||
|
||||
// Correction back to original price — must exactly restore original result
|
||||
ind.Update(new TValue(anchorTime, anchorPrice), isNew: false);
|
||||
Assert.Equal(anchorResult, ind.Last.Value, 1e-9);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// The MIT License (MIT)
|
||||
// Licensed under the Apache License, Version 2.0
|
||||
// © mihakralj
|
||||
//@version=6
|
||||
indicator("Ehlers Hilbert Transform Dominant Cycle Period (HT_DCPERIOD)", "HT_DCPERIOD", overlay=false)
|
||||
|
||||
@@ -139,8 +139,8 @@ public class HtDcphaseTests
|
||||
// First update (new bar)
|
||||
var result1 = ht.Update(new TValue(now.AddMinutes(70), 105), isNew: true);
|
||||
|
||||
// Same bar update
|
||||
var result2 = ht.Update(new TValue(now.AddMinutes(70), 106), isNew: false);
|
||||
// Same bar update with the same price — must yield identical result
|
||||
var result2 = ht.Update(new TValue(now.AddMinutes(70), 105), isNew: false);
|
||||
|
||||
Assert.Equal(result1.Value, result2.Value);
|
||||
}
|
||||
|
||||
@@ -92,4 +92,32 @@ public sealed class HtDcphaseValidationTests : IDisposable
|
||||
var q = new HtDcphase();
|
||||
Assert.Equal(talibLookback, q.WarmupPeriod);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HtDcphase_Correction_Recomputes()
|
||||
{
|
||||
var ind = new HtDcphase();
|
||||
var t0 = new DateTime(946_684_800_000_000_0L, DateTimeKind.Utc);
|
||||
|
||||
// Build state well past warmup
|
||||
for (int i = 0; i < 100; i++)
|
||||
{
|
||||
ind.Update(new TValue(t0.AddMinutes(i),
|
||||
100.0 + 10.0 * Math.Sin(2.0 * Math.PI * i / 20.0)), isNew: true);
|
||||
}
|
||||
|
||||
// Anchor bar
|
||||
var anchorTime = t0.AddMinutes(100);
|
||||
const double anchorPrice = 105.5;
|
||||
ind.Update(new TValue(anchorTime, anchorPrice), isNew: true);
|
||||
double anchorResult = ind.Last.Value;
|
||||
|
||||
// Correction with a dramatically different price — recompute must yield different result
|
||||
ind.Update(new TValue(anchorTime, anchorPrice * 10.0), isNew: false);
|
||||
Assert.NotEqual(anchorResult, ind.Last.Value);
|
||||
|
||||
// Correction back to original price — must exactly restore original result
|
||||
ind.Update(new TValue(anchorTime, anchorPrice), isNew: false);
|
||||
Assert.Equal(anchorResult, ind.Last.Value, 1e-9);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -217,12 +217,10 @@ public sealed class HtDcphase : AbstractBase
|
||||
}
|
||||
else
|
||||
{
|
||||
// Same-bar update: restore previous state and return cached result from Last
|
||||
_state = _p_state;
|
||||
Array.Copy(_p_circBuffer, _circBuffer, CIRC_BUFFER_SIZE);
|
||||
Array.Copy(_p_smoothPrice, _smoothPrice, SMOOTH_PRICE_SIZE);
|
||||
Array.Copy(_p_priceHistory, _priceHistory, PRICE_HISTORY_SIZE);
|
||||
return Last.Value;
|
||||
}
|
||||
|
||||
var s = _state;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// The MIT License (MIT)
|
||||
// Licensed under the Apache License, Version 2.0
|
||||
// © mihakralj
|
||||
//@version=6
|
||||
indicator("Ehlers Hilbert Transform Dominant Cycle Phase (HT_DCPHASE)", "HT_DCPHASE", overlay=false)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// The MIT License (MIT)
|
||||
// Licensed under the Apache License, Version 2.0
|
||||
// © mihakralj
|
||||
//@version=6
|
||||
indicator("Ehlers Hilbert Transform Phasor Components (HT_PHASOR)", "HT_PHASOR", overlay=false)
|
||||
|
||||
@@ -122,4 +122,34 @@ public sealed class HtSineValidationTests : IDisposable
|
||||
|
||||
Assert.Equal(talibLookback, htSine.WarmupPeriod);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HtSine_Correction_Recomputes()
|
||||
{
|
||||
var ind = new HtSine();
|
||||
var t0 = new DateTime(946_684_800_000_000_0L, DateTimeKind.Utc);
|
||||
|
||||
// Build state well past warmup
|
||||
for (int i = 0; i < 100; i++)
|
||||
{
|
||||
ind.Update(new TValue(t0.AddMinutes(i),
|
||||
100.0 + 10.0 * Math.Sin(2.0 * Math.PI * i / 20.0)), isNew: true);
|
||||
}
|
||||
|
||||
// Anchor bar
|
||||
var anchorTime = t0.AddMinutes(100);
|
||||
const double anchorPrice = 105.5;
|
||||
ind.Update(new TValue(anchorTime, anchorPrice), isNew: true);
|
||||
double anchorSine = ind.Last.Value;
|
||||
double anchorLeadSine = ind.LeadSine;
|
||||
|
||||
// Correction with a dramatically different price — recompute must yield different results
|
||||
ind.Update(new TValue(anchorTime, anchorPrice * 10.0), isNew: false);
|
||||
Assert.NotEqual(anchorSine, ind.Last.Value);
|
||||
|
||||
// Correction back to original price — must exactly restore original results
|
||||
ind.Update(new TValue(anchorTime, anchorPrice), isNew: false);
|
||||
Assert.Equal(anchorSine, ind.Last.Value, 1e-9);
|
||||
Assert.Equal(anchorLeadSine, ind.LeadSine, 1e-9);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -295,7 +295,6 @@ public sealed class HtSine : AbstractBase
|
||||
Array.Copy(_circBuffer, _p_circBuffer, CIRC_BUFFER_SIZE);
|
||||
Array.Copy(_smoothPrice, _p_smoothPrice, SMOOTH_PRICE_SIZE);
|
||||
Array.Copy(_priceHistory, _p_priceHistory, PRICE_HISTORY_SIZE);
|
||||
_state.Today++;
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -305,6 +304,8 @@ public sealed class HtSine : AbstractBase
|
||||
Array.Copy(_p_priceHistory, _priceHistory, PRICE_HISTORY_SIZE);
|
||||
}
|
||||
|
||||
_state.Today++;
|
||||
|
||||
// Local copy of state for struct promotion (AGENTS.md §2.5)
|
||||
var s = _state;
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// The MIT License (MIT)
|
||||
// Licensed under the Apache License, Version 2.0
|
||||
// © mihakralj
|
||||
//@version=6
|
||||
indicator("Ehlers Hilbert Transform SineWave (HT_SINE)", "HT_SINE", overlay=false)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// The MIT License (MIT)
|
||||
// Licensed under the Apache License, Version 2.0
|
||||
// © mihakralj
|
||||
//@version=6
|
||||
indicator("Lunar Phase (LUNAR)", "LUNAR", overlay=false)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// The MIT License (MIT)
|
||||
// Licensed under the Apache License, Version 2.0
|
||||
// © mihakralj
|
||||
//@version=6
|
||||
indicator("Solar Cycle (SOLAR)", "SOLAR", overlay=false)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// The MIT License (MIT)
|
||||
// Licensed under the Apache License, Version 2.0
|
||||
// © mihakralj
|
||||
//@version=6
|
||||
indicator("Ehlers SSF Detrended Synthetic Price (SSFDSP)", "SSF-DSP", overlay=false)
|
||||
|
||||
@@ -17,6 +17,8 @@ Dynamics indicators measure trend strength, speed, and direction. Unlike momentu
|
||||
| [CHOP](chop/Chop.md) | Choppiness Index | Trendiness measure. High values = choppy. Low = trending. |
|
||||
| [DMX](dmx/Dmx.md) | Jurik DMX | Smoothed bipolar DMI using Jurik smoothing. Low noise. |
|
||||
| [DX](dx/Dx.md) | Directional Movement Index | Raw directional strength. Unsmoothed ADX component. |
|
||||
| [MINUS_DI](minusdi/MinusDi.md) | Minus Directional Indicator | Downward directional movement as % of true range. 0-100. |
|
||||
| [MINUS_DM](minusdm/MinusDm.md) | Minus Directional Movement | Wilder-smoothed downward directional movement. Price units. |
|
||||
| [HT_TRENDMODE](ht_trendmode/Httrendmode.md) | Ehlers Hilbert Transform Trend vs Cycle Mode | Ehlers Hilbert Transform. Binary trend/cycle mode detection. |
|
||||
| [ICHIMOKU](ichimoku/Ichimoku.md) | Ichimoku Cloud | Five-line system. Cloud defines support/resistance zones. |
|
||||
| [IMPULSE](impulse/Impulse.md) | Elder Impulse System | EMA + MACD histogram alignment. Color-coded trend/momentum filter. |
|
||||
@@ -27,5 +29,7 @@ Dynamics indicators measure trend strength, speed, and direction. Unlike momentu
|
||||
| [VORTEX](vortex/Vortex.md) | Vortex Indicator | VI+ and VI- measure positive/negative trend movement. |
|
||||
| [GHLA](ghla/Ghla.md) | Gann High-Low Activator | SMA(High)/SMA(Low) alternating on crossover. |
|
||||
| [PFE](pfe/Pfe.md) | Polarized Fractal Efficiency | Trend efficiency: straight-line / total path distance. |
|
||||
| [PLUS_DI](plusdi/PlusDi.md) | Plus Directional Indicator | Upward directional movement as % of true range. 0-100. |
|
||||
| [PLUS_DM](plusdm/PlusDm.md) | Plus Directional Movement | Wilder-smoothed upward directional movement. Price units. |
|
||||
| [RAVI](ravi/Ravi.md) | Chande Range Action Verification Index | \|SMA(short) − SMA(long)\| / SMA(long) × 100. |
|
||||
| [VHF](vhf/Vhf.md) | Vertical Horizontal Filter | Max-min range / sum of absolute changes. |
|
||||
|
||||
@@ -91,6 +91,78 @@ public sealed class AdxValidationTests : IDisposable
|
||||
ValidationHelper.VerifyData(results, tulipResults, lookback: offset);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DiPlus_MatchesTalib()
|
||||
{
|
||||
var adx = new Adx(14);
|
||||
var diPlusResults = new List<double>();
|
||||
|
||||
for (int i = 0; i < _data.Bars.Count; i++)
|
||||
{
|
||||
adx.Update(_data.Bars[i]);
|
||||
diPlusResults.Add(adx.DiPlus.Value);
|
||||
}
|
||||
|
||||
double[] hData = _data.Bars.High.Select(x => x.Value).ToArray();
|
||||
double[] lData = _data.Bars.Low.Select(x => x.Value).ToArray();
|
||||
double[] cData = _data.Bars.Close.Select(x => x.Value).ToArray();
|
||||
double[] outReal = new double[_data.Bars.Count];
|
||||
|
||||
var retCode = Functions.PlusDI(hData, lData, cData, 0..^0, outReal, out var outRange, 14);
|
||||
Assert.Equal(TALib.Core.RetCode.Success, retCode);
|
||||
|
||||
int lookback = Functions.PlusDILookback(14);
|
||||
ValidationHelper.VerifyData(diPlusResults, outReal, outRange, lookback);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DiMinus_MatchesTalib()
|
||||
{
|
||||
var adx = new Adx(14);
|
||||
var diMinusResults = new List<double>();
|
||||
|
||||
for (int i = 0; i < _data.Bars.Count; i++)
|
||||
{
|
||||
adx.Update(_data.Bars[i]);
|
||||
diMinusResults.Add(adx.DiMinus.Value);
|
||||
}
|
||||
|
||||
double[] hData = _data.Bars.High.Select(x => x.Value).ToArray();
|
||||
double[] lData = _data.Bars.Low.Select(x => x.Value).ToArray();
|
||||
double[] cData = _data.Bars.Close.Select(x => x.Value).ToArray();
|
||||
double[] outReal = new double[_data.Bars.Count];
|
||||
|
||||
var retCode = Functions.MinusDI(hData, lData, cData, 0..^0, outReal, out var outRange, 14);
|
||||
Assert.Equal(TALib.Core.RetCode.Success, retCode);
|
||||
|
||||
int lookback = Functions.MinusDILookback(14);
|
||||
ValidationHelper.VerifyData(diMinusResults, outReal, outRange, lookback);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MatchesSkender_DiValues()
|
||||
{
|
||||
var adx = new Adx(14);
|
||||
var diPlusResults = new List<double>();
|
||||
var diMinusResults = new List<double>();
|
||||
|
||||
for (int i = 0; i < _data.Bars.Count; i++)
|
||||
{
|
||||
adx.Update(_data.Bars[i]);
|
||||
diPlusResults.Add(adx.DiPlus.Value);
|
||||
diMinusResults.Add(adx.DiMinus.Value);
|
||||
}
|
||||
|
||||
// Skender's GetAdx returns ADX with +DI and -DI values
|
||||
var skenderResults = _data.SkenderQuotes.GetAdx(14).ToList();
|
||||
|
||||
// Verify +DI
|
||||
ValidationHelper.VerifyData(diPlusResults, skenderResults, x => x.Pdi);
|
||||
|
||||
// Verify -DI
|
||||
ValidationHelper.VerifyData(diMinusResults, skenderResults, x => x.Mdi);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MatchesOoples()
|
||||
{
|
||||
|
||||
@@ -62,6 +62,16 @@ public sealed class Adx : ITValuePublisher
|
||||
/// </summary>
|
||||
public TValue DiMinus { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Current smoothed +DM value (RMA-smoothed raw plus directional movement, before TR normalization).
|
||||
/// </summary>
|
||||
public TValue DmPlus { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Current smoothed -DM value (RMA-smoothed raw minus directional movement, before TR normalization).
|
||||
/// </summary>
|
||||
public TValue DmMinus { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// True if the ADX has warmed up and is providing valid results.
|
||||
/// </summary>
|
||||
@@ -116,6 +126,8 @@ public sealed class Adx : ITValuePublisher
|
||||
Last = default;
|
||||
DiPlus = default;
|
||||
DiMinus = default;
|
||||
DmPlus = default;
|
||||
DmMinus = default;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
@@ -325,6 +337,8 @@ public sealed class Adx : ITValuePublisher
|
||||
|
||||
DiPlus = new TValue(input.Time, diPlus);
|
||||
DiMinus = new TValue(input.Time, diMinus);
|
||||
DmPlus = new TValue(input.Time, _samples >= _period ? _dmPlusSmooth : 0);
|
||||
DmMinus = new TValue(input.Time, _samples >= _period ? _dmMinusSmooth : 0);
|
||||
Last = new TValue(input.Time, finalAdx);
|
||||
|
||||
Pub?.Invoke(this, new TValueEventArgs { Value = Last, IsNew = isNew });
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// The MIT License (MIT)
|
||||
// Licensed under the Apache License, Version 2.0
|
||||
// © mihakralj
|
||||
//@version=6
|
||||
indicator("Average Directional Movement Index (ADX)", "ADX", overlay=false)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// The MIT License (MIT)
|
||||
// Licensed under the Apache License, Version 2.0
|
||||
// © mihakralj
|
||||
//@version=6
|
||||
indicator("Average Directional Movement Index Rating (ADXR)", "ADXR", overlay=false)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// The MIT License (MIT)
|
||||
// Licensed under the Apache License, Version 2.0
|
||||
// © mihakralj
|
||||
//@version=6
|
||||
indicator("Williams Alligator", "ALLIGATOR", overlay=true)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// The MIT License (MIT)
|
||||
// Licensed under the Apache License, Version 2.0
|
||||
// © mihakralj
|
||||
//@version=6
|
||||
indicator("Archer Moving Averages Trends (AMAT)", "AMAT", overlay=false)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// The MIT License (MIT)
|
||||
// Licensed under the Apache License, Version 2.0
|
||||
// © mihakralj
|
||||
//@version=6
|
||||
indicator("Aroon (AROON)", "AROON", overlay=false)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// The MIT License (MIT)
|
||||
// Licensed under the Apache License, Version 2.0
|
||||
// © mihakralj
|
||||
//@version=6
|
||||
indicator("Aroon Oscillator", "AROONOSC", overlay=false)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// The MIT License (MIT)
|
||||
// Licensed under the Apache License, Version 2.0
|
||||
// © mihakralj
|
||||
//@version=6
|
||||
indicator("Choppiness Index", "CHOP", overlay=false)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// The MIT License (MIT)
|
||||
// Licensed under the Apache License, Version 2.0
|
||||
// © mihakralj
|
||||
//@version=6
|
||||
indicator("Jurik Directional Movement Index (DMX)", "DMX", overlay=false)
|
||||
|
||||
@@ -102,6 +102,52 @@ public sealed class DxValidationTests : IDisposable
|
||||
ValidationHelper.VerifyData(diMinusResults, outReal, outRange, lookback);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DmPlus_MatchesTalib()
|
||||
{
|
||||
var dx = new Dx(14);
|
||||
var dmPlusResults = new List<double>();
|
||||
|
||||
for (int i = 0; i < _data.Bars.Count; i++)
|
||||
{
|
||||
dx.Update(_data.Bars[i]);
|
||||
dmPlusResults.Add(dx.DmPlus.Value);
|
||||
}
|
||||
|
||||
double[] hData = _data.Bars.High.Select(x => x.Value).ToArray();
|
||||
double[] lData = _data.Bars.Low.Select(x => x.Value).ToArray();
|
||||
double[] outReal = new double[_data.Bars.Count];
|
||||
|
||||
var retCode = Functions.PlusDM(hData, lData, 0..^0, outReal, out var outRange, 14);
|
||||
Assert.Equal(TALib.Core.RetCode.Success, retCode);
|
||||
|
||||
int lookback = Functions.PlusDMLookback(14);
|
||||
ValidationHelper.VerifyData(dmPlusResults, outReal, outRange, lookback);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DmMinus_MatchesTalib()
|
||||
{
|
||||
var dx = new Dx(14);
|
||||
var dmMinusResults = new List<double>();
|
||||
|
||||
for (int i = 0; i < _data.Bars.Count; i++)
|
||||
{
|
||||
dx.Update(_data.Bars[i]);
|
||||
dmMinusResults.Add(dx.DmMinus.Value);
|
||||
}
|
||||
|
||||
double[] hData = _data.Bars.High.Select(x => x.Value).ToArray();
|
||||
double[] lData = _data.Bars.Low.Select(x => x.Value).ToArray();
|
||||
double[] outReal = new double[_data.Bars.Count];
|
||||
|
||||
var retCode = Functions.MinusDM(hData, lData, 0..^0, outReal, out var outRange, 14);
|
||||
Assert.Equal(TALib.Core.RetCode.Success, retCode);
|
||||
|
||||
int lookback = Functions.MinusDMLookback(14);
|
||||
ValidationHelper.VerifyData(dmMinusResults, outReal, outRange, lookback);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MatchesSkender_DiValues()
|
||||
{
|
||||
|
||||
@@ -53,6 +53,18 @@ public sealed class Dx : ITValuePublisher
|
||||
/// </summary>
|
||||
public TValue DiMinus { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Current smoothed +DM value (Wilder-smoothed raw plus directional movement, before TR normalization).
|
||||
/// Equivalent to TA-Lib PLUS_DM.
|
||||
/// </summary>
|
||||
public TValue DmPlus { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Current smoothed -DM value (Wilder-smoothed raw minus directional movement, before TR normalization).
|
||||
/// Equivalent to TA-Lib MINUS_DM.
|
||||
/// </summary>
|
||||
public TValue DmMinus { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// True if the DX has warmed up and is providing valid results.
|
||||
/// </summary>
|
||||
@@ -106,6 +118,8 @@ public sealed class Dx : ITValuePublisher
|
||||
Last = default;
|
||||
DiPlus = default;
|
||||
DiMinus = default;
|
||||
DmPlus = default;
|
||||
DmMinus = default;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
@@ -276,6 +290,8 @@ public sealed class Dx : ITValuePublisher
|
||||
|
||||
DiPlus = new TValue(input.Time, diPlus);
|
||||
DiMinus = new TValue(input.Time, diMinus);
|
||||
DmPlus = new TValue(input.Time, _samples >= _period ? _dmPlusSmooth : 0);
|
||||
DmMinus = new TValue(input.Time, _samples >= _period ? _dmMinusSmooth : 0);
|
||||
Last = new TValue(input.Time, dx);
|
||||
|
||||
Pub?.Invoke(this, new TValueEventArgs { Value = Last, IsNew = isNew });
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// The MIT License (MIT)
|
||||
// Licensed under the Apache License, Version 2.0
|
||||
// © mihakralj
|
||||
//@version=6
|
||||
indicator("Directional Movement Index (DX)", "DX", overlay=false)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// The MIT License (MIT)
|
||||
// Licensed under the Apache License, Version 2.0
|
||||
// © mihakralj
|
||||
//@version=6
|
||||
indicator("Gann High-Low Activator", "GHLA", overlay=true)
|
||||
|
||||
@@ -197,4 +197,32 @@ public sealed class HtTrendmodeValidationTests : IDisposable
|
||||
Assert.Equal(results1[i], results2[i]);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HtTrendmode_Correction_Recomputes()
|
||||
{
|
||||
var ind = new HtTrendmode();
|
||||
var t0 = new DateTime(946_684_800_000_000_0L, DateTimeKind.Utc);
|
||||
|
||||
// Build state well past warmup
|
||||
for (int i = 0; i < 100; i++)
|
||||
{
|
||||
ind.Update(new TValue(t0.AddMinutes(i),
|
||||
100.0 + 10.0 * Math.Sin(2.0 * Math.PI * i / 20.0)), isNew: true);
|
||||
}
|
||||
|
||||
// Anchor bar
|
||||
var anchorTime = t0.AddMinutes(100);
|
||||
const double anchorPrice = 105.5;
|
||||
ind.Update(new TValue(anchorTime, anchorPrice), isNew: true);
|
||||
double anchorSmooth = ind.SmoothPeriod;
|
||||
|
||||
// Correction with a dramatically different price — SmoothPeriod must change
|
||||
ind.Update(new TValue(anchorTime, anchorPrice * 10.0), isNew: false);
|
||||
Assert.NotEqual(anchorSmooth, ind.SmoothPeriod);
|
||||
|
||||
// Correction back to original price — must exactly restore original SmoothPeriod
|
||||
ind.Update(new TValue(anchorTime, anchorPrice), isNew: false);
|
||||
Assert.Equal(anchorSmooth, ind.SmoothPeriod, 1e-9);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// The MIT License (MIT)
|
||||
// Licensed under the Apache License, Version 2.0
|
||||
// © mihakralj
|
||||
//@version=6
|
||||
indicator("Ehlers Hilbert Transform Trend vs Cycle Mode (HT_TRENDMODE)", "HT_TRENDMODE", overlay=false)
|
||||
|
||||
@@ -679,4 +679,32 @@ public sealed class IchimokuValidationTests : IDisposable
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
[Fact]
|
||||
public void Ichimoku_Correction_Recomputes()
|
||||
{
|
||||
var ind = new Ichimoku();
|
||||
var t0 = new DateTime(946_684_800_000_000_0L, DateTimeKind.Utc);
|
||||
|
||||
// Build state well past warmup
|
||||
for (int i = 0; i < 100; i++)
|
||||
{
|
||||
double p = 100.0 + 10.0 * Math.Sin(2.0 * Math.PI * i / 20.0);
|
||||
ind.Update(new TBar(t0.AddMinutes(i), p, p + 2, p - 2, p, 1000), isNew: true);
|
||||
}
|
||||
|
||||
// Anchor bar
|
||||
var anchorTime = t0.AddMinutes(100);
|
||||
const double anchorClose = 105.5;
|
||||
ind.Update(new TBar(anchorTime, anchorClose, anchorClose + 2, anchorClose - 2, anchorClose, 1000), isNew: true);
|
||||
double anchorTenkan = ind.Tenkan.Value;
|
||||
|
||||
// Correction with a dramatically different price — Tenkan must change
|
||||
ind.Update(new TBar(anchorTime, anchorClose * 10, (anchorClose + 2) * 10, (anchorClose - 2) * 10, anchorClose * 10, 1000), isNew: false);
|
||||
Assert.NotEqual(anchorTenkan, ind.Tenkan.Value);
|
||||
|
||||
// Correction back to original price — must exactly restore original Tenkan
|
||||
ind.Update(new TBar(anchorTime, anchorClose, anchorClose + 2, anchorClose - 2, anchorClose, 1000), isNew: false);
|
||||
Assert.Equal(anchorTenkan, ind.Tenkan.Value, 1e-9);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// The MIT License (MIT)
|
||||
// Licensed under the Apache License, Version 2.0
|
||||
// © mihakralj
|
||||
//@version=6
|
||||
indicator("Ichimoku Cloud", "ICHIMOKU", overlay=true)
|
||||
|
||||
@@ -0,0 +1,121 @@
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace QuanTAlib;
|
||||
|
||||
/// <summary>
|
||||
/// MINUS_DI: Minus Directional Indicator (Wilder, 1978)
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Measures downward directional movement as a percentage of true range.
|
||||
/// Extracted from the DX calculation: -DI = Smoothed(-DM) / Smoothed(TR) × 100.
|
||||
/// Range: 0 to 100. Higher values indicate stronger downward movement.
|
||||
/// </remarks>
|
||||
[SkipLocalsInit]
|
||||
public sealed class MinusDi : ITValuePublisher
|
||||
{
|
||||
private readonly Dx _dx;
|
||||
|
||||
/// <summary>Display name for the indicator.</summary>
|
||||
public string Name { get; }
|
||||
|
||||
public event TValuePublishedHandler? Pub;
|
||||
|
||||
/// <summary>Current -DI value.</summary>
|
||||
public TValue Last { get; private set; }
|
||||
|
||||
/// <summary>True when the indicator has warmed up.</summary>
|
||||
public bool IsHot => _dx.IsHot;
|
||||
|
||||
/// <summary>Bars required for warmup.</summary>
|
||||
public int WarmupPeriod => _dx.WarmupPeriod;
|
||||
|
||||
/// <summary>The period parameter.</summary>
|
||||
public int Period => _dx.Period;
|
||||
|
||||
/// <summary>
|
||||
/// Creates MinusDi with specified period.
|
||||
/// </summary>
|
||||
/// <param name="period">Wilder smoothing period (must be > 0)</param>
|
||||
public MinusDi(int period = 14)
|
||||
{
|
||||
if (period <= 0)
|
||||
{
|
||||
throw new ArgumentException("Period must be greater than 0", nameof(period));
|
||||
}
|
||||
_dx = new Dx(period);
|
||||
Name = $"MinusDi({period})";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates MinusDi and immediately processes the bar series.
|
||||
/// </summary>
|
||||
public MinusDi(TBarSeries source, int period = 14) : this(period)
|
||||
{
|
||||
var result = Batch(source, period);
|
||||
Last = result[^1];
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public TValue Update(TBar input, bool isNew = true)
|
||||
{
|
||||
_dx.Update(input, isNew);
|
||||
Last = _dx.DiMinus;
|
||||
if (isNew)
|
||||
{
|
||||
Pub?.Invoke(this, new TValueEventArgs { Value = Last, IsNew = isNew });
|
||||
}
|
||||
return Last;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public TValue Update(TValue input, bool isNew = true)
|
||||
{
|
||||
// DI requires OHLC data — scalar update not meaningful
|
||||
return Last;
|
||||
}
|
||||
|
||||
public TSeries Update(TBarSeries source)
|
||||
{
|
||||
var result = new TSeries(source.Count);
|
||||
foreach (var bar in source)
|
||||
{
|
||||
result.Add(Update(bar));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes the indicator state using the provided bar series history.
|
||||
/// </summary>
|
||||
public void Prime(TBarSeries source)
|
||||
{
|
||||
foreach (var bar in source)
|
||||
{
|
||||
Update(bar);
|
||||
}
|
||||
}
|
||||
|
||||
public void Prime(ReadOnlySpan<double> source, TimeSpan? step = null)
|
||||
{
|
||||
// Not applicable — DI requires OHLC bar data, not scalar values
|
||||
}
|
||||
|
||||
public static TSeries Batch(TBarSeries source, int period = 14)
|
||||
{
|
||||
var indicator = new MinusDi(period);
|
||||
return indicator.Update(source);
|
||||
}
|
||||
|
||||
public static (TSeries Results, MinusDi Indicator) Calculate(TBarSeries source, int period = 14)
|
||||
{
|
||||
var indicator = new MinusDi(period);
|
||||
return (indicator.Update(source), indicator);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void Reset()
|
||||
{
|
||||
_dx.Reset();
|
||||
Last = default;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
# MINUS_DI: Minus Directional Indicator
|
||||
|
||||
### TL;DR
|
||||
Measures downward directional movement strength as a percentage (0-100).
|
||||
|
||||
## Introduction
|
||||
The Minus Directional Indicator (-DI) measures the strength of downward price movement relative to the true range. It is one of the components of the Directional Movement System developed by J. Welles Wilder Jr.
|
||||
|
||||
When -DI is rising, downward price pressure is increasing. When -DI crosses above +DI, it signals a potential bearish trend. The -DI line is commonly plotted alongside +DI to visualize directional balance.
|
||||
|
||||
## Calculation
|
||||
-DI = Smoothed(-DM) / Smoothed(TR) × 100
|
||||
|
||||
Where:
|
||||
- -DM (Minus Directional Movement) = max(PrevLow - Low, 0) when PrevLow - Low > High - PrevHigh, else 0
|
||||
- TR (True Range) = max(High - Low, |High - PrevClose|, |Low - PrevClose|)
|
||||
- Smoothing uses Wilder's method: Smooth = Smooth - Smooth/N + Input
|
||||
|
||||
## Parameters
|
||||
| Parameter | Default | Range | Description |
|
||||
| :--- | :--- | :--- | :--- |
|
||||
| Period | 14 | 2-∞ | Wilder smoothing period |
|
||||
|
||||
## Interpretation
|
||||
- **Rising -DI:** Strengthening downward movement
|
||||
- **-DI > +DI:** Bears dominate; potential downtrend
|
||||
- **-DI crossover above +DI:** Bearish signal
|
||||
- **High -DI (>40):** Strong downward momentum
|
||||
|
||||
## References
|
||||
- Wilder, J. Welles Jr. "New Concepts in Technical Trading Systems" (1978)
|
||||
@@ -0,0 +1,121 @@
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace QuanTAlib;
|
||||
|
||||
/// <summary>
|
||||
/// MINUS_DM: Minus Directional Movement (Wilder, 1978)
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Wilder-smoothed downward directional movement in price units.
|
||||
/// Extracted from the DX calculation: Smoothed(-DM) using Wilder's method.
|
||||
/// Values ≥ 0 in price units. Higher values indicate stronger downward movement magnitude.
|
||||
/// </remarks>
|
||||
[SkipLocalsInit]
|
||||
public sealed class MinusDm : ITValuePublisher
|
||||
{
|
||||
private readonly Dx _dx;
|
||||
|
||||
/// <summary>Display name for the indicator.</summary>
|
||||
public string Name { get; }
|
||||
|
||||
public event TValuePublishedHandler? Pub;
|
||||
|
||||
/// <summary>Current smoothed -DM value.</summary>
|
||||
public TValue Last { get; private set; }
|
||||
|
||||
/// <summary>True when the indicator has warmed up.</summary>
|
||||
public bool IsHot => _dx.IsHot;
|
||||
|
||||
/// <summary>Bars required for warmup.</summary>
|
||||
public int WarmupPeriod => _dx.WarmupPeriod;
|
||||
|
||||
/// <summary>The period parameter.</summary>
|
||||
public int Period => _dx.Period;
|
||||
|
||||
/// <summary>
|
||||
/// Creates MinusDm with specified period.
|
||||
/// </summary>
|
||||
/// <param name="period">Wilder smoothing period (must be > 0)</param>
|
||||
public MinusDm(int period = 14)
|
||||
{
|
||||
if (period <= 0)
|
||||
{
|
||||
throw new ArgumentException("Period must be greater than 0", nameof(period));
|
||||
}
|
||||
_dx = new Dx(period);
|
||||
Name = $"MinusDm({period})";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates MinusDm and immediately processes the bar series.
|
||||
/// </summary>
|
||||
public MinusDm(TBarSeries source, int period = 14) : this(period)
|
||||
{
|
||||
var result = Batch(source, period);
|
||||
Last = result[^1];
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public TValue Update(TBar input, bool isNew = true)
|
||||
{
|
||||
_dx.Update(input, isNew);
|
||||
Last = _dx.DmMinus;
|
||||
if (isNew)
|
||||
{
|
||||
Pub?.Invoke(this, new TValueEventArgs { Value = Last, IsNew = isNew });
|
||||
}
|
||||
return Last;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public TValue Update(TValue input, bool isNew = true)
|
||||
{
|
||||
// DM requires OHLC data — scalar update not meaningful
|
||||
return Last;
|
||||
}
|
||||
|
||||
public TSeries Update(TBarSeries source)
|
||||
{
|
||||
var result = new TSeries(source.Count);
|
||||
foreach (var bar in source)
|
||||
{
|
||||
result.Add(Update(bar));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes the indicator state using the provided bar series history.
|
||||
/// </summary>
|
||||
public void Prime(TBarSeries source)
|
||||
{
|
||||
foreach (var bar in source)
|
||||
{
|
||||
Update(bar);
|
||||
}
|
||||
}
|
||||
|
||||
public void Prime(ReadOnlySpan<double> source, TimeSpan? step = null)
|
||||
{
|
||||
// Not applicable — DM requires OHLC bar data, not scalar values
|
||||
}
|
||||
|
||||
public static TSeries Batch(TBarSeries source, int period = 14)
|
||||
{
|
||||
var indicator = new MinusDm(period);
|
||||
return indicator.Update(source);
|
||||
}
|
||||
|
||||
public static (TSeries Results, MinusDm Indicator) Calculate(TBarSeries source, int period = 14)
|
||||
{
|
||||
var indicator = new MinusDm(period);
|
||||
return (indicator.Update(source), indicator);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void Reset()
|
||||
{
|
||||
_dx.Reset();
|
||||
Last = default;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
# MINUS_DM: Minus Directional Movement
|
||||
|
||||
### TL;DR
|
||||
Wilder-smoothed downward directional movement in price units (≥0).
|
||||
|
||||
## Introduction
|
||||
Minus Directional Movement (-DM) measures the magnitude of downward price movement, smoothed using Wilder's method. Unlike -DI which normalizes by true range to produce a percentage, -DM outputs raw smoothed values in price units.
|
||||
|
||||
-DM captures when the previous bar's low exceeds the current bar's low by more than the current bar's high exceeds the previous bar's high. It is the raw building block of the Directional Movement System.
|
||||
|
||||
## Calculation
|
||||
-DM = max(PrevLow - Low, 0) when PrevLow - Low > High - PrevHigh, else 0
|
||||
|
||||
Smoothed using Wilder's method: Smooth = Smooth - Smooth/N + Input
|
||||
|
||||
## Parameters
|
||||
| Parameter | Default | Range | Description |
|
||||
| :--- | :--- | :--- | :--- |
|
||||
| Period | 14 | 2-∞ | Wilder smoothing period |
|
||||
|
||||
## Interpretation
|
||||
- **Rising -DM:** Increasing downward price extension
|
||||
- **-DM > +DM:** Downward movement exceeds upward movement
|
||||
- **Zero -DM:** No downward directional movement on the bar
|
||||
- Values are in price units and scale with the instrument
|
||||
|
||||
## References
|
||||
- Wilder, J. Welles Jr. "New Concepts in Technical Trading Systems" (1978)
|
||||
@@ -1,4 +1,4 @@
|
||||
// The MIT License (MIT)
|
||||
// Licensed under the Apache License, Version 2.0
|
||||
// © mihakralj
|
||||
//@version=6
|
||||
indicator("PFE: Polarized Fractal Efficiency", "PFE", overlay=false)
|
||||
|
||||
@@ -0,0 +1,121 @@
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace QuanTAlib;
|
||||
|
||||
/// <summary>
|
||||
/// PLUS_DI: Plus Directional Indicator (Wilder, 1978)
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Measures upward directional movement as a percentage of true range.
|
||||
/// Extracted from the DX calculation: +DI = Smoothed(+DM) / Smoothed(TR) × 100.
|
||||
/// Range: 0 to 100. Higher values indicate stronger upward movement.
|
||||
/// </remarks>
|
||||
[SkipLocalsInit]
|
||||
public sealed class PlusDi : ITValuePublisher
|
||||
{
|
||||
private readonly Dx _dx;
|
||||
|
||||
/// <summary>Display name for the indicator.</summary>
|
||||
public string Name { get; }
|
||||
|
||||
public event TValuePublishedHandler? Pub;
|
||||
|
||||
/// <summary>Current +DI value.</summary>
|
||||
public TValue Last { get; private set; }
|
||||
|
||||
/// <summary>True when the indicator has warmed up.</summary>
|
||||
public bool IsHot => _dx.IsHot;
|
||||
|
||||
/// <summary>Bars required for warmup.</summary>
|
||||
public int WarmupPeriod => _dx.WarmupPeriod;
|
||||
|
||||
/// <summary>The period parameter.</summary>
|
||||
public int Period => _dx.Period;
|
||||
|
||||
/// <summary>
|
||||
/// Creates PlusDi with specified period.
|
||||
/// </summary>
|
||||
/// <param name="period">Wilder smoothing period (must be > 0)</param>
|
||||
public PlusDi(int period = 14)
|
||||
{
|
||||
if (period <= 0)
|
||||
{
|
||||
throw new ArgumentException("Period must be greater than 0", nameof(period));
|
||||
}
|
||||
_dx = new Dx(period);
|
||||
Name = $"PlusDi({period})";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates PlusDi and immediately processes the bar series.
|
||||
/// </summary>
|
||||
public PlusDi(TBarSeries source, int period = 14) : this(period)
|
||||
{
|
||||
var result = Batch(source, period);
|
||||
Last = result[^1];
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public TValue Update(TBar input, bool isNew = true)
|
||||
{
|
||||
_dx.Update(input, isNew);
|
||||
Last = _dx.DiPlus;
|
||||
if (isNew)
|
||||
{
|
||||
Pub?.Invoke(this, new TValueEventArgs { Value = Last, IsNew = isNew });
|
||||
}
|
||||
return Last;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public TValue Update(TValue input, bool isNew = true)
|
||||
{
|
||||
// DI requires OHLC data — scalar update not meaningful
|
||||
return Last;
|
||||
}
|
||||
|
||||
public TSeries Update(TBarSeries source)
|
||||
{
|
||||
var result = new TSeries(source.Count);
|
||||
foreach (var bar in source)
|
||||
{
|
||||
result.Add(Update(bar));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes the indicator state using the provided bar series history.
|
||||
/// </summary>
|
||||
public void Prime(TBarSeries source)
|
||||
{
|
||||
foreach (var bar in source)
|
||||
{
|
||||
Update(bar);
|
||||
}
|
||||
}
|
||||
|
||||
public void Prime(ReadOnlySpan<double> source, TimeSpan? step = null)
|
||||
{
|
||||
// Not applicable — DI requires OHLC bar data, not scalar values
|
||||
}
|
||||
|
||||
public static TSeries Batch(TBarSeries source, int period = 14)
|
||||
{
|
||||
var indicator = new PlusDi(period);
|
||||
return indicator.Update(source);
|
||||
}
|
||||
|
||||
public static (TSeries Results, PlusDi Indicator) Calculate(TBarSeries source, int period = 14)
|
||||
{
|
||||
var indicator = new PlusDi(period);
|
||||
return (indicator.Update(source), indicator);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void Reset()
|
||||
{
|
||||
_dx.Reset();
|
||||
Last = default;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
# PLUS_DI: Plus Directional Indicator
|
||||
|
||||
### TL;DR
|
||||
Measures upward directional movement strength as a percentage (0-100).
|
||||
|
||||
## Introduction
|
||||
The Plus Directional Indicator (+DI) measures the strength of upward price movement relative to the true range. It is one of the components of the Directional Movement System developed by J. Welles Wilder Jr.
|
||||
|
||||
When +DI is rising, upward price pressure is increasing. When +DI crosses above -DI, it signals a potential bullish trend. The +DI line is commonly plotted alongside -DI to visualize directional balance.
|
||||
|
||||
## Calculation
|
||||
+DI = Smoothed(+DM) / Smoothed(TR) × 100
|
||||
|
||||
Where:
|
||||
- +DM (Plus Directional Movement) = max(High - PrevHigh, 0) when High - PrevHigh > PrevLow - Low, else 0
|
||||
- TR (True Range) = max(High - Low, |High - PrevClose|, |Low - PrevClose|)
|
||||
- Smoothing uses Wilder's method: Smooth = Smooth - Smooth/N + Input
|
||||
|
||||
## Parameters
|
||||
| Parameter | Default | Range | Description |
|
||||
| :--- | :--- | :--- | :--- |
|
||||
| Period | 14 | 2-∞ | Wilder smoothing period |
|
||||
|
||||
## Interpretation
|
||||
- **Rising +DI:** Strengthening upward movement
|
||||
- **+DI > -DI:** Bulls dominate; potential uptrend
|
||||
- **+DI crossover above -DI:** Bullish signal
|
||||
- **High +DI (>40):** Strong upward momentum
|
||||
|
||||
## References
|
||||
- Wilder, J. Welles Jr. "New Concepts in Technical Trading Systems" (1978)
|
||||
@@ -0,0 +1,121 @@
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace QuanTAlib;
|
||||
|
||||
/// <summary>
|
||||
/// PLUS_DM: Plus Directional Movement (Wilder, 1978)
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Wilder-smoothed upward directional movement in price units.
|
||||
/// Extracted from the DX calculation: Smoothed(+DM) using Wilder's method.
|
||||
/// Values ≥ 0 in price units. Higher values indicate stronger upward movement magnitude.
|
||||
/// </remarks>
|
||||
[SkipLocalsInit]
|
||||
public sealed class PlusDm : ITValuePublisher
|
||||
{
|
||||
private readonly Dx _dx;
|
||||
|
||||
/// <summary>Display name for the indicator.</summary>
|
||||
public string Name { get; }
|
||||
|
||||
public event TValuePublishedHandler? Pub;
|
||||
|
||||
/// <summary>Current smoothed +DM value.</summary>
|
||||
public TValue Last { get; private set; }
|
||||
|
||||
/// <summary>True when the indicator has warmed up.</summary>
|
||||
public bool IsHot => _dx.IsHot;
|
||||
|
||||
/// <summary>Bars required for warmup.</summary>
|
||||
public int WarmupPeriod => _dx.WarmupPeriod;
|
||||
|
||||
/// <summary>The period parameter.</summary>
|
||||
public int Period => _dx.Period;
|
||||
|
||||
/// <summary>
|
||||
/// Creates PlusDm with specified period.
|
||||
/// </summary>
|
||||
/// <param name="period">Wilder smoothing period (must be > 0)</param>
|
||||
public PlusDm(int period = 14)
|
||||
{
|
||||
if (period <= 0)
|
||||
{
|
||||
throw new ArgumentException("Period must be greater than 0", nameof(period));
|
||||
}
|
||||
_dx = new Dx(period);
|
||||
Name = $"PlusDm({period})";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates PlusDm and immediately processes the bar series.
|
||||
/// </summary>
|
||||
public PlusDm(TBarSeries source, int period = 14) : this(period)
|
||||
{
|
||||
var result = Batch(source, period);
|
||||
Last = result[^1];
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public TValue Update(TBar input, bool isNew = true)
|
||||
{
|
||||
_dx.Update(input, isNew);
|
||||
Last = _dx.DmPlus;
|
||||
if (isNew)
|
||||
{
|
||||
Pub?.Invoke(this, new TValueEventArgs { Value = Last, IsNew = isNew });
|
||||
}
|
||||
return Last;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public TValue Update(TValue input, bool isNew = true)
|
||||
{
|
||||
// DM requires OHLC data — scalar update not meaningful
|
||||
return Last;
|
||||
}
|
||||
|
||||
public TSeries Update(TBarSeries source)
|
||||
{
|
||||
var result = new TSeries(source.Count);
|
||||
foreach (var bar in source)
|
||||
{
|
||||
result.Add(Update(bar));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes the indicator state using the provided bar series history.
|
||||
/// </summary>
|
||||
public void Prime(TBarSeries source)
|
||||
{
|
||||
foreach (var bar in source)
|
||||
{
|
||||
Update(bar);
|
||||
}
|
||||
}
|
||||
|
||||
public void Prime(ReadOnlySpan<double> source, TimeSpan? step = null)
|
||||
{
|
||||
// Not applicable — DM requires OHLC bar data, not scalar values
|
||||
}
|
||||
|
||||
public static TSeries Batch(TBarSeries source, int period = 14)
|
||||
{
|
||||
var indicator = new PlusDm(period);
|
||||
return indicator.Update(source);
|
||||
}
|
||||
|
||||
public static (TSeries Results, PlusDm Indicator) Calculate(TBarSeries source, int period = 14)
|
||||
{
|
||||
var indicator = new PlusDm(period);
|
||||
return (indicator.Update(source), indicator);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void Reset()
|
||||
{
|
||||
_dx.Reset();
|
||||
Last = default;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
# PLUS_DM: Plus Directional Movement
|
||||
|
||||
### TL;DR
|
||||
Wilder-smoothed upward directional movement in price units (≥0).
|
||||
|
||||
## Introduction
|
||||
Plus Directional Movement (+DM) measures the magnitude of upward price movement, smoothed using Wilder's method. Unlike +DI which normalizes by true range to produce a percentage, +DM outputs raw smoothed values in price units.
|
||||
|
||||
+DM captures when the current bar's high exceeds the previous bar's high by more than the previous bar's low exceeds the current bar's low. It is the raw building block of the Directional Movement System.
|
||||
|
||||
## Calculation
|
||||
+DM = max(High - PrevHigh, 0) when High - PrevHigh > PrevLow - Low, else 0
|
||||
|
||||
Smoothed using Wilder's method: Smooth = Smooth - Smooth/N + Input
|
||||
|
||||
## Parameters
|
||||
| Parameter | Default | Range | Description |
|
||||
| :--- | :--- | :--- | :--- |
|
||||
| Period | 14 | 2-∞ | Wilder smoothing period |
|
||||
|
||||
## Interpretation
|
||||
- **Rising +DM:** Increasing upward price extension
|
||||
- **+DM > -DM:** Upward movement exceeds downward movement
|
||||
- **Zero +DM:** No upward directional movement on the bar
|
||||
- Values are in price units and scale with the instrument
|
||||
|
||||
## References
|
||||
- Wilder, J. Welles Jr. "New Concepts in Technical Trading Systems" (1978)
|
||||
@@ -1,4 +1,4 @@
|
||||
// The MIT License (MIT)
|
||||
// Licensed under the Apache License, Version 2.0
|
||||
// © mihakralj
|
||||
//@version=6
|
||||
indicator("Qstick Indicator", "QSTICK", overlay=false)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// The MIT License (MIT)
|
||||
// Licensed under the Apache License, Version 2.0
|
||||
// © mihakralj
|
||||
//@version=6
|
||||
indicator("RAVI: Chande Range Action Verification Index", "RAVI", overlay=false)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// The MIT License (MIT)
|
||||
// Licensed under the Apache License, Version 2.0
|
||||
// © mihakralj
|
||||
//@version=6
|
||||
indicator("SuperTrend", "SUPER", overlay=true)
|
||||
|
||||
@@ -325,5 +325,33 @@ public class TtmSqueezeValidationTests
|
||||
Assert.InRange(squeeze.ColorCode, 0, 3);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TtmSqueeze_Correction_Recomputes()
|
||||
{
|
||||
var ind = new TtmSqueeze();
|
||||
var t0 = new DateTime(946_684_800_000_000_0L, DateTimeKind.Utc);
|
||||
|
||||
// Build state well past warmup
|
||||
for (int i = 0; i < 100; i++)
|
||||
{
|
||||
double p = 100.0 + 10.0 * Math.Sin(2.0 * Math.PI * i / 20.0);
|
||||
ind.Update(new TBar(t0.AddMinutes(i), p, p + 2, p - 2, p, 1000), isNew: true);
|
||||
}
|
||||
|
||||
// Anchor bar
|
||||
var anchorTime = t0.AddMinutes(100);
|
||||
const double anchorClose = 105.5;
|
||||
ind.Update(new TBar(anchorTime, anchorClose, anchorClose + 2, anchorClose - 2, anchorClose, 1000), isNew: true);
|
||||
double anchorMomentum = ind.Momentum.Value;
|
||||
|
||||
// Correction with a dramatically different price — Momentum must change
|
||||
ind.Update(new TBar(anchorTime, anchorClose * 10, (anchorClose + 2) * 10, (anchorClose - 2) * 10, anchorClose * 10, 1000), isNew: false);
|
||||
Assert.NotEqual(anchorMomentum, ind.Momentum.Value);
|
||||
|
||||
// Correction back to original price — must exactly restore original Momentum
|
||||
ind.Update(new TBar(anchorTime, anchorClose, anchorClose + 2, anchorClose - 2, anchorClose, 1000), isNew: false);
|
||||
Assert.Equal(anchorMomentum, ind.Momentum.Value, 1e-9);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
@@ -322,7 +322,7 @@ public sealed class TtmSqueeze : ITValuePublisher
|
||||
_priceSum -= oldest;
|
||||
_priceSumSquares -= oldest * oldest;
|
||||
}
|
||||
_priceBuffer.Add(close, isNew);
|
||||
_priceBuffer.Add(close);
|
||||
_priceSum += close;
|
||||
_priceSumSquares += close * close;
|
||||
|
||||
@@ -365,11 +365,11 @@ public sealed class TtmSqueeze : ITValuePublisher
|
||||
_prevSqueezeOn = squeezeOn;
|
||||
|
||||
// === Donchian Midline ===
|
||||
_highBuffer.Add(high, isNew);
|
||||
_lowBuffer.Add(low, isNew);
|
||||
_highBuffer.Add(high);
|
||||
_lowBuffer.Add(low);
|
||||
|
||||
double donchianHigh = GetMax(_highBuffer);
|
||||
double donchianLow = GetMin(_lowBuffer);
|
||||
double donchianHigh = _highBuffer.Max();
|
||||
double donchianLow = _lowBuffer.Min();
|
||||
double donchianMid = (donchianHigh + donchianLow) / 2;
|
||||
|
||||
// === Momentum (Linear Regression) ===
|
||||
@@ -383,7 +383,7 @@ public sealed class TtmSqueeze : ITValuePublisher
|
||||
_momentumSumXY = _momentumSumXY + prevSumY - _momPeriod * oldest;
|
||||
_momentumSumY -= oldest;
|
||||
}
|
||||
_momentumBuffer.Add(deviation, isNew);
|
||||
_momentumBuffer.Add(deviation);
|
||||
_momentumSumY += deviation;
|
||||
|
||||
// Recalculate sumXY during warmup (non-O(1), but short duration)
|
||||
@@ -535,6 +535,10 @@ public sealed class TtmSqueeze : ITValuePublisher
|
||||
_saved_prevMomentum = _prevMomentum;
|
||||
_saved_prevSqueezeOn = _prevSqueezeOn;
|
||||
_saved_barCount = _barCount;
|
||||
_priceBuffer.Snapshot();
|
||||
_highBuffer.Snapshot();
|
||||
_lowBuffer.Snapshot();
|
||||
_momentumBuffer.Snapshot();
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
@@ -552,43 +556,10 @@ public sealed class TtmSqueeze : ITValuePublisher
|
||||
_prevMomentum = _saved_prevMomentum;
|
||||
_prevSqueezeOn = _saved_prevSqueezeOn;
|
||||
_barCount = _saved_barCount;
|
||||
_priceBuffer.Restore();
|
||||
_highBuffer.Restore();
|
||||
_lowBuffer.Restore();
|
||||
_momentumBuffer.Restore();
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static double GetMax(RingBuffer buffer)
|
||||
{
|
||||
if (buffer.Count == 0)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
var span = buffer.GetSpan();
|
||||
double max = span[0];
|
||||
for (int i = 1; i < span.Length; i++)
|
||||
{
|
||||
if (span[i] > max)
|
||||
{
|
||||
max = span[i];
|
||||
}
|
||||
}
|
||||
return max;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static double GetMin(RingBuffer buffer)
|
||||
{
|
||||
if (buffer.Count == 0)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
var span = buffer.GetSpan();
|
||||
double min = span[0];
|
||||
for (int i = 1; i < span.Length; i++)
|
||||
{
|
||||
if (span[i] < min)
|
||||
{
|
||||
min = span[i];
|
||||
}
|
||||
}
|
||||
return min;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// The MIT License (MIT)
|
||||
// Licensed under the Apache License, Version 2.0
|
||||
// © mihakralj
|
||||
//@version=6
|
||||
indicator("TTM Trend", "TTM_TREND", overlay=true)
|
||||
|
||||
+1
-16
@@ -412,22 +412,7 @@ public sealed class Vhf : AbstractBase
|
||||
// Calculate VHF
|
||||
if (closeFilled >= closeBufSize && diffFilled >= period)
|
||||
{
|
||||
// Scan for max/min over close buffer
|
||||
double hi = double.MinValue;
|
||||
double lo = double.MaxValue;
|
||||
for (int k = 0; k < closeBufSize; k++)
|
||||
{
|
||||
double cv = closeBuf[k];
|
||||
if (cv > hi)
|
||||
{
|
||||
hi = cv;
|
||||
}
|
||||
if (cv < lo)
|
||||
{
|
||||
lo = cv;
|
||||
}
|
||||
}
|
||||
|
||||
var (lo, hi) = ((ReadOnlySpan<double>)closeBuf).MinMaxSIMD();
|
||||
double numerator = hi - lo;
|
||||
|
||||
if (diffSum > 1e-10)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// The MIT License (MIT)
|
||||
// Licensed under the Apache License, Version 2.0
|
||||
// © mihakralj
|
||||
//@version=6
|
||||
indicator("VHF: Vertical Horizontal Filter", "VHF", overlay=false)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// The MIT License (MIT)
|
||||
// Licensed under the Apache License, Version 2.0
|
||||
// © mihakralj
|
||||
//@version=6
|
||||
indicator("Vortex Indicator", "VORTEX", overlay=false)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// The MIT License (MIT)
|
||||
// Licensed under the Apache License, Version 2.0
|
||||
// © mihakralj
|
||||
//@version=6
|
||||
indicator("Huber Loss (HUBER)", "HUBER")
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// The MIT License (MIT)
|
||||
// Licensed under the Apache License, Version 2.0
|
||||
// © mihakralj
|
||||
//@version=6
|
||||
indicator("Log-Cosh Loss", "LogCosh", overlay=false)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// The MIT License (MIT)
|
||||
// Licensed under the Apache License, Version 2.0
|
||||
// © mihakralj
|
||||
//@version=6
|
||||
indicator("Mean Arctangent Absolute Percentage Error", "MAAPE", overlay=false, format=format.percent)
|
||||
|
||||
@@ -71,4 +71,30 @@ public sealed class MaeValidationTests : IDisposable
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Mae_Correction_Recomputes()
|
||||
{
|
||||
var ind = new Mae(20);
|
||||
|
||||
// Build state well past warmup
|
||||
for (int i = 0; i < 50; i++)
|
||||
{
|
||||
ind.Update(100.0 + i * 0.5, 98.0 + i * 0.5);
|
||||
}
|
||||
|
||||
// Anchor bar
|
||||
const double anchorActual = 125.0;
|
||||
const double anchorPredicted = 123.0;
|
||||
ind.Update(anchorActual, anchorPredicted, isNew: true);
|
||||
double anchorResult = ind.Last.Value;
|
||||
|
||||
// Correction with dramatically different values — recompute must yield different result
|
||||
ind.Update(anchorActual * 10, anchorPredicted * 10, isNew: false);
|
||||
Assert.NotEqual(anchorResult, ind.Last.Value);
|
||||
|
||||
// Correction back to original — must exactly restore original result
|
||||
ind.Update(anchorActual, anchorPredicted, isNew: false);
|
||||
Assert.Equal(anchorResult, ind.Last.Value, 1e-9);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// The MIT License (MIT)
|
||||
// Licensed under the Apache License, Version 2.0
|
||||
// © mihakralj
|
||||
//@version=6
|
||||
indicator("Mean Absolute Error (MAE)", "MAE")
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// The MIT License (MIT)
|
||||
// Licensed under the Apache License, Version 2.0
|
||||
// © mihakralj
|
||||
//@version=6
|
||||
indicator("Mean Absolute %Deviation (MAPD)", "MAPD")
|
||||
|
||||
@@ -84,4 +84,31 @@ public sealed class MapeValidationTests : IDisposable
|
||||
|
||||
return sum / actual.Length;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Mape_Correction_Recomputes()
|
||||
{
|
||||
var ind = new Mape(20);
|
||||
|
||||
// Build state well past warmup (actual always > 0 so MAPE denominator is valid)
|
||||
for (int i = 0; i < 50; i++)
|
||||
{
|
||||
ind.Update(100.0 + i * 0.5, 98.0 + i * 0.5);
|
||||
}
|
||||
|
||||
// Anchor bar
|
||||
const double anchorActual = 125.0;
|
||||
const double anchorPredicted = 123.0;
|
||||
ind.Update(anchorActual, anchorPredicted, isNew: true);
|
||||
double anchorResult = ind.Last.Value;
|
||||
|
||||
// MAPE is scale-invariant: ×10 on both actual and predicted leaves ratio unchanged.
|
||||
// Change only predicted to dramatically alter the error percentage.
|
||||
ind.Update(anchorActual, 10.0, isNew: false);
|
||||
Assert.NotEqual(anchorResult, ind.Last.Value);
|
||||
|
||||
// Correction back to original — must exactly restore original result
|
||||
ind.Update(anchorActual, anchorPredicted, isNew: false);
|
||||
Assert.Equal(anchorResult, ind.Last.Value, 1e-9);
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user