Merge branch 'dev' into main

This commit is contained in:
Miha Kralj
2026-03-16 12:46:19 -07:00
131 changed files with 1582 additions and 1583 deletions
+9 -9
View File
@@ -8,7 +8,7 @@
| [ACCEL](numerics/accel/Accel.md) | Acceleration | Numerics |
| [ACF](statistics/acf/Acf.md) | Autocorrelation Function | Statistics |
| [ADF](statistics/adf/Adf.md) | Augmented Dickey-Fuller Test | Statistics |
| [ADL](volume/adl/Adl.md) | Accumulation/Distribution Line | Volume |
| [AD](volume/ad/Ad.md) | Accumulation/Distribution Line | Volume |
| [ADOSC](volume/adosc/Adosc.md) | Chaikin A/D Oscillator | Volume |
| [ADR](volatility/adr/Adr.md) | Average Daily Range | Volatility |
| [ADX](dynamics/adx/Adx.md) | Average Directional Index | Dynamics |
@@ -75,7 +75,7 @@
| [CONV](trends_FIR/conv/Conv.md) | Convolution MA | Trends (FIR) |
| [COPPOCK](oscillators/coppock/Coppock.md) | Coppock Curve | Oscillators |
| [CORAL](trends_IIR/coral/Coral.md) | Coral Trend Filter | Trends (IIR) |
| [CORRELATION](statistics/correlation/Correlation.md) | Correlation | Statistics |
| [CORREL](statistics/correl/Correl.md) | Correlation | Statistics |
| [COVARIANCE](statistics/covariance/Covariance.md) | Covariance | Statistics |
| [CRMA](trends_FIR/crma/Crma.md) | Cubic Regression MA | Trends (FIR) |
| [CRSI](oscillators/crsi/Crsi.md) | Connors RSI | Oscillators |
@@ -83,7 +83,7 @@
| [CV](volatility/cv/Cv.md) | Coefficient of Variation | Volatility |
| [CVI](volatility/cvi/Cvi.md) | Chaikin Volatility | Volatility |
| [CWT](numerics/cwt/Cwt.md) | Continuous Wavelet Transform | Numerics |
| [DCHANNEL](channels/dchannel/Dchannel.md) | Donchian Channels | Channels |
| [DC](channels/dc/dc.md) | Donchian Channels | Channels |
| [DECAYCHANNEL](channels/decaychannel/decaychannel.md) | Decay Min-Max Channel | Channels |
| [DECO](oscillators/deco/Deco.md) | Ehlers Decycler Oscillator | Oscillators |
| [DEM](oscillators/dem/Dem.md) | DeMarker Oscillator | Oscillators |
@@ -92,7 +92,7 @@
| [DMX](dynamics/dmx/Dmx.md) | Jurik Directional Movement Index | Dynamics |
| [DOSC](oscillators/dosc/Dosc.md) | Derivative Oscillator | Oscillators |
| [DPO](oscillators/dpo/Dpo.md) | Detrended Price Oscillator | Oscillators |
| [DYMOI](oscillators/dymoi/Dymoi.md) | Dynamic Momentum Index | Oscillators |
| [DYMI](oscillators/dymi/Dymi.md) | Dynamic Momentum Index | Oscillators |
| [DSMA](trends_IIR/dsma/Dsma.md) | Deviation-Scaled MA | Trends (IIR) |
| [DSP](cycles/dsp/Dsp.md) | Ehlers Detrended Synthetic Price | Cycles |
| [DWMA](trends_FIR/dwma/Dwma.md) | Double Weighted MA | Trends (FIR) |
@@ -145,7 +145,7 @@
| [HOMOD](cycles/homod/Homod.md) | Ehlers Homodyne Discriminator | Cycles |
| [HP](filters/hp/Hp.md) | Hodrick-Prescott | Filters |
| [HPF](filters/hpf/Hpf.md) | Ehlers Highpass Filter | Filters |
| [HTIT](trends_IIR/htit/Htit.md) | Ehlers Hilbert Transform Instantaneous Trend (aka HT_TRENDLINE) | Trends (IIR) |
| [HT_TRENDLINE](trends_IIR/ht_trendline/HtTrendline.md) | Ehlers Hilbert Transform Instantaneous Trend | Trends (IIR) |
| [HT_DCPERIOD](cycles/ht_dcperiod/Htdcperiod.md) | Ehlers HT Dominant Cycle Period | Cycles |
| [HT_DCPHASE](cycles/ht_dcphase/Htdcphase.md) | Ehlers HT Dominant Cycle Phase | Cycles |
| [HT_PHASOR](cycles/ht_phasor/Htphasor.md) | Ehlers HT Phasor Components | Cycles |
@@ -172,7 +172,7 @@
| [KAISER](trends_FIR/kaiser/Kaiser.md) | Kaiser Window MA | Trends (FIR) |
| [KALMAN](filters/kalman/Kalman.md) | Kalman Filter | Filters |
| [KAMA](trends_IIR/kama/Kama.md) | Kaufman Adaptive MA | Trends (IIR) |
| [KCHANNEL](channels/kchannel/kchannel.md) | Keltner Channel | Channels |
| [KC](channels/kc/kc.md) | Keltner Channel | Channels |
| [KDJ](oscillators/kdj/Kdj.md) | KDJ Indicator | Oscillators |
| [KENDALL](statistics/kendall/Kendall.md) | Kendall Rank Correlation | Statistics |
| [KRI](oscillators/kri/Kri.md) | Kairi Relative Index | Oscillators |
@@ -238,7 +238,7 @@
| [ONEEURO](filters/oneeuro/OneEuro.md) | One Euro Filter | Filters |
| [PACF](statistics/pacf/Pacf.md) | Partial Autocorrelation Function | Statistics |
| [PARZEN](trends_FIR/parzen/Parzen.md) | Parzen Window MA | Trends (FIR) |
| [PCHANNEL](channels/pchannel/Pchannel.md) | Price Channel | Channels |
| [PC](channels/pc/pc.md) | Price Channel | Channels |
| [PERCENTILE](statistics/percentile/Percentile.md) | Percentile | Statistics |
| [PFE](dynamics/pfe/Pfe.md) | Polarized Fractal Efficiency | Dynamics |
| [PGO](oscillators/pgo/Pgo.md) | Pretty Good Oscillator | Oscillators |
@@ -253,8 +253,8 @@
| [POISSONDIST](numerics/poissondist/Poissondist.md) | Poisson Distribution | Numerics |
| [POLYFIT](statistics/polyfit/Polyfit.md) | Polynomial Fitting | Statistics |
| [PPO](momentum/ppo/Ppo.md) | Percentage Price Oscillator | Momentum |
| [PRS](momentum/prs/Prs.md) | Price Relative Strength | Momentum |
| [PSAR](reversals/psar/Psar.md) | Parabolic Stop And Reverse | Reversals |
| [RS](momentum/rs/Rs.md) | Price Relative Strength | Momentum |
| [SAR](reversals/sar/Sar.md) | Parabolic Stop And Reverse | Reversals |
| [PSL](oscillators/psl/Psl.md) | Psychological Line | Oscillators |
| [PSEUDOHUBER](errors/pseudohuber/Pseudohuber.md) | Pseudo-Huber Loss | Errors |
| [PVD](volume/pvd/Pvd.md) | Price Volume Divergence | Volume |
+3 -3
View File
@@ -12,14 +12,14 @@ Channels define dynamic support and resistance. Upper band shows where price ten
| [APZ](apz/Apz.md) | Adaptive Price Zone | Double-smoothed EMA volatility channel by Lee Leibfarth. Adapts to recent volatility. |
| [ATRBANDS](atrbands/Atrbands.md) | ATR Bands | ATR-based volatility bands around moving average. |
| [BBANDS](bbands/Bbands.md) | Bollinger Bands | Standard deviation bands around SMA. Classic volatility channel. |
| [DCHANNEL](dchannel/Dchannel.md) | Donchian Channels | Highest high and lowest low over N periods. Turtle trading foundation. |
| [DC](dc/dc.md) | Donchian Channels | Highest high and lowest low over N periods. Turtle trading foundation. |
| [DECAYCHANNEL](decaychannel/DecayChannel.md) | Decay Min-Max Channel | Exponentially decaying min-max channel. Half-life decay toward midpoint. |
| [FCB](fcb/Fcb.md) | Fractal Chaos Bands | Tracks fractal highs and lows. Identifies chaos-based support/resistance. |
| [JBANDS](jbands/Jbands.md) | Jurik Adaptive Envelope Bands | JMA's internal adaptive envelopes. Snap to extremes, decay toward price. |
| [KCHANNEL](kchannel/Kchannel.md) | Keltner Channel | EMA with ATR bands. Smoother than Bollinger. |
| [KC](kc/Kc.md) | Keltner Channel | EMA with ATR bands. Smoother than Bollinger. |
| [MAENV](maenv/Maenv.md) | Moving Average Envelope | Fixed percentage bands around moving average. Simple but effective. |
| [MMCHANNEL](mmchannel/MmChannel.md) | Min-Max Channel | Rolling highest high / lowest low using O(1) monotonic deques. |
| [PCHANNEL](pchannel/Pchannel.md) | Price Channel | Highest high and lowest low. Identical to Donchian Channels. |
| [PC](pc/pc.md) | Price Channel | Highest high and lowest low. Identical to Donchian Channels. |
| [REGCHANNEL](regchannel/RegChannel.md) | Linear Regression Channel | Linear regression line with standard deviation bands. |
| [SDCHANNEL](sdchannel/SdChannel.md) | Standard Deviation Channel | Moving average with standard deviation bands. |
| [STARCHANNEL](starchannel/StarChannel.md) | Stoller Average Range Channel | SMA with ATR bands. Similar to Keltner but uses SMA instead of EMA. |
+1 -1
View File
@@ -13,7 +13,7 @@
| **PineScript** | [accbands.pine](accbands.pine) |
- Acceleration Bands construct a volatility envelope using the intra-bar high-low range rather than close-to-close standard deviation, creating chann...
- **Similar:** [BBands](../bbands/bbands.md), [KChannel](../kchannel/kchannel.md) | **Complementary:** ADX for trend strength | **Trading note:** Wider than Bollinger Bands; effective for breakout trading using high-low range volatility.
- **Similar:** [BBands](../bbands/bbands.md), [KC](../kc/kc.md) | **Complementary:** ADX for trend strength | **Trading note:** Wider than Bollinger Bands; effective for breakout trading using high-low range volatility.
- Validated against TA-Lib, Skender, and Tulip reference implementations where available.
Acceleration Bands construct a volatility envelope using the intra-bar high-low range rather than close-to-close standard deviation, creating channels that accommodate the full price excursion of the underlying asset. Each bar's contribution to band width is normalized by price level ($w = (H-L)/(H+L)$), making the bands scale-invariant across instruments. Three independent Simple Moving Averages of the adjusted high, adjusted low, and close prices form the upper, lower, and middle bands respectively. Headley's original breakout rule declares a trend when price closes outside the bands for two consecutive bars.
+1 -1
View File
@@ -13,7 +13,7 @@
| **PineScript** | [apz.pine](apz.pine) |
- APZ constructs a volatility-adaptive envelope using double-smoothed exponential moving averages with an aggressive smoothing factor derived from $\...
- **Similar:** [BBands](../bbands/bbands.md), [KChannel](../kchannel/kchannel.md) | **Complementary:** RSI for overbought/oversold confirmation | **Trading note:** EMA-based deviation adapts faster than standard deviation bands; responsive to recent volatility changes.
- **Similar:** [BBands](../bbands/bbands.md), [KC](../kc/kc.md) | **Complementary:** RSI for overbought/oversold confirmation | **Trading note:** EMA-based deviation adapts faster than standard deviation bands; responsive to recent volatility changes.
- Validated against TA-Lib, Skender, and Tulip reference implementations where available.
APZ constructs a volatility-adaptive envelope using double-smoothed exponential moving averages with an aggressive smoothing factor derived from $\sqrt{\text{period}}$, making it significantly faster than standard EMA-based channels. The center line is a double-EMA of price; the band width is a double-EMA of the high-low range, scaled by a multiplier. Designed specifically for mean-reversion trading in non-trending markets, APZ identifies overbought/oversold extremes where price is likely to reverse rather than continue. A closing price outside the zone signals an immediate overshoot, not a breakout.
+1 -1
View File
@@ -13,7 +13,7 @@
| **PineScript** | [atrbands.pine](atrbands.pine) |
- ATR Bands create a volatility-adaptive envelope by projecting Wilder's Average True Range above and below a central Simple Moving Average.
- **Similar:** [KChannel](../kchannel/kchannel.md), [STBands](../stbands/stbands.md) | **Complementary:** ADX to distinguish trend vs range | **Trading note:** Volatility-normalized symmetric bands using ATR; adapts to true volatility including gaps.
- **Similar:** [KC](../kc/kc.md), [STBands](../stbands/stbands.md) | **Complementary:** ADX to distinguish trend vs range | **Trading note:** Volatility-normalized symmetric bands using ATR; adapts to true volatility including gaps.
- Validated against TA-Lib, Skender, and Tulip reference implementations where available.
ATR Bands create a volatility-adaptive envelope by projecting Wilder's Average True Range above and below a central Simple Moving Average. Unlike fixed-percentage envelopes or standard-deviation bands, ATR Bands use True Range to measure volatility, making them robust for assets with gaps, pre-market moves, and 24/7 trading where the "hidden" volatility between bars is significant. The True Range captures the maximum of intra-bar range, gap-up distance, and gap-down distance, ensuring that overnight gaps contribute fully to band width even when the current bar's open-to-close range is narrow.
+1 -1
View File
@@ -13,7 +13,7 @@
| **PineScript** | [bbands.pine](bbands.pine) |
- Bollinger Bands construct a volatility-adaptive envelope around a Simple Moving Average using population standard deviation as the width measure.
- **Similar:** [KChannel](../kchannel/kchannel.md), [SDChannel](../sdchannel/sdchannel.md), [ABERR](../aberr/aberr.md) | **Complementary:** %B and BandWidth for squeeze detection; RSI for momentum confirmation | **Trading note:** Most widely used band indicator; 2s captures ~95% of normally-distributed data but markets exhibit fat tails.
- **Similar:** [KC](../kc/kc.md), [SDChannel](../sdchannel/sdchannel.md), [ABERR](../aberr/aberr.md) | **Complementary:** %B and BandWidth for squeeze detection; RSI for momentum confirmation | **Trading note:** Most widely used band indicator; 2s captures ~95% of normally-distributed data but markets exhibit fat tails.
- Validated against TA-Lib, Skender, and Tulip reference implementations where available.
Bollinger Bands construct a volatility-adaptive envelope around a Simple Moving Average using population standard deviation as the width measure. The bands expand during high-volatility periods and contract during consolidation, dynamically adapting to changing market conditions. Under Gaussian assumptions, $\pm 2\sigma$ contains approximately 95.4% of price action, but financial returns exhibit fat tails and volatility clustering, so the bands function more as a volatility-normalized reference frame than a strict probability envelope. The derived metrics %B (price position as a fraction of band width) and BandWidth (normalized band spread) extend the raw bands into a complete analytical toolkit.
@@ -5,11 +5,11 @@ using static QuanTAlib.IndicatorExtensions;
namespace QuanTAlib;
/// <summary>
/// Pchannel: Price Channel - Quantower Indicator Adapter
/// Dc: Donchian Channels - Quantower Indicator Adapter
/// Upper = rolling highest high; Lower = rolling lowest low; Middle = (Upper + Lower) / 2.
/// Uses streaming O(1) deques with bar-correction support.
/// </summary>
public sealed class PchannelIndicator : Indicator, IWatchlistIndicator
public sealed class DcIndicator : Indicator, IWatchlistIndicator
{
[InputParameter("Period", sortIndex: 10, minimum: 1, maximum: 500, increment: 1, decimalPlaces: 0)]
public int Period { get; set; } = 20;
@@ -17,14 +17,14 @@ public sealed class PchannelIndicator : Indicator, IWatchlistIndicator
[InputParameter("Show Cold Values", sortIndex: 100)]
public bool ShowColdValues { get; set; } = true;
private Pchannel? _indicator;
private Dc? _indicator;
public int MinHistoryDepths => Period;
public override string ShortName => $"Pchannel({Period})";
public override string ShortName => $"Dc({Period})";
public PchannelIndicator()
public DcIndicator()
{
Name = "Pchannel - Price Channel";
Name = "Dc - Donchian Channels";
Description = "Price channel using rolling highest high / lowest low with midpoint average";
SeparateWindow = false;
OnBackGround = true;
@@ -32,7 +32,7 @@ public sealed class PchannelIndicator : Indicator, IWatchlistIndicator
protected override void OnInit()
{
_indicator = new Pchannel(Period);
_indicator = new Dc(Period);
AddLineSeries(new LineSeries("Middle", Color.DodgerBlue, 2, LineStyle.Solid));
AddLineSeries(new LineSeries("Upper", Color.FromArgb(255, 180, 180), 1, LineStyle.Dash));
@@ -5,13 +5,13 @@ using System.Runtime.InteropServices;
namespace QuanTAlib;
/// <summary>
/// DCHANNEL: Donchian Channels
/// DC: Donchian Channels
/// Upper = rolling highest high; Lower = rolling lowest low; Middle = (Upper + Lower) / 2.
/// Streaming path uses monotonic deques for O(1) amortized updates; corrections (isNew=false)
/// rebuild deques without allocations.
/// </summary>
[SkipLocalsInit]
public sealed class Dchannel : ITValuePublisher
public sealed class Dc : ITValuePublisher
{
private readonly int _period;
private readonly double[] _hBuf;
@@ -39,7 +39,7 @@ public sealed class Dchannel : ITValuePublisher
public event TValuePublishedHandler? Pub;
public Dchannel(int period)
public Dc(int period)
{
if (period <= 0)
{
@@ -56,12 +56,12 @@ public sealed class Dchannel : ITValuePublisher
_state = new State(double.NaN, double.NaN, false);
_p_state = _state;
Name = $"Dchannel({period})";
Name = $"Dc({period})";
WarmupPeriod = period;
_barHandler = HandleBar;
}
public Dchannel(TBarSeries source, int period) : this(period)
public Dc(TBarSeries source, int period) : this(period)
{
Prime(source);
source.Pub += _barHandler;
@@ -318,9 +318,9 @@ public sealed class Dchannel : ITValuePublisher
return (new TSeries(tMiddle, vMiddle), new TSeries(tUpper, vUpper), new TSeries(tLower, vLower));
}
public static ((TSeries Middle, TSeries Upper, TSeries Lower) Results, Dchannel Indicator) Calculate(TBarSeries source, int period)
public static ((TSeries Middle, TSeries Upper, TSeries Lower) Results, Dc Indicator) Calculate(TBarSeries source, int period)
{
var indicator = new Dchannel(source, period);
var indicator = new Dc(source, period);
var results = indicator.Update(source);
return (results, indicator);
}
@@ -1,4 +1,4 @@
# DCHANNEL: Donchian Channels
# DC: Donchian Channels
> *The highest high and lowest low over a window — Donchian's simplicity captures breakout potential in two lines.*
@@ -10,10 +10,10 @@
| **Outputs** | Multiple series (Upper, Lower) |
| **Output range** | Tracks input |
| **Warmup** | `period` bars |
| **PineScript** | [dchannel.pine](dchannel.pine) |
| **PineScript** | [dc.pine](dc.pine) |
- Donchian Channels track the highest high and lowest low over a fixed lookback period, defining the absolute price boundaries within which an asset ...
- **Similar:** [PChannel](../pchannel/pchannel.md), [UChannel](../uchannel/uchannel.md) | **Complementary:** Volume on breakouts; ATR for position sizing | **Trading note:** Pure price-based highest high/lowest low; used in the original Turtle Trading system.
- Donchian Channels track the highest high and lowest low over a fixed lookback period, defining the absolute price boundaries within which an asset has traded.
- **Similar:** [PC](../pc/pc.md), [UChannel](../uchannel/uchannel.md) | **Complementary:** Volume on breakouts; ATR for position sizing | **Trading note:** Pure price-based highest high/lowest low; used in the original Turtle Trading system.
- Validated against TA-Lib, Skender, and Tulip reference implementations where available.
Donchian Channels track the highest high and lowest low over a fixed lookback period, defining the absolute price boundaries within which an asset has traded. Unlike volatility-based bands that compute statistical dispersion, Donchian Channels represent actual historical extremes — the literal "price box." The implementation uses monotonic deques for $O(1)$ amortized sliding-window max/min, ensuring that computing a 500-period channel costs no more than a 20-period one. The midpoint of the upper and lower bands serves as a simple trend bias indicator.
@@ -75,7 +75,7 @@ The bands stay flat until either a new extreme occurs or the old extreme exits t
### Operation Count (Streaming Mode)
DCHANNEL uses two monotonic deques for $O(1)$ amortized sliding-window max/min:
DC uses two monotonic deques for $O(1)$ amortized sliding-window max/min:
| Operation | Count | Cost (cycles) | Subtotal |
| :--- | :---: | :---: | :---: |
@@ -1,7 +1,7 @@
// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Donchian Channels (DCHANNEL)", "DCHANNEL", overlay=true)
indicator("Donchian Channels (DC)", "DC", overlay=true)
//@function Calculates the Donchian Channel (DC) efficiently using monotonic deques
//@param hi Source series for the highest high calculation (usually high)
@@ -9,7 +9,7 @@ indicator("Donchian Channels (DCHANNEL)", "DCHANNEL", overlay=true)
//@param p Lookback period (p > 0)
//@returns Tuple containing [basis, upper_band, lower_band]
//@optimized Uses monotonic deque for O(1) amortized complexity per bar
dchannel(series float hi, series float lo, simple int p) =>
dc(series float hi, series float lo, simple int p) =>
if p <= 0
runtime.error("Period must be > 0")
var float[] hbuf = array.new_float(p, na)
@@ -41,7 +41,7 @@ i_high = input.source(high, "High Source")
i_low = input.source(low, "Low Source")
// Calculation
[basis, upper, lower] = dchannel(i_high, i_low, i_period)
[basis, upper, lower] = dc(i_high, i_low, i_period)
// Plot
plot(basis, "Basis", color=color.yellow, linewidth=2)
@@ -3,16 +3,16 @@ using Xunit;
namespace QuanTAlib.Tests;
public class PchannelIndicatorTests
public class DcIndicatorTests
{
[Fact]
public void Constructor_SetsDefaults()
{
var ind = new PchannelIndicator();
var ind = new DcIndicator();
Assert.Equal(20, ind.Period);
Assert.True(ind.ShowColdValues);
Assert.Equal("Pchannel - Price Channel", ind.Name);
Assert.Equal("Dc - Donchian Channels", ind.Name);
Assert.False(ind.SeparateWindow);
Assert.True(ind.OnBackGround);
}
@@ -20,21 +20,21 @@ public class PchannelIndicatorTests
[Fact]
public void MinHistoryDepths_EqualsPeriod()
{
var ind = new PchannelIndicator { Period = 15 };
var ind = new DcIndicator { Period = 15 };
Assert.Equal(15, ind.MinHistoryDepths);
}
[Fact]
public void ShortName_ReflectsParameters()
{
var ind = new PchannelIndicator { Period = 12 };
var ind = new DcIndicator { Period = 12 };
Assert.Contains("12", ind.ShortName, StringComparison.Ordinal);
}
[Fact]
public void Initialize_AddsThreeLineSeries()
{
var ind = new PchannelIndicator { Period = 14 };
var ind = new DcIndicator { Period = 14 };
ind.Initialize();
Assert.Equal(3, ind.LinesSeries.Count);
@@ -46,7 +46,7 @@ public class PchannelIndicatorTests
[Fact]
public void ProcessUpdate_Historical_ComputesValues()
{
var ind = new PchannelIndicator { Period = 3 };
var ind = new DcIndicator { Period = 3 };
ind.Initialize();
var now = DateTime.UtcNow;
@@ -63,7 +63,7 @@ public class PchannelIndicatorTests
[Fact]
public void ProcessUpdate_NewBar_Appends()
{
var ind = new PchannelIndicator { Period = 3 };
var ind = new DcIndicator { Period = 3 };
ind.Initialize();
var now = DateTime.UtcNow;
@@ -79,7 +79,7 @@ public class PchannelIndicatorTests
[Fact]
public void ProcessUpdate_NewTick_DoesNotThrow()
{
var ind = new PchannelIndicator { Period = 5 };
var ind = new DcIndicator { Period = 5 };
ind.Initialize();
var now = DateTime.UtcNow;
@@ -94,7 +94,7 @@ public class PchannelIndicatorTests
[Fact]
public void MultipleUpdates_ProducesFiniteSeries()
{
var ind = new PchannelIndicator { Period = 5 };
var ind = new DcIndicator { Period = 5 };
ind.Initialize();
var now = DateTime.UtcNow;
@@ -119,7 +119,7 @@ public class PchannelIndicatorTests
[Fact]
public void Bands_Order_Correct()
{
var ind = new PchannelIndicator { Period = 3 };
var ind = new DcIndicator { Period = 3 };
ind.Initialize();
var now = DateTime.UtcNow;
@@ -4,23 +4,23 @@ using Xunit;
namespace QuanTAlib.Tests;
public class DchannelTests
public class DcTests
{
[Fact]
public void Dchannel_Constructor_ValidatesInput()
public void Dc_Constructor_ValidatesInput()
{
Assert.Throws<ArgumentException>(() => new Dchannel(0));
Assert.Throws<ArgumentException>(() => new Dchannel(-5));
Assert.Throws<ArgumentException>(() => new Dc(0));
Assert.Throws<ArgumentException>(() => new Dc(-5));
var d = new Dchannel(10);
var d = new Dc(10);
Assert.Equal(10, d.WarmupPeriod);
Assert.Contains("Dchannel", d.Name, StringComparison.OrdinalIgnoreCase);
Assert.Contains("Dc", d.Name, StringComparison.OrdinalIgnoreCase);
}
[Fact]
public void Dchannel_InitialState_Defaults()
public void Dc_InitialState_Defaults()
{
var d = new Dchannel(5);
var d = new Dc(5);
Assert.Equal(0, d.Last.Value);
Assert.Equal(0, d.Upper.Value);
@@ -29,9 +29,9 @@ public class DchannelTests
}
[Fact]
public void Dchannel_CalculatesBands()
public void Dc_CalculatesBands()
{
var d = new Dchannel(3);
var d = new Dc(3);
d.Update(new TBar(DateTime.UtcNow, 100, 110, 90, 105, 1000));
d.Update(new TBar(DateTime.UtcNow, 105, 115, 95, 110, 1000));
@@ -45,9 +45,9 @@ public class DchannelTests
}
[Fact]
public void Dchannel_SlidingWindow_Updates()
public void Dc_SlidingWindow_Updates()
{
var d = new Dchannel(2);
var d = new Dc(2);
d.Update(new TBar(DateTime.UtcNow, 100, 110, 90, 100, 1000));
d.Update(new TBar(DateTime.UtcNow, 101, 111, 91, 101, 1000));
@@ -64,9 +64,9 @@ public class DchannelTests
}
[Fact]
public void Dchannel_IsHot_TurnsTrueAfterWarmup()
public void Dc_IsHot_TurnsTrueAfterWarmup()
{
var d = new Dchannel(4);
var d = new Dc(4);
for (int i = 0; i < 3; i++)
{
@@ -79,9 +79,9 @@ public class DchannelTests
}
[Fact]
public void Dchannel_IsNewFalse_RebuildsState()
public void Dc_IsNewFalse_RebuildsState()
{
var d = new Dchannel(3);
var d = new Dc(3);
var gbm = new GBM(startPrice: 100, mu: 0.01, sigma: 0.1, seed: 7);
TBar remembered = default;
@@ -109,9 +109,9 @@ public class DchannelTests
}
[Fact]
public void Dchannel_NaN_UsesLastValid()
public void Dc_NaN_UsesLastValid()
{
var d = new Dchannel(3);
var d = new Dc(3);
d.Update(new TBar(DateTime.UtcNow, 100, 110, 90, 105, 1000));
d.Update(new TBar(DateTime.UtcNow, 101, 111, 91, 106, 1000));
@@ -126,9 +126,9 @@ public class DchannelTests
}
[Fact]
public void Dchannel_Reset_Clears()
public void Dc_Reset_Clears()
{
var d = new Dchannel(3);
var d = new Dc(3);
d.Update(new TBar(DateTime.UtcNow, 100, 110, 90, 100, 1000));
d.Update(new TBar(DateTime.UtcNow, 101, 111, 91, 101, 1000));
@@ -144,9 +144,9 @@ public class DchannelTests
}
[Fact]
public void Dchannel_BatchVsStreaming_Match()
public void Dc_BatchVsStreaming_Match()
{
var dStream = new Dchannel(10);
var dStream = new Dc(10);
var gbm = new GBM(startPrice: 100, mu: 0.02, sigma: 0.1, seed: 42);
var series = new TBarSeries();
@@ -161,7 +161,7 @@ public class DchannelTests
double expectedUp = dStream.Upper.Value;
double expectedLo = dStream.Lower.Value;
var (midBatch, upBatch, loBatch) = Dchannel.Batch(series, 10);
var (midBatch, upBatch, loBatch) = Dc.Batch(series, 10);
Assert.Equal(expectedMid, midBatch.Last.Value, 1e-10);
Assert.Equal(expectedUp, upBatch.Last.Value, 1e-10);
@@ -169,7 +169,7 @@ public class DchannelTests
}
[Fact]
public void Dchannel_SpanBatch_Validates()
public void Dc_SpanBatch_Validates()
{
double[] high = [110, 115, 120];
double[] low = [90, 95, 100];
@@ -180,14 +180,14 @@ public class DchannelTests
double[] highShort = [110, 115];
double[] smallOut = new double[1];
Assert.Throws<ArgumentException>(() => Dchannel.Batch(high.AsSpan(), low.AsSpan(), middle.AsSpan(), upper.AsSpan(), lower.AsSpan(), 0));
Assert.Throws<ArgumentException>(() => Dchannel.Batch(high.AsSpan(), low.AsSpan(), middle.AsSpan(), upper.AsSpan(), lower.AsSpan(), -1));
Assert.Throws<ArgumentException>(() => Dchannel.Batch(highShort.AsSpan(), low.AsSpan(), middle.AsSpan(), upper.AsSpan(), lower.AsSpan(), 2));
Assert.Throws<ArgumentException>(() => Dchannel.Batch(high.AsSpan(), low.AsSpan(), smallOut.AsSpan(), upper.AsSpan(), lower.AsSpan(), 2));
Assert.Throws<ArgumentException>(() => Dc.Batch(high.AsSpan(), low.AsSpan(), middle.AsSpan(), upper.AsSpan(), lower.AsSpan(), 0));
Assert.Throws<ArgumentException>(() => Dc.Batch(high.AsSpan(), low.AsSpan(), middle.AsSpan(), upper.AsSpan(), lower.AsSpan(), -1));
Assert.Throws<ArgumentException>(() => Dc.Batch(highShort.AsSpan(), low.AsSpan(), middle.AsSpan(), upper.AsSpan(), lower.AsSpan(), 2));
Assert.Throws<ArgumentException>(() => Dc.Batch(high.AsSpan(), low.AsSpan(), smallOut.AsSpan(), upper.AsSpan(), lower.AsSpan(), 2));
}
[Fact]
public void Dchannel_SpanBatch_ComputesCorrectly()
public void Dc_SpanBatch_ComputesCorrectly()
{
double[] high = [110, 115, 120, 125];
double[] low = [90, 95, 100, 105];
@@ -195,7 +195,7 @@ public class DchannelTests
double[] upper = new double[4];
double[] lower = new double[4];
Dchannel.Batch(high.AsSpan(), low.AsSpan(), middle.AsSpan(), upper.AsSpan(), lower.AsSpan(), 3);
Dc.Batch(high.AsSpan(), low.AsSpan(), middle.AsSpan(), upper.AsSpan(), lower.AsSpan(), 3);
// Period=3: index 2 is first valid (indices 0,1,2)
// H=[110,115,120], L=[90,95,100] → Upper=120, Lower=90, Middle=105
@@ -210,14 +210,14 @@ public class DchannelTests
}
[Fact]
public void Dchannel_Calculate_ReturnsIndicatorAndResults()
public void Dc_Calculate_ReturnsIndicatorAndResults()
{
var series = new TBarSeries();
series.Add(DateTime.UtcNow, 100, 110, 90, 100, 1000);
series.Add(DateTime.UtcNow, 105, 115, 95, 105, 1000);
series.Add(DateTime.UtcNow, 110, 120, 100, 110, 1000);
var ((mid, up, lo), ind) = Dchannel.Calculate(series, 2);
var ((mid, up, lo), ind) = Dc.Calculate(series, 2);
Assert.True(ind.IsHot);
// Period=2: last 2 bars H=[115,120], L=[95,100] → Upper=120, Lower=95, Middle=107.5
@@ -233,10 +233,10 @@ public class DchannelTests
}
[Fact]
public void Dchannel_Event_Publishes()
public void Dc_Event_Publishes()
{
var src = new TBarSeries();
var d = new Dchannel(src, 2);
var d = new Dc(src, 2);
bool fired = false;
d.Pub += (object? sender, in TValueEventArgs args) => fired = true;
@@ -6,13 +6,13 @@ using OoplesFinance.StockIndicators.Models;
namespace QuanTAlib.Tests;
public sealed class DchannelValidationTests : IDisposable
public sealed class DcValidationTests : IDisposable
{
private readonly ValidationTestData _testData;
private readonly ITestOutputHelper _output;
private bool _disposed;
public DchannelValidationTests(ITestOutputHelper output)
public DcValidationTests(ITestOutputHelper output)
{
_output = output;
_testData = new ValidationTestData();
@@ -44,7 +44,7 @@ public sealed class DchannelValidationTests : IDisposable
series.Add(new TBar(t0.AddMinutes(1), 0, 14, 10, 12, 100));
series.Add(new TBar(t0.AddMinutes(2), 0, 16, 12, 14, 100));
var ind = new Dchannel(3);
var ind = new Dc(3);
var (mid, up, lo) = ind.Update(series);
Assert.Equal(16.0, up.Last.Value, 1e-10);
@@ -52,7 +52,7 @@ public sealed class DchannelValidationTests : IDisposable
Assert.Equal(12.0, mid.Last.Value, 1e-10);
Assert.True(ind.IsHot);
_output.WriteLine("Dchannel manual period-3 calculation validated");
_output.WriteLine("Dc manual period-3 calculation validated");
}
[Fact]
@@ -63,18 +63,18 @@ public sealed class DchannelValidationTests : IDisposable
foreach (int period in periods)
{
// Batch (instance)
var inst = new Dchannel(period);
var inst = new Dc(period);
var (bMid, bUp, bLo) = inst.Update(_testData.Bars);
// Static batch
var (sMid, sUp, sLo) = Dchannel.Batch(_testData.Bars, period);
var (sMid, sUp, sLo) = Dc.Batch(_testData.Bars, period);
ValidationHelper.VerifySeriesEqual(bMid, sMid);
ValidationHelper.VerifySeriesEqual(bUp, sUp);
ValidationHelper.VerifySeriesEqual(bLo, sLo);
// Streaming
var streaming = new Dchannel(period);
var streaming = new Dc(period);
var sMidStream = new TSeries();
var sUpStream = new TSeries();
var sLoStream = new TSeries();
@@ -96,7 +96,7 @@ public sealed class DchannelValidationTests : IDisposable
double[] spanMid = new double[high.Length];
double[] spanUp = new double[high.Length];
double[] spanLo = new double[high.Length];
Dchannel.Batch(high.AsSpan(), low.AsSpan(),
Dc.Batch(high.AsSpan(), low.AsSpan(),
spanMid.AsSpan(), spanUp.AsSpan(), spanLo.AsSpan(), period);
for (int i = 0; i < high.Length; i++)
@@ -107,7 +107,7 @@ public sealed class DchannelValidationTests : IDisposable
}
}
_output.WriteLine("Dchannel mode consistency validated (batch/stream/span)");
_output.WriteLine("Dc mode consistency validated (batch/stream/span)");
}
[Fact]
@@ -115,7 +115,7 @@ public sealed class DchannelValidationTests : IDisposable
{
const int period = 20;
var pub = new TBarSeries();
var evtInd = new Dchannel(pub, period);
var evtInd = new Dc(pub, period);
var evtMid = new TSeries();
var evtUp = new TSeries();
var evtLo = new TSeries();
@@ -128,20 +128,20 @@ public sealed class DchannelValidationTests : IDisposable
evtLo.Add(evtInd.Lower);
}
var (bMid, bUp, bLo) = Dchannel.Batch(_testData.Bars, period);
var (bMid, bUp, bLo) = Dc.Batch(_testData.Bars, period);
ValidationHelper.VerifySeriesEqual(bMid, evtMid);
ValidationHelper.VerifySeriesEqual(bUp, evtUp);
ValidationHelper.VerifySeriesEqual(bLo, evtLo);
_output.WriteLine("Dchannel eventing mode validated");
_output.WriteLine("Dc eventing mode validated");
}
[Fact]
public void Validate_Calculate_ReturnsHotIndicator()
{
const int period = 15;
var ((mid, up, lo), ind) = Dchannel.Calculate(_testData.Bars, period);
var ((mid, up, lo), ind) = Dc.Calculate(_testData.Bars, period);
Assert.True(ind.IsHot);
Assert.Equal(period, ind.WarmupPeriod);
@@ -154,7 +154,7 @@ public sealed class DchannelValidationTests : IDisposable
ind.Update(next);
Assert.True(ind.IsHot);
_output.WriteLine("Dchannel Calculate validated");
_output.WriteLine("Dc Calculate validated");
}
[Fact]
@@ -162,9 +162,9 @@ public sealed class DchannelValidationTests : IDisposable
{
const int period = 25;
var (bMid, bUp, bLo) = Dchannel.Batch(_testData.Bars, period);
var (bMid, bUp, bLo) = Dc.Batch(_testData.Bars, period);
var primed = new Dchannel(period);
var primed = new Dc(period);
var subset = new TBarSeries();
for (int i = 0; i < 200; i++)
{
@@ -182,13 +182,13 @@ public sealed class DchannelValidationTests : IDisposable
Assert.Equal(bUp.Last.Value, primed.Upper.Value, 1e-9);
Assert.Equal(bLo.Last.Value, primed.Lower.Value, 1e-9);
_output.WriteLine("Dchannel Prime validated against batch");
_output.WriteLine("Dc Prime validated against batch");
}
[Fact]
public void Validate_LargeDataset_FiniteOutputs()
{
var (mid, up, lo) = Dchannel.Batch(_testData.Bars, 50);
var (mid, up, lo) = Dc.Batch(_testData.Bars, 50);
ValidationHelper.VerifyAllFinite(mid, startIndex: 0);
ValidationHelper.VerifyAllFinite(up, startIndex: 0);
@@ -199,20 +199,20 @@ public sealed class DchannelValidationTests : IDisposable
Assert.True(up[i].Value >= lo[i].Value, $"Upper >= Lower at {i}");
}
_output.WriteLine("Dchannel large dataset validated");
_output.WriteLine("Dc large dataset validated");
}
[Fact]
public void Validate_Skender_Batch_UpperBand()
{
// Convention difference: Skender Donchian uses prior N bars [i-N, i-1] (excludes current bar)
// QuanTAlib Dchannel uses inclusive N bars [i-N+1, i] (includes current bar).
// QuanTAlib Dc uses inclusive N bars [i-N+1, i] (includes current bar).
// Therefore: QuanTAlib[i] should match Skender[i+1] for converged values.
int[] periods = { 5, 10, 20, 50, 100 };
foreach (var period in periods)
{
var (_, qUp, _) = Dchannel.Batch(_testData.Bars, period);
var (_, qUp, _) = Dc.Batch(_testData.Bars, period);
var sResult = _testData.SkenderQuotes.GetDonchian(period).ToList();
int count = Math.Min(qUp.Count, sResult.Count);
@@ -231,7 +231,7 @@ public sealed class DchannelValidationTests : IDisposable
$"Period={period}, Mismatch at q[{i}] vs s[{i + 1}]: QuanTAlib={qValue:G17}, Skender={sValue.Value:G17}");
}
}
_output.WriteLine("Dchannel upper band validated against Skender GetDonchian (offset +1)");
_output.WriteLine("Dc upper band validated against Skender GetDonchian (offset +1)");
}
[Fact]
@@ -242,7 +242,7 @@ public sealed class DchannelValidationTests : IDisposable
foreach (var period in periods)
{
var (_, _, qLo) = Dchannel.Batch(_testData.Bars, period);
var (_, _, qLo) = Dc.Batch(_testData.Bars, period);
var sResult = _testData.SkenderQuotes.GetDonchian(period).ToList();
int count = Math.Min(qLo.Count, sResult.Count);
@@ -261,7 +261,7 @@ public sealed class DchannelValidationTests : IDisposable
$"Period={period}, Mismatch at q[{i}] vs s[{i + 1}]: QuanTAlib={qValue:G17}, Skender={sValue.Value:G17}");
}
}
_output.WriteLine("Dchannel lower band validated against Skender GetDonchian (offset +1)");
_output.WriteLine("Dc lower band validated against Skender GetDonchian (offset +1)");
}
[Fact]
@@ -272,7 +272,7 @@ public sealed class DchannelValidationTests : IDisposable
foreach (var period in periods)
{
var (qMid, _, _) = Dchannel.Batch(_testData.Bars, period);
var (qMid, _, _) = Dc.Batch(_testData.Bars, period);
var sResult = _testData.SkenderQuotes.GetDonchian(period).ToList();
int count = Math.Min(qMid.Count, sResult.Count);
@@ -291,7 +291,7 @@ public sealed class DchannelValidationTests : IDisposable
$"Period={period}, Mismatch at q[{i}] vs s[{i + 1}]: QuanTAlib={qValue:G17}, Skender={sValue.Value:G17}");
}
}
_output.WriteLine("Dchannel centerline validated against Skender GetDonchian (offset +1)");
_output.WriteLine("Dc centerline validated against Skender GetDonchian (offset +1)");
}
[Fact]
@@ -302,12 +302,12 @@ public sealed class DchannelValidationTests : IDisposable
foreach (var period in periods)
{
var dchannel = new Dchannel(period);
var dc = new Dc(period);
var qUpResults = new TSeries();
foreach (var bar in _testData.Bars)
{
dchannel.Update(bar);
qUpResults.Add(dchannel.Upper);
dc.Update(bar);
qUpResults.Add(dc.Upper);
}
var sResult = _testData.SkenderQuotes.GetDonchian(period).ToList();
@@ -328,11 +328,11 @@ public sealed class DchannelValidationTests : IDisposable
$"Period={period}, Mismatch at q[{i}] vs s[{i + 1}]: QuanTAlib={qValue:G17}, Skender={sValue.Value:G17}");
}
}
_output.WriteLine("Dchannel streaming upper band validated against Skender GetDonchian (offset +1)");
_output.WriteLine("Dc streaming upper band validated against Skender GetDonchian (offset +1)");
}
[Fact]
public void Dchannel_MatchesOoples_Structural()
public void Dc_MatchesOoples_Structural()
{
var gbm = new GBM(startPrice: 100.0, mu: 0.02, sigma: 0.15, seed: 42);
var bars = gbm.Fetch(500, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
+1 -1
View File
@@ -13,7 +13,7 @@
| **PineScript** | [decaychannel.pine](decaychannel.pine) |
- Decay Channel combines the absolute price boundaries of Donchian Channels with exponential decay toward the midpoint, creating an envelope that exp...
- **Similar:** [DChannel](../dchannel/dchannel.md), [PChannel](../pchannel/pchannel.md) | **Complementary:** Trend indicators like ADX | **Trading note:** Bands decay toward price when no new extremes form, reducing lag compared to traditional channels.
- **Similar:** [DC](../dc/dc.md), [PC](../pc/pc.md) | **Complementary:** Trend indicators like ADX | **Trading note:** Bands decay toward price when no new extremes form, reducing lag compared to traditional channels.
- Validated against TA-Lib, Skender, and Tulip reference implementations where available.
Decay Channel combines the absolute price boundaries of Donchian Channels with exponential decay toward the midpoint, creating an envelope that expands instantly on new volatility but contracts smoothly during consolidation. While Donchian Channels hold their width until an extreme exits the lookback window, Decay Channel allows the bands to "forget" old extremes over time using a half-life model. The period parameter serves as the half-life: after that many bars without a new extreme, the band has decayed 50% of the distance back toward center. The decayed values are always clamped within Donchian bounds, ensuring they never extrapolate beyond actual price history.
+1 -1
View File
@@ -13,7 +13,7 @@
| **PineScript** | [fcb.pine](fcb.pine) |
- Fractal Chaos Bands filter raw price action through Bill Williams' fractal detection logic, tracking the highest confirmed fractal high and lowest ...
- **Similar:** [DChannel](../dchannel/dchannel.md), [PChannel](../pchannel/pchannel.md) | **Complementary:** Williams Fractals for additional confirmation | **Trading note:** Based on Bill Williams' fractal theory; bands update only on fractal pivots, creating a staircase pattern.
- **Similar:** [DC](../dc/dc.md), [PC](../pc/pc.md) | **Complementary:** Williams Fractals for additional confirmation | **Trading note:** Based on Bill Williams' fractal theory; bands update only on fractal pivots, creating a staircase pattern.
- Validated against TA-Lib, Skender, and Tulip reference implementations where available.
Fractal Chaos Bands filter raw price action through Bill Williams' fractal detection logic, tracking the highest confirmed fractal high and lowest confirmed fractal low over a lookback period. Unlike Donchian Channels which use every bar's high and low, FCB uses only structurally significant turning points — bars where the middle element of a 3-bar pattern is a local extremum. The result is a "cleaner" channel that ignores transient spikes and focuses on confirmed support and resistance levels. The bands tend to remain flat during trends and step discretely when new structural pivots form, making them useful for identifying genuine breakouts versus noise.
+1 -1
View File
@@ -13,7 +13,7 @@
| **PineScript** | [Jbands.pine](Jbands.pine) |
- JBANDS expose the internal adaptive envelope mechanism of the Jurik Moving Average (JMA), producing asymmetric bands that snap instantly to new pri...
- **Similar:** [BBands](../bbands/bbands.md), [KChannel](../kchannel/kchannel.md) | **Complementary:** Momentum oscillators for reversal timing | **Trading note:** Uses JMA (Jurik Moving Average) as center line for smoother, lower-lag bands compared to standard Bollinger.
- **Similar:** [BBands](../bbands/bbands.md), [KC](../kc/kc.md) | **Complementary:** Momentum oscillators for reversal timing | **Trading note:** Uses JMA (Jurik Moving Average) as center line for smoother, lower-lag bands compared to standard Bollinger.
- Validated against TA-Lib, Skender, and Tulip reference implementations where available.
JBANDS expose the internal adaptive envelope mechanism of the Jurik Moving Average (JMA), producing asymmetric bands that snap instantly to new price extremes and decay exponentially during consolidation. Unlike standard volatility bands (Bollinger, Keltner) which maintain symmetric width around a center line, JBANDS feature "snap-and-decay" hysteresis: expansion is instantaneous (plasticity), contraction is gradual (elasticity). The decay rate is dynamically modulated by a two-stage volatility estimator — a 10-bar SMA feeding a 128-bar trimmed mean — making the bands tight during quiet markets and expansive during trends. The center line is the full JMA: a 2-pole IIR filter with phase control and adaptive alpha.
@@ -5,14 +5,14 @@ using static QuanTAlib.IndicatorExtensions;
namespace QuanTAlib;
/// <summary>
/// Kchannel: Keltner Channel - Quantower Indicator Adapter
/// Kc: Keltner Channel - Quantower Indicator Adapter
/// A volatility-based envelope using EMA as the middle line and ATR for band width.
/// Middle = EMA(close, period) with warmup compensation
/// Upper = Middle + (multiplier × ATR)
/// Lower = Middle - (multiplier × ATR)
/// ATR uses RMA (Wilder's smoothing) with warmup compensation.
/// </summary>
public sealed class KchannelIndicator : Indicator, IWatchlistIndicator
public sealed class KcIndicator : Indicator, IWatchlistIndicator
{
[InputParameter("Period", sortIndex: 10, minimum: 1, maximum: 500, increment: 1, decimalPlaces: 0)]
public int Period { get; set; } = 20;
@@ -23,14 +23,14 @@ public sealed class KchannelIndicator : Indicator, IWatchlistIndicator
[InputParameter("Show Cold Values", sortIndex: 100)]
public bool ShowColdValues { get; set; } = true;
private Kchannel? _indicator;
private Kc? _indicator;
public int MinHistoryDepths => Period * 2;
public override string ShortName => $"Kchannel({Period},{Multiplier})";
public override string ShortName => $"Kc({Period},{Multiplier})";
public KchannelIndicator()
public KcIndicator()
{
Name = "Kchannel - Keltner Channel";
Name = "Kc - Keltner Channel";
Description = "EMA-based channel with ATR-derived band width";
SeparateWindow = false;
OnBackGround = true;
@@ -38,7 +38,7 @@ public sealed class KchannelIndicator : Indicator, IWatchlistIndicator
protected override void OnInit()
{
_indicator = new Kchannel(Period, Multiplier);
_indicator = new Kc(Period, Multiplier);
AddLineSeries(new LineSeries("Middle", Color.DodgerBlue, 2, LineStyle.Solid));
AddLineSeries(new LineSeries("Upper", Color.FromArgb(255, 180, 180), 1, LineStyle.Dash));
@@ -5,7 +5,7 @@ using System.Runtime.InteropServices;
namespace QuanTAlib;
/// <summary>
/// KCHANNEL: Keltner Channel
/// KC: Keltner Channel
/// A volatility-based envelope using EMA as the middle line and ATR for band width.
/// Middle = EMA(source, period) with warmup compensation
/// Upper = Middle + (multiplier × ATR)
@@ -13,7 +13,7 @@ namespace QuanTAlib;
/// ATR uses RMA (Wilder's smoothing) with warmup compensation.
/// </summary>
[SkipLocalsInit]
public sealed class Kchannel : ITValuePublisher
public sealed class Kc : ITValuePublisher
{
private readonly int _period;
private readonly double _multiplier;
@@ -49,7 +49,7 @@ public sealed class Kchannel : ITValuePublisher
public event TValuePublishedHandler? Pub;
public Kchannel(int period = 20, double multiplier = 2.0)
public Kc(int period = 20, double multiplier = 2.0)
{
if (period < 1)
{
@@ -68,13 +68,13 @@ public sealed class Kchannel : ITValuePublisher
WarmupPeriod = period * 2;
Name = $"Kchannel({period},{multiplier})";
Name = $"Kc({period},{multiplier})";
_barHandler = HandleBar;
Reset();
}
public Kchannel(TBarSeries source, int period = 20, double multiplier = 2.0) : this(period, multiplier)
public Kc(TBarSeries source, int period = 20, double multiplier = 2.0) : this(period, multiplier)
{
Prime(source);
source.Pub += _barHandler;
@@ -403,9 +403,9 @@ public sealed class Kchannel : ITValuePublisher
return (new TSeries(tMiddle, vMiddle), new TSeries(tUpper, vUpper), new TSeries(tLower, vLower));
}
public static ((TSeries Middle, TSeries Upper, TSeries Lower) Results, Kchannel Indicator) Calculate(TBarSeries source, int period = 20, double multiplier = 2.0)
public static ((TSeries Middle, TSeries Upper, TSeries Lower) Results, Kc Indicator) Calculate(TBarSeries source, int period = 20, double multiplier = 2.0)
{
var indicator = new Kchannel(source, period, multiplier);
var indicator = new Kc(source, period, multiplier);
var results = indicator.Update(source);
return (results, indicator);
}
@@ -1,4 +1,4 @@
# KCHANNEL: Keltner Channel
# KC: Keltner Channel
> *Keltner wraps an EMA in ATR-scaled bands — a volatility envelope that responds to both trend and range.*
@@ -10,10 +10,10 @@
| **Outputs** | Multiple series (Upper, Lower) |
| **Output range** | Tracks input |
| **Warmup** | `period * 2` bars |
| **PineScript** | [kchannel.pine](kchannel.pine) |
| **PineScript** | [kc.pine](kc.pine) |
- Keltner Channel constructs a volatility-adaptive envelope by projecting Average True Range above and below an Exponential Moving Average center line.
- **Similar:** [BBands](../bbands/bbands.md), [APZ](../apz/apz.md) | **Complementary:** Bollinger Band squeeze (BBands inside KChannel signals compression); MACD for trend direction | **Trading note:** ATR-based width adapts to true volatility including gaps; Chester Keltner's 1960 original used typical price and average range.
- **Similar:** [BBands](../bbands/bbands.md), [APZ](../apz/apz.md) | **Complementary:** Bollinger Band squeeze (BBands inside KC signals compression); MACD for trend direction | **Trading note:** ATR-based width adapts to true volatility including gaps; Chester Keltner's 1960 original used typical price and average range.
- Validated against TA-Lib, Skender, and Tulip reference implementations where available.
Keltner Channel constructs a volatility-adaptive envelope by projecting Average True Range above and below an Exponential Moving Average center line. The channel differs from ATR Bands solely in the center line: Keltner uses EMA (faster, more responsive) while ATR Bands use SMA (more stable, more lag). The EMA center combined with ATR width creates a channel that both tracks trend and adapts to volatility, making it one of the most widely used channel indicators for trend-following and mean-reversion strategies. The implementation uses EMA with warmup compensation for accurate early values and Wilder's smoothing (RMA) for ATR.
@@ -93,7 +93,7 @@ $O(1)$ per bar: one EMA update, one True Range computation, one RMA update, and
### Operation Count (Streaming Mode)
KCHANNEL combines an EMA with warmup compensation (center), True Range computation, and Wilder's RMA with warmup compensation (ATR):
KC combines an EMA with warmup compensation (center), True Range computation, and Wilder's RMA with warmup compensation (ATR):
| Operation | Count | Cost (cycles) | Subtotal |
| :--- | :---: | :---: | :---: |
@@ -1,7 +1,7 @@
// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Keltner Channel (KCHANNEL)", "KCHANNEL", overlay=true)
indicator("Keltner Channel (KC)", "KC", overlay=true)
//@function Calculates Keltner Channel using EMA and ATR
//@param source Series to calculate middle line from
@@ -9,7 +9,7 @@ indicator("Keltner Channel (KCHANNEL)", "KCHANNEL", overlay=true)
//@param mult ATR multiplier for band width
//@returns tuple with [middle, upper, lower] band values
//@optimized Uses EMA with warmup and ATR with compensator, O(1) complexity per bar
kchannel(series float source, simple int length, simple float mult) =>
kc(series float source, simple int length, simple float mult) =>
if length <= 0 or mult <= 0.0
runtime.error("Length and multiplier must be greater than 0")
var float alpha = 2.0 / (length + 1)
@@ -48,7 +48,7 @@ i_length = input.int(20, "Length", minval=1)
i_mult = input.float(2.0, "ATR Multiplier", minval=0.001)
// Calculation
[middle, upper, lower] = kchannel(i_source, i_length, i_mult)
[middle, upper, lower] = kc(i_source, i_length, i_mult)
// Plot
plot(middle, "Middle", color=color.yellow, linewidth=2)
@@ -3,17 +3,17 @@ using Xunit;
namespace QuanTAlib.Tests;
public class KchannelIndicatorTests
public class KcIndicatorTests
{
[Fact]
public void Constructor_SetsDefaults()
{
var ind = new KchannelIndicator();
var ind = new KcIndicator();
Assert.Equal(20, ind.Period);
Assert.Equal(2.0, ind.Multiplier);
Assert.True(ind.ShowColdValues);
Assert.Equal("Kchannel - Keltner Channel", ind.Name);
Assert.Equal("Kc - Keltner Channel", ind.Name);
Assert.False(ind.SeparateWindow);
Assert.True(ind.OnBackGround);
}
@@ -21,14 +21,14 @@ public class KchannelIndicatorTests
[Fact]
public void MinHistoryDepths_EqualsPeriodTimesTwo()
{
var ind = new KchannelIndicator { Period = 15 };
var ind = new KcIndicator { Period = 15 };
Assert.Equal(30, ind.MinHistoryDepths); // Period * 2
}
[Fact]
public void ShortName_ReflectsParameters()
{
var ind = new KchannelIndicator { Period = 12, Multiplier = 1.5 };
var ind = new KcIndicator { Period = 12, Multiplier = 1.5 };
Assert.Contains("12", ind.ShortName, StringComparison.Ordinal);
Assert.Contains("1.5", ind.ShortName, StringComparison.Ordinal);
}
@@ -36,7 +36,7 @@ public class KchannelIndicatorTests
[Fact]
public void Initialize_AddsThreeLineSeries()
{
var ind = new KchannelIndicator { Period = 14, Multiplier = 2.0 };
var ind = new KcIndicator { Period = 14, Multiplier = 2.0 };
ind.Initialize();
Assert.Equal(3, ind.LinesSeries.Count);
@@ -48,7 +48,7 @@ public class KchannelIndicatorTests
[Fact]
public void ProcessUpdate_Historical_ComputesValues()
{
var ind = new KchannelIndicator { Period = 3, Multiplier = 2.0 };
var ind = new KcIndicator { Period = 3, Multiplier = 2.0 };
ind.Initialize();
var now = DateTime.UtcNow;
@@ -65,7 +65,7 @@ public class KchannelIndicatorTests
[Fact]
public void ProcessUpdate_NewBar_Appends()
{
var ind = new KchannelIndicator { Period = 3, Multiplier = 2.0 };
var ind = new KcIndicator { Period = 3, Multiplier = 2.0 };
ind.Initialize();
var now = DateTime.UtcNow;
@@ -81,7 +81,7 @@ public class KchannelIndicatorTests
[Fact]
public void ProcessUpdate_NewTick_DoesNotThrow()
{
var ind = new KchannelIndicator { Period = 5, Multiplier = 2.0 };
var ind = new KcIndicator { Period = 5, Multiplier = 2.0 };
ind.Initialize();
var now = DateTime.UtcNow;
@@ -96,7 +96,7 @@ public class KchannelIndicatorTests
[Fact]
public void MultipleUpdates_ProducesFiniteSeries()
{
var ind = new KchannelIndicator { Period = 5, Multiplier = 2.0 };
var ind = new KcIndicator { Period = 5, Multiplier = 2.0 };
ind.Initialize();
var now = DateTime.UtcNow;
@@ -121,7 +121,7 @@ public class KchannelIndicatorTests
[Fact]
public void Bands_Order_Correct()
{
var ind = new KchannelIndicator { Period = 5, Multiplier = 2.0 };
var ind = new KcIndicator { Period = 5, Multiplier = 2.0 };
ind.Initialize();
var now = DateTime.UtcNow;
@@ -144,7 +144,7 @@ public class KchannelIndicatorTests
[Fact]
public void Bands_Expand_WithVolatility()
{
var ind = new KchannelIndicator { Period = 5, Multiplier = 2.0 };
var ind = new KcIndicator { Period = 5, Multiplier = 2.0 };
ind.Initialize();
var now = DateTime.UtcNow;
@@ -173,7 +173,7 @@ public class KchannelIndicatorTests
[Fact]
public void FirstBar_AllBandsEqualClose()
{
var ind = new KchannelIndicator { Period = 10, Multiplier = 2.0 };
var ind = new KcIndicator { Period = 10, Multiplier = 2.0 };
ind.Initialize();
var now = DateTime.UtcNow;
@@ -193,8 +193,8 @@ public class KchannelIndicatorTests
[Fact]
public void Multiplier_AffectsBandWidth()
{
var ind1 = new KchannelIndicator { Period = 10, Multiplier = 1.0 };
var ind2 = new KchannelIndicator { Period = 10, Multiplier = 2.0 };
var ind1 = new KcIndicator { Period = 10, Multiplier = 1.0 };
var ind2 = new KcIndicator { Period = 10, Multiplier = 2.0 };
ind1.Initialize();
ind2.Initialize();
@@ -4,25 +4,25 @@ using Xunit;
namespace QuanTAlib.Tests;
public class KchannelTests
public class KcTests
{
[Fact]
public void Kchannel_Constructor_ValidatesInput()
public void Kc_Constructor_ValidatesInput()
{
Assert.Throws<ArgumentOutOfRangeException>(() => new Kchannel(0));
Assert.Throws<ArgumentOutOfRangeException>(() => new Kchannel(-5));
Assert.Throws<ArgumentOutOfRangeException>(() => new Kchannel(10, 0.0));
Assert.Throws<ArgumentOutOfRangeException>(() => new Kchannel(10, -1.0));
Assert.Throws<ArgumentOutOfRangeException>(() => new Kc(0));
Assert.Throws<ArgumentOutOfRangeException>(() => new Kc(-5));
Assert.Throws<ArgumentOutOfRangeException>(() => new Kc(10, 0.0));
Assert.Throws<ArgumentOutOfRangeException>(() => new Kc(10, -1.0));
var k = new Kchannel(10, 2.0);
var k = new Kc(10, 2.0);
Assert.Equal(20, k.WarmupPeriod); // period * 2
Assert.Contains("Kchannel", k.Name, StringComparison.OrdinalIgnoreCase);
Assert.Contains("Kc", k.Name, StringComparison.OrdinalIgnoreCase);
}
[Fact]
public void Kchannel_InitialState_Defaults()
public void Kc_InitialState_Defaults()
{
var k = new Kchannel(5);
var k = new Kc(5);
Assert.Equal(0, k.Last.Value);
Assert.Equal(0, k.Upper.Value);
@@ -31,9 +31,9 @@ public class KchannelTests
}
[Fact]
public void Kchannel_FirstBar_AllBandsEqualClose()
public void Kc_FirstBar_AllBandsEqualClose()
{
var k = new Kchannel(10, 2.0);
var k = new Kc(10, 2.0);
var result = k.Update(new TBar(DateTime.UtcNow, 100, 105, 95, 102, 1000));
@@ -44,9 +44,9 @@ public class KchannelTests
}
[Fact]
public void Kchannel_SecondBar_BandsExpand()
public void Kc_SecondBar_BandsExpand()
{
var k = new Kchannel(10, 2.0);
var k = new Kc(10, 2.0);
k.Update(new TBar(DateTime.UtcNow, 100, 105, 95, 100, 1000));
@@ -59,11 +59,11 @@ public class KchannelTests
}
[Fact]
public void Kchannel_BandWidth_ProportionalToATR()
public void Kc_BandWidth_ProportionalToATR()
{
var k1 = new Kchannel(10, 1.0);
var k2 = new Kchannel(10, 2.0);
var k3 = new Kchannel(10, 3.0);
var k1 = new Kc(10, 1.0);
var k2 = new Kc(10, 2.0);
var k3 = new Kc(10, 3.0);
var gbm = new GBM(startPrice: 100, mu: 0.01, sigma: 0.2, seed: 42);
@@ -85,9 +85,9 @@ public class KchannelTests
}
[Fact]
public void Kchannel_BandOrder_Correct()
public void Kc_BandOrder_Correct()
{
var k = new Kchannel(10, 2.0);
var k = new Kc(10, 2.0);
var gbm = new GBM(startPrice: 100, mu: 0.01, sigma: 0.15, seed: 42);
for (int i = 0; i < 50; i++)
@@ -105,9 +105,9 @@ public class KchannelTests
}
[Fact]
public void Kchannel_MiddleIsEMA()
public void Kc_MiddleIsEMA()
{
var k = new Kchannel(10, 2.0);
var k = new Kc(10, 2.0);
var gbm = new GBM(startPrice: 100, mu: 0.01, sigma: 0.1, seed: 42);
for (int i = 0; i < 50; i++)
@@ -121,9 +121,9 @@ public class KchannelTests
}
[Fact]
public void Kchannel_BandSymmetry()
public void Kc_BandSymmetry()
{
var k = new Kchannel(10, 2.0);
var k = new Kc(10, 2.0);
var gbm = new GBM(startPrice: 100, mu: 0.01, sigma: 0.1, seed: 42);
for (int i = 0; i < 50; i++)
@@ -139,9 +139,9 @@ public class KchannelTests
}
[Fact]
public void Kchannel_IsHot_TurnsTrueAfterWarmup()
public void Kc_IsHot_TurnsTrueAfterWarmup()
{
var k = new Kchannel(5);
var k = new Kc(5);
// WarmupPeriod = 5 * 2 = 10
for (int i = 0; i < 9; i++)
@@ -155,9 +155,9 @@ public class KchannelTests
}
[Fact]
public void Kchannel_IsNewFalse_RebuildsState()
public void Kc_IsNewFalse_RebuildsState()
{
var k = new Kchannel(10, 2.0);
var k = new Kc(10, 2.0);
var gbm = new GBM(startPrice: 100, mu: 0.01, sigma: 0.1, seed: 7);
TBar remembered = default;
@@ -187,9 +187,9 @@ public class KchannelTests
}
[Fact]
public void Kchannel_NaN_UsesLastValid()
public void Kc_NaN_UsesLastValid()
{
var k = new Kchannel(10, 2.0);
var k = new Kc(10, 2.0);
k.Update(new TBar(DateTime.UtcNow, 100, 110, 90, 105, 1000));
k.Update(new TBar(DateTime.UtcNow, 101, 111, 91, 106, 1000));
@@ -204,9 +204,9 @@ public class KchannelTests
}
[Fact]
public void Kchannel_Reset_Clears()
public void Kc_Reset_Clears()
{
var k = new Kchannel(10, 2.0);
var k = new Kc(10, 2.0);
k.Update(new TBar(DateTime.UtcNow, 100, 110, 90, 100, 1000));
k.Update(new TBar(DateTime.UtcNow, 101, 111, 91, 101, 1000));
k.Update(new TBar(DateTime.UtcNow, 102, 112, 92, 102, 1000));
@@ -223,9 +223,9 @@ public class KchannelTests
}
[Fact]
public void Kchannel_BatchVsStreaming_Match()
public void Kc_BatchVsStreaming_Match()
{
var kStream = new Kchannel(20, 1.5);
var kStream = new Kc(20, 1.5);
var gbm = new GBM(startPrice: 100, mu: 0.02, sigma: 0.15, seed: 42);
var series = new TBarSeries();
@@ -240,7 +240,7 @@ public class KchannelTests
double expectedUp = kStream.Upper.Value;
double expectedLo = kStream.Lower.Value;
var (midBatch, upBatch, loBatch) = Kchannel.Batch(series, 20, 1.5);
var (midBatch, upBatch, loBatch) = Kc.Batch(series, 20, 1.5);
Assert.Equal(expectedMid, midBatch.Last.Value, 1e-10);
Assert.Equal(expectedUp, upBatch.Last.Value, 1e-10);
@@ -248,7 +248,7 @@ public class KchannelTests
}
[Fact]
public void Kchannel_SpanBatch_Validates()
public void Kc_SpanBatch_Validates()
{
double[] high = [110, 115, 120];
double[] low = [90, 95, 100];
@@ -260,15 +260,15 @@ public class KchannelTests
double[] highShort = [110, 115];
double[] smallOut = new double[1];
Assert.Throws<ArgumentOutOfRangeException>(() => Kchannel.Batch(high.AsSpan(), low.AsSpan(), close.AsSpan(), middle.AsSpan(), upper.AsSpan(), lower.AsSpan(), 0));
Assert.Throws<ArgumentOutOfRangeException>(() => Kchannel.Batch(high.AsSpan(), low.AsSpan(), close.AsSpan(), middle.AsSpan(), upper.AsSpan(), lower.AsSpan(), -1));
Assert.Throws<ArgumentOutOfRangeException>(() => Kchannel.Batch(high.AsSpan(), low.AsSpan(), close.AsSpan(), middle.AsSpan(), upper.AsSpan(), lower.AsSpan(), 10, 0.0));
Assert.Throws<ArgumentException>(() => Kchannel.Batch(highShort.AsSpan(), low.AsSpan(), close.AsSpan(), middle.AsSpan(), upper.AsSpan(), lower.AsSpan(), 2));
Assert.Throws<ArgumentException>(() => Kchannel.Batch(high.AsSpan(), low.AsSpan(), close.AsSpan(), smallOut.AsSpan(), upper.AsSpan(), lower.AsSpan(), 2));
Assert.Throws<ArgumentOutOfRangeException>(() => Kc.Batch(high.AsSpan(), low.AsSpan(), close.AsSpan(), middle.AsSpan(), upper.AsSpan(), lower.AsSpan(), 0));
Assert.Throws<ArgumentOutOfRangeException>(() => Kc.Batch(high.AsSpan(), low.AsSpan(), close.AsSpan(), middle.AsSpan(), upper.AsSpan(), lower.AsSpan(), -1));
Assert.Throws<ArgumentOutOfRangeException>(() => Kc.Batch(high.AsSpan(), low.AsSpan(), close.AsSpan(), middle.AsSpan(), upper.AsSpan(), lower.AsSpan(), 10, 0.0));
Assert.Throws<ArgumentException>(() => Kc.Batch(highShort.AsSpan(), low.AsSpan(), close.AsSpan(), middle.AsSpan(), upper.AsSpan(), lower.AsSpan(), 2));
Assert.Throws<ArgumentException>(() => Kc.Batch(high.AsSpan(), low.AsSpan(), close.AsSpan(), smallOut.AsSpan(), upper.AsSpan(), lower.AsSpan(), 2));
}
[Fact]
public void Kchannel_SpanBatch_ComputesCorrectly()
public void Kc_SpanBatch_ComputesCorrectly()
{
double[] high = [105, 110, 115, 112, 118];
double[] low = [95, 100, 105, 102, 108];
@@ -277,7 +277,7 @@ public class KchannelTests
double[] upper = new double[5];
double[] lower = new double[5];
Kchannel.Batch(high.AsSpan(), low.AsSpan(), close.AsSpan(), middle.AsSpan(), upper.AsSpan(), lower.AsSpan(), 3);
Kc.Batch(high.AsSpan(), low.AsSpan(), close.AsSpan(), middle.AsSpan(), upper.AsSpan(), lower.AsSpan(), 3);
// First bar: all equal close
Assert.Equal(100.0, middle[0], 1e-10);
@@ -293,14 +293,14 @@ public class KchannelTests
}
[Fact]
public void Kchannel_Calculate_ReturnsIndicatorAndResults()
public void Kc_Calculate_ReturnsIndicatorAndResults()
{
var series = new TBarSeries();
series.Add(DateTime.UtcNow, 100, 110, 90, 100, 1000);
series.Add(DateTime.UtcNow, 105, 115, 95, 105, 1000);
series.Add(DateTime.UtcNow, 102, 112, 92, 102, 1000);
var ((mid, up, lo), ind) = Kchannel.Calculate(series, 2);
var ((mid, up, lo), ind) = Kc.Calculate(series, 2);
Assert.True(double.IsFinite(mid.Last.Value));
Assert.True(double.IsFinite(up.Last.Value));
@@ -314,10 +314,10 @@ public class KchannelTests
}
[Fact]
public void Kchannel_Event_Publishes()
public void Kc_Event_Publishes()
{
var src = new TBarSeries();
var k = new Kchannel(src, 2);
var k = new Kc(src, 2);
bool fired = false;
k.Pub += (object? sender, in TValueEventArgs args) => fired = true;
@@ -326,10 +326,10 @@ public class KchannelTests
}
[Fact]
public void Kchannel_HighVolatility_WiderBands()
public void Kc_HighVolatility_WiderBands()
{
var kLow = new Kchannel(20, 2.0);
var kHigh = new Kchannel(20, 2.0);
var kLow = new Kc(20, 2.0);
var kHigh = new Kc(20, 2.0);
// Low volatility data
for (int i = 0; i < 50; i++)
@@ -350,10 +350,10 @@ public class KchannelTests
}
[Fact]
public void Kchannel_ShorterPeriod_FasterResponse()
public void Kc_ShorterPeriod_FasterResponse()
{
var kShort = new Kchannel(5, 2.0);
var kLong = new Kchannel(20, 2.0);
var kShort = new Kc(5, 2.0);
var kLong = new Kc(20, 2.0);
// Initial stable period
for (int i = 0; i < 30; i++)
@@ -382,9 +382,9 @@ public class KchannelTests
}
[Fact]
public void Kchannel_TrueRange_IncludesGaps()
public void Kc_TrueRange_IncludesGaps()
{
var k = new Kchannel(3, 2.0);
var k = new Kc(3, 2.0);
// Bar 1: normal range
k.Update(new TBar(DateTime.UtcNow, 100, 105, 95, 100, 1000));
@@ -404,10 +404,10 @@ public class KchannelTests
}
[Fact]
public void Kchannel_WarmupCompensation_ReducesStartupBias()
public void Kc_WarmupCompensation_ReducesStartupBias()
{
// Warmup compensation should make early values more accurate
var k = new Kchannel(20, 2.0);
var k = new Kc(20, 2.0);
// Create bars with consistent volatility
for (int i = 0; i < 100; i++)
@@ -426,9 +426,9 @@ public class KchannelTests
}
[Fact]
public void Kchannel_LongSeriesStability()
public void Kc_LongSeriesStability()
{
var k = new Kchannel(20, 2.0);
var k = new Kc(20, 2.0);
var gbm = new GBM(startPrice: 100, mu: 0.001, sigma: 0.02, seed: 123);
for (int i = 0; i < 10000; i++)
@@ -6,13 +6,13 @@ using OoplesFinance.StockIndicators.Models;
namespace QuanTAlib.Tests;
public sealed class KchannelValidationTests : IDisposable
public sealed class KcValidationTests : IDisposable
{
private readonly ValidationTestData _testData;
private readonly ITestOutputHelper _output;
private bool _disposed;
public KchannelValidationTests(ITestOutputHelper output)
public KcValidationTests(ITestOutputHelper output)
{
_output = output;
_testData = new ValidationTestData();
@@ -49,7 +49,7 @@ public sealed class KchannelValidationTests : IDisposable
// Bar 2: close=105, high=112, low=100 (range=12, prevClose=102, TR=max(12,10,2)=12)
series.Add(new TBar(t0.AddMinutes(2), 105, 112, 100, 105, 100));
var ind = new Kchannel(10, 2.0);
var ind = new Kc(10, 2.0);
var (mid, up, lo) = ind.Update(series);
// First bar: all equal close
@@ -72,7 +72,7 @@ public sealed class KchannelValidationTests : IDisposable
Assert.Equal(upperDist, lowerDist, 1e-10);
}
_output.WriteLine("Kchannel manual calculation validated");
_output.WriteLine("Kc manual calculation validated");
}
[Fact]
@@ -86,18 +86,18 @@ public sealed class KchannelValidationTests : IDisposable
foreach (double multiplier in multipliers)
{
// Batch (instance)
var inst = new Kchannel(period, multiplier);
var inst = new Kc(period, multiplier);
var (bMid, bUp, bLo) = inst.Update(_testData.Bars);
// Static batch
var (sMid, sUp, sLo) = Kchannel.Batch(_testData.Bars, period, multiplier);
var (sMid, sUp, sLo) = Kc.Batch(_testData.Bars, period, multiplier);
ValidationHelper.VerifySeriesEqual(bMid, sMid);
ValidationHelper.VerifySeriesEqual(bUp, sUp);
ValidationHelper.VerifySeriesEqual(bLo, sLo);
// Streaming
var streaming = new Kchannel(period, multiplier);
var streaming = new Kc(period, multiplier);
var sMidStream = new TSeries();
var sUpStream = new TSeries();
var sLoStream = new TSeries();
@@ -120,7 +120,7 @@ public sealed class KchannelValidationTests : IDisposable
double[] spanMid = new double[high.Length];
double[] spanUp = new double[high.Length];
double[] spanLo = new double[high.Length];
Kchannel.Batch(high.AsSpan(), low.AsSpan(), close.AsSpan(),
Kc.Batch(high.AsSpan(), low.AsSpan(), close.AsSpan(),
spanMid.AsSpan(), spanUp.AsSpan(), spanLo.AsSpan(), period, multiplier);
for (int i = 0; i < high.Length; i++)
@@ -132,7 +132,7 @@ public sealed class KchannelValidationTests : IDisposable
}
}
_output.WriteLine("Kchannel mode consistency validated (batch/stream/span)");
_output.WriteLine("Kc mode consistency validated (batch/stream/span)");
}
[Fact]
@@ -142,7 +142,7 @@ public sealed class KchannelValidationTests : IDisposable
const double multiplier = 2.0;
var pub = new TBarSeries();
var evtInd = new Kchannel(pub, period, multiplier);
var evtInd = new Kc(pub, period, multiplier);
var evtMid = new TSeries();
var evtUp = new TSeries();
var evtLo = new TSeries();
@@ -155,13 +155,13 @@ public sealed class KchannelValidationTests : IDisposable
evtLo.Add(evtInd.Lower);
}
var (bMid, bUp, bLo) = Kchannel.Batch(_testData.Bars, period, multiplier);
var (bMid, bUp, bLo) = Kc.Batch(_testData.Bars, period, multiplier);
ValidationHelper.VerifySeriesEqual(bMid, evtMid);
ValidationHelper.VerifySeriesEqual(bUp, evtUp);
ValidationHelper.VerifySeriesEqual(bLo, evtLo);
_output.WriteLine("Kchannel eventing mode validated");
_output.WriteLine("Kc eventing mode validated");
}
[Fact]
@@ -170,7 +170,7 @@ public sealed class KchannelValidationTests : IDisposable
const int period = 15;
const double multiplier = 2.5;
var ((mid, up, lo), ind) = Kchannel.Calculate(_testData.Bars, period, multiplier);
var ((mid, up, lo), ind) = Kc.Calculate(_testData.Bars, period, multiplier);
Assert.True(ind.IsHot);
Assert.Equal(period * 2, ind.WarmupPeriod);
@@ -183,7 +183,7 @@ public sealed class KchannelValidationTests : IDisposable
ind.Update(next);
Assert.True(ind.IsHot);
_output.WriteLine("Kchannel Calculate validated");
_output.WriteLine("Kc Calculate validated");
}
[Fact]
@@ -192,9 +192,9 @@ public sealed class KchannelValidationTests : IDisposable
const int period = 25;
const double multiplier = 1.5;
var (bMid, bUp, bLo) = Kchannel.Batch(_testData.Bars, period, multiplier);
var (bMid, bUp, bLo) = Kc.Batch(_testData.Bars, period, multiplier);
var primed = new Kchannel(period, multiplier);
var primed = new Kc(period, multiplier);
var subset = new TBarSeries();
for (int i = 0; i < 200; i++)
{
@@ -212,13 +212,13 @@ public sealed class KchannelValidationTests : IDisposable
Assert.Equal(bUp.Last.Value, primed.Upper.Value, 1e-9);
Assert.Equal(bLo.Last.Value, primed.Lower.Value, 1e-9);
_output.WriteLine("Kchannel Prime validated against batch");
_output.WriteLine("Kc Prime validated against batch");
}
[Fact]
public void Validate_LargeDataset_FiniteOutputs()
{
var (mid, up, lo) = Kchannel.Batch(_testData.Bars, 50, 2.0);
var (mid, up, lo) = Kc.Batch(_testData.Bars, 50, 2.0);
ValidationHelper.VerifyAllFinite(mid, startIndex: 0);
ValidationHelper.VerifyAllFinite(up, startIndex: 0);
@@ -230,13 +230,13 @@ public sealed class KchannelValidationTests : IDisposable
Assert.True(up[i].Value > lo[i].Value, $"Upper > Lower at {i}");
}
_output.WriteLine("Kchannel large dataset validated");
_output.WriteLine("Kc large dataset validated");
}
[Fact]
public void Validate_BandSymmetry_AllBars()
{
var ind = new Kchannel(20, 2.0);
var ind = new Kc(20, 2.0);
var (mid, up, lo) = ind.Update(_testData.Bars);
for (int i = 0; i < mid.Count; i++)
@@ -246,7 +246,7 @@ public sealed class KchannelValidationTests : IDisposable
Assert.Equal(upperWidth, lowerWidth, 1e-10);
}
_output.WriteLine("Kchannel band symmetry validated for all bars");
_output.WriteLine("Kc band symmetry validated for all bars");
}
[Fact]
@@ -257,7 +257,7 @@ public sealed class KchannelValidationTests : IDisposable
for (int i = 0; i < multipliers.Length; i++)
{
var ind = new Kchannel(20, multipliers[i]);
var ind = new Kc(20, multipliers[i]);
foreach (var bar in _testData.Bars)
{
ind.Update(bar);
@@ -273,7 +273,7 @@ public sealed class KchannelValidationTests : IDisposable
Assert.Equal(expected, widths[i], 1e-9);
}
_output.WriteLine("Kchannel multiplier scaling validated");
_output.WriteLine("Kc multiplier scaling validated");
}
[Fact]
@@ -284,7 +284,7 @@ public sealed class KchannelValidationTests : IDisposable
for (int i = 0; i < periods.Length; i++)
{
var ind = new Kchannel(periods[i], 2.0);
var ind = new Kc(periods[i], 2.0);
foreach (var bar in _testData.Bars)
{
ind.Update(bar);
@@ -298,7 +298,7 @@ public sealed class KchannelValidationTests : IDisposable
Assert.True(double.IsFinite(m));
}
_output.WriteLine("Kchannel period effect validated");
_output.WriteLine("Kc period effect validated");
}
[Fact]
@@ -315,7 +315,7 @@ public sealed class KchannelValidationTests : IDisposable
// Bar 2: gap down (prev close=115, new high=100)
series.Add(new TBar(t0.AddMinutes(2), 95, 100, 90, 95, 100));
var ind = new Kchannel(3, 2.0);
var ind = new Kc(3, 2.0);
var (mid, up, lo) = ind.Update(series);
// Bands should expand due to gaps
@@ -325,7 +325,7 @@ public sealed class KchannelValidationTests : IDisposable
Assert.True(width > 0, $"Band width > 0 at bar {i}");
}
_output.WriteLine("Kchannel ATR true range validated with gaps");
_output.WriteLine("Kc ATR true range validated with gaps");
}
[Fact]
@@ -340,7 +340,7 @@ public sealed class KchannelValidationTests : IDisposable
series.Add(new TBar(t0.AddMinutes(i), 100, 105, 95, 100, 100));
}
var ind = new Kchannel(20, 2.0);
var ind = new Kc(20, 2.0);
var (mid, _, _) = ind.Update(series);
// After warmup, middle should be very close to constant price
@@ -349,13 +349,13 @@ public sealed class KchannelValidationTests : IDisposable
Assert.InRange(mid[i].Value, 99.9, 100.1);
}
_output.WriteLine("Kchannel warmup compensation validated");
_output.WriteLine("Kc warmup compensation validated");
}
[Fact]
public void Validate_StateRestoration_Iterative()
{
var ind = new Kchannel(15, 2.5);
var ind = new Kc(15, 2.5);
var gbm = new GBM(startPrice: 100, mu: 0.01, sigma: 0.1, seed: 42);
// Build up state
@@ -382,7 +382,7 @@ public sealed class KchannelValidationTests : IDisposable
Assert.True(double.IsFinite(ind.Upper.Value));
Assert.True(double.IsFinite(ind.Lower.Value));
_output.WriteLine("Kchannel state restoration validated");
_output.WriteLine("Kc state restoration validated");
}
[Fact]
@@ -398,7 +398,7 @@ public sealed class KchannelValidationTests : IDisposable
foreach (var period in periods)
{
var (qMiddle, _, _) = Kchannel.Batch(_testData.Bars, period, multiplier);
var (qMiddle, _, _) = Kc.Batch(_testData.Bars, period, multiplier);
// Skender: atrPeriods = period to match QuanTAlib's single-period design
var sResult = _testData.SkenderQuotes
@@ -408,7 +408,7 @@ public sealed class KchannelValidationTests : IDisposable
// Compare middle band (EMA of close) using ValidationHelper
ValidationHelper.VerifyData(qMiddle, sResult, s => s.Centerline);
}
_output.WriteLine("Kchannel middle band validated against Skender for all periods");
_output.WriteLine("Kc middle band validated against Skender for all periods");
}
[Fact]
@@ -419,7 +419,7 @@ public sealed class KchannelValidationTests : IDisposable
foreach (var period in periods)
{
var (_, up, _) = Kchannel.Batch(_testData.Bars, period, multiplier);
var (_, up, _) = Kc.Batch(_testData.Bars, period, multiplier);
var sResult = _testData.SkenderQuotes
.GetKeltner(period, multiplier, period)
@@ -427,7 +427,7 @@ public sealed class KchannelValidationTests : IDisposable
ValidationHelper.VerifyData(up, sResult, s => s.UpperBand);
}
_output.WriteLine("Kchannel upper band validated against Skender for all periods");
_output.WriteLine("Kc upper band validated against Skender for all periods");
}
[Fact]
@@ -438,7 +438,7 @@ public sealed class KchannelValidationTests : IDisposable
foreach (var period in periods)
{
var (_, _, lo) = Kchannel.Batch(_testData.Bars, period, multiplier);
var (_, _, lo) = Kc.Batch(_testData.Bars, period, multiplier);
var sResult = _testData.SkenderQuotes
.GetKeltner(period, multiplier, period)
@@ -446,7 +446,7 @@ public sealed class KchannelValidationTests : IDisposable
ValidationHelper.VerifyData(lo, sResult, s => s.LowerBand);
}
_output.WriteLine("Kchannel lower band validated against Skender for all periods");
_output.WriteLine("Kc lower band validated against Skender for all periods");
}
[Fact]
@@ -460,7 +460,7 @@ public sealed class KchannelValidationTests : IDisposable
.GetKeltner(period, multiplier, period)
.ToList();
var (ourMid, ourUp, ourLo) = Kchannel.Batch(_testData.Bars, period, multiplier);
var (ourMid, ourUp, ourLo) = Kc.Batch(_testData.Bars, period, multiplier);
int warmup = period * 2;
for (int i = warmup; i < ourMid.Count && i < sResult.Count; i++)
@@ -475,7 +475,7 @@ public sealed class KchannelValidationTests : IDisposable
}
}
_output.WriteLine($"Kchannel vs Skender band structure validated");
_output.WriteLine($"Kc vs Skender band structure validated");
}
[Fact]
@@ -486,7 +486,7 @@ public sealed class KchannelValidationTests : IDisposable
foreach (int period in periods)
{
var (mid, up, lo) = Kchannel.Batch(_testData.Bars, period, 2.0);
var (mid, up, lo) = Kc.Batch(_testData.Bars, period, 2.0);
// Band width should be exactly 2x ATR (multiplier * ATR)
for (int i = 1; i < mid.Count; i++)
@@ -501,7 +501,7 @@ public sealed class KchannelValidationTests : IDisposable
}
}
_output.WriteLine("Kchannel band width consistency validated");
_output.WriteLine("Kc band width consistency validated");
}
[Fact]
@@ -519,7 +519,7 @@ public sealed class KchannelValidationTests : IDisposable
// Bar 2: TR = max(105-95, |105-100|, |95-100|) = max(10, 5, 5) = 10
series.Add(new TBar(t0.AddMinutes(2), 100, 105, 95, 100, 100));
var ind = new Kchannel(3, 1.0); // multiplier=1 so width = 2*ATR
var ind = new Kc(3, 1.0); // multiplier=1 so width = 2*ATR
var (mid, up, lo) = ind.Update(series);
// All outputs should be finite
@@ -537,11 +537,11 @@ public sealed class KchannelValidationTests : IDisposable
Assert.True(width > 0, $"Band width > 0 at bar {i}");
}
_output.WriteLine("Kchannel ATR calculation validated");
_output.WriteLine("Kc ATR calculation validated");
}
[Fact]
public void Kchannel_MatchesOoples_Structural()
public void Kc_MatchesOoples_Structural()
{
var gbm = new GBM(startPrice: 100.0, mu: 0.02, sigma: 0.15, seed: 42);
var bars = gbm.Fetch(500, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
+1 -1
View File
@@ -13,7 +13,7 @@
| **PineScript** | [mmchannel.pine](mmchannel.pine) |
- Min-Max Channel tracks the highest high and lowest low over a lookback period, creating a pure price envelope without any midpoint calculation.
- **Similar:** [DChannel](../dchannel/dchannel.md), [PChannel](../pchannel/pchannel.md) | **Complementary:** ATR for volatility context | **Trading note:** Min/Max channel; breakout system based on highest high and lowest low.
- **Similar:** [DC](../dc/dc.md), [PC](../pc/pc.md) | **Complementary:** ATR for volatility context | **Trading note:** Min/Max channel; breakout system based on highest high and lowest low.
- Validated against TA-Lib, Skender, and Tulip reference implementations where available.
Min-Max Channel tracks the highest high and lowest low over a lookback period, creating a pure price envelope without any midpoint calculation. Unlike Donchian Channels which include a middle band, MMCHANNEL delivers only the raw extremes. The implementation uses monotonic deques for O(1) amortized updates: each element enters the deque once and leaves at most once, so total work over $N$ bars is $O(N)$ regardless of period length.
@@ -187,21 +187,21 @@ public sealed class MmchannelValidationTests : IDisposable
}
[Fact]
public void Validate_AgainstDchannel_Bands()
public void Validate_AgainstDc_Bands()
{
// Mmchannel upper/lower should exactly match Dchannel upper/lower
// Mmchannel upper/lower should exactly match Dc upper/lower
int[] periods = { 10, 20, 50 };
foreach (int period in periods)
{
var (_, dcUp, dcLo) = Dchannel.Batch(_testData.Bars, period);
var (_, dcUp, dcLo) = Dc.Batch(_testData.Bars, period);
var (mmUp, mmLo) = Mmchannel.Batch(_testData.Bars, period);
ValidationHelper.VerifySeriesEqual(dcUp, mmUp);
ValidationHelper.VerifySeriesEqual(dcLo, mmLo);
}
_output.WriteLine("Mmchannel matches Dchannel upper/lower bands");
_output.WriteLine("Mmchannel matches Dc upper/lower bands");
}
[Fact]
@@ -209,7 +209,7 @@ public sealed class MmchannelValidationTests : IDisposable
{
// Note: Skender's Donchian uses lookbackPeriods+1 for the window size (includes current bar differently)
// This test validates that we get finite, reasonable results, but exact match is not expected
// due to this convention difference. The exact match is validated via Dchannel comparison above.
// due to this convention difference. The exact match is validated via Dc comparison above.
const int period = 20;
var skenderResult = _testData.SkenderQuotes
@@ -5,11 +5,11 @@ using static QuanTAlib.IndicatorExtensions;
namespace QuanTAlib;
/// <summary>
/// Dchannel: Donchian Channels - Quantower Indicator Adapter
/// Pc: Price Channel - Quantower Indicator Adapter
/// Upper = rolling highest high; Lower = rolling lowest low; Middle = (Upper + Lower) / 2.
/// Uses streaming O(1) deques with bar-correction support.
/// </summary>
public sealed class DchannelIndicator : Indicator, IWatchlistIndicator
public sealed class PcIndicator : Indicator, IWatchlistIndicator
{
[InputParameter("Period", sortIndex: 10, minimum: 1, maximum: 500, increment: 1, decimalPlaces: 0)]
public int Period { get; set; } = 20;
@@ -17,14 +17,14 @@ public sealed class DchannelIndicator : Indicator, IWatchlistIndicator
[InputParameter("Show Cold Values", sortIndex: 100)]
public bool ShowColdValues { get; set; } = true;
private Dchannel? _indicator;
private Pc? _indicator;
public int MinHistoryDepths => Period;
public override string ShortName => $"Dchannel({Period})";
public override string ShortName => $"Pc({Period})";
public DchannelIndicator()
public PcIndicator()
{
Name = "Dchannel - Donchian Channels";
Name = "Pc - Price Channel";
Description = "Price channel using rolling highest high / lowest low with midpoint average";
SeparateWindow = false;
OnBackGround = true;
@@ -32,7 +32,7 @@ public sealed class DchannelIndicator : Indicator, IWatchlistIndicator
protected override void OnInit()
{
_indicator = new Dchannel(Period);
_indicator = new Pc(Period);
AddLineSeries(new LineSeries("Middle", Color.DodgerBlue, 2, LineStyle.Solid));
AddLineSeries(new LineSeries("Upper", Color.FromArgb(255, 180, 180), 1, LineStyle.Dash));
@@ -5,14 +5,14 @@ using System.Runtime.InteropServices;
namespace QuanTAlib;
/// <summary>
/// PCHANNEL: Price Channel
/// PC: Price Channel
/// Upper = rolling highest high; Lower = rolling lowest low; Middle = (Upper + Lower) / 2.
/// Functionally equivalent to Donchian Channels (DCHANNEL).
/// Functionally equivalent to Donchian Channels (DC).
/// Streaming path uses monotonic deques for O(1) amortized updates; corrections (isNew=false)
/// rebuild deques without allocations.
/// </summary>
[SkipLocalsInit]
public sealed class Pchannel : ITValuePublisher
public sealed class Pc : ITValuePublisher
{
private readonly int _period;
private readonly double[] _hBuf;
@@ -48,7 +48,7 @@ public sealed class Pchannel : ITValuePublisher
public event TValuePublishedHandler? Pub;
public Pchannel(int period)
public Pc(int period)
{
if (period <= 0)
{
@@ -69,12 +69,12 @@ public sealed class Pchannel : ITValuePublisher
_state = new State(double.NaN, double.NaN, false);
_p_state = _state;
Name = $"Pchannel({period})";
Name = $"Pc({period})";
WarmupPeriod = period;
_barHandler = HandleBar;
}
public Pchannel(TBarSeries source, int period) : this(period)
public Pc(TBarSeries source, int period) : this(period)
{
Prime(source);
source.Pub += _barHandler;
@@ -425,9 +425,9 @@ public sealed class Pchannel : ITValuePublisher
return (new TSeries(tMiddle, vMiddle), new TSeries(tUpper, vUpper), new TSeries(tLower, vLower));
}
public static ((TSeries Middle, TSeries Upper, TSeries Lower) Results, Pchannel Indicator) Calculate(TBarSeries source, int period)
public static ((TSeries Middle, TSeries Upper, TSeries Lower) Results, Pc Indicator) Calculate(TBarSeries source, int period)
{
var indicator = new Pchannel(source, period);
var indicator = new Pc(source, period);
var results = indicator.Update(source);
return (results, indicator);
}
@@ -1,4 +1,4 @@
# PCHANNEL: Price Channel
# PC: Price Channel
> *Price channels frame the trading range by its own high-low extremes, defining the field of play.*
@@ -10,10 +10,10 @@
| **Outputs** | Multiple series (Upper, Lower) |
| **Output range** | Tracks input |
| **Warmup** | `period` bars |
| **PineScript** | [pchannel.pine](pchannel.pine) |
| **PineScript** | [pc.pine](pc.pine) |
- Price Channel tracks the highest high and lowest low over a lookback period with a midpoint average, creating a three-line price envelope that defi...
- **Similar:** [DChannel](../dchannel/dchannel.md), [MMChannel](../mmchannel/mmchannel.md) | **Complementary:** Volume confirmation on breakouts | **Trading note:** Price channel based on percentage offset from midpoint.
- Price Channel tracks the highest high and lowest low over a lookback period with a midpoint average, creating a three-line price envelope that defines where the market has been.
- **Similar:** [DC](../dc/dc.md), [MMChannel](../mmchannel/mmchannel.md) | **Complementary:** Volume confirmation on breakouts | **Trading note:** Price channel based on percentage offset from midpoint.
- Validated against TA-Lib, Skender, and Tulip reference implementations where available.
Price Channel tracks the highest high and lowest low over a lookback period with a midpoint average, creating a three-line price envelope that defines where the market has been. Functionally identical to Donchian Channels, the indicator uses actual price extremes rather than volatility estimates, producing bands that represent real support and resistance levels. This implementation uses monotonic deques for O(1) amortized updates instead of the naive O(n) rescan that most platforms use internally.
@@ -56,7 +56,7 @@ $$
M_t = \frac{U_t + L_t}{2}
$$
This represents the equilibrium price of the lookback window. Unlike MMCHANNEL which omits the midpoint, PCHANNEL always emits all three lines.
This represents the equilibrium price of the lookback window. Unlike MMCHANNEL which omits the midpoint, PC always emits all three lines.
### 4. Monotonic Deque Mechanism
@@ -93,7 +93,7 @@ Streaming: $O(1)$ amortized per bar. Each element enters and exits each deque at
### Operation Count (Streaming Mode)
PCHANNEL uses two monotonic deques for $O(1)$ amortized sliding-window max/min plus a midpoint:
PC uses two monotonic deques for $O(1)$ amortized sliding-window max/min plus a midpoint:
| Operation | Count | Cost (cycles) | Subtotal |
| :--- | :---: | :---: | :---: |
@@ -105,7 +105,7 @@ PCHANNEL uses two monotonic deques for $O(1)$ amortized sliding-window max/min p
| MUL (× 0.5 for middle) | 1 | 3 | 3 |
| **Total (amortized)** | **~6** | — | **~8 cycles** |
Identical to DCHANNEL in cost. Each element enters and exits each deque exactly once over the full series, yielding $O(N)$ total work across $N$ bars regardless of period.
Identical to DC in cost. Each element enters and exits each deque exactly once over the full series, yielding $O(N)$ total work across $N$ bars regardless of period.
### Batch Mode (SIMD Analysis)
@@ -1,13 +1,13 @@
// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Price Channel (PCHANNEL)", "PCHANNEL", overlay=true)
indicator("Price Channel (PC)", "PC", overlay=true)
//@function Calculates Price Channel
//@param length_param Lookback period for determining the highest high and lowest low
//@returns tuple [upperChannel, middleChannel, lowerChannel]
//@optimized Uses monotonic deque for O(1) amortized complexity per bar
pchannel(simple int length_param) =>
pc(simple int length_param) =>
if length_param <= 0
runtime.error("Length must be greater than 0")
var deque_hi = array.new_int(0)
@@ -55,7 +55,7 @@ pchannel(simple int length_param) =>
i_length = input.int(20, "Length", minval=1)
// Calculation
[upperCh, middleCh, lowerCh] = pchannel(i_length)
[upperCh, middleCh, lowerCh] = pc(i_length)
// Plot
plot(middleCh, "Middle Channel", color=color.yellow, linewidth=2)
@@ -3,16 +3,16 @@ using Xunit;
namespace QuanTAlib.Tests;
public class DchannelIndicatorTests
public class PcIndicatorTests
{
[Fact]
public void Constructor_SetsDefaults()
{
var ind = new DchannelIndicator();
var ind = new PcIndicator();
Assert.Equal(20, ind.Period);
Assert.True(ind.ShowColdValues);
Assert.Equal("Dchannel - Donchian Channels", ind.Name);
Assert.Equal("Pc - Price Channel", ind.Name);
Assert.False(ind.SeparateWindow);
Assert.True(ind.OnBackGround);
}
@@ -20,21 +20,21 @@ public class DchannelIndicatorTests
[Fact]
public void MinHistoryDepths_EqualsPeriod()
{
var ind = new DchannelIndicator { Period = 15 };
var ind = new PcIndicator { Period = 15 };
Assert.Equal(15, ind.MinHistoryDepths);
}
[Fact]
public void ShortName_ReflectsParameters()
{
var ind = new DchannelIndicator { Period = 12 };
var ind = new PcIndicator { Period = 12 };
Assert.Contains("12", ind.ShortName, StringComparison.Ordinal);
}
[Fact]
public void Initialize_AddsThreeLineSeries()
{
var ind = new DchannelIndicator { Period = 14 };
var ind = new PcIndicator { Period = 14 };
ind.Initialize();
Assert.Equal(3, ind.LinesSeries.Count);
@@ -46,7 +46,7 @@ public class DchannelIndicatorTests
[Fact]
public void ProcessUpdate_Historical_ComputesValues()
{
var ind = new DchannelIndicator { Period = 3 };
var ind = new PcIndicator { Period = 3 };
ind.Initialize();
var now = DateTime.UtcNow;
@@ -63,7 +63,7 @@ public class DchannelIndicatorTests
[Fact]
public void ProcessUpdate_NewBar_Appends()
{
var ind = new DchannelIndicator { Period = 3 };
var ind = new PcIndicator { Period = 3 };
ind.Initialize();
var now = DateTime.UtcNow;
@@ -79,7 +79,7 @@ public class DchannelIndicatorTests
[Fact]
public void ProcessUpdate_NewTick_DoesNotThrow()
{
var ind = new DchannelIndicator { Period = 5 };
var ind = new PcIndicator { Period = 5 };
ind.Initialize();
var now = DateTime.UtcNow;
@@ -94,7 +94,7 @@ public class DchannelIndicatorTests
[Fact]
public void MultipleUpdates_ProducesFiniteSeries()
{
var ind = new DchannelIndicator { Period = 5 };
var ind = new PcIndicator { Period = 5 };
ind.Initialize();
var now = DateTime.UtcNow;
@@ -119,7 +119,7 @@ public class DchannelIndicatorTests
[Fact]
public void Bands_Order_Correct()
{
var ind = new DchannelIndicator { Period = 3 };
var ind = new PcIndicator { Period = 3 };
ind.Initialize();
var now = DateTime.UtcNow;
@@ -4,23 +4,23 @@ using Xunit;
namespace QuanTAlib.Tests;
public class PchannelTests
public class PcTests
{
[Fact]
public void Pchannel_Constructor_ValidatesInput()
public void Pc_Constructor_ValidatesInput()
{
Assert.Throws<ArgumentException>(() => new Pchannel(0));
Assert.Throws<ArgumentException>(() => new Pchannel(-5));
Assert.Throws<ArgumentException>(() => new Pc(0));
Assert.Throws<ArgumentException>(() => new Pc(-5));
var pc = new Pchannel(10);
var pc = new Pc(10);
Assert.Equal(10, pc.WarmupPeriod);
Assert.Contains("Pchannel", pc.Name, StringComparison.OrdinalIgnoreCase);
Assert.Contains("Pc", pc.Name, StringComparison.OrdinalIgnoreCase);
}
[Fact]
public void Pchannel_InitialState_Defaults()
public void Pc_InitialState_Defaults()
{
var pc = new Pchannel(5);
var pc = new Pc(5);
Assert.Equal(0, pc.Last.Value);
Assert.Equal(0, pc.Upper.Value);
@@ -29,9 +29,9 @@ public class PchannelTests
}
[Fact]
public void Pchannel_CalculatesBands()
public void Pc_CalculatesBands()
{
var pc = new Pchannel(3);
var pc = new Pc(3);
pc.Update(new TBar(DateTime.UtcNow, 100, 110, 90, 105, 1000));
pc.Update(new TBar(DateTime.UtcNow, 105, 115, 95, 110, 1000));
@@ -45,9 +45,9 @@ public class PchannelTests
}
[Fact]
public void Pchannel_SlidingWindow_Updates()
public void Pc_SlidingWindow_Updates()
{
var pc = new Pchannel(2);
var pc = new Pc(2);
pc.Update(new TBar(DateTime.UtcNow, 100, 110, 90, 100, 1000));
pc.Update(new TBar(DateTime.UtcNow, 101, 111, 91, 101, 1000));
@@ -65,9 +65,9 @@ public class PchannelTests
}
[Fact]
public void Pchannel_IsHot_TurnsTrueAfterWarmup()
public void Pc_IsHot_TurnsTrueAfterWarmup()
{
var pc = new Pchannel(4);
var pc = new Pc(4);
for (int i = 0; i < 3; i++)
{
@@ -80,9 +80,9 @@ public class PchannelTests
}
[Fact]
public void Pchannel_IsNewFalse_RebuildsState()
public void Pc_IsNewFalse_RebuildsState()
{
var pc = new Pchannel(3);
var pc = new Pc(3);
var gbm = new GBM(startPrice: 100, mu: 0.01, sigma: 0.1, seed: 7);
TBar remembered = default;
@@ -110,9 +110,9 @@ public class PchannelTests
}
[Fact]
public void Pchannel_NaN_UsesLastValid()
public void Pc_NaN_UsesLastValid()
{
var pc = new Pchannel(3);
var pc = new Pc(3);
pc.Update(new TBar(DateTime.UtcNow, 100, 110, 90, 105, 1000));
pc.Update(new TBar(DateTime.UtcNow, 101, 111, 91, 106, 1000));
@@ -127,9 +127,9 @@ public class PchannelTests
}
[Fact]
public void Pchannel_Reset_Clears()
public void Pc_Reset_Clears()
{
var pc = new Pchannel(3);
var pc = new Pc(3);
pc.Update(new TBar(DateTime.UtcNow, 100, 110, 90, 100, 1000));
pc.Update(new TBar(DateTime.UtcNow, 101, 111, 91, 101, 1000));
@@ -145,9 +145,9 @@ public class PchannelTests
}
[Fact]
public void Pchannel_BatchVsStreaming_Match()
public void Pc_BatchVsStreaming_Match()
{
var pcStream = new Pchannel(10);
var pcStream = new Pc(10);
var gbm = new GBM(startPrice: 100, mu: 0.02, sigma: 0.1, seed: 42);
var series = new TBarSeries();
@@ -162,7 +162,7 @@ public class PchannelTests
double expectedUp = pcStream.Upper.Value;
double expectedLo = pcStream.Lower.Value;
var (midBatch, upBatch, loBatch) = Pchannel.Batch(series, 10);
var (midBatch, upBatch, loBatch) = Pc.Batch(series, 10);
Assert.Equal(expectedMid, midBatch.Last.Value, 1e-10);
Assert.Equal(expectedUp, upBatch.Last.Value, 1e-10);
@@ -170,7 +170,7 @@ public class PchannelTests
}
[Fact]
public void Pchannel_SpanBatch_Validates()
public void Pc_SpanBatch_Validates()
{
double[] high = [110, 115, 120];
double[] low = [90, 95, 100];
@@ -181,14 +181,14 @@ public class PchannelTests
double[] highShort = [110, 115];
double[] smallOut = new double[1];
Assert.Throws<ArgumentException>(() => Pchannel.Batch(high.AsSpan(), low.AsSpan(), middle.AsSpan(), upper.AsSpan(), lower.AsSpan(), 0));
Assert.Throws<ArgumentException>(() => Pchannel.Batch(high.AsSpan(), low.AsSpan(), middle.AsSpan(), upper.AsSpan(), lower.AsSpan(), -1));
Assert.Throws<ArgumentException>(() => Pchannel.Batch(highShort.AsSpan(), low.AsSpan(), middle.AsSpan(), upper.AsSpan(), lower.AsSpan(), 2));
Assert.Throws<ArgumentException>(() => Pchannel.Batch(high.AsSpan(), low.AsSpan(), smallOut.AsSpan(), upper.AsSpan(), lower.AsSpan(), 2));
Assert.Throws<ArgumentException>(() => Pc.Batch(high.AsSpan(), low.AsSpan(), middle.AsSpan(), upper.AsSpan(), lower.AsSpan(), 0));
Assert.Throws<ArgumentException>(() => Pc.Batch(high.AsSpan(), low.AsSpan(), middle.AsSpan(), upper.AsSpan(), lower.AsSpan(), -1));
Assert.Throws<ArgumentException>(() => Pc.Batch(highShort.AsSpan(), low.AsSpan(), middle.AsSpan(), upper.AsSpan(), lower.AsSpan(), 2));
Assert.Throws<ArgumentException>(() => Pc.Batch(high.AsSpan(), low.AsSpan(), smallOut.AsSpan(), upper.AsSpan(), lower.AsSpan(), 2));
}
[Fact]
public void Pchannel_SpanBatch_ComputesCorrectly()
public void Pc_SpanBatch_ComputesCorrectly()
{
double[] high = [110, 115, 120, 125];
double[] low = [90, 95, 100, 105];
@@ -196,7 +196,7 @@ public class PchannelTests
double[] upper = new double[4];
double[] lower = new double[4];
Pchannel.Batch(high.AsSpan(), low.AsSpan(), middle.AsSpan(), upper.AsSpan(), lower.AsSpan(), 3);
Pc.Batch(high.AsSpan(), low.AsSpan(), middle.AsSpan(), upper.AsSpan(), lower.AsSpan(), 3);
// Period=3: index 2 is first valid (indices 0,1,2)
// H=[110,115,120], L=[90,95,100] → Upper=120, Lower=90, Middle=105
@@ -211,14 +211,14 @@ public class PchannelTests
}
[Fact]
public void Pchannel_Calculate_ReturnsIndicatorAndResults()
public void Pc_Calculate_ReturnsIndicatorAndResults()
{
var series = new TBarSeries();
series.Add(DateTime.UtcNow, 100, 110, 90, 100, 1000);
series.Add(DateTime.UtcNow, 105, 115, 95, 105, 1000);
series.Add(DateTime.UtcNow, 110, 120, 100, 110, 1000);
var ((mid, up, lo), ind) = Pchannel.Calculate(series, 2);
var ((mid, up, lo), ind) = Pc.Calculate(series, 2);
Assert.True(ind.IsHot);
// Period=2: last 2 bars H=[115,120], L=[95,100] → Upper=120, Lower=95, Middle=107.5
@@ -234,10 +234,10 @@ public class PchannelTests
}
[Fact]
public void Pchannel_Event_Publishes()
public void Pc_Event_Publishes()
{
var src = new TBarSeries();
var pc = new Pchannel(src, 2);
var pc = new Pc(src, 2);
bool fired = false;
pc.Pub += (object? sender, in TValueEventArgs args) => fired = true;
@@ -246,9 +246,9 @@ public class PchannelTests
}
[Fact]
public void Pchannel_MiddleIsMidpoint()
public void Pc_MiddleIsMidpoint()
{
var pc = new Pchannel(3);
var pc = new Pc(3);
pc.Update(new TBar(DateTime.UtcNow, 100, 110, 90, 105, 1000));
pc.Update(new TBar(DateTime.UtcNow, 105, 115, 95, 110, 1000));
pc.Update(new TBar(DateTime.UtcNow, 110, 120, 100, 115, 1000));
@@ -258,9 +258,9 @@ public class PchannelTests
}
[Fact]
public void Pchannel_UpperGreaterOrEqualLower()
public void Pc_UpperGreaterOrEqualLower()
{
var pc = new Pchannel(5);
var pc = new Pc(5);
var gbm = new GBM(startPrice: 100, mu: 0.02, sigma: 0.2, seed: 123);
for (int i = 0; i < 100; i++)
@@ -3,13 +3,13 @@ using Xunit.Abstractions;
namespace QuanTAlib.Tests;
public sealed class PchannelValidationTests : IDisposable
public sealed class PcValidationTests : IDisposable
{
private readonly ValidationTestData _testData;
private readonly ITestOutputHelper _output;
private bool _disposed;
public PchannelValidationTests(ITestOutputHelper output)
public PcValidationTests(ITestOutputHelper output)
{
_output = output;
_testData = new ValidationTestData();
@@ -40,7 +40,7 @@ public sealed class PchannelValidationTests : IDisposable
series.Add(new TBar(t0.AddMinutes(1), 0, 14, 10, 12, 100));
series.Add(new TBar(t0.AddMinutes(2), 0, 16, 12, 14, 100));
var ind = new Pchannel(3);
var ind = new Pc(3);
var (mid, up, lo) = ind.Update(series);
Assert.Equal(16.0, up.Last.Value, 1e-10);
@@ -48,7 +48,7 @@ public sealed class PchannelValidationTests : IDisposable
Assert.Equal(12.0, mid.Last.Value, 1e-10);
Assert.True(ind.IsHot);
_output.WriteLine("Pchannel manual period-3 calculation validated");
_output.WriteLine("Pc manual period-3 calculation validated");
}
[Fact]
@@ -58,16 +58,16 @@ public sealed class PchannelValidationTests : IDisposable
foreach (int period in periods)
{
var inst = new Pchannel(period);
var inst = new Pc(period);
var (bMid, bUp, bLo) = inst.Update(_testData.Bars);
var (sMid, sUp, sLo) = Pchannel.Batch(_testData.Bars, period);
var (sMid, sUp, sLo) = Pc.Batch(_testData.Bars, period);
ValidationHelper.VerifySeriesEqual(bMid, sMid);
ValidationHelper.VerifySeriesEqual(bUp, sUp);
ValidationHelper.VerifySeriesEqual(bLo, sLo);
var streaming = new Pchannel(period);
var streaming = new Pc(period);
var sMidStream = new TSeries();
var sUpStream = new TSeries();
var sLoStream = new TSeries();
@@ -88,7 +88,7 @@ public sealed class PchannelValidationTests : IDisposable
double[] spanMid = new double[high.Length];
double[] spanUp = new double[high.Length];
double[] spanLo = new double[high.Length];
Pchannel.Batch(high.AsSpan(), low.AsSpan(),
Pc.Batch(high.AsSpan(), low.AsSpan(),
spanMid.AsSpan(), spanUp.AsSpan(), spanLo.AsSpan(), period);
for (int i = 0; i < high.Length; i++)
@@ -99,7 +99,7 @@ public sealed class PchannelValidationTests : IDisposable
}
}
_output.WriteLine("Pchannel mode consistency validated (batch/stream/span)");
_output.WriteLine("Pc mode consistency validated (batch/stream/span)");
}
[Fact]
@@ -107,7 +107,7 @@ public sealed class PchannelValidationTests : IDisposable
{
const int period = 20;
var pub = new TBarSeries();
var evtInd = new Pchannel(pub, period);
var evtInd = new Pc(pub, period);
var evtMid = new TSeries();
var evtUp = new TSeries();
var evtLo = new TSeries();
@@ -120,39 +120,39 @@ public sealed class PchannelValidationTests : IDisposable
evtLo.Add(evtInd.Lower);
}
var (bMid, bUp, bLo) = Pchannel.Batch(_testData.Bars, period);
var (bMid, bUp, bLo) = Pc.Batch(_testData.Bars, period);
ValidationHelper.VerifySeriesEqual(bMid, evtMid);
ValidationHelper.VerifySeriesEqual(bUp, evtUp);
ValidationHelper.VerifySeriesEqual(bLo, evtLo);
_output.WriteLine("Pchannel eventing mode validated");
_output.WriteLine("Pc eventing mode validated");
}
[Fact]
public void Validate_AgainstDchannel_ExactMatch()
public void Validate_AgainstDc_ExactMatch()
{
// Pchannel should produce identical results to Dchannel
// Pc should produce identical results to Dc
int[] periods = { 10, 20, 50 };
foreach (int period in periods)
{
var (dcMid, dcUp, dcLo) = Dchannel.Batch(_testData.Bars, period);
var (pcMid, pcUp, pcLo) = Pchannel.Batch(_testData.Bars, period);
var (dcMid, dcUp, dcLo) = Dc.Batch(_testData.Bars, period);
var (pcMid, pcUp, pcLo) = Pc.Batch(_testData.Bars, period);
ValidationHelper.VerifySeriesEqual(dcMid, pcMid);
ValidationHelper.VerifySeriesEqual(dcUp, pcUp);
ValidationHelper.VerifySeriesEqual(dcLo, pcLo);
}
_output.WriteLine("Pchannel matches Dchannel exactly");
_output.WriteLine("Pc matches Dc exactly");
}
[Fact]
public void Validate_Calculate_ReturnsHotIndicator()
{
const int period = 15;
var ((mid, up, lo), ind) = Pchannel.Calculate(_testData.Bars, period);
var ((mid, up, lo), ind) = Pc.Calculate(_testData.Bars, period);
Assert.True(ind.IsHot);
Assert.Equal(period, ind.WarmupPeriod);
@@ -164,7 +164,7 @@ public sealed class PchannelValidationTests : IDisposable
ind.Update(next);
Assert.True(ind.IsHot);
_output.WriteLine("Pchannel Calculate validated");
_output.WriteLine("Pc Calculate validated");
}
[Fact]
@@ -172,9 +172,9 @@ public sealed class PchannelValidationTests : IDisposable
{
const int period = 25;
var (bMid, bUp, bLo) = Pchannel.Batch(_testData.Bars, period);
var (bMid, bUp, bLo) = Pc.Batch(_testData.Bars, period);
var primed = new Pchannel(period);
var primed = new Pc(period);
var subset = new TBarSeries();
for (int i = 0; i < 200; i++)
{
@@ -192,13 +192,13 @@ public sealed class PchannelValidationTests : IDisposable
Assert.Equal(bUp.Last.Value, primed.Upper.Value, 1e-9);
Assert.Equal(bLo.Last.Value, primed.Lower.Value, 1e-9);
_output.WriteLine("Pchannel Prime validated against batch");
_output.WriteLine("Pc Prime validated against batch");
}
[Fact]
public void Validate_LargeDataset_FiniteOutputs()
{
var (mid, up, lo) = Pchannel.Batch(_testData.Bars, 50);
var (mid, up, lo) = Pc.Batch(_testData.Bars, 50);
ValidationHelper.VerifyAllFinite(mid, startIndex: 0);
ValidationHelper.VerifyAllFinite(up, startIndex: 0);
@@ -209,13 +209,13 @@ public sealed class PchannelValidationTests : IDisposable
Assert.True(up[i].Value >= lo[i].Value, $"Upper >= Lower at {i}");
}
_output.WriteLine("Pchannel large dataset validated");
_output.WriteLine("Pc large dataset validated");
}
[Fact]
public void Validate_StateRestoration_Iterative()
{
var ind = new Pchannel(15);
var ind = new Pc(15);
var gbm = new GBM(startPrice: 100, mu: 0.01, sigma: 0.1, seed: 42);
for (int i = 0; i < 50; i++)
@@ -242,6 +242,6 @@ public sealed class PchannelValidationTests : IDisposable
Assert.Equal(savedUp, ind.Upper.Value, 1e-10);
Assert.Equal(savedLo, ind.Lower.Value, 1e-10);
_output.WriteLine("Pchannel state restoration validated");
_output.WriteLine("Pc state restoration validated");
}
}
+1 -1
View File
@@ -13,7 +13,7 @@
| **PineScript** | [starchannel.pine](starchannel.pine) |
- Stoller Average Range Channel creates a volatility-adaptive price envelope using Average True Range (ATR) to determine band width around a simple m...
- **Similar:** [KChannel](../kchannel/kchannel.md), [ATRBands](../atrbands/atrbands.md) | **Complementary:** ADX for trend confirmation | **Trading note:** Stoller Average Range Channel; ATR-based bands around a moving average.
- **Similar:** [KC](../kc/kc.md), [ATRBands](../atrbands/atrbands.md) | **Complementary:** ADX for trend confirmation | **Trading note:** Stoller Average Range Channel; ATR-based bands around a moving average.
- Validated against TA-Lib, Skender, and Tulip reference implementations where available.
Stoller Average Range Channel creates a volatility-adaptive price envelope using Average True Range (ATR) to determine band width around a simple moving average centerline. The bands automatically expand during volatile periods and contract during calmer markets. The implementation uses a circular buffer for the SMA running sum and Wilder's RMA with a warmup compensator for ATR, achieving O(1) streaming updates per bar.
@@ -501,16 +501,16 @@ public sealed class StarchannelValidationTests : IDisposable
}
[Fact]
public void Validate_KchannelComparison_Structure()
public void Validate_KcComparison_Structure()
{
// Compare structural properties with Kchannel (EMA vs SMA middle)
// Compare structural properties with Kc (EMA vs SMA middle)
// Both use ATR for bands, so band calculation should be similar
const int period = 20;
const double multiplier = 2.0;
var star = new Starchannel(period, multiplier);
var kelt = new Kchannel(period, multiplier);
var kelt = new Kc(period, multiplier);
foreach (var bar in _testData.Bars)
{
@@ -543,18 +543,18 @@ public sealed class StarchannelValidationTests : IDisposable
double keltLowerDist = kelt.Last.Value - kelt.Lower.Value;
Assert.Equal(keltUpperDist, keltLowerDist, 1e-10);
_output.WriteLine("Starchannel vs Kchannel structure validated");
_output.WriteLine("Starchannel vs Kc structure validated");
}
[Fact]
public void Validate_Starchannel_DifferentFromKchannel()
public void Validate_Starchannel_DifferentFromKc()
{
// Starchannel (SMA) should differ from Kchannel (EMA) in the middle line
// Starchannel (SMA) should differ from Kc (EMA) in the middle line
const int period = 20;
const double multiplier = 2.0;
var star = new Starchannel(period, multiplier);
var kelt = new Kchannel(period, multiplier);
var kelt = new Kc(period, multiplier);
foreach (var bar in _testData.Bars)
{
@@ -569,7 +569,7 @@ public sealed class StarchannelValidationTests : IDisposable
// Just verify they're both finite and reasonable
Assert.True(double.IsFinite(diff));
_output.WriteLine($"Starchannel vs Kchannel middle difference: {diff:F6}");
_output.WriteLine($"Starchannel vs Kc middle difference: {diff:F6}");
}
// ═══════════════════════════════════════════════════════════════
+1 -1
View File
@@ -13,7 +13,7 @@
| **PineScript** | [stbands.pine](stbands.pine) |
- Super Trend Bands provide ATR-based dynamic support and resistance levels with asymmetric ratchet logic: the upper band only tightens downward duri...
- **Similar:** [BBands](../bbands/bbands.md), [KChannel](../kchannel/kchannel.md) | **Complementary:** Volume for breakout validation | **Trading note:** Stoller bands use ATR multiplier instead of standard deviation.
- **Similar:** [BBands](../bbands/bbands.md), [KC](../kc/kc.md) | **Complementary:** Volume for breakout validation | **Trading note:** Stoller bands use ATR multiplier instead of standard deviation.
- Validated against TA-Lib, Skender, and Tulip reference implementations where available.
Super Trend Bands provide ATR-based dynamic support and resistance levels with asymmetric ratchet logic: the upper band only tightens downward during downtrends, and the lower band only tightens upward during uptrends. This creates natural trailing stop-loss levels that respect market momentum. A trend direction signal ($+1$ or $-1$) flips when price breaches the opposite band. The ATR is computed as a simple moving average of True Range via a ring buffer with running sum, providing O(1) streaming updates.
+1 -1
View File
@@ -13,7 +13,7 @@
| **PineScript** | [uchannel.pine](uchannel.pine) |
- Ehlers Ultimate Channel applies the Ultrasmooth Filter (USF) twice: once to the close price for the centerline and once to True Range for band widt...
- **Similar:** [DChannel](../dchannel/dchannel.md), [PChannel](../pchannel/pchannel.md) | **Complementary:** Volume for breakout confirmation | **Trading note:** Upper/lower channel based on recent price extremes.
- **Similar:** [DC](../dc/dc.md), [PC](../pc/pc.md) | **Complementary:** Volume for breakout confirmation | **Trading note:** Upper/lower channel based on recent price extremes.
- Validated against TA-Lib, Skender, and Tulip reference implementations where available.
Ehlers Ultimate Channel applies the Ultrasmooth Filter (USF) twice: once to the close price for the centerline and once to True Range for band width, creating a channel where both the trend estimate and the volatility measure share the same low-lag, zero-overshoot filter characteristics. Unlike UBANDS which uses RMS of price residuals, UCHANNEL uses Smoothed True Range (STR) for band width, making it responsive to gap-inclusive volatility. Separate period parameters allow independent tuning of centerline smoothness and band-width responsiveness.
+1 -1
View File
@@ -13,7 +13,7 @@
| **PineScript** | [ghla.pine](ghla.pine) |
- The Gann High-Low Activator (GHLA) is a trend-following stop/reversal indicator that alternates between the Simple Moving Average of Highs and the ...
- **Similar:** [Super](../super/Super.md), [PSAR](../../reversals/psar/Psar.md) | **Complementary:** ATR for stop placement | **Trading note:** Gann High-Low Activator; trend-following stop based on prior period's median.
- **Similar:** [Super](../super/Super.md), [SAR](../../reversals/sar/Sar.md) | **Complementary:** ATR for stop placement | **Trading note:** Gann High-Low Activator; trend-following stop based on prior period's median.
- Validated against TA-Lib, Skender, and Tulip reference implementations where available.
The Gann High-Low Activator (GHLA) is a trend-following stop/reversal indicator that alternates between the Simple Moving Average of Highs and the Simple Moving Average of Lows based on a three-state crossover rule. Developed by Robert Krausz and published in *Technical Analysis of Stocks & Commodities* (February 1998), the indicator produces a single trailing line: SMA(Low) during uptrends (acting as dynamic support) and SMA(High) during downtrends (acting as dynamic resistance). The flip between states occurs only when price closes decisively beyond the opposing SMA, creating a hysteresis zone that filters minor whipsaws. With a default period of 3 bars, GHLA responds aggressively to trend changes while requiring just $O(N)$ additions and one comparison per bar.
+1 -1
View File
@@ -13,7 +13,7 @@
| **PineScript** | [super.pine](super.pine) |
- SuperTrend is a trend-following overlay that uses ATR-scaled bands around the HL2 midpoint, switching between upper and lower bands based on close ...
- **Similar:** [PSAR](../../reversals/psar/Psar.md), [Ghla](../ghla/Ghla.md) | **Complementary:** ADX for trend strength | **Trading note:** SuperTrend; ATR-based trailing stop that flips direction. Popular in crypto and forex.
- **Similar:** [SAR](../../reversals/sar/Sar.md), [Ghla](../ghla/Ghla.md) | **Complementary:** ADX for trend strength | **Trading note:** SuperTrend; ATR-based trailing stop that flips direction. Popular in crypto and forex.
- Validated against TA-Lib, Skender, and Tulip reference implementations where available.
SuperTrend is a trend-following overlay that uses ATR-scaled bands around the HL2 midpoint, switching between upper and lower bands based on close price breakouts. A ratchet mechanism prevents the active band from moving against the trend, creating a step-like trailing stop that adapts to volatility. The indicator is a two-state machine (bullish/bearish) with O(1) per-bar updates and zero allocations in the hot path.
+1 -1
View File
@@ -13,7 +13,7 @@
| **PineScript** | [rsquared.pine](rsquared.pine) |
- The Coefficient of Determination (R²) measures the proportion of variance in the actual values that is predictable from the predicted values.
- **Similar:** [RSE](../rse/Rse.md), [Correlation](../../statistics/correlation/Correlation.md) | **Trading note:** R-squared (coefficient of determination); 1.0 = perfect fit, 0 = no better than mean.
- **Similar:** [RSE](../rse/Rse.md), [Correl](../../statistics/correl/Correl.md) | **Trading note:** R-squared (coefficient of determination); 1.0 = perfect fit, 0 = no better than mean.
- Validated against TA-Lib, Skender, and Tulip reference implementations where available.
The Coefficient of Determination (R²) measures the proportion of variance in the actual values that is predictable from the predicted values. R² ranges from negative infinity to 1, where 1 indicates perfect predictions.
+1 -1
View File
@@ -16,7 +16,7 @@ Momentum indicators measure the velocity and acceleration of price changes. Unli
| [MOM](mom/Mom.md) | Momentum | Raw price change over specified period. |
| [PMO](pmo/Pmo.md) | Price Momentum Oscillator | Double-smoothed ROC oscillator. |
| [PPO](ppo/Ppo.md) | Percentage Price Oscillator | MACD expressed as percentage for cross-instrument comparison. |
| [PRS](prs/Prs.md) | Price Relative Strength | Performance ratio between two assets. |
| [RS](rs/Rs.md) | Price Relative Strength | Performance ratio between two assets. |
| [ROC](roc/Roc.md) | Rate of Change | Absolute price change over N periods. |
| [ROCP](rocp/Rocp.md) | Rate of Change Percentage | Percentage price change over N periods. |
| [ROCR](rocr/Rocr.md) | Rate of Change Ratio | Price ratio over N periods. |
@@ -5,20 +5,20 @@ using TradingPlatform.BusinessLayer;
namespace QuanTAlib;
/// <summary>
/// Quantower adapter for PRS (Price Relative Strength) indicator.
/// Quantower adapter for RS (Price Relative Strength) indicator.
/// Compares two price sources from the same symbol to identify relative performance.
/// </summary>
/// <remarks>
/// This adapter compares two different price sources from the same symbol (e.g., Close vs Open,
/// Close vs Volume, High vs Low). For cross-symbol relative strength analysis, use the core
/// PRS class directly with data from multiple symbols.
/// RS class directly with data from multiple symbols.
///
/// The output is the ratio of Base/Comparison with optional EMA smoothing.
/// Values above 1.0 indicate the base source is higher than comparison.
/// Rising values indicate base is outperforming, falling values indicate underperformance.
/// </remarks>
[SkipLocalsInit]
public sealed class PrsIndicator : Indicator, IWatchlistIndicator
public sealed class RsIndicator : Indicator, IWatchlistIndicator
{
[InputParameter("Smooth Period", sortIndex: 0, minimum: 1, maximum: 10000)]
public int SmoothPeriod { get; set; } = 1;
@@ -32,7 +32,7 @@ public sealed class PrsIndicator : Indicator, IWatchlistIndicator
[InputParameter("Show cold values", sortIndex: 21)]
public bool ShowColdValues { get; set; } = true;
private Prs _prs = null!;
private Rs _rs = null!;
private readonly LineSeries _series;
private string _sourceName = null!;
private Func<IHistoryItem, double> _priceSelector = null!;
@@ -42,16 +42,16 @@ public sealed class PrsIndicator : Indicator, IWatchlistIndicator
int IWatchlistIndicator.MinHistoryDepths => MinHistoryDepths;
public override string ShortName => SmoothPeriod == 1
? $"PRS:{_sourceName}/{Source2}"
: $"PRS({SmoothPeriod}):{_sourceName}/{Source2}";
? $"RS:{_sourceName}/{Source2}"
: $"RS({SmoothPeriod}):{_sourceName}/{Source2}";
public PrsIndicator()
public RsIndicator()
{
OnBackGround = true;
SeparateWindow = true;
Name = "PRS - Price Relative Strength";
Name = "RS - Price Relative Strength";
Description = "Compares relative performance between two price sources. Ratio > 1 means base is higher than comparison.";
_series = new LineSeries(name: "PRS", color: IndicatorExtensions.Momentum, width: 2, style: LineStyle.Solid);
_series = new LineSeries(name: "RS", color: IndicatorExtensions.Momentum, width: 2, style: LineStyle.Solid);
AddLineSeries(_series);
}
@@ -60,7 +60,7 @@ public sealed class PrsIndicator : Indicator, IWatchlistIndicator
_priceSelector = Source.GetPriceSelector();
_priceSelector2 = Source2.GetPriceSelector();
_sourceName = Source.ToString();
_prs = new Prs(SmoothPeriod);
_rs = new Rs(SmoothPeriod);
base.OnInit();
}
@@ -77,7 +77,7 @@ public sealed class PrsIndicator : Indicator, IWatchlistIndicator
var tvalBase = new TValue(item.TimeLeft.Ticks, baseValue);
var tvalComp = new TValue(item.TimeLeft.Ticks, compValue);
double value = _prs.Update(tvalBase, tvalComp, isNew).Value;
_series.SetValue(value, _prs.IsHot, ShowColdValues);
double value = _rs.Update(tvalBase, tvalComp, isNew).Value;
_series.SetValue(value, _rs.IsHot, ShowColdValues);
}
}
@@ -1,4 +1,4 @@
// PRS: Price Relative Strength
// RS: Price Relative Strength
// Compares the performance of one asset to another by calculating the ratio
// and optionally applying EMA smoothing for trend identification.
@@ -8,15 +8,15 @@ using static System.Math;
namespace QuanTAlib;
/// <summary>
/// PRS: Price Relative Strength
/// RS: Price Relative Strength
/// </summary>
/// <remarks>
/// Measures relative performance between two assets by calculating their price ratio.
/// A rising PRS indicates the base asset is outperforming the comparison asset.
/// A falling PRS indicates underperformance. Optional EMA smoothing reduces noise.
/// A rising RS indicates the base asset is outperforming the comparison asset.
/// A falling RS indicates underperformance. Optional EMA smoothing reduces noise.
///
/// Key characteristics:
/// - Ratio-based: PRS = Base / Comparison
/// - Ratio-based: RS = Base / Comparison
/// - Trend indicator: Rising = outperformance, Falling = underperformance
/// - Smoothing: Optional EMA with bias compensation for warmup
/// - Division by zero: Returns NaN when comparison is zero
@@ -28,14 +28,14 @@ namespace QuanTAlib;
/// </code>
///
/// Interpretation:
/// - PRS > 1.0: Base asset is worth more per unit
/// - PRS increasing: Base outperforming comparison
/// - PRS decreasing: Base underperforming comparison
/// - RS > 1.0: Base asset is worth more per unit
/// - RS increasing: Base outperforming comparison
/// - RS decreasing: Base underperforming comparison
/// - Use with baseline (1.0 or initial ratio) for normalized view
/// </remarks>
/// <seealso href="Prs.md">Detailed documentation</seealso>
/// <seealso href="Rs.md">Detailed documentation</seealso>
[SkipLocalsInit]
public sealed class Prs : AbstractBase
public sealed class Rs : AbstractBase
{
private const double Epsilon = 1e-10;
private readonly int _smoothPeriod;
@@ -75,7 +75,7 @@ public sealed class Prs : AbstractBase
/// Creates a new Price Relative Strength indicator.
/// </summary>
/// <param name="smoothPeriod">Smoothing period for EMA (1 = no smoothing)</param>
public Prs(int smoothPeriod = 1)
public Rs(int smoothPeriod = 1)
{
if (smoothPeriod < 1)
{
@@ -87,12 +87,12 @@ public sealed class Prs : AbstractBase
_isWarmup = true;
_e = 1.0;
Name = smoothPeriod == 1 ? "Prs" : $"Prs({smoothPeriod})";
Name = smoothPeriod == 1 ? "Rs" : $"Rs({smoothPeriod})";
WarmupPeriod = smoothPeriod;
}
/// <summary>
/// Updates the PRS indicator with new values from both series.
/// Updates the RS indicator with new values from both series.
/// </summary>
/// <param name="baseValue">Base asset price</param>
/// <param name="compValue">Comparison asset price</param>
@@ -152,16 +152,16 @@ public sealed class Prs : AbstractBase
return Update(new TValue(now, baseValue), new TValue(now, compValue), isNew);
}
/// <summary>Not supported for bi-input indicator. Use Update(baseValue, compValue) instead.</summary>
/// <remarks>PRS requires paired base/comparison inputs; single-input updates are invalid.</remarks>
/// <remarks>RS requires paired base/comparison inputs; single-input updates are invalid.</remarks>
public override TValue Update(TValue input, bool isNew = true)
{
throw new NotSupportedException("PRS requires two inputs (base and comparison). Use Update(baseValue, compValue).");
throw new NotSupportedException("RS requires two inputs (base and comparison). Use Update(baseValue, compValue).");
}
/// <summary>Not supported for bi-input indicator. Use Calculate(baseSeries, compSeries, period) instead.</summary>
/// <remarks>PRS requires paired base/comparison series; single-series updates are invalid.</remarks>
/// <remarks>RS requires paired base/comparison series; single-series updates are invalid.</remarks>
public override TSeries Update(TSeries source)
{
throw new NotSupportedException("PRS requires two inputs. Use Batch(baseSeries, compSeries, period).");
throw new NotSupportedException("RS requires two inputs. Use Batch(baseSeries, compSeries, period).");
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
@@ -246,7 +246,7 @@ public sealed class Prs : AbstractBase
}
public override void Prime(ReadOnlySpan<double> source, TimeSpan? step = null)
{
throw new NotSupportedException("PRS requires two inputs. Use Prime(baseSource, compSource).");
throw new NotSupportedException("RS requires two inputs. Use Prime(baseSource, compSource).");
}
/// <summary>
@@ -290,7 +290,7 @@ public sealed class Prs : AbstractBase
}
/// <summary>
/// Calculates PRS for two time series.
/// Calculates RS for two time series.
/// </summary>
public static TSeries Batch(TSeries baseSeries, TSeries compSeries, int smoothPeriod = 1)
{
@@ -299,7 +299,7 @@ public sealed class Prs : AbstractBase
throw new ArgumentException("Series must have the same length", nameof(compSeries));
}
var indicator = new Prs(smoothPeriod);
var indicator = new Rs(smoothPeriod);
var result = new TSeries(baseSeries.Count);
var times = baseSeries.Times;
@@ -340,7 +340,7 @@ public sealed class Prs : AbstractBase
throw new ArgumentException("Smoothing period must be >= 1", nameof(smoothPeriod));
}
var indicator = new Prs(smoothPeriod);
var indicator = new Rs(smoothPeriod);
for (int i = 0; i < baseSeries.Length; i++)
{
@@ -349,9 +349,9 @@ public sealed class Prs : AbstractBase
}
}
public static (TSeries Results, Prs Indicator) Calculate(TSeries baseSeries, TSeries compSeries, int smoothPeriod = 1)
public static (TSeries Results, Rs Indicator) Calculate(TSeries baseSeries, TSeries compSeries, int smoothPeriod = 1)
{
var indicator = new Prs(smoothPeriod);
var indicator = new Rs(smoothPeriod);
TSeries results = Batch(baseSeries, compSeries, smoothPeriod);
return (results, indicator);
}
@@ -1,4 +1,4 @@
# PRS: Price Relative Strength
# RS: Price Relative Strength
> *Price relative strength ratios two instruments, revealing which one leads and which one lags in the race.*
@@ -7,10 +7,10 @@
| **Category** | Momentum |
| **Inputs** | Source (close) |
| **Parameters** | `smoothPeriod` (default 1) |
| **Outputs** | Single series (PRS) |
| **Outputs** | Single series (RS) |
| **Output range** | Varies (see docs) |
| **Warmup** | `smoothPeriod` bars |
| **PineScript** | [prs.pine](prs.pine) |
| **PineScript** | [rs.pine](rs.pine) |
- **Category:** Momentum **Also known as:** Relative Strength Comparison, Price Ratio, Performance Ratio
- **Similar:** [ROC](../roc/Roc.md), [Bias](../bias/Bias.md) | **Complementary:** Relative strength vs benchmark | **Trading note:** Price Relative Strength; ratio or spread between two series. Sector rotation tool.
@@ -19,13 +19,13 @@
**Category:** Momentum
**Also known as:** Relative Strength Comparison, Price Ratio, Performance Ratio
[Pine Script Implementation of PRS](https://github.com/mihakralj/pinescript/blob/main/indicators/momentum/prs.pine)
[Pine Script Implementation of RS](https://github.com/mihakralj/pinescript/blob/main/indicators/momentum/rs.pine)
## Overview
Price Relative Strength (PRS) measures the performance of one asset relative to another by calculating the ratio between their prices. This indicator helps identify which asset is outperforming and is fundamental for sector rotation, pairs trading, and relative performance analysis.
Price Relative Strength (RS) measures the performance of one asset relative to another by calculating the ratio between their prices. This indicator helps identify which asset is outperforming and is fundamental for sector rotation, pairs trading, and relative performance analysis.
Unlike the Relative Strength Index (RSI) which measures momentum within a single asset, PRS compares two different price series. A rising PRS indicates the base asset is outperforming the comparison asset; a falling PRS indicates underperformance.
Unlike the Relative Strength Index (RSI) which measures momentum within a single asset, RS compares two different price series. A rising RS indicates the base asset is outperforming the comparison asset; a falling RS indicates underperformance.
## Core Concepts
@@ -46,7 +46,7 @@ Unlike the Relative Strength Index (RSI) which measures momentum within a single
## Calculation
**Simplified Explanation:**
PRS divides the base asset's price by the comparison asset's price, then optionally smooths the result with an EMA.
RS divides the base asset's price by the comparison asset's price, then optionally smooths the result with an EMA.
**Technical Formula:**
@@ -68,9 +68,9 @@ Smoothed = compensation × EMA_biased
## Interpretation Details
**Trend Analysis:**
- **PRS rising:** Base asset outperforming comparison (bullish for base)
- **PRS falling:** Base asset underperforming comparison (bearish for base)
- **PRS flat:** Both assets moving in tandem
- **RS rising:** Base asset outperforming comparison (bullish for base)
- **RS falling:** Base asset underperforming comparison (bearish for base)
- **RS flat:** Both assets moving in tandem
**Common Comparisons:**
- Stock vs. sector ETF (stock relative to its sector)
@@ -80,15 +80,15 @@ Smoothed = compensation × EMA_biased
- Commodity vs. currency (inflation dynamics)
**Trading Signals:**
- **PRS crossover above prior high:** Breakout in relative strength
- **PRS crossover below prior low:** Breakdown in relative strength
- **Divergence:** Price makes new high, PRS does not = warning
- **RS crossover above prior high:** Breakout in relative strength
- **RS crossover below prior low:** Breakdown in relative strength
- **Divergence:** Price makes new high, RS does not = warning
## Performance Profile
### Operation Count (Streaming Mode)
PRS with smoothing is three scalar operations: one division for the raw ratio, one FMA for the EMA update, and one divide for the bias compensation factor. Without smoothing (smoothPeriod = 1), the bias step is skipped entirely.
RS with smoothing is three scalar operations: one division for the raw ratio, one FMA for the EMA update, and one divide for the bias compensation factor. Without smoothing (smoothPeriod = 1), the bias step is skipped entirely.
| Operation | Count | Cost (cycles) | Subtotal |
| :--- | :---: | :---: | :---: |
@@ -1,14 +1,14 @@
// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Price Relative Strength (PRS)", "PRS", overlay=false)
indicator("Price Relative Strength (RS)", "RS", overlay=false)
//@function Calculates Price Relative Strength comparing two assets
//@param base Base asset price series
//@param comp Compare asset price series
//@param smooth_len Smoothing period for ratio
//@returns Tuple containing raw ratio and smoothed ratio
prs(series float base, series float comp, simple int smooth_len=1)=>
rs(series float base, series float comp, simple int smooth_len=1)=>
if smooth_len<=0
runtime.error("Smoothing length must be greater than 0")
float ratio = na
@@ -44,8 +44,8 @@ i_log = input.bool(false, "Logarithmic Scale")
// Get comparison data
float comp_close = request.security(i_comp, timeframe.period, close, barmerge.gaps_off, barmerge.lookahead_off)
// Calculate PRS
[raw_ratio, smooth_ratio] = prs(i_base, comp_close, i_smooth)
// Calculate RS
[raw_ratio, smooth_ratio] = rs(i_base, comp_close, i_smooth)
// Apply optional normalization
if i_norm
@@ -2,54 +2,54 @@ using TradingPlatform.BusinessLayer;
namespace QuanTAlib.Tests;
public class PrsIndicatorTests
public class RsIndicatorTests
{
[Fact]
public void PrsIndicator_Constructor_SetsDefaults()
public void RsIndicator_Constructor_SetsDefaults()
{
var indicator = new PrsIndicator();
var indicator = new RsIndicator();
Assert.Equal(1, indicator.SmoothPeriod);
Assert.Equal(SourceType.Close, indicator.Source);
Assert.Equal(SourceType.Open, indicator.Source2);
Assert.True(indicator.ShowColdValues);
Assert.Equal("PRS - Price Relative Strength", indicator.Name);
Assert.Equal("RS - Price Relative Strength", indicator.Name);
Assert.True(indicator.SeparateWindow);
Assert.True(indicator.OnBackGround);
}
[Fact]
public void PrsIndicator_MinHistoryDepths_EqualsOne()
public void RsIndicator_MinHistoryDepths_EqualsOne()
{
var indicator = new PrsIndicator();
var indicator = new RsIndicator();
Assert.Equal(1, PrsIndicator.MinHistoryDepths);
Assert.Equal(1, RsIndicator.MinHistoryDepths);
Assert.Equal(1, ((IWatchlistIndicator)indicator).MinHistoryDepths);
}
[Fact]
public void PrsIndicator_ShortName_NoSmoothing_ContainsSourceTypes()
public void RsIndicator_ShortName_NoSmoothing_ContainsSourceTypes()
{
var indicator = new PrsIndicator { SmoothPeriod = 1 };
var indicator = new RsIndicator { SmoothPeriod = 1 };
Assert.Contains("PRS", indicator.ShortName, StringComparison.Ordinal);
Assert.Contains("RS", indicator.ShortName, StringComparison.Ordinal);
Assert.DoesNotContain("(", indicator.ShortName, StringComparison.Ordinal);
}
[Fact]
public void PrsIndicator_ShortName_WithSmoothing_IncludesPeriod()
public void RsIndicator_ShortName_WithSmoothing_IncludesPeriod()
{
var indicator = new PrsIndicator { SmoothPeriod = 14 };
var indicator = new RsIndicator { SmoothPeriod = 14 };
indicator.Initialize();
Assert.Contains("PRS", indicator.ShortName, StringComparison.Ordinal);
Assert.Contains("RS", indicator.ShortName, StringComparison.Ordinal);
Assert.Contains("14", indicator.ShortName, StringComparison.Ordinal);
}
[Fact]
public void PrsIndicator_Initialize_CreatesPrsInstance()
public void RsIndicator_Initialize_CreatesRsInstance()
{
var indicator = new PrsIndicator { SmoothPeriod = 10 };
var indicator = new RsIndicator { SmoothPeriod = 10 };
// Initialize should not throw
indicator.Initialize();
@@ -59,9 +59,9 @@ public class PrsIndicatorTests
}
[Fact]
public void PrsIndicator_ProcessUpdate_HistoricalBar_ComputesValue()
public void RsIndicator_ProcessUpdate_HistoricalBar_ComputesValue()
{
var indicator = new PrsIndicator { SmoothPeriod = 1 };
var indicator = new RsIndicator { SmoothPeriod = 1 };
indicator.Initialize();
// Add historical data
@@ -77,9 +77,9 @@ public class PrsIndicatorTests
}
[Fact]
public void PrsIndicator_ProcessUpdate_NewBar_ComputesValue()
public void RsIndicator_ProcessUpdate_NewBar_ComputesValue()
{
var indicator = new PrsIndicator { SmoothPeriod = 5 };
var indicator = new RsIndicator { SmoothPeriod = 5 };
indicator.Initialize();
var now = DateTime.UtcNow;
@@ -93,9 +93,9 @@ public class PrsIndicatorTests
}
[Fact]
public void PrsIndicator_ProcessUpdate_NewTick_ProcessesWithoutError()
public void RsIndicator_ProcessUpdate_NewTick_ProcessesWithoutError()
{
var indicator = new PrsIndicator { SmoothPeriod = 1 };
var indicator = new RsIndicator { SmoothPeriod = 1 };
indicator.Initialize();
var now = DateTime.UtcNow;
@@ -114,9 +114,9 @@ public class PrsIndicatorTests
}
[Fact]
public void PrsIndicator_MultipleUpdates_ProducesSequence()
public void RsIndicator_MultipleUpdates_ProducesSequence()
{
var indicator = new PrsIndicator { SmoothPeriod = 3 };
var indicator = new RsIndicator { SmoothPeriod = 3 };
indicator.Initialize();
var now = DateTime.UtcNow;
@@ -133,10 +133,10 @@ public class PrsIndicatorTests
}
[Fact]
public void PrsIndicator_CloseVsOpen_ReturnsPositiveRatio()
public void RsIndicator_CloseVsOpen_ReturnsPositiveRatio()
{
// Test with Close vs Open - Close should be higher than Open in uptrend
var indicator = new PrsIndicator { SmoothPeriod = 1, Source = SourceType.Close, Source2 = SourceType.Open };
var indicator = new RsIndicator { SmoothPeriod = 1, Source = SourceType.Close, Source2 = SourceType.Open };
indicator.Initialize();
var now = DateTime.UtcNow;
@@ -154,10 +154,10 @@ public class PrsIndicatorTests
}
[Fact]
public void PrsIndicator_OpenVsClose_ReturnsInverseRatio()
public void RsIndicator_OpenVsClose_ReturnsInverseRatio()
{
// Reverse the sources - Open vs Close
var indicator = new PrsIndicator { SmoothPeriod = 1, Source = SourceType.Open, Source2 = SourceType.Close };
var indicator = new RsIndicator { SmoothPeriod = 1, Source = SourceType.Open, Source2 = SourceType.Close };
indicator.Initialize();
var now = DateTime.UtcNow;
@@ -174,13 +174,13 @@ public class PrsIndicatorTests
}
[Fact]
public void PrsIndicator_DifferentSourceTypes_Work()
public void RsIndicator_DifferentSourceTypes_Work()
{
var sources = new[] { SourceType.Open, SourceType.High, SourceType.Low, SourceType.Close, SourceType.HL2, SourceType.HLC3 };
foreach (var source in sources)
{
var indicator = new PrsIndicator { SmoothPeriod = 1, Source = source, Source2 = SourceType.Close };
var indicator = new RsIndicator { SmoothPeriod = 1, Source = source, Source2 = SourceType.Close };
indicator.Initialize();
var now = DateTime.UtcNow;
@@ -193,13 +193,13 @@ public class PrsIndicatorTests
}
[Fact]
public void PrsIndicator_DifferentSource2Types_Work()
public void RsIndicator_DifferentSource2Types_Work()
{
var source2Types = new[] { SourceType.Open, SourceType.High, SourceType.Low, SourceType.HL2 };
foreach (var source2 in source2Types)
{
var indicator = new PrsIndicator { SmoothPeriod = 1, Source = SourceType.Close, Source2 = source2 };
var indicator = new RsIndicator { SmoothPeriod = 1, Source = SourceType.Close, Source2 = source2 };
indicator.Initialize();
var now = DateTime.UtcNow;
@@ -211,9 +211,9 @@ public class PrsIndicatorTests
}
[Fact]
public void PrsIndicator_SmoothPeriod_CanBeChanged()
public void RsIndicator_SmoothPeriod_CanBeChanged()
{
var indicator = new PrsIndicator { SmoothPeriod = 50 };
var indicator = new RsIndicator { SmoothPeriod = 50 };
Assert.Equal(50, indicator.SmoothPeriod);
@@ -222,9 +222,9 @@ public class PrsIndicatorTests
}
[Fact]
public void PrsIndicator_Source2_CanBeChanged()
public void RsIndicator_Source2_CanBeChanged()
{
var indicator = new PrsIndicator { Source2 = SourceType.High };
var indicator = new RsIndicator { Source2 = SourceType.High };
Assert.Equal(SourceType.High, indicator.Source2);
@@ -233,9 +233,9 @@ public class PrsIndicatorTests
}
[Fact]
public void PrsIndicator_ReInitialize_ResetsState()
public void RsIndicator_ReInitialize_ResetsState()
{
var indicator = new PrsIndicator { SmoothPeriod = 5 };
var indicator = new RsIndicator { SmoothPeriod = 5 };
indicator.Initialize();
var now = DateTime.UtcNow;
@@ -248,7 +248,7 @@ public class PrsIndicatorTests
Assert.Equal(10, indicator.LinesSeries[0].Count);
// Re-initialize with new instance
var indicator2 = new PrsIndicator { SmoothPeriod = 5 };
var indicator2 = new RsIndicator { SmoothPeriod = 5 };
indicator2.Initialize();
indicator2.HistoricalData.AddBar(now.AddMinutes(100), 200, 210, 190, 205);
indicator2.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
@@ -257,10 +257,10 @@ public class PrsIndicatorTests
}
[Fact]
public void PrsIndicator_HighVsLow_AlwaysGreaterThanOne()
public void RsIndicator_HighVsLow_AlwaysGreaterThanOne()
{
// High is always > Low, so ratio should always be > 1
var indicator = new PrsIndicator { SmoothPeriod = 1, Source = SourceType.High, Source2 = SourceType.Low };
var indicator = new RsIndicator { SmoothPeriod = 1, Source = SourceType.High, Source2 = SourceType.Low };
indicator.Initialize();
var now = DateTime.UtcNow;
@@ -286,19 +286,19 @@ public class PrsIndicatorTests
}
[Fact]
public void PrsIndicator_Description_IsSet()
public void RsIndicator_Description_IsSet()
{
var indicator = new PrsIndicator();
var indicator = new RsIndicator();
Assert.Contains("relative", indicator.Description, StringComparison.OrdinalIgnoreCase);
Assert.Contains("performance", indicator.Description, StringComparison.OrdinalIgnoreCase);
}
[Fact]
public void PrsIndicator_SameSourceAndSource2_ReturnsOne()
public void RsIndicator_SameSourceAndSource2_ReturnsOne()
{
// When comparing a source to itself, ratio should be 1.0
var indicator = new PrsIndicator { SmoothPeriod = 1, Source = SourceType.Close, Source2 = SourceType.Close };
var indicator = new RsIndicator { SmoothPeriod = 1, Source = SourceType.Close, Source2 = SourceType.Close };
indicator.Initialize();
var now = DateTime.UtcNow;
@@ -320,11 +320,11 @@ public class PrsIndicatorTests
}
[Fact]
public void PrsIndicator_SmoothingReducesVariance()
public void RsIndicator_SmoothingReducesVariance()
{
// Compare unsmoothed vs smoothed - smoothed should have less variance
var unsmoothed = new PrsIndicator { SmoothPeriod = 1, Source = SourceType.Close, Source2 = SourceType.Open };
var smoothed = new PrsIndicator { SmoothPeriod = 10, Source = SourceType.Close, Source2 = SourceType.Open };
var unsmoothed = new RsIndicator { SmoothPeriod = 1, Source = SourceType.Close, Source2 = SourceType.Open };
var smoothed = new RsIndicator { SmoothPeriod = 10, Source = SourceType.Close, Source2 = SourceType.Open };
unsmoothed.Initialize();
smoothed.Initialize();
@@ -2,7 +2,7 @@ using Xunit;
namespace QuanTAlib.Tests;
public class PrsTests
public class RsTests
{
private const double Epsilon = 1e-10;
@@ -10,99 +10,99 @@ public class PrsTests
[Fact]
public void Constructor_DefaultParameters()
{
var prs = new Prs();
Assert.Equal("Prs", prs.Name);
Assert.Equal(1, prs.SmoothPeriod);
var rs = new Rs();
Assert.Equal("Rs", rs.Name);
Assert.Equal(1, rs.SmoothPeriod);
}
[Fact]
public void Constructor_CustomSmoothPeriod()
{
var prs = new Prs(10);
Assert.Equal("Prs(10)", prs.Name);
Assert.Equal(10, prs.SmoothPeriod);
var rs = new Rs(10);
Assert.Equal("Rs(10)", rs.Name);
Assert.Equal(10, rs.SmoothPeriod);
}
[Fact]
public void Constructor_ZeroPeriod_ThrowsException()
{
Assert.Throws<ArgumentException>(() => new Prs(0));
Assert.Throws<ArgumentException>(() => new Rs(0));
}
[Fact]
public void Constructor_NegativePeriod_ThrowsException()
{
Assert.Throws<ArgumentException>(() => new Prs(-1));
Assert.Throws<ArgumentException>(() => new Rs(-1));
}
// ==================== BASIC CALCULATIONS ====================
[Fact]
public void Update_SimpleRatio_ReturnsCorrectValue()
{
var prs = new Prs();
var result = prs.Update(100.0, 50.0);
var rs = new Rs();
var result = rs.Update(100.0, 50.0);
Assert.Equal(2.0, result.Value, 10);
}
[Fact]
public void Update_FractionRatio_ReturnsCorrectValue()
{
var prs = new Prs();
var result = prs.Update(50.0, 100.0);
var rs = new Rs();
var result = rs.Update(50.0, 100.0);
Assert.Equal(0.5, result.Value, 10);
}
[Fact]
public void Update_EqualValues_ReturnsOne()
{
var prs = new Prs();
var result = prs.Update(100.0, 100.0);
var rs = new Rs();
var result = rs.Update(100.0, 100.0);
Assert.Equal(1.0, result.Value, 10);
}
[Fact]
public void Update_DivisionByZero_ReturnsNaN()
{
var prs = new Prs();
var result = prs.Update(100.0, 0.0);
var rs = new Rs();
var result = rs.Update(100.0, 0.0);
Assert.True(double.IsNaN(result.Value));
}
[Fact]
public void RawRatio_MatchesUnsmoothedResult()
{
var prs = new Prs();
prs.Update(200.0, 100.0);
Assert.Equal(2.0, prs.RawRatio, 10);
Assert.Equal(prs.RawRatio, prs.Last.Value, 10);
var rs = new Rs();
rs.Update(200.0, 100.0);
Assert.Equal(2.0, rs.RawRatio, 10);
Assert.Equal(rs.RawRatio, rs.Last.Value, 10);
}
// ==================== SMOOTHING ====================
[Fact]
public void Update_WithSmoothing_SmoothsRatio()
{
var prs = new Prs(5);
var rs = new Rs(5);
var results = new List<double>();
// Feed increasing prices with 2:1 base ratio
for (int i = 0; i < 20; i++)
{
double basePrice = 200.0 + i;
double compPrice = 100.0 + (i * 0.5);
prs.Update(basePrice, compPrice);
results.Add(prs.Last.Value);
double compPrice = 100.0 + i * 0.5;
rs.Update(basePrice, compPrice);
results.Add(rs.Last.Value);
}
// After warmup, smoothed values should be less volatile
Assert.True(prs.IsHot);
Assert.True(rs.IsHot);
Assert.True(results[^1] > 1.5); // Base is outperforming
}
[Fact]
public void Update_SmoothedVsRaw_DifferAfterMultipleUpdates()
{
var prsRaw = new Prs(1);
var prsSmoothed = new Prs(10);
var prsRaw = new Rs(1);
var prsSmoothed = new Rs(10);
var baseBars = new GBM(seed: 42).Fetch(30, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
var compBars = new GBM(seed: 123).Fetch(30, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
@@ -120,36 +120,36 @@ public class PrsTests
[Fact]
public void IsHot_BeforeWarmup_ReturnsFalse()
{
var prs = new Prs(10);
prs.Update(100.0, 50.0);
Assert.False(prs.IsHot);
var rs = new Rs(10);
rs.Update(100.0, 50.0);
Assert.False(rs.IsHot);
}
[Fact]
public void IsHot_AfterWarmup_ReturnsTrue()
{
var prs = new Prs(5);
var rs = new Rs(5);
for (int i = 0; i < 10; i++)
{
prs.Update(100.0 + i, 50.0 + i);
rs.Update(100.0 + i, 50.0 + i);
}
Assert.True(prs.IsHot);
Assert.True(rs.IsHot);
}
[Fact]
public void IsHot_NoSmoothing_TrueImmediately()
{
var prs = new Prs(1);
prs.Update(100.0, 50.0);
Assert.True(prs.IsHot);
var rs = new Rs(1);
rs.Update(100.0, 50.0);
Assert.True(rs.IsHot);
}
// ==================== BAR CORRECTION ====================
[Fact]
public void Update_BarCorrection_RestoresState()
{
var prs1 = new Prs(5);
var prs2 = new Prs(5);
var prs1 = new Rs(5);
var prs2 = new Rs(5);
// Feed same initial data
for (int i = 0; i < 10; i++)
@@ -172,42 +172,42 @@ public class PrsTests
[Fact]
public void Update_MultipleCorrections_FinalValueCorrect()
{
var prs = new Prs(3);
var rs = new Rs(3);
for (int i = 0; i < 5; i++)
{
prs.Update(100.0 + i, 50.0 + i);
rs.Update(100.0 + i, 50.0 + i);
}
// Add bar
prs.Update(110.0, 55.0, true);
rs.Update(110.0, 55.0, true);
// Multiple corrections
prs.Update(115.0, 60.0, false);
prs.Update(120.0, 65.0, false);
prs.Update(110.0, 55.0, false); // Back to original
rs.Update(115.0, 60.0, false);
rs.Update(120.0, 65.0, false);
rs.Update(110.0, 55.0, false); // Back to original
Assert.True(double.IsFinite(prs.Last.Value));
Assert.True(double.IsFinite(rs.Last.Value));
}
// ==================== RESET ====================
[Fact]
public void Reset_ClearsState()
{
var prs = new Prs(5);
var rs = new Rs(5);
for (int i = 0; i < 10; i++)
{
prs.Update(100.0 + i, 50.0 + i);
rs.Update(100.0 + i, 50.0 + i);
}
Assert.NotEqual(default, prs.Last);
Assert.True(prs.IsHot);
Assert.NotEqual(default, rs.Last);
Assert.True(rs.IsHot);
prs.Reset();
rs.Reset();
Assert.Equal(default, prs.Last);
Assert.False(prs.IsHot);
Assert.Equal(default, rs.Last);
Assert.False(rs.IsHot);
}
// ==================== STATIC CALCULATE ====================
@@ -223,7 +223,7 @@ public class PrsTests
compSeries.Add(new TValue(DateTime.Now.AddMinutes(i), 50.0 + i));
}
var result = Prs.Batch(baseSeries, compSeries, 5);
var result = Rs.Batch(baseSeries, compSeries, 5);
Assert.Equal(baseSeries.Count, result.Count);
}
@@ -243,7 +243,7 @@ public class PrsTests
compSeries.Add(new TValue(DateTime.Now.AddMinutes(i), 50.0 + i));
}
Assert.Throws<ArgumentException>(() => Prs.Batch(baseSeries, compSeries));
Assert.Throws<ArgumentException>(() => Rs.Batch(baseSeries, compSeries));
}
[Fact]
@@ -253,7 +253,7 @@ public class PrsTests
double[] compArr = new double[5];
double[] output = new double[10];
Assert.Throws<ArgumentException>(() => Prs.Batch(baseArr, compArr, output));
Assert.Throws<ArgumentException>(() => Rs.Batch(baseArr, compArr, output));
}
[Fact]
@@ -263,7 +263,7 @@ public class PrsTests
double[] compArr = new double[10];
double[] output = new double[5];
Assert.Throws<ArgumentException>(() => Prs.Batch(baseArr, compArr, output));
Assert.Throws<ArgumentException>(() => Rs.Batch(baseArr, compArr, output));
}
[Fact]
@@ -273,19 +273,19 @@ public class PrsTests
double[] compArr = new double[10];
double[] output = new double[10];
Assert.Throws<ArgumentException>(() => Prs.Batch(baseArr, compArr, output, 0));
Assert.Throws<ArgumentException>(() => Rs.Batch(baseArr, compArr, output, 0));
}
// ==================== EDGE CASES ====================
[Fact]
public void Update_NaNInput_UsesLastValidValue()
{
var prs = new Prs();
var rs = new Rs();
prs.Update(100.0, 50.0);
_ = prs.Last.Value;
rs.Update(100.0, 50.0);
_ = rs.Last.Value;
var result = prs.Update(double.NaN, double.NaN);
var result = rs.Update(double.NaN, double.NaN);
// Should use last valid values (100/50 pattern OR fallback)
Assert.True(double.IsFinite(result.Value));
}
@@ -293,19 +293,19 @@ public class PrsTests
[Fact]
public void Update_InfinityInput_UsesLastValidValue()
{
var prs = new Prs();
var rs = new Rs();
prs.Update(100.0, 50.0);
rs.Update(100.0, 50.0);
var result = prs.Update(double.PositiveInfinity, 50.0);
var result = rs.Update(double.PositiveInfinity, 50.0);
Assert.True(double.IsFinite(result.Value));
}
[Fact]
public void Update_VerySmallComparison_HandlesCorrectly()
{
var prs = new Prs();
var result = prs.Update(100.0, 1e-15);
var rs = new Rs();
var result = rs.Update(100.0, 1e-15);
// Values below epsilon (1e-10) are treated as zero -> returns NaN
Assert.True(double.IsNaN(result.Value));
@@ -314,10 +314,10 @@ public class PrsTests
[Fact]
public void Update_NegativeValues_HandlesCorrectly()
{
var prs = new Prs();
var rs = new Rs();
// Negative values (like P&L or temperature)
var result = prs.Update(-50.0, -25.0);
var result = rs.Update(-50.0, -25.0);
Assert.Equal(2.0, result.Value, 10);
}
@@ -325,104 +325,104 @@ public class PrsTests
[Fact]
public void Prime_Single_ThrowsNotSupported()
{
var prs = new Prs(5);
var rs = new Rs(5);
double[] data = [100, 101, 102, 103, 104];
Assert.Throws<NotSupportedException>(() => prs.Prime(data));
Assert.Throws<NotSupportedException>(() => rs.Prime(data));
}
[Fact]
public void Prime_Dual_InitializesState()
{
var prs = new Prs(3);
var rs = new Rs(3);
double[] baseData = [100, 102, 104, 106, 108, 110];
double[] compData = [50, 51, 52, 53, 54, 55];
prs.Prime(baseData, compData);
rs.Prime(baseData, compData);
Assert.NotEqual(default, prs.Last);
Assert.NotEqual(default, rs.Last);
}
[Fact]
public void Prime_MismatchedLengths_ThrowsException()
{
var prs = new Prs(3);
var rs = new Rs(3);
double[] baseData = [100, 102, 104];
double[] compData = [50, 51];
Assert.Throws<ArgumentException>(() => prs.Prime(baseData, compData));
Assert.Throws<ArgumentException>(() => rs.Prime(baseData, compData));
}
// ==================== NOT SUPPORTED ====================
[Fact]
public void Update_SingleInput_ThrowsNotSupported()
{
var prs = new Prs();
var rs = new Rs();
var input = new TValue(DateTime.Now, 100.0);
Assert.Throws<NotSupportedException>(() => prs.Update(input));
Assert.Throws<NotSupportedException>(() => rs.Update(input));
}
[Fact]
public void Update_TSeries_ThrowsNotSupported()
{
var prs = new Prs();
var rs = new Rs();
var source = new TSeries();
source.Add(new TValue(DateTime.Now, 100.0));
Assert.Throws<NotSupportedException>(() => prs.Update(source));
Assert.Throws<NotSupportedException>(() => rs.Update(source));
}
// ==================== PERFORMANCE SCENARIOS ====================
[Fact]
public void Update_OutperformanceScenario_IncreasingRatio()
{
var prs = new Prs(5);
var rs = new Rs(5);
// Base grows faster than comparison
for (int i = 0; i < 20; i++)
{
double basePrice = 100.0 + (i * 2); // +2 per bar
double compPrice = 100.0 + (i * 1); // +1 per bar
prs.Update(basePrice, compPrice);
double basePrice = 100.0 + i * 2; // +2 per bar
double compPrice = 100.0 + i * 1; // +1 per bar
rs.Update(basePrice, compPrice);
}
// Ratio should be increasing (base outperforming)
Assert.True(prs.Last.Value > 1.0);
Assert.True(rs.Last.Value > 1.0);
}
[Fact]
public void Update_UnderperformanceScenario_DecreasingRatio()
{
var prs = new Prs(5);
var rs = new Rs(5);
// Base grows slower than comparison
for (int i = 0; i < 20; i++)
{
double basePrice = 100.0 + (i * 1); // +1 per bar
double compPrice = 100.0 + (i * 2); // +2 per bar
prs.Update(basePrice, compPrice);
double basePrice = 100.0 + i * 1; // +1 per bar
double compPrice = 100.0 + i * 2; // +2 per bar
rs.Update(basePrice, compPrice);
}
// Ratio should be decreasing (base underperforming)
Assert.True(prs.Last.Value < 1.0);
Assert.True(rs.Last.Value < 1.0);
}
[Fact]
public void Update_ParallelMovement_StableRatio()
{
var prs = new Prs(5);
var rs = new Rs(5);
// Both grow at same rate
for (int i = 0; i < 20; i++)
{
double basePrice = 100.0 + (i * 2);
double compPrice = 50.0 + (i * 1);
prs.Update(basePrice, compPrice);
double basePrice = 100.0 + i * 2;
double compPrice = 50.0 + i * 1;
rs.Update(basePrice, compPrice);
}
// Initial ratio was 2.0, should stay around there
Assert.InRange(prs.Last.Value, 1.9, 2.1);
Assert.InRange(rs.Last.Value, 1.9, 2.1);
}
// ==================== BATCH CALCULATION ====================
@@ -435,14 +435,14 @@ public class PrsTests
var compSeries = compBars.Close;
// Batch calculation
var batchResult = Prs.Batch(baseSeries, compSeries, 5);
var batchResult = Rs.Batch(baseSeries, compSeries, 5);
// Streaming calculation
var prs = new Prs(5);
var rs = new Rs(5);
var streamingResults = new List<double>();
for (int i = 0; i < baseSeries.Count; i++)
{
streamingResults.Add(prs.Update(baseSeries[i], compSeries[i], true).Value);
streamingResults.Add(rs.Update(baseSeries[i], compSeries[i], true).Value);
}
// Compare
@@ -4,19 +4,19 @@ using Xunit;
namespace QuanTAlib.Tests;
/// <summary>
/// Validation tests for PRS (Price Relative Strength) indicator.
/// PRS compares relative performance between two assets via their price ratio.
/// Note: PRS is a unique indicator without direct equivalents in TA-Lib, Skender, etc.
/// Validation tests for RS (Price Relative Strength) indicator.
/// RS compares relative performance between two assets via their price ratio.
/// Note: RS is a unique indicator without direct equivalents in TA-Lib, Skender, etc.
/// These tests validate mathematical correctness and expected behavior.
/// </summary>
public class PrsValidationTests
public class RsValidationTests
{
#region Mathematical Validation
[Fact]
public void Prs_ManualCalculation_MatchesExpected()
public void Rs_ManualCalculation_MatchesExpected()
{
var prs = new Prs(1); // No smoothing
var rs = new Rs(1); // No smoothing
var time = DateTime.UtcNow;
// Test data: base and comparison prices
@@ -26,67 +26,67 @@ public class PrsValidationTests
// Expected: ratios should be 1.0, 1.05, 1.10, 1.15, 1.20
for (int i = 0; i < baseValues.Length; i++)
{
var result = prs.Update(
var result = rs.Update(
new TValue(time.AddSeconds(i), baseValues[i]),
new TValue(time.AddSeconds(i), compValues[i]),
true);
double expected = baseValues[i] / compValues[i];
Assert.Equal(expected, result.Value, 10);
Assert.Equal(expected, prs.RawRatio, 10);
Assert.Equal(expected, rs.RawRatio, 10);
}
}
[Fact]
public void Prs_EqualPrices_ReturnsOne()
public void Rs_EqualPrices_ReturnsOne()
{
var prs = new Prs(1);
var rs = new Rs(1);
var result = prs.Update(50.0, 50.0, true);
var result = rs.Update(50.0, 50.0, true);
Assert.Equal(1.0, result.Value, 10);
}
[Fact]
public void Prs_BaseHigherThanComp_ReturnsGreaterThanOne()
public void Rs_BaseHigherThanComp_ReturnsGreaterThanOne()
{
var prs = new Prs(1);
var rs = new Rs(1);
var result = prs.Update(120.0, 100.0, true);
var result = rs.Update(120.0, 100.0, true);
Assert.Equal(1.2, result.Value, 10);
Assert.True(result.Value > 1.0);
}
[Fact]
public void Prs_BaseLowerThanComp_ReturnsLessThanOne()
public void Rs_BaseLowerThanComp_ReturnsLessThanOne()
{
var prs = new Prs(1);
var rs = new Rs(1);
var result = prs.Update(80.0, 100.0, true);
var result = rs.Update(80.0, 100.0, true);
Assert.Equal(0.8, result.Value, 10);
Assert.True(result.Value < 1.0);
}
[Fact]
public void Prs_DivisionByZero_ReturnsNaN()
public void Rs_DivisionByZero_ReturnsNaN()
{
var prs = new Prs(1);
var rs = new Rs(1);
var result = prs.Update(100.0, 0.0, true);
var result = rs.Update(100.0, 0.0, true);
Assert.True(double.IsNaN(result.Value));
Assert.True(double.IsNaN(prs.RawRatio));
Assert.True(double.IsNaN(rs.RawRatio));
}
[Fact]
public void Prs_VerySmallDenominator_ReturnsNaN()
public void Rs_VerySmallDenominator_ReturnsNaN()
{
var prs = new Prs(1);
var rs = new Rs(1);
// Value smaller than epsilon (1e-10) should be treated as zero
var result = prs.Update(100.0, 1e-11, true);
var result = rs.Update(100.0, 1e-11, true);
Assert.True(double.IsNaN(result.Value));
}
@@ -96,32 +96,32 @@ public class PrsValidationTests
#region Smoothing Validation
[Fact]
public void Prs_SmoothedFirstValue_EqualsRawRatio()
public void Rs_SmoothedFirstValue_EqualsRawRatio()
{
var prs = new Prs(10);
var rs = new Rs(10);
var time = DateTime.UtcNow;
var result = prs.Update(
var result = rs.Update(
new TValue(time, 100.0),
new TValue(time, 50.0),
true);
// First value should equal raw ratio
Assert.Equal(prs.RawRatio, result.Value, 10);
Assert.Equal(rs.RawRatio, result.Value, 10);
Assert.Equal(2.0, result.Value, 10);
}
[Fact]
public void Prs_SmoothedConvergesToRatio_WhenConstant()
public void Rs_SmoothedConvergesToRatio_WhenConstant()
{
var prs = new Prs(5);
var rs = new Rs(5);
var time = DateTime.UtcNow;
// Feed constant ratio (100/50 = 2.0) repeatedly
TValue result = default;
for (int i = 0; i < 50; i++)
{
result = prs.Update(
result = rs.Update(
new TValue(time.AddSeconds(i), 100.0),
new TValue(time.AddSeconds(i), 50.0),
true);
@@ -132,9 +132,9 @@ public class PrsValidationTests
}
[Fact]
public void Prs_NoSmoothing_RawRatioEqualsSmoothed()
public void Rs_NoSmoothing_RawRatioEqualsSmoothed()
{
var prs = new Prs(1); // No smoothing
var rs = new Rs(1); // No smoothing
var values = new (double b, double c)[]
{
@@ -146,16 +146,16 @@ public class PrsValidationTests
foreach (var (b, c) in values)
{
var result = prs.Update(b, c, true);
Assert.Equal(prs.RawRatio, result.Value, 10);
var result = rs.Update(b, c, true);
Assert.Equal(rs.RawRatio, result.Value, 10);
}
}
[Fact]
public void Prs_SmoothingReducesVolatility()
public void Rs_SmoothingReducesVolatility()
{
var prsNoSmooth = new Prs(1);
var prsSmooth = new Prs(10);
var prsNoSmooth = new Rs(1);
var prsSmooth = new Rs(10);
// Create volatile ratio series
var baseVals = new double[] { 100, 120, 80, 130, 70, 140, 60, 150 };
@@ -191,9 +191,9 @@ public class PrsValidationTests
#region Trend Interpretation
[Fact]
public void Prs_IncreasingRatio_IndicatesOutperformance()
public void Rs_IncreasingRatio_IndicatesOutperformance()
{
var prs = new Prs(1);
var rs = new Rs(1);
// Base outperforms: grows faster than comparison
var results = new List<double>();
@@ -202,7 +202,7 @@ public class PrsValidationTests
{
double basePrice = 100 + (i * 5); // 100, 105, 110...
double compPrice = 100 + (i * 2); // 100, 102, 104...
var result = prs.Update(basePrice, compPrice, true);
var result = rs.Update(basePrice, compPrice, true);
results.Add(result.Value);
}
@@ -215,9 +215,9 @@ public class PrsValidationTests
}
[Fact]
public void Prs_DecreasingRatio_IndicatesUnderperformance()
public void Rs_DecreasingRatio_IndicatesUnderperformance()
{
var prs = new Prs(1);
var rs = new Rs(1);
// Base underperforms: grows slower than comparison
var results = new List<double>();
@@ -226,7 +226,7 @@ public class PrsValidationTests
{
double basePrice = 100 + (i * 2); // 100, 102, 104...
double compPrice = 100 + (i * 5); // 100, 105, 110...
var result = prs.Update(basePrice, compPrice, true);
var result = rs.Update(basePrice, compPrice, true);
results.Add(result.Value);
}
@@ -239,9 +239,9 @@ public class PrsValidationTests
}
[Fact]
public void Prs_SameGrowthRate_ConstantRatio()
public void Rs_SameGrowthRate_ConstantRatio()
{
var prs = new Prs(1);
var rs = new Rs(1);
// Both grow at same rate - ratio stays constant at 2.0
var results = new List<double>();
@@ -250,7 +250,7 @@ public class PrsValidationTests
{
double basePrice = 100 * (1 + (i * 0.05)); // 5% growth
double compPrice = 50 * (1 + (i * 0.05)); // 5% growth
var result = prs.Update(basePrice, compPrice, true);
var result = rs.Update(basePrice, compPrice, true);
results.Add(result.Value);
}
@@ -266,57 +266,57 @@ public class PrsValidationTests
#region Edge Cases and Robustness
[Fact]
public void Prs_NegativeValues_HandlesCorrectly()
public void Rs_NegativeValues_HandlesCorrectly()
{
var prs = new Prs(1);
var rs = new Rs(1);
// While unusual, PRS should handle negative values mathematically
var result = prs.Update(-100.0, -50.0, true);
// While unusual, RS should handle negative values mathematically
var result = rs.Update(-100.0, -50.0, true);
Assert.Equal(2.0, result.Value, 10); // -100/-50 = 2.0
}
[Fact]
public void Prs_MixedSigns_HandlesCorrectly()
public void Rs_MixedSigns_HandlesCorrectly()
{
var prs = new Prs(1);
var rs = new Rs(1);
// Base positive, comp negative
var result = prs.Update(100.0, -50.0, true);
var result = rs.Update(100.0, -50.0, true);
Assert.Equal(-2.0, result.Value, 10);
}
[Fact]
public void Prs_VeryLargeValues_MaintainsPrecision()
public void Rs_VeryLargeValues_MaintainsPrecision()
{
var prs = new Prs(1);
var rs = new Rs(1);
var result = prs.Update(1e15, 1e14, true);
var result = rs.Update(1e15, 1e14, true);
// 1e15 / 1e14 = 10
Assert.Equal(10.0, result.Value, 6);
}
[Fact]
public void Prs_VerySmallValues_MaintainsPrecision()
public void Rs_VerySmallValues_MaintainsPrecision()
{
var prs = new Prs(1);
var rs = new Rs(1);
var result = prs.Update(1e-5, 1e-6, true);
var result = rs.Update(1e-5, 1e-6, true);
// 1e-5 / 1e-6 = 10
Assert.Equal(10.0, result.Value, 6);
}
[Fact]
public void Prs_NaNBase_PropagatesNaN()
public void Rs_NaNBase_PropagatesNaN()
{
var prs = new Prs(1);
var rs = new Rs(1);
// Should fallback to last valid or 0, resulting in 0/comp
prs.Update(100.0, 50.0, true); // First valid value
var result = prs.Update(double.NaN, 50.0, true);
rs.Update(100.0, 50.0, true); // First valid value
var result = rs.Update(double.NaN, 50.0, true);
// NaN base with valid comparison should use fallback (previous: 100) or 0
// Result will depend on sanitization logic - actual behavior uses last valid
@@ -324,24 +324,24 @@ public class PrsValidationTests
}
[Fact]
public void Prs_NaNComp_PropagatesNaN()
public void Rs_NaNComp_PropagatesNaN()
{
var prs = new Prs(1);
var rs = new Rs(1);
prs.Update(100.0, 50.0, true); // First valid value
var result = prs.Update(100.0, double.NaN, true);
rs.Update(100.0, 50.0, true); // First valid value
var result = rs.Update(100.0, double.NaN, true);
// NaN comp should use fallback (previous: 50) -> 100/50 = 2.0
Assert.Equal(2.0, result.Value, 6);
}
[Fact]
public void Prs_InfinityBase_HandledGracefully()
public void Rs_InfinityBase_HandledGracefully()
{
var prs = new Prs(1);
var rs = new Rs(1);
prs.Update(100.0, 50.0, true); // First valid
var result = prs.Update(double.PositiveInfinity, 50.0, true);
rs.Update(100.0, 50.0, true); // First valid
var result = rs.Update(double.PositiveInfinity, 50.0, true);
// Should fallback to last valid (100/50 = 2.0)
Assert.Equal(2.0, result.Value, 6);
@@ -352,10 +352,10 @@ public class PrsValidationTests
#region Batch Calculation Validation
[Fact]
public void Prs_BatchCalculate_MatchesStreaming()
public void Rs_BatchCalculate_MatchesStreaming()
{
int smoothPeriod = 5;
var prsStream = new Prs(smoothPeriod);
var prsStream = new Rs(smoothPeriod);
var baseValues = new double[] { 100, 105, 110, 108, 115, 120, 118, 125, 130, 128 };
var compValues = new double[] { 100, 100, 100, 100, 100, 100, 100, 100, 100, 100 };
@@ -369,7 +369,7 @@ public class PrsValidationTests
// Batch calculation
var batchOutput = new double[baseValues.Length];
Prs.Batch(baseValues, compValues, batchOutput, smoothPeriod);
Rs.Batch(baseValues, compValues, batchOutput, smoothPeriod);
// Results should match
for (int i = 0; i < baseValues.Length; i++)
@@ -379,10 +379,10 @@ public class PrsValidationTests
}
[Fact]
public void Prs_TSeriesCalculate_MatchesStreaming()
public void Rs_TSeriesCalculate_MatchesStreaming()
{
int smoothPeriod = 3;
var prsStream = new Prs(smoothPeriod);
var prsStream = new Rs(smoothPeriod);
var time = DateTime.UtcNow;
var baseSeries = new TSeries(10);
@@ -405,7 +405,7 @@ public class PrsValidationTests
}
// TSeries batch
var batchResult = Prs.Batch(baseSeries, compSeries, smoothPeriod);
var batchResult = Rs.Batch(baseSeries, compSeries, smoothPeriod);
for (int i = 0; i < baseValues.Length; i++)
{
@@ -418,60 +418,60 @@ public class PrsValidationTests
#region Properties and State
[Fact]
public void Prs_IsHot_BecomesTrue_AfterSmoothPeriod()
public void Rs_IsHot_BecomesTrue_AfterSmoothPeriod()
{
var prs = new Prs(5);
var rs = new Rs(5);
for (int i = 0; i < 10; i++)
{
prs.Update(100 + i, 100.0, true);
rs.Update(100 + i, 100.0, true);
if (i < 4) // 0-4 = first 5 values
{
Assert.False(prs.IsHot, $"Should not be hot at index {i}");
Assert.False(rs.IsHot, $"Should not be hot at index {i}");
}
else
{
Assert.True(prs.IsHot, $"Should be hot at index {i}");
Assert.True(rs.IsHot, $"Should be hot at index {i}");
}
}
}
[Fact]
public void Prs_Reset_ClearsState()
public void Rs_Reset_ClearsState()
{
var prs = new Prs(5);
var rs = new Rs(5);
// Add values
for (int i = 0; i < 10; i++)
{
prs.Update(100 + i, 50.0, true);
rs.Update(100 + i, 50.0, true);
}
Assert.True(prs.IsHot);
Assert.True(prs.RawRatio > 0);
Assert.True(rs.IsHot);
Assert.True(rs.RawRatio > 0);
// Reset
prs.Reset();
rs.Reset();
Assert.False(prs.IsHot);
Assert.Equal(0.0, prs.RawRatio);
Assert.Equal(default(TValue), prs.Last);
Assert.False(rs.IsHot);
Assert.Equal(0.0, rs.RawRatio);
Assert.Equal(default(TValue), rs.Last);
}
[Fact]
public void Prs_Prime_InitializesState()
public void Rs_Prime_InitializesState()
{
var prs = new Prs(5);
var rs = new Rs(5);
var baseSource = new double[] { 100, 105, 110, 115, 120, 125, 130 };
var compSource = new double[] { 100, 100, 100, 100, 100, 100, 100 };
prs.Prime(baseSource, compSource);
rs.Prime(baseSource, compSource);
Assert.True(prs.IsHot);
Assert.Equal(1.30, prs.RawRatio, 10);
Assert.Equal(130.0 / 100.0, prs.RawRatio, 10);
Assert.True(rs.IsHot);
Assert.Equal(1.30, rs.RawRatio, 10);
Assert.Equal(130.0 / 100.0, rs.RawRatio, 10);
}
#endregion
@@ -479,29 +479,29 @@ public class PrsValidationTests
#region Performance Properties
[Fact]
public void Prs_SmoothPeriod_ExposesCorrectValue()
public void Rs_SmoothPeriod_ExposesCorrectValue()
{
var prs = new Prs(14);
var rs = new Rs(14);
Assert.Equal(14, prs.SmoothPeriod);
Assert.Equal(14, rs.SmoothPeriod);
}
[Fact]
public void Prs_WarmupPeriod_EqualsSmoothPeriod()
public void Rs_WarmupPeriod_EqualsSmoothPeriod()
{
var prs = new Prs(20);
var rs = new Rs(20);
Assert.Equal(20, prs.WarmupPeriod);
Assert.Equal(20, rs.WarmupPeriod);
}
[Fact]
public void Prs_Name_IncludesPeriodIfSmoothed()
public void Rs_Name_IncludesPeriodIfSmoothed()
{
var prsNoSmooth = new Prs(1);
var prsSmooth = new Prs(14);
var prsNoSmooth = new Rs(1);
var prsSmooth = new Rs(14);
Assert.Equal("Prs", prsNoSmooth.Name);
Assert.Equal("Prs(14)", prsSmooth.Name);
Assert.Equal("Rs", prsNoSmooth.Name);
Assert.Equal("Rs(14)", prsSmooth.Name);
}
#endregion
@@ -511,11 +511,11 @@ public class PrsValidationTests
/// <summary>
/// Structural validation against Skender <c>GetPrs</c>.
/// Skender PRS computes price ratio between two quote series.
/// QuanTAlib PRS also computes base/comparison ratio with optional smoothing.
/// QuanTAlib RS also computes base/comparison ratio with optional smoothing.
/// With period=1 (no smoothing), raw ratios should match exactly.
/// </summary>
[Fact]
public void Validate_Skender_Prs_Streaming()
public void Validate_Skender_Rs_Streaming()
{
using var evalData = new ValidationTestData();
// Create a second quote series for comparison (different seed)
@@ -531,27 +531,27 @@ public class PrsValidationTests
Volume = (decimal)b.Volume
}).ToList();
// QuanTAlib PRS (streaming, no smoothing)
var prs = new Prs(1);
// QuanTAlib RS (streaming, no smoothing)
var rs = new Rs(1);
var qResults = new List<double>();
for (int i = 0; i < evalData.Bars.Count; i++)
{
double evalClose = evalData.Bars[i].Close;
double baseClose = baseBars[i].Close;
qResults.Add(prs.Update(evalClose, baseClose, true).Value);
qResults.Add(rs.Update(evalClose, baseClose, true).Value);
}
// Skender PRS: quotesEval.GetPrs(quotesBase)
var sResult = evalData.SkenderQuotes.GetPrs(baseQuotes).ToList();
// Cross-validate: raw PRS ratio (no smoothing)
// Cross-validate: raw RS ratio (no smoothing)
ValidationHelper.VerifyData(qResults, sResult, s => s.Prs, tolerance: ValidationHelper.SkenderTolerance);
}
[Fact]
public void Prs_Correction_Recomputes()
public void Rs_Correction_Recomputes()
{
var ind = new Prs(smoothPeriod: 5);
var ind = new Rs(smoothPeriod: 5);
// Build state well past warmup
for (int i = 0; i < 50; i++)
+1 -1
View File
@@ -19,7 +19,7 @@ Oscillators fluctuate above and below a centerline or within bounded ranges. Use
| [DEM](dem/Dem.md) | DeMarker Oscillator | Bounded 0-1 oscillator comparing sequential highs and lows. |
| [DOSC](dosc/Dosc.md) | Derivative Oscillator | Double-smoothed RSI minus signal line. Momentum acceleration. |
| [DPO](dpo/Dpo.md) | Detrended Price Oscillator | Removes trend via displaced SMA. Reveals cycles. |
| [DYMOI](dymoi/Dymoi.md) | Dynamic Momentum Index | RSI with volatility-adaptive period. Shorter in volatile markets. |
| [DYMI](dymi/Dymi.md) | Dynamic Momentum Index | RSI with volatility-adaptive period. Shorter in volatile markets. |
| [ER](er/Er.md) | Efficiency Ratio | Measures directional efficiency. Net movement / total path length. |
| [ERI](eri/Eri.md) | Elder Ray Index | Separates bull and bear power relative to EMA. |
| [FI](fi/Fi.md) | Force Index | Combines price change, direction, and volume to measure buying/selling power. |
@@ -5,7 +5,7 @@ using TradingPlatform.BusinessLayer;
namespace QuanTAlib;
[SkipLocalsInit]
public sealed class DymoiIndicator : Indicator, IWatchlistIndicator
public sealed class DymiIndicator : Indicator, IWatchlistIndicator
{
[InputParameter("Base RSI Period", sortIndex: 1, 2, 500, 1, 0)]
public int BasePeriod { get; set; } = 14;
@@ -28,33 +28,33 @@ public sealed class DymoiIndicator : Indicator, IWatchlistIndicator
[InputParameter("Show cold values", sortIndex: 21)]
public bool ShowColdValues { get; set; } = true;
private Dymoi _dymoi = null!;
private Dymi _dymi = null!;
private readonly LineSeries _series;
public static int MinHistoryDepths => 0;
int IWatchlistIndicator.MinHistoryDepths => MinHistoryDepths;
public override string ShortName =>
$"DYMOI ({BasePeriod},{ShortPeriod},{LongPeriod},{MinPeriod},{MaxPeriod})";
$"DYMI ({BasePeriod},{ShortPeriod},{LongPeriod},{MinPeriod},{MaxPeriod})";
public override string SourceCodeLink =>
"https://github.com/mihakralj/QuanTAlib/blob/main/lib/oscillators/dymoi/Dymoi.Quantower.cs";
"https://github.com/mihakralj/QuanTAlib/blob/main/lib/oscillators/dymi/Dymi.Quantower.cs";
public DymoiIndicator()
public DymiIndicator()
{
OnBackGround = true;
SeparateWindow = true;
Name = "DYMOI - Dynamic Momentum Index";
Name = "DYMI - Dynamic Momentum Index";
Description = "Volatility-adaptive RSI by Chande & Kroll: period shortens in volatile markets, lengthens in quiet ones.";
_series = new LineSeries("DYMOI", Color.Yellow, 2, LineStyle.Solid);
_series = new LineSeries("DYMI", Color.Yellow, 2, LineStyle.Solid);
AddLineSeries(_series);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
protected override void OnInit()
{
_dymoi = new Dymoi(BasePeriod, ShortPeriod, LongPeriod, MinPeriod, MaxPeriod);
_dymi = new Dymi(BasePeriod, ShortPeriod, LongPeriod, MinPeriod, MaxPeriod);
base.OnInit();
}
@@ -66,9 +66,9 @@ public sealed class DymoiIndicator : Indicator, IWatchlistIndicator
double price = priceSelector(item);
TValue input = new(item.TimeLeft, price);
TValue result = _dymoi.Update(input, args.IsNewBar());
TValue result = _dymi.Update(input, args.IsNewBar());
if (!_dymoi.IsHot && !ShowColdValues)
if (!_dymi.IsHot && !ShowColdValues)
{
return;
}
@@ -4,7 +4,7 @@ using System.Runtime.InteropServices;
namespace QuanTAlib;
/// <summary>
/// DYMOI: Dynamic Momentum Index
/// DYMI: Dynamic Momentum Index
/// </summary>
/// <remarks>
/// Volatility-adaptive RSI by Tushar Chande and Stanley Kroll (1994).
@@ -18,10 +18,10 @@ namespace QuanTAlib;
///
/// References:
/// Chande, T. &amp; Kroll, S. (1994). The New Technical Trader.
/// PineScript reference: dymoi.pine
/// PineScript reference: dymi.pine
/// </remarks>
[SkipLocalsInit]
public sealed class Dymoi : AbstractBase
public sealed class Dymi : AbstractBase
{
private readonly int _basePeriod;
private readonly int _shortPeriod;
@@ -57,14 +57,14 @@ public sealed class Dymoi : AbstractBase
private State _s, _ps;
/// <summary>
/// Creates DYMOI with specified parameters.
/// Creates DYMI with specified parameters.
/// </summary>
/// <param name="basePeriod">Base RSI period (must be &gt;= 2)</param>
/// <param name="shortPeriod">Short StdDev window (must be &gt;= 2)</param>
/// <param name="longPeriod">Long StdDev window (must be &gt;= 2 and &gt; shortPeriod)</param>
/// <param name="minPeriod">Minimum dynamic period (must be &gt;= 2)</param>
/// <param name="maxPeriod">Maximum dynamic period (must be &gt;= minPeriod)</param>
public Dymoi(int basePeriod = 14, int shortPeriod = 5, int longPeriod = 10,
public Dymi(int basePeriod = 14, int shortPeriod = 5, int longPeriod = 10,
int minPeriod = 3, int maxPeriod = 30)
{
if (basePeriod < 2)
@@ -106,14 +106,14 @@ public sealed class Dymoi : AbstractBase
_s = new State(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1.0, true, double.NaN, double.NaN);
_ps = _s;
Name = $"Dymoi({basePeriod},{shortPeriod},{longPeriod},{minPeriod},{maxPeriod})";
Name = $"Dymi({basePeriod},{shortPeriod},{longPeriod},{minPeriod},{maxPeriod})";
WarmupPeriod = longPeriod + maxPeriod;
}
/// <summary>
/// Creates DYMOI with event-based source chaining.
/// Creates DYMI with event-based source chaining.
/// </summary>
public Dymoi(ITValuePublisher source, int basePeriod = 14, int shortPeriod = 5,
public Dymi(ITValuePublisher source, int basePeriod = 14, int shortPeriod = 5,
int longPeriod = 10, int minPeriod = 3, int maxPeriod = 30)
: this(basePeriod, shortPeriod, longPeriod, minPeriod, maxPeriod)
{
@@ -234,7 +234,7 @@ public sealed class Dymoi : AbstractBase
}
// ── Stage 3: Wilder RMA RSI with adaptive alpha ──
double dymoi = 50.0;
double dymi = 50.0;
if (!double.IsNaN(s.PrevClose))
{
double alpha = 1.0 / dynPeriod;
@@ -253,7 +253,7 @@ public sealed class Dymoi : AbstractBase
double aG = s.AvgGain * c;
double aL = s.AvgLoss * c;
double total = aG + aL;
dymoi = total != 0.0 ? 100.0 * aG / total : 50.0;
dymi = total != 0.0 ? 100.0 * aG / total : 50.0;
if (s.E <= 1e-10)
{
s.Warmup = false;
@@ -262,15 +262,15 @@ public sealed class Dymoi : AbstractBase
else
{
double total = s.AvgGain + s.AvgLoss;
dymoi = total != 0.0 ? 100.0 * s.AvgGain / total : 50.0;
dymi = total != 0.0 ? 100.0 * s.AvgGain / total : 50.0;
}
}
s.PrevClose = value;
_s = s;
dymoi = Math.Max(0.0, Math.Min(100.0, dymoi));
Last = new TValue(input.Time, dymoi);
dymi = Math.Max(0.0, Math.Min(100.0, dymi));
Last = new TValue(input.Time, dymi);
PubEvent(Last, isNew);
return Last;
}
@@ -322,8 +322,8 @@ public sealed class Dymoi : AbstractBase
public static TSeries Batch(TSeries source, int basePeriod = 14, int shortPeriod = 5,
int longPeriod = 10, int minPeriod = 3, int maxPeriod = 30)
{
var dymoi = new Dymoi(basePeriod, shortPeriod, longPeriod, minPeriod, maxPeriod);
return dymoi.Update(source);
var dymi = new Dymi(basePeriod, shortPeriod, longPeriod, minPeriod, maxPeriod);
return dymi.Update(source);
}
/// <summary>
@@ -480,7 +480,7 @@ public sealed class Dymoi : AbstractBase
}
// Wilder RMA RSI
double dymoi = 50.0;
double dymi = 50.0;
if (!double.IsNaN(prevClose))
{
double alpha = 1.0 / dynPeriod;
@@ -499,7 +499,7 @@ public sealed class Dymoi : AbstractBase
double aG = avgGain * c;
double aL = avgLoss * c;
double total = aG + aL;
dymoi = total != 0.0 ? 100.0 * aG / total : 50.0;
dymi = total != 0.0 ? 100.0 * aG / total : 50.0;
if (e <= 1e-10)
{
warmup = false;
@@ -508,12 +508,12 @@ public sealed class Dymoi : AbstractBase
else
{
double total = avgGain + avgLoss;
dymoi = total != 0.0 ? 100.0 * avgGain / total : 50.0;
dymi = total != 0.0 ? 100.0 * avgGain / total : 50.0;
}
}
prevClose = val;
output[i] = Math.Max(0.0, Math.Min(100.0, dymoi));
output[i] = Math.Max(0.0, Math.Min(100.0, dymi));
}
}
finally
@@ -1,4 +1,4 @@
# DYMOI: Dynamic Momentum Index
# DYMI: Dynamic Momentum Index
> *The market is not a fixed-frequency oscillator. Why would you analyze it with one?*
@@ -7,20 +7,20 @@
| **Category** | Oscillator |
| **Inputs** | Source (close) |
| **Parameters** | `basePeriod` (default 14), `shortPeriod` (default 5), `longPeriod` (default 10), `minPeriod` (default 3), `maxPeriod` (default 30) |
| **Outputs** | Single series (Dymoi) |
| **Outputs** | Single series (Dymi) |
| **Output range** | Varies (see docs) |
| **Warmup** | 1 bar |
| **PineScript** | [dymoi.pine](dymoi.pine) |
| **PineScript** | [dymi.pine](dymi.pine) |
- DYMOI is a volatility-adaptive RSI: when recent price swings are large relative to longer-term swings, the RSI period shortens and the indicator be...
- DYMI is a volatility-adaptive RSI: when recent price swings are large relative to longer-term swings, the RSI period shortens and the indicator be...
- **Similar:** [RSI](../../momentum/rsi/Rsi.md), [Stoch](../stoch/Stoch.md) | **Complementary:** ATR | **Trading note:** Dynamic Momentum Index; RSI with variable lookback based on volatility. Faster in calm, slower in volatile markets.
- Validated against TA-Lib, Skender, and Tulip reference implementations where available.
DYMOI is a volatility-adaptive RSI: when recent price swings are large relative to longer-term swings, the RSI period shortens and the indicator becomes more responsive; when price action tightens, the period extends and the output smooths. The result is an oscillator that self-adjusts its sensitivity to the market's current state, avoiding both the lag of long fixed-period RSIs in trending regimes and the noise of short-period RSIs in ranging ones.
DYMI is a volatility-adaptive RSI: when recent price swings are large relative to longer-term swings, the RSI period shortens and the indicator becomes more responsive; when price action tightens, the period extends and the output smooths. The result is an oscillator that self-adjusts its sensitivity to the market's current state, avoiding both the lag of long fixed-period RSIs in trending regimes and the noise of short-period RSIs in ranging ones.
## Historical Context
Tushar Chande and Stanley Kroll introduced DYMOI in *The New Technical Trader* (1994) as a practical answer to a genuine problem: the standard RSI's fixed period is a blunt instrument. A 14-bar RSI responds identically whether the market has been oscillating ±5% per day or ±0.2%. Chande and Kroll observed that a shorter period in high-volatility environments catches reversals earlier; a longer period in quiet conditions eliminates whipsaws.
Tushar Chande and Stanley Kroll introduced DYMI in *The New Technical Trader* (1994) as a practical answer to a genuine problem: the standard RSI's fixed period is a blunt instrument. A 14-bar RSI responds identically whether the market has been oscillating ±5% per day or ±0.2%. Chande and Kroll observed that a shorter period in high-volatility environments catches reversals earlier; a longer period in quiet conditions eliminates whipsaws.
The mechanism they chose was straightforward: compute the ratio of short-term to long-term price standard deviation. When this ratio exceeds 1, the market is more volatile than its recent baseline — shorten the period. When the ratio is below 1, lengthen it. The result gets clamped to a configurable `[minPeriod, maxPeriod]` range, and a standard Wilder RSI runs on the resulting dynamic period.
@@ -32,7 +32,7 @@ The indicator has no widely adopted C# open-source implementation, which is why
| :--- | :--- | :---: | :---: |
| RSI (Wilder) | None — fixed period | 0100 | period+1 |
| CRSI (Connors) | Three-component composite, no period adaptation | 0100 | rankPeriod+rsiPeriod |
| DYMOI (Chande/Kroll) | Dual StdDev ratio drives period selection | 0100 | longPeriod+maxPeriod |
| DYMI (Chande/Kroll) | Dual StdDev ratio drives period selection | 0100 | longPeriod+maxPeriod |
| LRSI (Ehlers Laguerre) | Cycle-adaptive Laguerre filter stages | 01 | 4 |
## Architecture & Physics
@@ -115,7 +115,7 @@ $$n_t = \operatorname{clamp}\!\left(\left\lfloor \frac{n_{\text{base}}}{V_t} + 0
$$\overline{G}_t = \alpha_t \cdot G_t + (1 - \alpha_t) \cdot \overline{G}_{t-1}$$
$$\text{DYMOI}_t = 100 \cdot \frac{\overline{G}_t}{\overline{G}_t + \overline{L}_t}$$
$$\text{DYMI}_t = 100 \cdot \frac{\overline{G}_t}{\overline{G}_t + \overline{L}_t}$$
### Degenerate Cases
@@ -131,7 +131,7 @@ $$\text{DYMOI}_t = 100 \cdot \frac{\overline{G}_t}{\overline{G}_t + \overline{L}
### Operation Count (Streaming Mode)
DYMOI computes a dynamic momentum oscillator using an EMA-smoothed velocity + acceleration blend.
DYMI computes a dynamic momentum oscillator using an EMA-smoothed velocity + acceleration blend.
| Operation | Count | Cost (cycles) | Subtotal |
| :--- | :---: | :---: | :---: |
@@ -181,7 +181,7 @@ SIMD is not applicable to the streaming `Update` path because the period changes
## Validation
No external C# library (Skender, TA-Lib, Tulip, Ooples) implements DYMOI. Validation is self-consistency only.
No external C# library (Skender, TA-Lib, Tulip, Ooples) implements DYMI. Validation is self-consistency only.
| Test | Method | Tolerance | Result |
| :--- | :--- | :---: | :--- |
@@ -193,7 +193,7 @@ No external C# library (Skender, TA-Lib, Tulip, Ooples) implements DYMOI. Valida
| Fixed period identity | minPeriod=maxPeriod=basePeriod | 1e-9 | Pass |
| Determinism | Two identical GBM seeds | 1e-10 | Pass |
**Mathematical identity test:** When `minPeriod == maxPeriod == basePeriod`, the dynamic period is always fixed at `basePeriod` regardless of the volatility ratio. Under this constraint, DYMOI produces output numerically identical to `Rsi(basePeriod)` (verified at tolerance 1e-9).
**Mathematical identity test:** When `minPeriod == maxPeriod == basePeriod`, the dynamic period is always fixed at `basePeriod` regardless of the volatility ratio. Under this constraint, DYMI produces output numerically identical to `Rsi(basePeriod)` (verified at tolerance 1e-9).
## Common Pitfalls
@@ -201,9 +201,9 @@ No external C# library (Skender, TA-Lib, Tulip, Ooples) implements DYMOI. Valida
2. **Zero-variance series (flat price)**: When `σ_long = 0`, the ratio is undefined; the implementation defaults to `V = 1``n_dyn = n_base`. This is correct — a flat series should produce neutral RSI(=50) at the base period rate, not a degenerate output.
3. **Warmup period misinterpretation**: `WarmupPeriod = longPeriod + maxPeriod`. The dominant warmup is the Wilder RMA, which takes `maxPeriod` bars to settle after the long StdDev window fills. Using DYMOI output before `IsHot = true` will produce compensated but less accurate values.
3. **Warmup period misinterpretation**: `WarmupPeriod = longPeriod + maxPeriod`. The dominant warmup is the Wilder RMA, which takes `maxPeriod` bars to settle after the long StdDev window fills. Using DYMI output before `IsHot = true` will produce compensated but less accurate values.
4. **Period clamp masking pathology**: If `minPeriod` and `maxPeriod` are very close (e.g., both 14), the adaptive behavior is effectively disabled and DYMOI degenerates to standard RSI. This is a valid use case but should be intentional.
4. **Period clamp masking pathology**: If `minPeriod` and `maxPeriod` are very close (e.g., both 14), the adaptive behavior is effectively disabled and DYMI degenerates to standard RSI. This is a valid use case but should be intentional.
5. **Floating-point drift in running sums**: The O(1) variance formula $E[x^2] - E[x]^2$ is numerically unstable for large values or large windows — specifically, catastrophic cancellation can occur. For price data in the range [0.01, 100000] and periods ≤ 100, drift is negligible in practice. For exotic inputs, a periodic full-recalculation reset (every N steps) would be appropriate; the current implementation does not perform this.
@@ -1,7 +1,7 @@
// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Dynamic Momentum Index (DYMOI)", "DYMOI", overlay=false)
indicator("Dynamic Momentum Index (DYMI)", "DYMI", overlay=false)
//@description Dynamic Momentum Index by Tushar Chande and Stanley Kroll (1994).
// A three-stage pipeline that produces a volatility-adaptive RSI:
@@ -89,11 +89,11 @@ rsi_wilder(series float source, series int dynPeriod) =>
//@param longPeriod Long StdDev window (default 10)
//@param minPeriod Minimum dynamic period (default 3)
//@param maxPeriod Maximum dynamic period (default 30)
//@returns DYMOI value in [0, 100]
//@returns DYMI value in [0, 100]
//@optimized Uses circular buffers for O(1) StdDev; adaptive Wilder RMA for RSI
dymoi(series float source, simple int basePeriod, simple int shortPeriod, simple int longPeriod, simple int minPeriod, simple int maxPeriod) =>
dymi(series float source, simple int basePeriod, simple int shortPeriod, simple int longPeriod, simple int minPeriod, simple int maxPeriod) =>
if basePeriod < 2 or shortPeriod < 2 or longPeriod <= shortPeriod or minPeriod < 2 or maxPeriod < minPeriod
runtime.error("Invalid DYMOI parameters")
runtime.error("Invalid DYMI parameters")
// Stage 1: dual StdDev volatility ratio
float sdShort = stddev_circ(source, shortPeriod)
@@ -118,9 +118,9 @@ i_minPeriod = input.int(3, "Min Period", minval=2, maxval=500)
i_maxPeriod = input.int(30, "Max Period", minval=2, maxval=500)
i_source = input.source(close, "Source")
dymoi_val = dymoi(i_source, i_basePeriod, i_shortPeriod, i_longPeriod, i_minPeriod, i_maxPeriod)
dymi_val = dymi(i_source, i_basePeriod, i_shortPeriod, i_longPeriod, i_minPeriod, i_maxPeriod)
plot(dymoi_val, "DYMOI", color=color.yellow, linewidth=2)
plot(dymi_val, "DYMI", color=color.yellow, linewidth=2)
hline(70, "Overbought", color=color.gray, linestyle=hline.style_dotted)
hline(50, "Midline", color=color.gray, linestyle=hline.style_dotted)
hline(30, "Oversold", color=color.gray, linestyle=hline.style_dotted)
@@ -3,12 +3,12 @@ using QuanTAlib;
namespace QuanTAlib.Tests;
public sealed class DymoiIndicatorTests
public sealed class DymiIndicatorTests
{
[Fact]
public void DymoiIndicator_Constructor_SetsDefaults()
public void DymiIndicator_Constructor_SetsDefaults()
{
var indicator = new DymoiIndicator();
var indicator = new DymiIndicator();
Assert.Equal(14, indicator.BasePeriod);
Assert.Equal(5, indicator.ShortPeriod);
@@ -17,25 +17,25 @@ public sealed class DymoiIndicatorTests
Assert.Equal(30, indicator.MaxPeriod);
Assert.Equal(SourceType.Close, indicator.Source);
Assert.True(indicator.ShowColdValues);
Assert.Equal("DYMOI - Dynamic Momentum Index", indicator.Name);
Assert.Equal("DYMI - Dynamic Momentum Index", indicator.Name);
Assert.True(indicator.SeparateWindow);
Assert.True(indicator.OnBackGround);
}
[Fact]
public void DymoiIndicator_MinHistoryDepths_EqualsZero()
public void DymiIndicator_MinHistoryDepths_EqualsZero()
{
var indicator = new DymoiIndicator();
var indicator = new DymiIndicator();
Assert.Equal(0, DymoiIndicator.MinHistoryDepths);
Assert.Equal(0, DymiIndicator.MinHistoryDepths);
IWatchlistIndicator watchlistIndicator = indicator;
Assert.Equal(0, watchlistIndicator.MinHistoryDepths);
}
[Fact]
public void DymoiIndicator_ShortName_IncludesParameters()
public void DymiIndicator_ShortName_IncludesParameters()
{
var indicator = new DymoiIndicator
var indicator = new DymiIndicator
{
BasePeriod = 10,
ShortPeriod = 4,
@@ -45,7 +45,7 @@ public sealed class DymoiIndicatorTests
};
indicator.Initialize();
Assert.Contains("DYMOI", indicator.ShortName, StringComparison.Ordinal);
Assert.Contains("DYMI", indicator.ShortName, StringComparison.Ordinal);
Assert.Contains("10", indicator.ShortName, StringComparison.Ordinal);
Assert.Contains("4", indicator.ShortName, StringComparison.Ordinal);
Assert.Contains("8", indicator.ShortName, StringComparison.Ordinal);
@@ -53,18 +53,18 @@ public sealed class DymoiIndicatorTests
}
[Fact]
public void DymoiIndicator_SourceCodeLink_IsValid()
public void DymiIndicator_SourceCodeLink_IsValid()
{
var indicator = new DymoiIndicator();
var indicator = new DymiIndicator();
Assert.Contains("github.com", indicator.SourceCodeLink, StringComparison.Ordinal);
Assert.Contains("Dymoi.Quantower.cs", indicator.SourceCodeLink, StringComparison.Ordinal);
Assert.Contains("Dymi.Quantower.cs", indicator.SourceCodeLink, StringComparison.Ordinal);
}
[Fact]
public void DymoiIndicator_Initialize_CreatesLineSeries()
public void DymiIndicator_Initialize_CreatesLineSeries()
{
var indicator = new DymoiIndicator
var indicator = new DymiIndicator
{
BasePeriod = 14,
ShortPeriod = 5,
@@ -79,9 +79,9 @@ public sealed class DymoiIndicatorTests
}
[Fact]
public void DymoiIndicator_ProcessUpdate_HistoricalBar_ComputesValue()
public void DymiIndicator_ProcessUpdate_HistoricalBar_ComputesValue()
{
var indicator = new DymoiIndicator
var indicator = new DymiIndicator
{
BasePeriod = 14,
ShortPeriod = 5,
@@ -103,13 +103,13 @@ public sealed class DymoiIndicatorTests
double value = indicator.LinesSeries[0].GetValue(0);
Assert.True(double.IsFinite(value));
Assert.True(value >= 0.0 && value <= 100.0, $"DYMOI={value} out of [0,100]");
Assert.True(value >= 0.0 && value <= 100.0, $"DYMI={value} out of [0,100]");
}
[Fact]
public void DymoiIndicator_ProcessUpdate_NewBar_ComputesValue()
public void DymiIndicator_ProcessUpdate_NewBar_ComputesValue()
{
var indicator = new DymoiIndicator
var indicator = new DymiIndicator
{
BasePeriod = 14,
ShortPeriod = 5,
@@ -135,11 +135,11 @@ public sealed class DymoiIndicatorTests
}
[Fact]
public void DymoiIndicator_DifferentSourceTypes_ComputeWithoutError()
public void DymiIndicator_DifferentSourceTypes_ComputeWithoutError()
{
foreach (var sourceType in new[] { SourceType.Close, SourceType.Open, SourceType.High, SourceType.Low })
{
var indicator = new DymoiIndicator
var indicator = new DymiIndicator
{
BasePeriod = 14,
ShortPeriod = 5,
@@ -2,7 +2,7 @@ using Xunit;
namespace QuanTAlib.Tests;
public sealed class DymoiTests
public sealed class DymiTests
{
private const double Tolerance = 1e-10;
@@ -11,62 +11,62 @@ public sealed class DymoiTests
[Fact]
public void Constructor_BasePeriodOne_ThrowsArgumentException()
{
var ex = Assert.Throws<ArgumentException>(() => new Dymoi(basePeriod: 1));
var ex = Assert.Throws<ArgumentException>(() => new Dymi(basePeriod: 1));
Assert.Equal("basePeriod", ex.ParamName);
}
[Fact]
public void Constructor_ShortPeriodOne_ThrowsArgumentException()
{
var ex = Assert.Throws<ArgumentException>(() => new Dymoi(shortPeriod: 1));
var ex = Assert.Throws<ArgumentException>(() => new Dymi(shortPeriod: 1));
Assert.Equal("shortPeriod", ex.ParamName);
}
[Fact]
public void Constructor_LongPeriodEqualShortPeriod_ThrowsArgumentException()
{
var ex = Assert.Throws<ArgumentException>(() => new Dymoi(shortPeriod: 5, longPeriod: 5));
var ex = Assert.Throws<ArgumentException>(() => new Dymi(shortPeriod: 5, longPeriod: 5));
Assert.Equal("longPeriod", ex.ParamName);
}
[Fact]
public void Constructor_LongPeriodLessThanShortPeriod_ThrowsArgumentException()
{
var ex = Assert.Throws<ArgumentException>(() => new Dymoi(shortPeriod: 10, longPeriod: 5));
var ex = Assert.Throws<ArgumentException>(() => new Dymi(shortPeriod: 10, longPeriod: 5));
Assert.Equal("longPeriod", ex.ParamName);
}
[Fact]
public void Constructor_MinPeriodOne_ThrowsArgumentException()
{
var ex = Assert.Throws<ArgumentException>(() => new Dymoi(minPeriod: 1));
var ex = Assert.Throws<ArgumentException>(() => new Dymi(minPeriod: 1));
Assert.Equal("minPeriod", ex.ParamName);
}
[Fact]
public void Constructor_MaxPeriodLessThanMinPeriod_ThrowsArgumentException()
{
var ex = Assert.Throws<ArgumentException>(() => new Dymoi(minPeriod: 10, maxPeriod: 5));
var ex = Assert.Throws<ArgumentException>(() => new Dymi(minPeriod: 10, maxPeriod: 5));
Assert.Equal("maxPeriod", ex.ParamName);
}
[Fact]
public void Constructor_ValidDefaults_SetsProperties()
{
var d = new Dymoi();
var d = new Dymi();
Assert.Equal(14, d.BasePeriod);
Assert.Equal(5, d.ShortPeriod);
Assert.Equal(10, d.LongPeriod);
Assert.Equal(3, d.MinPeriod);
Assert.Equal(30, d.MaxPeriod);
Assert.Equal("Dymoi(14,5,10,3,30)", d.Name);
Assert.Equal("Dymi(14,5,10,3,30)", d.Name);
Assert.False(d.IsHot);
}
[Fact]
public void Constructor_CustomPeriods_SetsProperties()
{
var d = new Dymoi(basePeriod: 10, shortPeriod: 3, longPeriod: 7, minPeriod: 2, maxPeriod: 20);
var d = new Dymi(basePeriod: 10, shortPeriod: 3, longPeriod: 7, minPeriod: 2, maxPeriod: 20);
Assert.Equal(10, d.BasePeriod);
Assert.Equal(3, d.ShortPeriod);
Assert.Equal(7, d.LongPeriod);
@@ -79,7 +79,7 @@ public sealed class DymoiTests
{
var src = new double[] { 1, 2, 3 };
var out1 = new double[4];
var ex = Assert.Throws<ArgumentException>(() => Dymoi.Batch(src, out1));
var ex = Assert.Throws<ArgumentException>(() => Dymi.Batch(src, out1));
Assert.Equal("output", ex.ParamName);
}
@@ -88,7 +88,7 @@ public sealed class DymoiTests
{
var src = new double[] { 1, 2, 3 };
var out1 = new double[3];
var ex = Assert.Throws<ArgumentException>(() => Dymoi.Batch(src, out1, basePeriod: 1));
var ex = Assert.Throws<ArgumentException>(() => Dymi.Batch(src, out1, basePeriod: 1));
Assert.Equal("basePeriod", ex.ParamName);
}
@@ -97,7 +97,7 @@ public sealed class DymoiTests
{
var src = new double[] { 1, 2, 3 };
var out1 = new double[3];
var ex = Assert.Throws<ArgumentException>(() => Dymoi.Batch(src, out1, shortPeriod: 5, longPeriod: 5));
var ex = Assert.Throws<ArgumentException>(() => Dymi.Batch(src, out1, shortPeriod: 5, longPeriod: 5));
Assert.Equal("longPeriod", ex.ParamName);
}
@@ -106,7 +106,7 @@ public sealed class DymoiTests
{
var src = new double[] { 1, 2, 3 };
var out1 = new double[3];
var ex = Assert.Throws<ArgumentException>(() => Dymoi.Batch(src, out1, minPeriod: 5, maxPeriod: 3));
var ex = Assert.Throws<ArgumentException>(() => Dymi.Batch(src, out1, minPeriod: 5, maxPeriod: 3));
Assert.Equal("maxPeriod", ex.ParamName);
}
@@ -115,7 +115,7 @@ public sealed class DymoiTests
[Fact]
public void Update_ReturnsTValue()
{
var d = new Dymoi();
var d = new Dymi();
var result = d.Update(new TValue(DateTime.UtcNow, 100.0));
Assert.IsType<TValue>(result);
}
@@ -123,29 +123,29 @@ public sealed class DymoiTests
[Fact]
public void Update_OutputInRange0To100()
{
var d = new Dymoi(basePeriod: 14, shortPeriod: 5, longPeriod: 10, minPeriod: 3, maxPeriod: 30);
var d = new Dymi(basePeriod: 14, shortPeriod: 5, longPeriod: 10, minPeriod: 3, maxPeriod: 30);
var gbm = new GBM(startPrice: 100.0, mu: 0.02, sigma: 0.3, seed: 42);
var bars = gbm.Fetch(300, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
foreach (var bar in bars.Close)
{
double v = d.Update(bar).Value;
Assert.True(v >= 0.0 && v <= 100.0, $"DYMOI={v} out of [0,100]");
Assert.True(v >= 0.0 && v <= 100.0, $"DYMI={v} out of [0,100]");
}
}
[Fact]
public void Update_NameIsAccessible()
{
var d = new Dymoi(14, 5, 10, 3, 30);
var d = new Dymi(14, 5, 10, 3, 30);
_ = d.Update(new TValue(DateTime.UtcNow, 100.0));
Assert.Equal("Dymoi(14,5,10,3,30)", d.Name);
Assert.Equal("Dymi(14,5,10,3,30)", d.Name);
}
[Fact]
public void Update_LastIsAccessible()
{
var d = new Dymoi();
var d = new Dymi();
var t = new TValue(DateTime.UtcNow, 100.0);
var result = d.Update(t);
Assert.Equal(result, d.Last);
@@ -156,7 +156,7 @@ public sealed class DymoiTests
[Fact]
public void Update_IsNewTrue_AdvancesState()
{
var d = new Dymoi(basePeriod: 14, shortPeriod: 5, longPeriod: 10, minPeriod: 3, maxPeriod: 30);
var d = new Dymi(basePeriod: 14, shortPeriod: 5, longPeriod: 10, minPeriod: 3, maxPeriod: 30);
var t = DateTime.UtcNow;
d.Update(new TValue(t, 100.0), isNew: true);
var v1 = d.Last;
@@ -169,7 +169,7 @@ public sealed class DymoiTests
[Fact]
public void Update_IsNewFalse_RollsBack()
{
var d = new Dymoi(basePeriod: 14, shortPeriod: 5, longPeriod: 10, minPeriod: 3, maxPeriod: 30);
var d = new Dymi(basePeriod: 14, shortPeriod: 5, longPeriod: 10, minPeriod: 3, maxPeriod: 30);
double[] prices = [100, 102, 104, 103, 105, 107, 106, 108, 110, 109, 111, 113];
var t = DateTime.UtcNow;
for (int i = 0; i < prices.Length; i++)
@@ -191,7 +191,7 @@ public sealed class DymoiTests
[Fact]
public void Update_IterativeCorrections_Restore()
{
var d = new Dymoi(basePeriod: 14, shortPeriod: 5, longPeriod: 10, minPeriod: 3, maxPeriod: 30);
var d = new Dymi(basePeriod: 14, shortPeriod: 5, longPeriod: 10, minPeriod: 3, maxPeriod: 30);
double[] prices = [100, 102, 98, 105, 103, 107, 101, 108, 100, 109, 102, 110];
var t = DateTime.UtcNow;
for (int i = 0; i < prices.Length; i++)
@@ -214,7 +214,7 @@ public sealed class DymoiTests
[Fact]
public void Update_Reset_ClearsState()
{
var d = new Dymoi(basePeriod: 14, shortPeriod: 5, longPeriod: 10, minPeriod: 3, maxPeriod: 30);
var d = new Dymi(basePeriod: 14, shortPeriod: 5, longPeriod: 10, minPeriod: 3, maxPeriod: 30);
var gbm = new GBM(startPrice: 100.0, mu: 0.01, sigma: 0.2, seed: 7);
var bars = gbm.Fetch(50, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
@@ -234,7 +234,7 @@ public sealed class DymoiTests
public void IsHot_FlipsAfterWarmup()
{
// Use small periods to make warmup manageable
var d = new Dymoi(basePeriod: 5, shortPeriod: 3, longPeriod: 5, minPeriod: 2, maxPeriod: 10);
var d = new Dymi(basePeriod: 5, shortPeriod: 3, longPeriod: 5, minPeriod: 2, maxPeriod: 10);
var gbm = new GBM(startPrice: 100.0, mu: 0.01, sigma: 0.3, seed: 11);
var bars = gbm.Fetch(200, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
@@ -249,13 +249,13 @@ public sealed class DymoiTests
}
}
Assert.True(everHot, "DYMOI should become hot within 200 bars");
Assert.True(everHot, "DYMI should become hot within 200 bars");
}
[Fact]
public void WarmupPeriod_IsLongPeriodPlusMaxPeriod()
{
var d = new Dymoi(basePeriod: 14, shortPeriod: 5, longPeriod: 10, minPeriod: 3, maxPeriod: 30);
var d = new Dymi(basePeriod: 14, shortPeriod: 5, longPeriod: 10, minPeriod: 3, maxPeriod: 30);
Assert.Equal(40, d.WarmupPeriod); // longPeriod(10) + maxPeriod(30)
}
@@ -264,7 +264,7 @@ public sealed class DymoiTests
[Fact]
public void Update_NaN_UsesLastValid()
{
var d = new Dymoi(basePeriod: 14, shortPeriod: 5, longPeriod: 10, minPeriod: 3, maxPeriod: 30);
var d = new Dymi(basePeriod: 14, shortPeriod: 5, longPeriod: 10, minPeriod: 3, maxPeriod: 30);
var t = DateTime.UtcNow;
// Feed valid values first
@@ -281,7 +281,7 @@ public sealed class DymoiTests
[Fact]
public void Update_Infinity_UsesLastValid()
{
var d = new Dymoi(basePeriod: 14, shortPeriod: 5, longPeriod: 10, minPeriod: 3, maxPeriod: 30);
var d = new Dymi(basePeriod: 14, shortPeriod: 5, longPeriod: 10, minPeriod: 3, maxPeriod: 30);
var t = DateTime.UtcNow;
for (int i = 0; i < 20; i++)
@@ -296,7 +296,7 @@ public sealed class DymoiTests
[Fact]
public void Update_BatchNaN_AllFinite()
{
var d = new Dymoi(basePeriod: 14, shortPeriod: 5, longPeriod: 10, minPeriod: 3, maxPeriod: 30);
var d = new Dymi(basePeriod: 14, shortPeriod: 5, longPeriod: 10, minPeriod: 3, maxPeriod: 30);
var t = DateTime.UtcNow;
// Mix NaN into sequence
@@ -319,7 +319,7 @@ public sealed class DymoiTests
TSeries source = bars.Close;
// Streaming
var streaming = new Dymoi(14, 5, 10, 3, 30);
var streaming = new Dymi(14, 5, 10, 3, 30);
var streamVals = new double[source.Count];
for (int i = 0; i < source.Count; i++)
{
@@ -327,7 +327,7 @@ public sealed class DymoiTests
}
// Batch TSeries
TSeries batchTs = Dymoi.Batch(source, 14, 5, 10, 3, 30);
TSeries batchTs = Dymi.Batch(source, 14, 5, 10, 3, 30);
for (int i = 0; i < source.Count; i++)
{
@@ -342,10 +342,10 @@ public sealed class DymoiTests
var bars = gbm.Fetch(300, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
TSeries source = bars.Close;
TSeries batchTs = Dymoi.Batch(source, 14, 5, 10, 3, 30);
TSeries batchTs = Dymi.Batch(source, 14, 5, 10, 3, 30);
var spanOut = new double[source.Count];
Dymoi.Batch(source.Values, spanOut, 14, 5, 10, 3, 30);
Dymi.Batch(source.Values, spanOut, 14, 5, 10, 3, 30);
for (int i = 0; i < source.Count; i++)
{
@@ -361,7 +361,7 @@ public sealed class DymoiTests
TSeries source = bars.Close;
// Streaming
var streaming = new Dymoi(14, 5, 10, 3, 30);
var streaming = new Dymi(14, 5, 10, 3, 30);
var streamVals = new double[source.Count];
for (int i = 0; i < source.Count; i++)
{
@@ -370,12 +370,12 @@ public sealed class DymoiTests
// Event-based
var eventTs = new TSeries();
var eventDymoi = new Dymoi(eventTs, 14, 5, 10, 3, 30);
var eventDymi = new Dymi(eventTs, 14, 5, 10, 3, 30);
var eventVals = new double[source.Count];
for (int i = 0; i < source.Count; i++)
{
eventTs.Add(source[i]);
eventVals[i] = eventDymoi.Last.Value;
eventVals[i] = eventDymi.Last.Value;
}
for (int i = 0; i < source.Count; i++)
@@ -391,7 +391,7 @@ public sealed class DymoiTests
{
var src = Array.Empty<double>();
var out1 = Array.Empty<double>();
Dymoi.Batch(src, out1);
Dymi.Batch(src, out1);
Assert.Empty(out1);
}
@@ -405,7 +405,7 @@ public sealed class DymoiTests
var out1 = new double[n];
// Should not throw StackOverflowException — uses ArrayPool for large buffers
Dymoi.Batch(src, out1);
Dymi.Batch(src, out1);
bool anyFinite = false;
for (int i = 0; i < n; i++)
@@ -428,7 +428,7 @@ public sealed class DymoiTests
var src = bars.Close.Values;
var out1 = new double[src.Length];
Dymoi.Batch(src, out1);
Dymi.Batch(src, out1);
for (int i = 0; i < src.Length; i++)
{
@@ -442,7 +442,7 @@ public sealed class DymoiTests
public void Chainability_PubFires()
{
var source = new TSeries();
var d = new Dymoi(source, 14, 5, 10, 3, 30);
var d = new Dymi(source, 14, 5, 10, 3, 30);
int count = 0;
d.Pub += (object? _, in TValueEventArgs e) => count++;
@@ -460,7 +460,7 @@ public sealed class DymoiTests
public void Chainability_EventBasedChaining_Works()
{
var source = new TSeries();
var d = new Dymoi(source, 14, 5, 10, 3, 30);
var d = new Dymi(source, 14, 5, 10, 3, 30);
var output = new TSeries();
d.Pub += (object? _, in TValueEventArgs e) => output.Add(e.Value);
@@ -6,14 +6,14 @@ using OoplesFinance.StockIndicators.Models;
namespace QuanTAlib.Tests;
/// <summary>
/// Self-consistency validation for DYMOI.
/// No external library implements DYMOI in C# bindings, so validation uses:
/// Self-consistency validation for DYMI.
/// No external library implements DYMI in C# bindings, so validation uses:
/// 1. Mathematical identity: when shortPeriod == longPeriod → V ≈ 1 → dynPeriod ≈ basePeriod → matches standard RSI(basePeriod)
/// 2. Batch == streaming == span == eventing consistency
/// 3. Output always in [0, 100]
/// 4. Period adapts: shorter in high-vol, longer in low-vol
/// </summary>
public sealed class DymoiValidationTests
public sealed class DymiValidationTests
{
private const double Tolerance = 1e-10;
@@ -27,7 +27,7 @@ public sealed class DymoiValidationTests
TSeries source = bars.Close;
// Streaming
var streaming = new Dymoi(14, 5, 10, 3, 30);
var streaming = new Dymi(14, 5, 10, 3, 30);
var streamVals = new double[source.Count];
for (int i = 0; i < source.Count; i++)
{
@@ -35,7 +35,7 @@ public sealed class DymoiValidationTests
}
// Batch TSeries
TSeries batchTs = Dymoi.Batch(source, 14, 5, 10, 3, 30);
TSeries batchTs = Dymi.Batch(source, 14, 5, 10, 3, 30);
for (int i = 0; i < source.Count; i++)
{
@@ -51,11 +51,11 @@ public sealed class DymoiValidationTests
TSeries source = bars.Close;
// Batch TSeries
TSeries batchTs = Dymoi.Batch(source, 14, 5, 10, 3, 30);
TSeries batchTs = Dymi.Batch(source, 14, 5, 10, 3, 30);
// Span batch
var spanOut = new double[source.Count];
Dymoi.Batch(source.Values, spanOut, 14, 5, 10, 3, 30);
Dymi.Batch(source.Values, spanOut, 14, 5, 10, 3, 30);
for (int i = 0; i < source.Count; i++)
{
@@ -71,7 +71,7 @@ public sealed class DymoiValidationTests
TSeries source = bars.Close;
// Streaming
var streaming = new Dymoi(14, 5, 10, 3, 30);
var streaming = new Dymi(14, 5, 10, 3, 30);
var streamVals = new double[source.Count];
for (int i = 0; i < source.Count; i++)
{
@@ -80,12 +80,12 @@ public sealed class DymoiValidationTests
// Event-based
var eventTs = new TSeries();
var eventDymoi = new Dymoi(eventTs, 14, 5, 10, 3, 30);
var eventDymi = new Dymi(eventTs, 14, 5, 10, 3, 30);
var eventVals = new double[source.Count];
for (int i = 0; i < source.Count; i++)
{
eventTs.Add(source[i]);
eventVals[i] = eventDymoi.Last.Value;
eventVals[i] = eventDymi.Last.Value;
}
for (int i = 0; i < source.Count; i++)
@@ -102,11 +102,11 @@ public sealed class DymoiValidationTests
var gbm = new GBM(startPrice: 50.0, mu: 0.05, sigma: 0.8, seed: 3004);
var bars = gbm.Fetch(500, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
var d = new Dymoi(14, 5, 10, 3, 30);
var d = new Dymi(14, 5, 10, 3, 30);
foreach (var bar in bars.Close)
{
double v = d.Update(bar).Value;
Assert.True(v >= 0.0 && v <= 100.0, $"DYMOI={v} at high vol");
Assert.True(v >= 0.0 && v <= 100.0, $"DYMI={v} at high vol");
}
}
@@ -117,11 +117,11 @@ public sealed class DymoiValidationTests
var gbm = new GBM(startPrice: 100.0, mu: 0.001, sigma: 0.01, seed: 3005);
var bars = gbm.Fetch(200, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
var d = new Dymoi(14, 5, 10, 3, 30);
var d = new Dymi(14, 5, 10, 3, 30);
foreach (var bar in bars.Close)
{
double v = d.Update(bar).Value;
Assert.True(v >= 0.0 && v <= 100.0, $"DYMOI={v} at low vol");
Assert.True(v >= 0.0 && v <= 100.0, $"DYMI={v} at low vol");
}
}
@@ -139,7 +139,7 @@ public sealed class DymoiValidationTests
// For the true identity test: construct a series with constant differences
// such that a window of any size yields the same stddev.
// A simpler verification: at V=1, dynPeriod = round(basePeriod/1) = basePeriod.
// We verify that DYMOI output matches Rsi(basePeriod) on a constant-drift series.
// We verify that DYMI output matches Rsi(basePeriod) on a constant-drift series.
// Construct a series with perfectly constant increments → stddev of close levels
// is the same in short and long windows only if windows cover the same prices,
@@ -147,19 +147,19 @@ public sealed class DymoiValidationTests
// close periods and checking that output is nearly identical to standard RSI.
// Using longPeriod just 1 more than shortPeriod and monitoring range
var d = new Dymoi(basePeriod: 14, shortPeriod: 9, longPeriod: 10, minPeriod: 14, maxPeriod: 14);
var d = new Dymi(basePeriod: 14, shortPeriod: 9, longPeriod: 10, minPeriod: 14, maxPeriod: 14);
var rsi = new Rsi(14);
var gbm = new GBM(startPrice: 100.0, mu: 0.01, sigma: 0.15, seed: 3006);
var bars = gbm.Fetch(500, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
// When minPeriod == maxPeriod == basePeriod, dynPeriod is always fixed at basePeriod
// → DYMOI is identical to standard RSI(basePeriod)
// → DYMI is identical to standard RSI(basePeriod)
foreach (var bar in bars.Close)
{
double dymoiVal = d.Update(bar).Value;
double dymiVal = d.Update(bar).Value;
double rsiVal = rsi.Update(bar).Value;
// With fixed dynPeriod=14, both should match
Assert.Equal(rsiVal, dymoiVal, 1e-9);
Assert.Equal(rsiVal, dymiVal, 1e-9);
}
}
@@ -170,8 +170,8 @@ public sealed class DymoiValidationTests
{
// When short-term vol > long-term vol (V > 1), dynPeriod < basePeriod.
// We test this indirectly: high-vol data should produce faster RSI transitions.
// In high-vol regime, DYMOI changes more rapidly than fixed-period RSI.
var d = new Dymoi(basePeriod: 14, shortPeriod: 3, longPeriod: 20, minPeriod: 3, maxPeriod: 30);
// In high-vol regime, DYMI changes more rapidly than fixed-period RSI.
var d = new Dymi(basePeriod: 14, shortPeriod: 3, longPeriod: 20, minPeriod: 3, maxPeriod: 30);
var gbm = new GBM(startPrice: 100.0, mu: 0.0, sigma: 0.4, seed: 3007);
var bars = gbm.Fetch(200, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
@@ -191,8 +191,8 @@ public sealed class DymoiValidationTests
var bars1 = gbm1.Fetch(150, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
var bars2 = gbm2.Fetch(150, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
var d1 = new Dymoi(14, 5, 10, 3, 30);
var d2 = new Dymoi(14, 5, 10, 3, 30);
var d1 = new Dymi(14, 5, 10, 3, 30);
var d2 = new Dymi(14, 5, 10, 3, 30);
for (int i = 0; i < bars1.Close.Count; i++)
{
@@ -207,7 +207,7 @@ public sealed class DymoiValidationTests
{
var src = Array.Empty<double>();
var out1 = Array.Empty<double>();
Dymoi.Batch(src, out1);
Dymi.Batch(src, out1);
Assert.Empty(out1);
}
@@ -215,7 +215,7 @@ public sealed class DymoiValidationTests
public void Streaming_ConstantPrice_ProducesStable50()
{
// When price is constant, gain=0, loss=0 → RSI = 50
var d = new Dymoi(basePeriod: 14, shortPeriod: 5, longPeriod: 10, minPeriod: 3, maxPeriod: 30);
var d = new Dymi(basePeriod: 14, shortPeriod: 5, longPeriod: 10, minPeriod: 3, maxPeriod: 30);
var t = DateTime.UtcNow;
double last = 50.0;
for (int i = 0; i < 100; i++)
@@ -228,7 +228,7 @@ public sealed class DymoiValidationTests
}
[Fact]
public void Dymoi_MatchesOoples_Structural()
public void Dymi_MatchesOoples_Structural()
{
var gbm = new GBM(startPrice: 100.0, mu: 0.02, sigma: 0.15, seed: 42);
var bars = gbm.Fetch(500, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
+1 -1
View File
@@ -13,7 +13,7 @@ Reversal indicators identify potential turning points where price may change dir
| [PIVOTEXT](pivotext/Pivotext.md) | Extended Traditional Pivots | Extended pivots with 11 levels (R1-R5, S1-S5) for volatile markets. |
| [PIVOTFIB](pivotfib/Pivotfib.md) | Fibonacci Pivot Points | Fibonacci-ratio based pivots; Golden Ratio (61.8%) at R2/S2. |
| [PIVOTWOOD](pivotwood/Pivotwood.md) | Woodie's Pivot Points | Weighted close pivots (2× close weight) for intraday trading. |
| [PSAR](psar/Psar.md) | Parabolic Stop And Reverse | Trailing stop that accelerates with trend; SAR dots mark entry/exit signals. |
| [SAR](sar/Sar.md) | Parabolic Stop And Reverse | Trailing stop that accelerates with trend; SAR dots mark entry/exit signals. |
| [SAREXT](sarext/Sarext.md) | Parabolic SAR Extended | PSAR with asymmetric long/short acceleration factors. Sign-encoded output. |
| [SWINGS](swings/Swings.md) | Swing High/Low Detection | Configurable-lookback pattern detector for swing highs/lows; dual SwingHigh/SwingLow. |
| [TTM_SCALPER](ttm_scalper/TtmScalper.md) | TTM Scalper Alert | 3-bar pivot high/low detection for scalping entries. John Carter. |
+1 -1
View File
@@ -13,7 +13,7 @@
| **PineScript** | [chandelier.pine](chandelier.pine) |
- The Chandelier Exit computes ATR-based trailing stop levels that hang from the highest high (for longs) or rise from the lowest low (for shorts) ov...
- **Similar:** [PSAR](../psar/Psar.md), [Super](../../dynamics/super/Super.md) | **Complementary:** ADX for trend strength | **Trading note:** Chandelier Exit; ATR-based trailing stop from highest high. Risk management tool.
- **Similar:** [SAR](../sar/Sar.md), [Super](../../dynamics/super/Super.md) | **Complementary:** ADX for trend strength | **Trading note:** Chandelier Exit; ATR-based trailing stop from highest high. Risk management tool.
- Validated against TA-Lib, Skender, and Tulip reference implementations where available.
The Chandelier Exit computes ATR-based trailing stop levels that hang from the highest high (for longs) or rise from the lowest low (for shorts) over a lookback period. It produces two overlay lines: ExitLong (trailing stop for long positions) and ExitShort (trailing stop for short positions). Developed by Charles Le Beau and popularized by Alexander Elder. Default parameters: period 22, multiplier 3.0.
+1 -1
View File
@@ -13,7 +13,7 @@
| **PineScript** | [ckstop.pine](ckstop.pine) |
- The Chande Kroll Stop computes adaptive trailing stop levels using ATR-smoothed volatility envelopes around rolling extremes.
- **Similar:** [Chandelier](../chandelier/Chandelier.md), [PSAR](../psar/Psar.md) | **Complementary:** ATR | **Trading note:** Chuck LeBeau's Chandelier stop; ATR trailing stop with configurable multiplier.
- **Similar:** [Chandelier](../chandelier/Chandelier.md), [SAR](../sar/Sar.md) | **Complementary:** ATR | **Trading note:** Chuck LeBeau's Chandelier stop; ATR trailing stop with configurable multiplier.
- Validated against TA-Lib, Skender, and Tulip reference implementations where available.
The Chande Kroll Stop computes adaptive trailing stop levels using ATR-smoothed volatility envelopes around rolling extremes. It produces two lines: StopLong (support) and StopShort (resistance). When price trades above both stops, the trend is bullish. When below both, bearish. Crossovers between the two stops signal potential reversals.
@@ -5,7 +5,7 @@ using TradingPlatform.BusinessLayer;
namespace QuanTAlib;
[SkipLocalsInit]
public sealed class PsarIndicator : Indicator, IWatchlistIndicator
public sealed class SarIndicator : Indicator, IWatchlistIndicator
{
[InputParameter("Start AF", sortIndex: 0, 0.001, 1.0, 0.001, 3)]
public double AfStart { get; set; } = 0.02;
@@ -19,20 +19,20 @@ public sealed class PsarIndicator : Indicator, IWatchlistIndicator
[InputParameter("Show cold values", sortIndex: 21)]
public bool ShowColdValues { get; set; } = true;
private Psar _indicator = null!;
private Sar _indicator = null!;
private readonly LineSeries _sarSeries;
public static int MinHistoryDepths => 0;
int IWatchlistIndicator.MinHistoryDepths => MinHistoryDepths;
public override string ShortName => $"PSAR({AfStart:F2},{AfIncrement:F2},{AfMax:F2})";
public override string SourceCodeLink => "https://github.com/mihakralj/QuanTAlib/blob/main/lib/reversals/psar/Psar.cs";
public override string ShortName => $"SAR({AfStart:F2},{AfIncrement:F2},{AfMax:F2})";
public override string SourceCodeLink => "https://github.com/mihakralj/QuanTAlib/blob/main/lib/reversals/sar/Sar.cs";
public PsarIndicator()
public SarIndicator()
{
OnBackGround = true;
SeparateWindow = false;
Name = "PSAR - Parabolic Stop And Reverse";
Name = "SAR - Parabolic Stop And Reverse";
Description = "Trend-following trailing stop indicator. SAR accelerates toward price as trend progresses, flipping on reversal.";
_sarSeries = new LineSeries(name: "SAR", color: Color.DodgerBlue, width: 2, style: LineStyle.Dot);
@@ -43,7 +43,7 @@ public sealed class PsarIndicator : Indicator, IWatchlistIndicator
[MethodImpl(MethodImplOptions.AggressiveInlining)]
protected override void OnInit()
{
_indicator = new Psar(AfStart, AfIncrement, AfMax);
_indicator = new Sar(AfStart, AfIncrement, AfMax);
base.OnInit();
}
@@ -52,6 +52,6 @@ public sealed class PsarIndicator : Indicator, IWatchlistIndicator
{
_ = _indicator.Update(this.GetInputBar(args), args.IsNewBar());
_sarSeries.SetValue(_indicator.Sar, _indicator.IsHot, ShowColdValues);
_sarSeries.SetValue(_indicator.SarValue, _indicator.IsHot, ShowColdValues);
}
}
@@ -1,4 +1,4 @@
// PSAR: Parabolic Stop And Reverse (Wilder, 1978)
// SAR: Parabolic Stop And Reverse (Wilder, 1978)
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
@@ -8,7 +8,7 @@ using System.Runtime.InteropServices;
namespace QuanTAlib;
/// <summary>
/// PSAR: Parabolic Stop And Reverse
/// SAR: Parabolic Stop And Reverse
/// </summary>
/// <remarks>
/// Trend-following overlay indicator developed by J. Welles Wilder Jr. (1978).
@@ -30,9 +30,9 @@ namespace QuanTAlib;
/// - SAR clamped to prior 2 bars' extremes to prevent crossover artifacts
/// - Default parameters: afStart=0.02, afIncrement=0.02, afMax=0.20 (Wilder's originals)
/// </remarks>
/// <seealso href="Psar.md">Detailed documentation</seealso>
/// <seealso href="Sar.md">Detailed documentation</seealso>
[SkipLocalsInit]
public sealed class Psar : ITValuePublisher
public sealed class Sar : ITValuePublisher
{
private const double DefaultAfStart = 0.02;
private const double DefaultAfIncrement = 0.02;
@@ -80,9 +80,9 @@ public sealed class Psar : ITValuePublisher
public int WarmupPeriod { get; }
/// <summary>Current SAR value (the stop level).</summary>
public double Sar { get; private set; }
public double SarValue { get; private set; }
/// <summary>True when the PSAR is in long (uptrend) mode.</summary>
/// <summary>True when the SAR is in long (uptrend) mode.</summary>
public bool IsLong => _s.IsLong;
/// <summary>Primary output value (SAR as TValue for overlay plotting).</summary>
@@ -99,7 +99,7 @@ public sealed class Psar : ITValuePublisher
/// <param name="afStart">Initial acceleration factor (default 0.02).</param>
/// <param name="afIncrement">AF increment per new extreme (default 0.02).</param>
/// <param name="afMax">Maximum acceleration factor (default 0.20).</param>
public Psar(double afStart = DefaultAfStart, double afIncrement = DefaultAfIncrement, double afMax = DefaultAfMax)
public Sar(double afStart = DefaultAfStart, double afIncrement = DefaultAfIncrement, double afMax = DefaultAfMax)
{
if (afStart <= 0)
{
@@ -138,7 +138,7 @@ public sealed class Psar : ITValuePublisher
LastValidClose: double.NaN);
_ps = _s;
Name = $"Psar({afStart:F2},{afIncrement:F2},{afMax:F2})";
Name = $"Sar({afStart:F2},{afIncrement:F2},{afMax:F2})";
WarmupPeriod = 1;
_barHandler = HandleBar;
}
@@ -146,7 +146,7 @@ public sealed class Psar : ITValuePublisher
/// <summary>
/// Creates a Parabolic SAR chained to a TBarSeries source.
/// </summary>
public Psar(TBarSeries source, double afStart = DefaultAfStart, double afIncrement = DefaultAfIncrement, double afMax = DefaultAfMax)
public Sar(TBarSeries source, double afStart = DefaultAfStart, double afIncrement = DefaultAfIncrement, double afMax = DefaultAfMax)
: this(afStart, afIncrement, afMax)
{
Prime(source);
@@ -297,7 +297,7 @@ public sealed class Psar : ITValuePublisher
}
}
Sar = sarResult;
SarValue = sarResult;
_s = s;
Last = new TValue(input.Time, sarResult);
@@ -389,7 +389,7 @@ public sealed class Psar : ITValuePublisher
LastValidLow: double.NaN,
LastValidClose: double.NaN);
_ps = _s;
Sar = double.NaN;
SarValue = double.NaN;
Last = default;
}
@@ -432,7 +432,7 @@ public sealed class Psar : ITValuePublisher
}
// Compute via streaming instance for correctness (state machine prevents SIMD)
var indicator = new Psar(afStart, afIncrement, afMax);
var indicator = new Sar(afStart, afIncrement, afMax);
long baseTime = DateTime.UtcNow.Ticks;
for (int i = 0; i < len; i++)
@@ -440,7 +440,7 @@ public sealed class Psar : ITValuePublisher
_ = indicator.Update(
new TBar(baseTime + i, open[i], high[i], low[i], close[i], 0),
isNew: true);
output[i] = indicator.Sar;
output[i] = indicator.SarValue;
}
}
@@ -467,10 +467,10 @@ public sealed class Psar : ITValuePublisher
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static (TSeries Results, Psar Indicator) Calculate(
public static (TSeries Results, Sar Indicator) Calculate(
TBarSeries source, double afStart = DefaultAfStart, double afIncrement = DefaultAfIncrement, double afMax = DefaultAfMax)
{
var indicator = new Psar(afStart, afIncrement, afMax);
var indicator = new Sar(afStart, afIncrement, afMax);
var results = indicator.Update(source);
return (results, indicator);
}
@@ -1,4 +1,4 @@
# PSAR: Parabolic Stop And Reverse
# SAR: Parabolic Stop And Reverse
> *The trend is your friend until the end when it bends.*
@@ -7,28 +7,28 @@
| **Category** | Reversal |
| **Inputs** | OHLCV bar (TBar) |
| **Parameters** | `afStart` (default 0.02), `afIncrement` (default 0.02), `afMax` (default 0.20) |
| **Outputs** | Single series (Psar) |
| **Outputs** | Single series (Sar) |
| **Output range** | Varies (see docs) |
| **Warmup** | `1` bars |
| **PineScript** | [psar.pine](psar.pine) |
| **PineScript** | [sar.pine](sar.pine) |
- The Parabolic Stop And Reverse (PSAR) is a trend-following overlay indicator created by J.
- The Parabolic Stop And Reverse (SAR) is a trend-following overlay indicator created by J.
- **Similar:** [Super](../../dynamics/super/Super.md), [Chandelier](../chandelier/Chandelier.md) | **Complementary:** ADX for trend confirmation | **Trading note:** Wilder's Parabolic SAR; trailing stop that accelerates. Dots flip on reversal. Classic trend-following exit.
- Validated against TA-Lib, Skender, and Tulip reference implementations where available.
## Introduction
The Parabolic Stop And Reverse (PSAR) is a trend-following overlay indicator created by J. Welles Wilder Jr. in 1978. It produces a trailing stop level that accelerates toward price as the trend extends, then flips to the opposite side when price crosses the stop. The acceleration mechanism is the key differentiator: SAR starts slow and tightens progressively, creating the characteristic parabolic curve that gives the indicator its name. Default parameters (0.02 start, 0.02 increment, 0.20 maximum) produce approximately 1030 reversals per 500 bars on typical equity data.
The Parabolic Stop And Reverse (SAR) is a trend-following overlay indicator created by J. Welles Wilder Jr. in 1978. It produces a trailing stop level that accelerates toward price as the trend extends, then flips to the opposite side when price crosses the stop. The acceleration mechanism is the key differentiator: SAR starts slow and tightens progressively, creating the characteristic parabolic curve that gives the indicator its name. Default parameters (0.02 start, 0.02 increment, 0.20 maximum) produce approximately 1030 reversals per 500 bars on typical equity data.
## Historical Context
Wilder introduced PSAR alongside RSI, ATR, and ADX in *New Concepts in Technical Trading Systems* (1978). Unlike fixed-percentage trailing stops, PSAR uses an acceleration factor (AF) that increases each time price makes a new extreme in the trend direction, creating time-dependent tightening. This was novel for 1978: most trailing stops were static. The parabolic shape emerges because SAR converges on price at an accelerating rate, mathematically similar to a particle under constant acceleration. Most implementations today follow Wilder's original specification with minor variations in initialization logic (first-bar handling).
Wilder introduced SAR alongside RSI, ATR, and ADX in *New Concepts in Technical Trading Systems* (1978). Unlike fixed-percentage trailing stops, SAR uses an acceleration factor (AF) that increases each time price makes a new extreme in the trend direction, creating time-dependent tightening. This was novel for 1978: most trailing stops were static. The parabolic shape emerges because SAR converges on price at an accelerating rate, mathematically similar to a particle under constant acceleration. Most implementations today follow Wilder's original specification with minor variations in initialization logic (first-bar handling).
## Architecture and Physics
### 1. State Machine
PSAR operates as a two-state machine: **Long** (uptrend) and **Short** (downtrend). Each state tracks three variables:
SAR operates as a two-state machine: **Long** (uptrend) and **Short** (downtrend). Each state tracks three variables:
- **SAR**: Current stop level
- **EP** (Extreme Point): Highest high in long mode, lowest low in short mode
@@ -115,7 +115,7 @@ O(1) per bar. FMA computes SAR update in a single instruction. Reversal branchin
### SIMD Analysis
PSAR cannot be vectorized. The state machine has data-dependent branches (reversal detection) and sequential dependencies (SAR depends on prior SAR). The Batch API delegates to streaming for correctness.
SAR cannot be vectorized. The state machine has data-dependent branches (reversal detection) and sequential dependencies (SAR depends on prior SAR). The Batch API delegates to streaming for correctness.
### Quality Metrics (110 Scale)
@@ -139,7 +139,7 @@ Note: Different libraries may vary on first-bar initialization (close > open vs.
## Common Pitfalls
1. **Whipsaw in ranges**: PSAR reverses on every price crossover. In tight ranges, this produces rapid alternation. Mitigation: combine with ADX filter (only follow PSAR when ADX > 25). Impact: 3050% of signals may be false in ranging markets.
1. **Whipsaw in ranges**: SAR reverses on every price crossover. In tight ranges, this produces rapid alternation. Mitigation: combine with ADX filter (only follow SAR when ADX > 25). Impact: 3050% of signals may be false in ranging markets.
2. **AF sensitivity**: Setting afStart too high (e.g., 0.10) makes SAR track price so tightly that minor retracements trigger reversals. Setting afMax too low (e.g., 0.05) makes SAR lag badly in strong trends.
@@ -1,7 +1,7 @@
// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Parabolic SAR", "PSAR", overlay=true)
indicator("Parabolic SAR", "SAR", overlay=true)
//@function Calculates Parabolic Stop And Reverse (SAR)
//@param af_start Initial acceleration factor (Wilder's original: 0.02)
@@ -9,7 +9,7 @@ indicator("Parabolic SAR", "PSAR", overlay=true)
//@param af_max Maximum acceleration factor (Wilder's original: 0.20)
//@returns SAR value (stop level for current trend)
//@optimized Minimal state variables, O(1) per bar
psar(simple float af_start=0.02, simple float af_increment=0.02, simple float af_max=0.20) =>
sar(simple float af_start=0.02, simple float af_increment=0.02, simple float af_max=0.20) =>
if af_start <= 0 or af_start > af_max
runtime.error("Start AF must be > 0 and <= Max AF")
if af_increment <= 0
@@ -67,11 +67,11 @@ i_af_increment = input.float(0.02, "AF Increment", minval=0.001, maxval=1.0, ste
i_af_max = input.float(0.20, "Max AF", minval=0.001, maxval=1.0, step=0.01)
// Calculation
psar = psar(i_af_start, i_af_increment, i_af_max)
psar_above = psar > close ? psar : na
psar_below = psar < close ? psar : na
sar = sar(i_af_start, i_af_increment, i_af_max)
psar_above = sar > close ? sar : na
psar_below = sar < close ? sar : na
// Plot
plot(psar_above, "PSAR Above", color=color.red, style=plot.style_linebr, linewidth=2)
plot(psar_below, "PSAR Below", color=color.green, style=plot.style_linebr, linewidth=2)
plot(psar_above, "SAR Above", color=color.red, style=plot.style_linebr, linewidth=2)
plot(psar_below, "SAR Below", color=color.green, style=plot.style_linebr, linewidth=2)
@@ -3,55 +3,55 @@ using QuanTAlib;
namespace QuanTAlib.Tests;
public sealed class PsarIndicatorTests
public sealed class SarIndicatorTests
{
[Fact]
public void PsarIndicator_Constructor_SetsDefaults()
public void SarIndicator_Constructor_SetsDefaults()
{
var indicator = new PsarIndicator();
var indicator = new SarIndicator();
Assert.Equal(0.02, indicator.AfStart);
Assert.Equal(0.02, indicator.AfIncrement);
Assert.Equal(0.20, indicator.AfMax);
Assert.True(indicator.ShowColdValues);
Assert.Contains("PSAR", indicator.Name, StringComparison.Ordinal);
Assert.Contains("SAR", indicator.Name, StringComparison.Ordinal);
Assert.False(indicator.SeparateWindow);
Assert.True(indicator.OnBackGround);
}
[Fact]
public void PsarIndicator_MinHistoryDepths_EqualsZero()
public void SarIndicator_MinHistoryDepths_EqualsZero()
{
var indicator = new PsarIndicator();
var indicator = new SarIndicator();
Assert.Equal(0, PsarIndicator.MinHistoryDepths);
Assert.Equal(0, SarIndicator.MinHistoryDepths);
IWatchlistIndicator watchlistIndicator = indicator;
Assert.Equal(0, watchlistIndicator.MinHistoryDepths);
}
[Fact]
public void PsarIndicator_ShortName_IncludesParameters()
public void SarIndicator_ShortName_IncludesParameters()
{
var indicator = new PsarIndicator { AfStart = 0.02, AfMax = 0.20 };
var indicator = new SarIndicator { AfStart = 0.02, AfMax = 0.20 };
indicator.Initialize();
Assert.Contains("PSAR", indicator.ShortName, StringComparison.Ordinal);
Assert.Contains("SAR", indicator.ShortName, StringComparison.Ordinal);
Assert.Contains("0.02", indicator.ShortName, StringComparison.Ordinal);
}
[Fact]
public void PsarIndicator_SourceCodeLink_IsValid()
public void SarIndicator_SourceCodeLink_IsValid()
{
var indicator = new PsarIndicator();
var indicator = new SarIndicator();
Assert.Contains("github.com", indicator.SourceCodeLink, StringComparison.Ordinal);
Assert.Contains("Psar", indicator.SourceCodeLink, StringComparison.Ordinal);
Assert.Contains("Sar", indicator.SourceCodeLink, StringComparison.Ordinal);
}
[Fact]
public void PsarIndicator_Initialize_CreatesInternalIndicator()
public void SarIndicator_Initialize_CreatesInternalIndicator()
{
var indicator = new PsarIndicator();
var indicator = new SarIndicator();
indicator.Initialize();
@@ -60,9 +60,9 @@ public sealed class PsarIndicatorTests
}
[Fact]
public void PsarIndicator_ProcessUpdate_HistoricalBar_ComputesValue()
public void SarIndicator_ProcessUpdate_HistoricalBar_ComputesValue()
{
var indicator = new PsarIndicator { AfStart = 0.02, AfIncrement = 0.02, AfMax = 0.20 };
var indicator = new SarIndicator { AfStart = 0.02, AfIncrement = 0.02, AfMax = 0.20 };
indicator.Initialize();
var now = DateTime.UtcNow;
@@ -80,9 +80,9 @@ public sealed class PsarIndicatorTests
}
[Fact]
public void PsarIndicator_ProcessUpdate_NewBar_ComputesValue()
public void SarIndicator_ProcessUpdate_NewBar_ComputesValue()
{
var indicator = new PsarIndicator { AfStart = 0.02, AfIncrement = 0.02, AfMax = 0.20 };
var indicator = new SarIndicator { AfStart = 0.02, AfIncrement = 0.02, AfMax = 0.20 };
indicator.Initialize();
var now = DateTime.UtcNow;
@@ -104,9 +104,9 @@ public sealed class PsarIndicatorTests
}
[Fact]
public void PsarIndicator_SingleLineSeries_IsPresent()
public void SarIndicator_SingleLineSeries_IsPresent()
{
var indicator = new PsarIndicator();
var indicator = new SarIndicator();
indicator.Initialize();
var now = DateTime.UtcNow;
@@ -122,9 +122,9 @@ public sealed class PsarIndicatorTests
}
[Fact]
public void PsarIndicator_Description_IsSet()
public void SarIndicator_Description_IsSet()
{
var indicator = new PsarIndicator();
var indicator = new SarIndicator();
Assert.NotNull(indicator.Description);
Assert.NotEmpty(indicator.Description);
@@ -1,85 +1,85 @@
// PSAR Tests - Parabolic Stop And Reverse
// SAR Tests - Parabolic Stop And Reverse
namespace QuanTAlib.Tests;
// ── A) Constructor Validation ────────────────────────────────────────────
public sealed class PsarConstructorTests
public sealed class SarConstructorTests
{
[Fact]
public void Constructor_ZeroAfStart_Throws()
{
var ex = Assert.Throws<ArgumentException>(() => new Psar(afStart: 0));
var ex = Assert.Throws<ArgumentException>(() => new Sar(afStart: 0));
Assert.Equal("afStart", ex.ParamName);
}
[Fact]
public void Constructor_NegativeAfStart_Throws()
{
var ex = Assert.Throws<ArgumentException>(() => new Psar(afStart: -0.01));
var ex = Assert.Throws<ArgumentException>(() => new Sar(afStart: -0.01));
Assert.Equal("afStart", ex.ParamName);
}
[Fact]
public void Constructor_ZeroAfIncrement_Throws()
{
var ex = Assert.Throws<ArgumentException>(() => new Psar(afIncrement: 0));
var ex = Assert.Throws<ArgumentException>(() => new Sar(afIncrement: 0));
Assert.Equal("afIncrement", ex.ParamName);
}
[Fact]
public void Constructor_NegativeAfIncrement_Throws()
{
var ex = Assert.Throws<ArgumentException>(() => new Psar(afIncrement: -0.01));
var ex = Assert.Throws<ArgumentException>(() => new Sar(afIncrement: -0.01));
Assert.Equal("afIncrement", ex.ParamName);
}
[Fact]
public void Constructor_AfMaxEqualAfStart_Throws()
{
var ex = Assert.Throws<ArgumentException>(() => new Psar(afStart: 0.02, afMax: 0.02));
var ex = Assert.Throws<ArgumentException>(() => new Sar(afStart: 0.02, afMax: 0.02));
Assert.Equal("afMax", ex.ParamName);
}
[Fact]
public void Constructor_AfMaxLessThanAfStart_Throws()
{
var ex = Assert.Throws<ArgumentException>(() => new Psar(afStart: 0.10, afMax: 0.05));
var ex = Assert.Throws<ArgumentException>(() => new Sar(afStart: 0.10, afMax: 0.05));
Assert.Equal("afStart", ex.ParamName);
}
[Fact]
public void Constructor_ValidDefaults_SetsProperties()
{
var psar = new Psar();
var sar = new Sar();
Assert.Equal(0.02, psar.AfStart);
Assert.Equal(0.02, psar.AfIncrement);
Assert.Equal(0.20, psar.AfMax);
Assert.Equal(1, psar.WarmupPeriod);
Assert.Contains("Psar", psar.Name, StringComparison.Ordinal);
Assert.Equal(0.02, sar.AfStart);
Assert.Equal(0.02, sar.AfIncrement);
Assert.Equal(0.20, sar.AfMax);
Assert.Equal(1, sar.WarmupPeriod);
Assert.Contains("Sar", sar.Name, StringComparison.Ordinal);
}
[Fact]
public void Constructor_CustomParams_SetsProperties()
{
var psar = new Psar(afStart: 0.01, afIncrement: 0.01, afMax: 0.10);
var sar = new Sar(afStart: 0.01, afIncrement: 0.01, afMax: 0.10);
Assert.Equal(0.01, psar.AfStart);
Assert.Equal(0.01, psar.AfIncrement);
Assert.Equal(0.10, psar.AfMax);
Assert.Equal(0.01, sar.AfStart);
Assert.Equal(0.01, sar.AfIncrement);
Assert.Equal(0.10, sar.AfMax);
}
}
// ── B) Basic Calculation ─────────────────────────────────────────────────
public sealed class PsarBasicTests
public sealed class SarBasicTests
{
[Fact]
public void Update_ReturnsTValue()
{
var psar = new Psar();
var sar = new Sar();
var bar = new TBar(DateTime.UtcNow, 100, 95, 98, 97, 1000);
TValue result = psar.Update(bar);
TValue result = sar.Update(bar);
Assert.IsType<TValue>(result);
}
@@ -87,123 +87,123 @@ public sealed class PsarBasicTests
[Fact]
public void Update_Last_IsAccessible()
{
var psar = new Psar();
var sar = new Sar();
var bar = new TBar(DateTime.UtcNow, 100, 95, 98, 97, 1000);
_ = psar.Update(bar);
_ = sar.Update(bar);
Assert.True(double.IsFinite(psar.Last.Value) || double.IsNaN(psar.Last.Value));
Assert.True(double.IsFinite(sar.Last.Value) || double.IsNaN(sar.Last.Value));
}
[Fact]
public void Update_Sar_IsAccessible()
{
var psar = new Psar();
var sar = new Sar();
// Feed enough bars
for (int i = 0; i < 5; i++)
{
double price = 100.0 + i;
_ = psar.Update(new TBar(DateTime.UtcNow.AddMinutes(i),
_ = sar.Update(new TBar(DateTime.UtcNow.AddMinutes(i),
price + 2, price - 2, price + 1, price, 1000));
}
Assert.True(double.IsFinite(psar.Sar));
Assert.True(double.IsFinite(sar.SarValue));
}
[Fact]
public void Name_ContainsParameters()
{
var psar = new Psar(afStart: 0.01, afIncrement: 0.02, afMax: 0.10);
var sar = new Sar(afStart: 0.01, afIncrement: 0.02, afMax: 0.10);
Assert.Contains("0.01", psar.Name, StringComparison.Ordinal);
Assert.Contains("0.10", psar.Name, StringComparison.Ordinal);
Assert.Contains("0.01", sar.Name, StringComparison.Ordinal);
Assert.Contains("0.10", sar.Name, StringComparison.Ordinal);
}
[Fact]
public void FirstBar_Uptrend_SarEqualsLow()
{
var psar = new Psar();
var sar = new Sar();
// Close(105) > Open(95) → long mode → SAR = low(90)
_ = psar.Update(new TBar(DateTime.UtcNow, 95, 110, 90, 105, 1000));
_ = sar.Update(new TBar(DateTime.UtcNow, 95, 110, 90, 105, 1000));
Assert.Equal(90.0, psar.Sar);
Assert.True(psar.IsLong);
Assert.Equal(90.0, sar.SarValue);
Assert.True(sar.IsLong);
}
[Fact]
public void FirstBar_Downtrend_SarEqualsHigh()
{
var psar = new Psar();
var sar = new Sar();
// Close(90) < Open(105) → short mode → SAR = high(110)
_ = psar.Update(new TBar(DateTime.UtcNow, 105, 110, 85, 90, 1000));
_ = sar.Update(new TBar(DateTime.UtcNow, 105, 110, 85, 90, 1000));
Assert.Equal(110.0, psar.Sar);
Assert.False(psar.IsLong);
Assert.Equal(110.0, sar.SarValue);
Assert.False(sar.IsLong);
}
[Fact]
public void Sar_BelowPrice_InUptrend()
{
var psar = new Psar();
var sar = new Sar();
// Steady uptrend - SAR should trail below
for (int i = 0; i < 20; i++)
{
double price = 100.0 + (i * 2);
_ = psar.Update(new TBar(DateTime.UtcNow.AddMinutes(i),
double price = 100.0 + i * 2;
_ = sar.Update(new TBar(DateTime.UtcNow.AddMinutes(i),
price + 1, price - 1, price + 0.5, price, 1000));
}
double lastClose = 100.0 + (19 * 2);
Assert.True(psar.Sar < lastClose, "SAR should be below price in uptrend");
Assert.True(psar.IsLong, "Should be in long mode during uptrend");
double lastClose = 100.0 + 19 * 2;
Assert.True(sar.SarValue < lastClose, "SAR should be below price in uptrend");
Assert.True(sar.IsLong, "Should be in long mode during uptrend");
}
[Fact]
public void Sar_AbovePrice_InDowntrend()
{
var psar = new Psar();
var sar = new Sar();
// Steady downtrend - SAR should trail above
for (int i = 0; i < 20; i++)
{
double price = 200.0 - (i * 2);
_ = psar.Update(new TBar(DateTime.UtcNow.AddMinutes(i),
double price = 200.0 - i * 2;
_ = sar.Update(new TBar(DateTime.UtcNow.AddMinutes(i),
price + 1, price - 1, price + 0.5, price, 1000));
}
double lastClose = 200.0 - (19 * 2);
Assert.True(psar.Sar > lastClose, "SAR should be above price in downtrend");
Assert.False(psar.IsLong, "Should be in short mode during downtrend");
double lastClose = 200.0 - 19 * 2;
Assert.True(sar.SarValue > lastClose, "SAR should be above price in downtrend");
Assert.False(sar.IsLong, "Should be in short mode during downtrend");
}
[Fact]
public void IsHot_TrueAfterFirstBar()
{
var psar = new Psar();
var sar = new Sar();
Assert.False(psar.IsHot);
Assert.False(sar.IsHot);
_ = psar.Update(new TBar(DateTime.UtcNow, 100, 95, 98, 97, 1000));
_ = sar.Update(new TBar(DateTime.UtcNow, 100, 95, 98, 97, 1000));
Assert.True(psar.IsHot);
Assert.True(sar.IsHot);
}
}
// ── C) State + Bar Correction ────────────────────────────────────────────
public sealed class PsarStateCorrectionTests
public sealed class SarStateCorrectionTests
{
[Fact]
public void IsNew_True_AdvancesState()
{
var psar = new Psar();
var sar = new Sar();
_ = psar.Update(new TBar(DateTime.UtcNow, 105, 95, 100, 100, 1000), isNew: true);
var first = psar.Last;
_ = sar.Update(new TBar(DateTime.UtcNow, 105, 95, 100, 100, 1000), isNew: true);
var first = sar.Last;
_ = psar.Update(new TBar(DateTime.UtcNow.AddMinutes(1), 110, 100, 105, 105, 1000), isNew: true);
var second = psar.Last;
_ = sar.Update(new TBar(DateTime.UtcNow.AddMinutes(1), 110, 100, 105, 105, 1000), isNew: true);
var second = sar.Last;
Assert.NotEqual(first.Time, second.Time);
}
@@ -211,28 +211,28 @@ public sealed class PsarStateCorrectionTests
[Fact]
public void IsNew_False_CorrectionRestoresState()
{
var psar = new Psar();
var sar = new Sar();
var dt = DateTime.UtcNow;
// Feed some bars to warm up
for (int i = 0; i < 5; i++)
{
double price = 100.0 + i;
_ = psar.Update(new TBar(dt.AddMinutes(i), price + 2, price - 2, price + 1, price, 1000), isNew: true);
_ = sar.Update(new TBar(dt.AddMinutes(i), price + 2, price - 2, price + 1, price, 1000), isNew: true);
}
// New bar
_ = psar.Update(new TBar(dt.AddMinutes(5), 110, 105, 108, 107, 1000), isNew: true);
_ = sar.Update(new TBar(dt.AddMinutes(5), 110, 105, 108, 107, 1000), isNew: true);
// Correct the bar (isNew=false with different values)
_ = psar.Update(new TBar(dt.AddMinutes(5), 111, 104, 109, 108, 1000), isNew: false);
_ = sar.Update(new TBar(dt.AddMinutes(5), 111, 104, 109, 108, 1000), isNew: false);
// Another correction should produce same result
_ = psar.Update(new TBar(dt.AddMinutes(5), 111, 104, 109, 108, 1000), isNew: false);
var corrected1 = psar.Sar;
_ = sar.Update(new TBar(dt.AddMinutes(5), 111, 104, 109, 108, 1000), isNew: false);
var corrected1 = sar.SarValue;
_ = psar.Update(new TBar(dt.AddMinutes(5), 111, 104, 109, 108, 1000), isNew: false);
var corrected2 = psar.Sar;
_ = sar.Update(new TBar(dt.AddMinutes(5), 111, 104, 109, 108, 1000), isNew: false);
var corrected2 = sar.SarValue;
Assert.Equal(corrected1, corrected2);
}
@@ -240,23 +240,23 @@ public sealed class PsarStateCorrectionTests
[Fact]
public void IterativeCorrections_ProduceSameResult()
{
var psar = new Psar();
var sar = new Sar();
var dt = DateTime.UtcNow;
for (int i = 0; i < 5; i++)
{
double price = 100.0 + i;
_ = psar.Update(new TBar(dt.AddMinutes(i), price + 2, price - 2, price + 1, price, 1000), isNew: true);
_ = sar.Update(new TBar(dt.AddMinutes(i), price + 2, price - 2, price + 1, price, 1000), isNew: true);
}
// Add new bar then correct 3 times
_ = psar.Update(new TBar(dt.AddMinutes(5), 110, 100, 108, 105, 1000), isNew: true);
_ = sar.Update(new TBar(dt.AddMinutes(5), 110, 100, 108, 105, 1000), isNew: true);
double[] results = new double[3];
for (int i = 0; i < 3; i++)
{
_ = psar.Update(new TBar(dt.AddMinutes(5), 112, 101, 110, 107, 1000), isNew: false);
results[i] = psar.Sar;
_ = sar.Update(new TBar(dt.AddMinutes(5), 112, 101, 110, 107, 1000), isNew: false);
results[i] = sar.SarValue;
}
Assert.Equal(results[0], results[1]);
@@ -266,100 +266,100 @@ public sealed class PsarStateCorrectionTests
[Fact]
public void Reset_ClearsAllState()
{
var psar = new Psar();
var sar = new Sar();
for (int i = 0; i < 10; i++)
{
double price = 100.0 + i;
_ = psar.Update(new TBar(DateTime.UtcNow.AddMinutes(i), price + 2, price - 2, price + 1, price, 1000));
_ = sar.Update(new TBar(DateTime.UtcNow.AddMinutes(i), price + 2, price - 2, price + 1, price, 1000));
}
Assert.True(psar.IsHot);
Assert.True(sar.IsHot);
psar.Reset();
sar.Reset();
Assert.False(psar.IsHot);
Assert.True(double.IsNaN(psar.Sar));
Assert.False(sar.IsHot);
Assert.True(double.IsNaN(sar.SarValue));
}
}
// ── D) Warmup / Convergence ──────────────────────────────────────────────
public sealed class PsarWarmupTests
public sealed class SarWarmupTests
{
[Fact]
public void IsHot_FlipsAfterFirstBar()
{
var psar = new Psar();
var sar = new Sar();
Assert.False(psar.IsHot);
Assert.False(sar.IsHot);
_ = psar.Update(new TBar(DateTime.UtcNow, 100, 95, 98, 97, 1000));
_ = sar.Update(new TBar(DateTime.UtcNow, 100, 95, 98, 97, 1000));
Assert.True(psar.IsHot);
Assert.True(sar.IsHot);
}
[Fact]
public void WarmupPeriod_EqualsOne()
{
var psar = new Psar();
var sar = new Sar();
Assert.Equal(1, psar.WarmupPeriod);
Assert.Equal(1, sar.WarmupPeriod);
}
}
// ── E) Robustness ────────────────────────────────────────────────────────
public sealed class PsarRobustnessTests
public sealed class SarRobustnessTests
{
[Fact]
public void NaN_Input_UsesLastValidValue()
{
var psar = new Psar();
var sar = new Sar();
var dt = DateTime.UtcNow;
// Feed valid bars
for (int i = 0; i < 5; i++)
{
double price = 100.0 + i;
_ = psar.Update(new TBar(dt.AddMinutes(i), price + 2, price - 2, price + 1, price, 1000));
_ = sar.Update(new TBar(dt.AddMinutes(i), price + 2, price - 2, price + 1, price, 1000));
}
// Feed NaN bar
_ = psar.Update(new TBar(dt.AddMinutes(5), double.NaN, double.NaN, double.NaN, double.NaN, 0));
_ = sar.Update(new TBar(dt.AddMinutes(5), double.NaN, double.NaN, double.NaN, double.NaN, 0));
Assert.True(double.IsFinite(psar.Sar));
Assert.True(double.IsFinite(sar.SarValue));
}
[Fact]
public void Infinity_Input_UsesLastValidValue()
{
var psar = new Psar();
var sar = new Sar();
var dt = DateTime.UtcNow;
for (int i = 0; i < 5; i++)
{
double price = 100.0 + i;
_ = psar.Update(new TBar(dt.AddMinutes(i), price + 2, price - 2, price + 1, price, 1000));
_ = sar.Update(new TBar(dt.AddMinutes(i), price + 2, price - 2, price + 1, price, 1000));
}
_ = psar.Update(new TBar(dt.AddMinutes(5),
_ = sar.Update(new TBar(dt.AddMinutes(5),
double.PositiveInfinity, double.NegativeInfinity, double.PositiveInfinity, double.PositiveInfinity, 0));
Assert.True(double.IsFinite(psar.Sar));
Assert.True(double.IsFinite(sar.SarValue));
}
[Fact]
public void FirstBar_NaN_ReturnsNaN()
{
var psar = new Psar();
var sar = new Sar();
_ = psar.Update(new TBar(DateTime.UtcNow, double.NaN, double.NaN, double.NaN, double.NaN, 0));
_ = sar.Update(new TBar(DateTime.UtcNow, double.NaN, double.NaN, double.NaN, double.NaN, 0));
Assert.True(double.IsNaN(psar.Last.Value));
Assert.True(double.IsNaN(sar.Last.Value));
}
}
// ── F) Consistency ───────────────────────────────────────────────────────
public sealed class PsarConsistencyTests
public sealed class SarConsistencyTests
{
private static TBarSeries CreateGbmBars(int count = 500)
{
@@ -373,16 +373,16 @@ public sealed class PsarConsistencyTests
var bars = CreateGbmBars();
// Streaming
var streaming = new Psar();
var streaming = new Sar();
var streamResults = new double[bars.Count];
for (int i = 0; i < bars.Count; i++)
{
_ = streaming.Update(bars[i], isNew: true);
streamResults[i] = streaming.Sar;
streamResults[i] = streaming.SarValue;
}
// Batch
var batchResults = Psar.Batch(bars);
var batchResults = Sar.Batch(bars);
for (int i = 0; i < bars.Count; i++)
{
@@ -393,8 +393,8 @@ public sealed class PsarConsistencyTests
[Fact]
public void TValue_Update_MatchesTBar_Update()
{
var ch1 = new Psar();
var ch2 = new Psar();
var ch1 = new Sar();
var ch2 = new Sar();
double[] prices = [100, 102, 98, 105, 99, 103, 107, 95, 110, 108];
@@ -407,35 +407,35 @@ public sealed class PsarConsistencyTests
_ = ch2.Update(new TValue(DateTime.UtcNow.AddMinutes(i), p), isNew: true);
}
Assert.Equal(ch1.Sar, ch2.Sar);
Assert.Equal(ch1.SarValue, ch2.SarValue);
}
[Fact]
public void Reversal_DetectedOnPriceCrossover()
{
var psar = new Psar();
var sar = new Sar();
var dt = DateTime.UtcNow;
// Start in uptrend
_ = psar.Update(new TBar(dt, 100, 90, 95, 105, 1000), isNew: true);
Assert.True(psar.IsLong);
_ = sar.Update(new TBar(dt, 100, 90, 95, 105, 1000), isNew: true);
Assert.True(sar.IsLong);
// Continue uptrend
for (int i = 1; i <= 5; i++)
{
double price = 105 + (i * 2);
_ = psar.Update(new TBar(dt.AddMinutes(i),
double price = 105 + i * 2;
_ = sar.Update(new TBar(dt.AddMinutes(i),
price + 1, price - 1, price + 0.5, price, 1000), isNew: true);
}
Assert.True(psar.IsLong);
Assert.True(sar.IsLong);
// Sharp reversal — price drops below SAR
double sarBeforeReversal = psar.Sar;
_ = psar.Update(new TBar(dt.AddMinutes(10),
double sarBeforeReversal = sar.SarValue;
_ = sar.Update(new TBar(dt.AddMinutes(10),
sarBeforeReversal - 5, sarBeforeReversal - 20,
sarBeforeReversal - 18, sarBeforeReversal - 15, 1000), isNew: true);
Assert.False(psar.IsLong, "Should reverse to short after price crosses below SAR");
Assert.False(sar.IsLong, "Should reverse to short after price crosses below SAR");
}
[Fact]
@@ -444,29 +444,29 @@ public sealed class PsarConsistencyTests
var bars = CreateGbmBars(100);
// Streaming
var streaming = new Psar();
var streaming = new Sar();
for (int i = 0; i < bars.Count; i++)
{
_ = streaming.Update(bars[i], isNew: true);
}
double streamLast = streaming.Sar;
double streamLast = streaming.SarValue;
// TSeries batch
var batch = new Psar();
var batch = new Sar();
_ = batch.Update(bars);
Assert.Equal(streamLast, batch.Sar, precision: 10);
Assert.Equal(streamLast, batch.SarValue, precision: 10);
}
}
// ── G) Span API Tests ────────────────────────────────────────────────────
public sealed class PsarSpanTests
public sealed class SarSpanTests
{
[Fact]
public void Batch_Span_InvalidAfStart_Throws()
{
var ex = Assert.Throws<ArgumentException>(() =>
Psar.Batch(new double[10], new double[10], new double[10], new double[10], new double[10], afStart: 0));
Sar.Batch(new double[10], new double[10], new double[10], new double[10], new double[10], afStart: 0));
Assert.Equal("afStart", ex.ParamName);
}
@@ -474,7 +474,7 @@ public sealed class PsarSpanTests
public void Batch_Span_MismatchedLengths_Throws()
{
var ex = Assert.Throws<ArgumentException>(() =>
Psar.Batch(new double[10], new double[10], new double[5], new double[10], new double[10]));
Sar.Batch(new double[10], new double[10], new double[5], new double[10], new double[10]));
Assert.Equal("high", ex.ParamName);
}
@@ -482,7 +482,7 @@ public sealed class PsarSpanTests
public void Batch_Span_OutputTooShort_Throws()
{
var ex = Assert.Throws<ArgumentException>(() =>
Psar.Batch(new double[10], new double[10], new double[10], new double[10], new double[5]));
Sar.Batch(new double[10], new double[10], new double[10], new double[10], new double[5]));
Assert.Equal("output", ex.ParamName);
}
@@ -491,24 +491,24 @@ public sealed class PsarSpanTests
{
var output = Array.Empty<double>();
var ex = Record.Exception(() =>
Psar.Batch(ReadOnlySpan<double>.Empty, ReadOnlySpan<double>.Empty,
Sar.Batch(ReadOnlySpan<double>.Empty, ReadOnlySpan<double>.Empty,
ReadOnlySpan<double>.Empty, ReadOnlySpan<double>.Empty, output.AsSpan()));
Assert.Null(ex);
}
}
// ── H) Event / Chainability ──────────────────────────────────────────────
public sealed class PsarEventTests
public sealed class SarEventTests
{
[Fact]
public void Pub_FiresOnUpdate()
{
var psar = new Psar();
var sar = new Sar();
int fireCount = 0;
psar.Pub += (object? _, in TValueEventArgs _e) => { fireCount++; };
sar.Pub += (object? _, in TValueEventArgs _e) => { fireCount++; };
_ = psar.Update(new TBar(DateTime.UtcNow, 100, 95, 98, 97, 1000));
_ = sar.Update(new TBar(DateTime.UtcNow, 100, 95, 98, 97, 1000));
Assert.Equal(1, fireCount);
}
@@ -516,15 +516,15 @@ public sealed class PsarEventTests
[Fact]
public void Pub_FiresOnEachUpdate()
{
var psar = new Psar();
var sar = new Sar();
int fireCount = 0;
psar.Pub += (object? _, in TValueEventArgs _e) => { fireCount++; };
sar.Pub += (object? _, in TValueEventArgs _e) => { fireCount++; };
for (int i = 0; i < 5; i++)
{
double price = 100.0 + i;
_ = psar.Update(new TBar(DateTime.UtcNow.AddMinutes(i), price + 2, price - 2, price + 1, price, 1000));
_ = sar.Update(new TBar(DateTime.UtcNow.AddMinutes(i), price + 2, price - 2, price + 1, price, 1000));
}
Assert.Equal(5, fireCount);
@@ -532,7 +532,7 @@ public sealed class PsarEventTests
}
// ── I) Prime Tests ───────────────────────────────────────────────────────
public sealed class PsarPrimeTests
public sealed class SarPrimeTests
{
[Fact]
public void Prime_TBarSeries_SetsState()
@@ -540,21 +540,21 @@ public sealed class PsarPrimeTests
var gbm = new GBM(startPrice: 100.0, mu: 0.05, sigma: 0.20, seed: 42);
var bars = gbm.Fetch(50, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
var psar = new Psar();
psar.Prime(bars);
var sar = new Sar();
sar.Prime(bars);
Assert.True(psar.IsHot);
Assert.True(double.IsFinite(psar.Sar));
Assert.True(sar.IsHot);
Assert.True(double.IsFinite(sar.SarValue));
}
[Fact]
public void Prime_EmptySource_NoException()
{
var psar = new Psar();
var sar = new Sar();
var bars = new TBarSeries();
var ex = Record.Exception(() => psar.Prime(bars));
var ex = Record.Exception(() => sar.Prime(bars));
Assert.Null(ex);
Assert.False(psar.IsHot);
Assert.False(sar.IsHot);
}
}
@@ -1,4 +1,4 @@
// PSAR Validation Tests - Parabolic Stop And Reverse
// SAR Validation Tests - Parabolic Stop And Reverse
// Cross-validated against Skender.Stock.Indicators GetParabolicSar(), TALib SAR, and OoplesFinance CalculateParabolicSAR.
using OoplesFinance.StockIndicators;
@@ -8,7 +8,7 @@ using TALib;
namespace QuanTAlib.Tests;
public sealed class PsarValidationTests
public sealed class SarValidationTests
{
private static TBarSeries CreateGbmBars(int count = 500, int seed = 42)
{
@@ -29,12 +29,12 @@ public sealed class PsarValidationTests
.ToList();
// QuanTAlib streaming
var psar = new Psar(afStart: 0.02, afIncrement: 0.02, afMax: 0.20);
var sar = new Sar(afStart: 0.02, afIncrement: 0.02, afMax: 0.20);
var ourValues = new double[_data.Bars.Count];
for (int i = 0; i < _data.Bars.Count; i++)
{
_ = psar.Update(_data.Bars[i], isNew: true);
ourValues[i] = psar.Sar;
_ = sar.Update(_data.Bars[i], isNew: true);
ourValues[i] = sar.SarValue;
}
// Compare warm values (skip first bar where SAR is initialization)
@@ -63,16 +63,16 @@ public sealed class PsarValidationTests
var bars = CreateGbmBars();
// Streaming
var streaming = new Psar();
var streaming = new Sar();
var streamValues = new double[bars.Count];
for (int i = 0; i < bars.Count; i++)
{
_ = streaming.Update(bars[i], isNew: true);
streamValues[i] = streaming.Sar;
streamValues[i] = streaming.SarValue;
}
// Batch
var batchResults = Psar.Batch(bars);
var batchResults = Sar.Batch(bars);
for (int i = 0; i < bars.Count; i++)
{
@@ -88,17 +88,17 @@ public sealed class PsarValidationTests
var bars = CreateGbmBars();
// Streaming
var streaming = new Psar();
var streaming = new Sar();
var streamValues = new double[bars.Count];
for (int i = 0; i < bars.Count; i++)
{
_ = streaming.Update(bars[i], isNew: true);
streamValues[i] = streaming.Sar;
streamValues[i] = streaming.SarValue;
}
// Span
var spanOutput = new double[bars.Count];
Psar.Batch(bars.OpenValues, bars.HighValues, bars.LowValues, bars.CloseValues, spanOutput);
Sar.Batch(bars.OpenValues, bars.HighValues, bars.LowValues, bars.CloseValues, spanOutput);
for (int i = 0; i < bars.Count; i++)
{
@@ -113,8 +113,8 @@ public sealed class PsarValidationTests
{
var bars = CreateGbmBars(count: 100);
var slow = new Psar(afStart: 0.01, afIncrement: 0.01, afMax: 0.20);
var fast = new Psar(afStart: 0.10, afIncrement: 0.05, afMax: 0.50);
var slow = new Sar(afStart: 0.01, afIncrement: 0.01, afMax: 0.20);
var fast = new Sar(afStart: 0.10, afIncrement: 0.05, afMax: 0.50);
for (int i = 0; i < bars.Count; i++)
{
@@ -124,8 +124,8 @@ public sealed class PsarValidationTests
// Higher AF = more responsive = SAR closer to price
// Just verify both produce finite output (direction depends on data)
Assert.True(double.IsFinite(slow.Sar));
Assert.True(double.IsFinite(fast.Sar));
Assert.True(double.IsFinite(slow.SarValue));
Assert.True(double.IsFinite(fast.SarValue));
}
// ── Determinism ──────────────────────────────────────────────────────
@@ -135,8 +135,8 @@ public sealed class PsarValidationTests
{
var bars = CreateGbmBars(count: 200, seed: 123);
var psar1 = new Psar();
var psar2 = new Psar();
var psar1 = new Sar();
var psar2 = new Sar();
for (int i = 0; i < bars.Count; i++)
{
@@ -144,7 +144,7 @@ public sealed class PsarValidationTests
_ = psar2.Update(bars[i], isNew: true);
}
Assert.Equal(psar1.Sar, psar2.Sar);
Assert.Equal(psar1.SarValue, psar2.SarValue);
}
// ── Calculate Returns Valid Indicator ─────────────────────────────────
@@ -154,12 +154,12 @@ public sealed class PsarValidationTests
{
var bars = CreateGbmBars(count: 100);
var (results, indicator) = Psar.Calculate(bars);
var (results, indicator) = Sar.Calculate(bars);
Assert.NotNull(results);
Assert.Equal(bars.Count, results.Count);
Assert.True(indicator.IsHot);
Assert.True(double.IsFinite(indicator.Sar));
Assert.True(double.IsFinite(indicator.SarValue));
}
// ── Reversal Count Is Reasonable ─────────────────────────────────────
@@ -168,20 +168,20 @@ public sealed class PsarValidationTests
public void ReversalCount_IsReasonable()
{
var bars = CreateGbmBars(count: 500);
var psar = new Psar();
var sar = new Sar();
int reversals = 0;
bool prevIsLong = true;
for (int i = 0; i < bars.Count; i++)
{
_ = psar.Update(bars[i], isNew: true);
_ = sar.Update(bars[i], isNew: true);
if (i > 0 && psar.IsLong != prevIsLong)
if (i > 0 && sar.IsLong != prevIsLong)
{
reversals++;
}
prevIsLong = psar.IsLong;
prevIsLong = sar.IsLong;
}
// In 500 bars of GBM data, expect several reversals but not every bar
@@ -216,12 +216,12 @@ public sealed class PsarValidationTests
Assert.True(length > 100, $"TALib SAR produced only {length} values");
// QuanTAlib streaming
var psar = new Psar(afStart: afStep, afIncrement: afStep, afMax: afMax);
var sar = new Sar(afStart: afStep, afIncrement: afStep, afMax: afMax);
var qlSar = new double[_data.Bars.Count];
for (int i = 0; i < _data.Bars.Count; i++)
{
_ = psar.Update(_data.Bars[i], isNew: true);
qlSar[i] = psar.Sar;
_ = sar.Update(_data.Bars[i], isNew: true);
qlSar[i] = sar.SarValue;
}
// Skip the first ~5 bars (initialization divergence), then require exact match.
@@ -250,13 +250,13 @@ public sealed class PsarValidationTests
/// <summary>
/// Structural validation against Ooples <c>CalculateParabolicSAR</c>.
/// Ooples PSAR uses the same Wilder acceleration factor algorithm (start=0.02, increment=0.02, max=0.2).
/// Ooples SAR uses the same Wilder acceleration factor algorithm (start=0.02, increment=0.02, max=0.2).
/// Cross-library numeric equality is not asserted because reversal-point initialization
/// diverges across implementations when the very first bar direction is ambiguous.
/// Both must produce finite, positive output on the same OHLCV data.
/// </summary>
[Fact]
public void Psar_MatchesOoples_Structural()
public void Sar_MatchesOoples_Structural()
{
var _data = new ValidationTestData();
@@ -274,14 +274,14 @@ public sealed class PsarValidationTests
var oResult = stockData.CalculateParabolicSAR(start: 0.02, increment: 0.02, maximum: 0.2);
var oValues = oResult.OutputValues.Values.First();
var psar = new Psar(afStart: 0.02, afIncrement: 0.02, afMax: 0.20);
var sar = new Sar(afStart: 0.02, afIncrement: 0.02, afMax: 0.20);
var qValues = new System.Collections.Generic.List<double>();
foreach (var bar in _data.Data)
{
qValues.Add(psar.Update(bar).Value);
qValues.Add(sar.Update(bar).Value);
}
Assert.True(oValues.Count > 0, "Ooples PSAR must produce output");
Assert.True(oValues.Count > 0, "Ooples SAR must produce output");
int finiteCount = 0;
int warmup = 5;
@@ -293,7 +293,7 @@ public sealed class PsarValidationTests
}
}
Assert.True(finiteCount > 100, $"Expected >100 finite positive PSAR pairs, got {finiteCount}");
Assert.True(finiteCount > 100, $"Expected >100 finite positive SAR pairs, got {finiteCount}");
_data.Dispose();
}
+1 -1
View File
@@ -13,7 +13,7 @@
| **PineScript** | [swings.pine](swings.pine) |
- Swing High/Low detection identifies local price extremes using a configurable lookback window.
- **Similar:** [Fractal](../../oscillators/fisher/Fisher.md), [ZigZag](../psar/Psar.md) | **Complementary:** Volume for confirmation | **Trading note:** Swing high/low detector; identifies pivots for support/resistance and chart pattern analysis.
- **Similar:** [Fractal](../../oscillators/fisher/Fisher.md), [ZigZag](../sar/Sar.md) | **Complementary:** Volume for confirmation | **Trading note:** Swing high/low detector; identifies pivots for support/resistance and chart pattern analysis.
- Validated against TA-Lib, Skender, and Tulip reference implementations where available.
Swing High/Low detection identifies local price extremes using a configurable lookback window. A Swing High marks a bar whose high strictly exceeds the highs of all bars within the lookback window on each side. A Swing Low marks a bar whose low is strictly less than all corresponding lows. The lookback parameter controls sensitivity: larger lookback windows require more confirmation and produce fewer, more significant signals. This generalizes Williams' fixed five-bar Fractals into a flexible structural analysis tool.
+1 -1
View File
@@ -9,7 +9,7 @@ Statistical tools applied to price and returns. These indicators quantify relati
| [BETA](beta/Beta.md) | Beta Coefficient | Asset volatility relative to market. β=1 means market-matched risk. |
| [CMA](cma/Cma.md) | Cumulative Moving Average | Running average of all values. Welford's algorithm. No window. |
| [COINTEGRATION](cointegration/Cointegration.md) | Cointegration | Tests if series share long-term equilibrium. Pairs trading foundation. |
| [CORRELATION](correlation/Correlation.md) | Correlation | Linear relationship between two variables. Range: -1 to +1. |
| [CORREL](correl/Correl.md) | Correlation | Linear relationship between two variables. Range: -1 to +1. |
| [COVARIANCE](covariance/Covariance.md) | Covariance | Joint variability of two random variables. Building block for β. |
| [ENTROPY](entropy/Entropy.md) | Shannon Entropy | Measures uncertainty/randomness. Higher entropy = less predictable. |
| [GEOMEAN](geomean/Geomean.md) | Geometric Mean | nth root of product. Use for growth rates and ratios. |
+1 -1
View File
@@ -13,7 +13,7 @@
| **PineScript** | [acf.pine](acf.pine) |
- The Autocorrelation Function (ACF) measures the correlation of a time series with a lagged copy of itself.
- **Similar:** [PACF](../pacf/Pacf.md), [Correlation](../correlation/Correlation.md) | **Trading note:** Autocorrelation function; detects mean-reversion (negative ACF) vs momentum (positive ACF) in returns.
- **Similar:** [PACF](../pacf/Pacf.md), [Correl](../correl/Correl.md) | **Trading note:** Autocorrelation function; detects mean-reversion (negative ACF) vs momentum (positive ACF) in returns.
- Validated against mathematical properties and theoretical AR-process expectations.
The Autocorrelation Function (ACF) measures the correlation of a time series with a lagged copy of itself. It is fundamental for identifying repeating patterns, seasonal effects, and determining the order of time series models like ARMA/ARIMA.
+3 -1
View File
@@ -13,7 +13,9 @@
| **PineScript** | [adf.pine](adf.pine) |
- Tests the null hypothesis that a time series contains a unit root (non-stationary). Output near **0** → stationary; output near **1** → unit root.
- **Similar:** [Hurst](../hurst/Hurst.md), [Cointegration](../cointegration/Cointegration.md) | **Complementary:** Z-Score, Variance | **Trading note:** ADF < 0.05 confirms mean-reversion suitability.
- **Similar indicators:** [Hurst](../hurst/Hurst.md), [Cointegration](../cointegration/Cointegration.md)
- **Complementary indicators:** [Z-Score](../zscore/Zscore.md), [Variance](../variance/Variance.md)
- **Trading note:** ADF < 0.05 confirms mean-reversion suitability.
- Validated against Python `statsmodels.tsa.stattools.adfuller` reference implementation.
The Augmented Dickey-Fuller test is the gold standard for detecting whether a financial time series is stationary or contains a unit root. Unlike the original Dickey-Fuller test, the augmented version includes lagged difference terms $\Delta y_{t-i}$ to absorb serial correlation, ensuring the test statistic follows the correct distribution. The p-value output uses MacKinnon (1994, 2010) polynomial interpolation with a standard normal CDF approximation, providing machine-precision results without lookup tables.
+1 -1
View File
@@ -13,7 +13,7 @@
| **PineScript** | [beta.pine](beta.pine) |
- Beta measures the volatility of an asset in relation to the overall market.
- **Similar:** [Correlation](../correlation/Correlation.md), [Covariance](../covariance/Covariance.md) | **Trading note:** Beta coefficient; measures systematic risk vs benchmark. β>1 = amplifies market moves.
- **Similar:** [Correl](../correl/Correl.md), [Covariance](../covariance/Covariance.md) | **Trading note:** Beta coefficient; measures systematic risk vs benchmark. β>1 = amplifies market moves.
- Validated against TA-Lib, Skender, and Tulip reference implementations where available.
Beta measures the volatility of an asset in relation to the overall market. It's the slope of the regression line between the asset's returns and the market's returns. A beta of 1.0 means the asset moves in lockstep with the market. A beta of 2.0 means the asset is twice as volatile as the market.
@@ -13,7 +13,7 @@
| **PineScript** | [cointegration.pine](cointegration.pine) |
- The Cointegration indicator measures the long-run equilibrium relationship between two price series using the Engle-Granger two-step method with an...
- **Similar:** [Correlation](../correlation/Correlation.md), [Granger](../granger/Granger.md) | **Trading note:** Tests if two series share a long-run equilibrium. Foundation of statistical arbitrage (pairs trading).
- **Similar:** [Correl](../correl/Correl.md), [Granger](../granger/Granger.md) | **Trading note:** Tests if two series share a long-run equilibrium. Foundation of statistical arbitrage (pairs trading).
- Validated against TradingView PineScript reference and statistical property tests.
The Cointegration indicator measures the long-run equilibrium relationship between two price series using the Engle-Granger two-step method with an Augmented Dickey-Fuller (ADF) test. Unlike correlation, which measures short-term co-movement, cointegration tests whether two non-stationary series share a common stochastic trend—meaning they may diverge temporarily but are statistically bound to revert to their equilibrium relationship.
@@ -28,7 +28,7 @@ namespace QuanTAlib;
/// - |r| < 0.3: Weak correlation
/// </remarks>
[SkipLocalsInit]
public sealed class Correlation : AbstractBase
public sealed class Correl : AbstractBase
{
private readonly RingBuffer _bufferX;
private readonly RingBuffer _bufferY;
@@ -58,10 +58,10 @@ public sealed class Correlation : AbstractBase
public override bool IsHot => _bufferX.Count >= WarmupPeriod;
/// <summary>
/// Creates a new Correlation indicator.
/// Creates a new Correl indicator.
/// </summary>
/// <param name="period">Lookback period for calculation (must be > 1)</param>
public Correlation(int period = 20)
public Correl(int period = 20)
{
if (period <= 1)
{
@@ -71,7 +71,7 @@ public sealed class Correlation : AbstractBase
_bufferX = new RingBuffer(period);
_bufferY = new RingBuffer(period);
Name = $"Correlation({period})";
Name = $"Correl({period})";
WarmupPeriod = period;
}
@@ -118,7 +118,7 @@ public sealed class Correlation : AbstractBase
ProcessBarCorrection(x, y);
}
double correlation = CalculateCorrelation();
double correlation = CalculateCorrel();
Last = new TValue(seriesX.Time, correlation);
PubEvent(Last);
@@ -143,13 +143,13 @@ public sealed class Correlation : AbstractBase
/// <remarks>Not supported for bi-input indicator. Use Update(seriesX, seriesY) instead.</remarks>
public override TValue Update(TValue input, bool isNew = true)
{
throw new NotSupportedException("Correlation requires two inputs (seriesX and seriesY). Use Update(seriesX, seriesY).");
throw new NotSupportedException("Correl requires two inputs (seriesX and seriesY). Use Update(seriesX, seriesY).");
}
/// <summary>Not supported. This indicator requires two inputs; use <see cref="Batch(TSeries, TSeries, int)"/> instead.</summary>
/// <remarks>Not supported for bi-input indicator. Use Calculate(seriesX, seriesY, period) instead.</remarks>
public override TSeries Update(TSeries source)
{
throw new NotSupportedException("Correlation requires two inputs. Use Batch(seriesX, seriesY, period).");
throw new NotSupportedException("Correl requires two inputs. Use Batch(seriesX, seriesY, period).");
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
@@ -316,7 +316,7 @@ public sealed class Correlation : AbstractBase
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private double CalculateCorrelation()
private double CalculateCorrel()
{
int n = _bufferX.Count;
if (n < 2)
@@ -355,7 +355,7 @@ public sealed class Correlation : AbstractBase
/// <summary>Not supported. This indicator requires two input spans.</summary>
public override void Prime(ReadOnlySpan<double> source, TimeSpan? step = null)
{
throw new NotSupportedException("Correlation requires two inputs.");
throw new NotSupportedException("Correl requires two inputs.");
}
/// <inheritdoc />
@@ -414,7 +414,7 @@ public sealed class Correlation : AbstractBase
throw new ArgumentException("Period must be greater than 1", nameof(period));
}
var indicator = new Correlation(period);
var indicator = new Correl(period);
for (int i = 0; i < seriesX.Length; i++)
{
@@ -426,14 +426,14 @@ public sealed class Correlation : AbstractBase
/// <summary>
/// Calculates Pearson correlation for two time series and returns both the result series and the live indicator instance.
/// </summary>
public static (TSeries Results, Correlation Indicator) Calculate(TSeries seriesX, TSeries seriesY, int period = 20)
public static (TSeries Results, Correl Indicator) Calculate(TSeries seriesX, TSeries seriesY, int period = 20)
{
if (seriesX.Count != seriesY.Count)
{
throw new ArgumentException("Series must have the same length", nameof(seriesY));
}
var indicator = new Correlation(period);
var indicator = new Correl(period);
var result = new TSeries(seriesX.Count);
var timesX = seriesX.Times;
@@ -10,7 +10,7 @@
| **Outputs** | Single series (Pearson r) |
| **Output range** | Varies (see docs) |
| **Warmup** | `period` bars |
| **PineScript** | [correlation.pine](correlation.pine) |
| **PineScript** | [correl.pine](correl.pine) |
- The Pearson Correlation Coefficient measures the linear relationship between two variables, returning a value from -1 (perfect negative correlation...
- **Similar:** [Spearman](../spearman/Spearman.md), [Kendall](../kendall/Kendall.md) | **Trading note:** Pearson correlation; measures linear relationship strength. Used for portfolio diversification and pairs trading.
@@ -189,7 +189,7 @@ Monitor correlation stability:
### Streaming Mode (Bi-Input)
```csharp
var corr = new Correlation(period: 20);
var corr = new Correl(period: 20);
foreach (var (priceA, priceB) in pricePairs)
{
var result = corr.Update(priceA, priceB);
@@ -206,7 +206,7 @@ foreach (var (priceA, priceB) in pricePairs)
var seriesA = new TSeries();
var seriesB = new TSeries();
// ... populate series ...
var results = Correlation.Calculate(seriesA, seriesB, period: 20);
var results = Correl.Calculate(seriesA, seriesB, period: 20);
```
### Span Mode (Zero Allocation)
@@ -216,13 +216,13 @@ double[] pricesA = new double[1000];
double[] pricesB = new double[1000];
double[] output = new double[1000];
// ... populate inputs ...
Correlation.Batch(pricesA.AsSpan(), pricesB.AsSpan(), output.AsSpan(), period: 20);
Correl.Batch(pricesA.AsSpan(), pricesB.AsSpan(), output.AsSpan(), period: 20);
```
### Bar Correction Support
```csharp
var corr = new Correlation(20);
var corr = new Correl(20);
// New bar
corr.Update(100.0, 50.0, isNew: true); // r = 0.85
@@ -1,7 +1,7 @@
// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Pearson's Correlation (CORRELATION)", "CORRELATION", overlay=false)
indicator("Pearson's Correlation (CORREL)", "CORREL", overlay=false)
//@function Calculates Pearson correlation coefficient using single pass with circular buffer
//@param src1 series float First series to analyze
@@ -1,42 +1,42 @@
namespace QuanTAlib.Tests;
public class CorrelationTests
public class CorrelTests
{
[Fact]
public void Constructor_ValidPeriod_CreatesIndicator()
{
var indicator = new Correlation(20);
Assert.Equal("Correlation(20)", indicator.Name);
var indicator = new Correl(20);
Assert.Equal("Correl(20)", indicator.Name);
Assert.Equal(20, indicator.WarmupPeriod);
}
[Fact]
public void Constructor_MinimumValidPeriod_CreatesIndicator()
{
var indicator = new Correlation(2);
Assert.Equal("Correlation(2)", indicator.Name);
var indicator = new Correl(2);
Assert.Equal("Correl(2)", indicator.Name);
}
[Fact]
public void Constructor_InvalidPeriod_ThrowsArgumentException()
{
Assert.Throws<ArgumentException>(() => new Correlation(1));
Assert.Throws<ArgumentException>(() => new Correlation(0));
Assert.Throws<ArgumentException>(() => new Correlation(-5));
Assert.Throws<ArgumentException>(() => new Correl(1));
Assert.Throws<ArgumentException>(() => new Correl(0));
Assert.Throws<ArgumentException>(() => new Correl(-5));
}
[Fact]
public void Update_SingleValue_ReturnsNaN()
{
var indicator = new Correlation(5);
var indicator = new Correl(5);
var result = indicator.Update(100.0, 200.0, true);
Assert.True(double.IsNaN(result.Value));
}
[Fact]
public void Update_TwoValues_ReturnsValidCorrelation()
public void Update_TwoValues_ReturnsValidCorrel()
{
var indicator = new Correlation(5);
var indicator = new Correl(5);
indicator.Update(100.0, 200.0, true);
var result = indicator.Update(102.0, 204.0, true);
Assert.True(double.IsFinite(result.Value));
@@ -45,7 +45,7 @@ public class CorrelationTests
[Fact]
public void Update_PerfectPositiveCorrelation_ReturnsOne()
{
var indicator = new Correlation(5);
var indicator = new Correl(5);
// Same values scaled by constant should give correlation = 1
for (int i = 0; i < 10; i++)
@@ -62,7 +62,7 @@ public class CorrelationTests
[Fact]
public void Update_PerfectNegativeCorrelation_ReturnsMinusOne()
{
var indicator = new Correlation(5);
var indicator = new Correl(5);
// Opposite movements should give correlation = -1
for (int i = 0; i < 10; i++)
@@ -79,7 +79,7 @@ public class CorrelationTests
[Fact]
public void Update_ConstantValues_ReturnsNaN()
{
var indicator = new Correlation(5);
var indicator = new Correl(5);
// Constant values have zero variance, so correlation is undefined
for (int i = 0; i < 10; i++)
@@ -93,8 +93,8 @@ public class CorrelationTests
[Fact]
public void Update_BarCorrection_RestoresState()
{
var indicator1 = new Correlation(5);
var indicator2 = new Correlation(5);
var indicator1 = new Correl(5);
var indicator2 = new Correl(5);
// Feed same initial data
for (int i = 0; i < 10; i++)
@@ -119,8 +119,8 @@ public class CorrelationTests
[Fact]
public void Update_IterativeCorrections_Restore()
{
var corrected = new Correlation(5);
var direct = new Correlation(5);
var corrected = new Correl(5);
var direct = new Correl(5);
// Feed identical initial state
for (int i = 0; i < 8; i++)
@@ -151,7 +151,7 @@ public class CorrelationTests
[Fact]
public void Update_NaNInput_UsesLastValidValue()
{
var indicator = new Correlation(5);
var indicator = new Correl(5);
// Add valid data
for (int i = 0; i < 5; i++)
@@ -169,7 +169,7 @@ public class CorrelationTests
[Fact]
public void Update_InfinityInput_UsesLastValidValue()
{
var indicator = new Correlation(5);
var indicator = new Correl(5);
// Add valid data
for (int i = 0; i < 5; i++)
@@ -185,7 +185,7 @@ public class CorrelationTests
[Fact]
public void IsHot_BelowPeriod_ReturnsFalse()
{
var indicator = new Correlation(10);
var indicator = new Correl(10);
indicator.Update(100.0, 200.0, true);
Assert.False(indicator.IsHot);
}
@@ -193,7 +193,7 @@ public class CorrelationTests
[Fact]
public void IsHot_AtPeriod_ReturnsTrue()
{
var indicator = new Correlation(10);
var indicator = new Correl(10);
for (int i = 0; i < 10; i++)
{
indicator.Update(100.0 + i, 200.0 + i, true);
@@ -204,7 +204,7 @@ public class CorrelationTests
[Fact]
public void Reset_ClearsState()
{
var indicator = new Correlation(5);
var indicator = new Correl(5);
// Add data
for (int i = 0; i < 10; i++)
@@ -224,14 +224,14 @@ public class CorrelationTests
[Fact]
public void Update_TValue_ThrowsNotSupportedException()
{
var indicator = new Correlation(5);
var indicator = new Correl(5);
Assert.Throws<NotSupportedException>(() => indicator.Update(new TValue(DateTime.UtcNow, 100.0)));
}
[Fact]
public void Update_TSeries_ThrowsNotSupportedException()
{
var indicator = new Correlation(5);
var indicator = new Correl(5);
var series = new TSeries(10);
Assert.Throws<NotSupportedException>(() => indicator.Update(series));
}
@@ -239,7 +239,7 @@ public class CorrelationTests
[Fact]
public void Prime_ThrowsNotSupportedException()
{
var indicator = new Correlation(5);
var indicator = new Correl(5);
Assert.Throws<NotSupportedException>(() => indicator.Prime(new double[] { 1, 2, 3 }));
}
@@ -255,7 +255,7 @@ public class CorrelationTests
seriesY.Add(new TValue(DateTime.UtcNow.AddMinutes(i), 200.0 + (i * 2)));
}
var result = Correlation.Batch(seriesX, seriesY, 5);
var result = Correl.Batch(seriesX, seriesY, 5);
Assert.Equal(20, result.Count);
}
@@ -275,7 +275,7 @@ public class CorrelationTests
seriesY.Add(new TValue(DateTime.UtcNow.AddMinutes(i), 200.0 + i));
}
Assert.Throws<ArgumentException>(() => Correlation.Batch(seriesX, seriesY, 5));
Assert.Throws<ArgumentException>(() => Correl.Batch(seriesX, seriesY, 5));
}
[Fact]
@@ -291,7 +291,7 @@ public class CorrelationTests
seriesY[i] = 200.0 + (i * 2);
}
Correlation.Batch(seriesX, seriesY, output, 5);
Correl.Batch(seriesX, seriesY, output, 5);
// First value should be NaN (not enough data)
Assert.True(double.IsNaN(output[0]));
@@ -307,7 +307,7 @@ public class CorrelationTests
double[] seriesY = new double[15];
double[] output = new double[10];
Assert.Throws<ArgumentException>(() => Correlation.Batch(seriesX, seriesY, output, 5));
Assert.Throws<ArgumentException>(() => Correl.Batch(seriesX, seriesY, output, 5));
}
[Fact]
@@ -317,7 +317,7 @@ public class CorrelationTests
double[] seriesY = new double[20];
double[] output = new double[10];
Assert.Throws<ArgumentException>(() => Correlation.Batch(seriesX, seriesY, output, 5));
Assert.Throws<ArgumentException>(() => Correl.Batch(seriesX, seriesY, output, 5));
}
[Fact]
@@ -327,13 +327,13 @@ public class CorrelationTests
double[] seriesY = new double[20];
double[] output = new double[20];
Assert.Throws<ArgumentException>(() => Correlation.Batch(seriesX, seriesY, output, 1));
Assert.Throws<ArgumentException>(() => Correl.Batch(seriesX, seriesY, output, 1));
}
[Fact]
public void CorrelationRange_AlwaysBetweenMinusOneAndOne()
{
var indicator = new Correlation(10);
var indicator = new Correl(10);
var gbmX = new GBM(startPrice: 100, mu: 0.02, sigma: 0.3, seed: 12345);
var gbmY = new GBM(startPrice: 200, mu: 0.01, sigma: 0.5, seed: 54321);
@@ -369,7 +369,7 @@ public class CorrelationTests
}
// Streaming calculation
var indicator = new Correlation(period);
var indicator = new Correl(period);
double[] streamingResults = new double[length];
for (int i = 0; i < length; i++)
{
@@ -378,7 +378,7 @@ public class CorrelationTests
// Batch calculation
double[] batchResults = new double[length];
Correlation.Batch(seriesX, seriesY, batchResults, period);
Correl.Batch(seriesX, seriesY, batchResults, period);
// Compare last 50 values (after warmup)
for (int i = length - 50; i < length; i++)
@@ -8,13 +8,13 @@ namespace QuanTAlib.Tests;
/// Validation tests for Correlation (Pearson Correlation Coefficient) indicator.
/// Validates against Skender.Stock.Indicators.GetCorrelation and mathematical properties.
/// </summary>
public sealed class CorrelationValidationTests : IDisposable
public sealed class CorrelValidationTests : IDisposable
{
private const double Tolerance = 1e-10;
private readonly ValidationTestData _data;
private readonly ITestOutputHelper _output;
public CorrelationValidationTests(ITestOutputHelper output)
public CorrelValidationTests(ITestOutputHelper output)
{
_data = new ValidationTestData();
_output = output;
@@ -29,7 +29,7 @@ public sealed class CorrelationValidationTests : IDisposable
#region External Library Validation Skender
[Fact]
public void Validate_Skender_Correlation()
public void Validate_Skender_Correl()
{
// === DESCRIPTION ===
// Compares QuanTAlib Correlation against Skender.Stock.Indicators.GetCorrelation
@@ -65,7 +65,7 @@ public sealed class CorrelationValidationTests : IDisposable
var sResult = quotesA.GetCorrelation(quotesB, period).ToList();
// --- QuanTAlib: streaming API ---
var corr = new Correlation(period);
var corr = new Correl(period);
var qValues = new List<double>();
for (int i = 0; i < _data.Count; i++)
@@ -126,7 +126,7 @@ public sealed class CorrelationValidationTests : IDisposable
{
var sResult = _data.SkenderQuotes.GetCorrelation(quotesB, period).ToList();
var corr = new Correlation(period);
var corr = new Correl(period);
int matched = 0;
for (int i = 0; i < _data.Count; i++)
@@ -175,7 +175,7 @@ public sealed class CorrelationValidationTests : IDisposable
var sResult = quotesA.GetCorrelation(quotesB, period).ToList();
var corr = new Correlation(period);
var corr = new Correl(period);
int matched = 0;
for (int i = 0; i < _data.Count; i++)
@@ -207,7 +207,7 @@ public sealed class CorrelationValidationTests : IDisposable
public void Correlation_PerfectLinearPositive_ReturnsOne()
{
// y = a + b*x with b > 0 should give r = 1
var indicator = new Correlation(20);
var indicator = new Correl(20);
for (int i = 0; i < 50; i++)
{
@@ -223,7 +223,7 @@ public sealed class CorrelationValidationTests : IDisposable
public void Correlation_PerfectLinearNegative_ReturnsMinusOne()
{
// y = a + b*x with b < 0 should give r = -1
var indicator = new Correlation(20);
var indicator = new Correl(20);
for (int i = 0; i < 50; i++)
{
@@ -238,9 +238,9 @@ public sealed class CorrelationValidationTests : IDisposable
[Fact]
public void Correlation_SymmetryProperty_XY_Equals_YX()
{
// Correlation(X, Y) should equal Correlation(Y, X)
var indicatorXY = new Correlation(10);
var indicatorYX = new Correlation(10);
// Correl(X, Y) should equal Correl(Y, X)
var indicatorXY = new Correl(10);
var indicatorYX = new Correl(10);
var gbmX = new GBM(startPrice: 100, mu: 0.02, sigma: 0.2, seed: 12345);
var gbmY = new GBM(startPrice: 50, mu: 0.01, sigma: 0.15, seed: 54321);
@@ -260,8 +260,8 @@ public sealed class CorrelationValidationTests : IDisposable
{
// Correlation is invariant under positive linear transformations
// corr(X, Y) = corr(aX + b, cY + d) when a, c > 0
var indicator1 = new Correlation(10);
var indicator2 = new Correlation(10);
var indicator1 = new Correl(10);
var indicator2 = new Correl(10);
var gbmX = new GBM(startPrice: 100, mu: 0.02, sigma: 0.2, seed: 12345);
var gbmY = new GBM(startPrice: 50, mu: 0.01, sigma: 0.15, seed: 54321);
@@ -283,7 +283,7 @@ public sealed class CorrelationValidationTests : IDisposable
public void Correlation_BoundedProperty_AlwaysBetweenMinusOneAndOne()
{
// Correlation coefficient is always in [-1, 1]
var indicator = new Correlation(10);
var indicator = new Correl(10);
var gbmX = new GBM(startPrice: 100, mu: 0.1, sigma: 0.5, seed: 12345);
var gbmY = new GBM(startPrice: 50, mu: -0.05, sigma: 0.3, seed: 54321);
@@ -304,7 +304,7 @@ public sealed class CorrelationValidationTests : IDisposable
public void Correlation_ZeroVariance_ReturnsNaN()
{
// When one or both series have zero variance, correlation is undefined
var indicator = new Correlation(10);
var indicator = new Correl(10);
for (int i = 0; i < 20; i++)
{
@@ -332,7 +332,7 @@ public sealed class CorrelationValidationTests : IDisposable
// r = Cov(X,Y) / sqrt(Var(X) * Var(Y)) = 1.2 / sqrt(2 * 1.2) = 1.2 / sqrt(2.4)
// = 1.2 / 1.5492 ≈ 0.7746
var indicator = new Correlation(5);
var indicator = new Correl(5);
double[] x = [1, 2, 3, 4, 5];
double[] y = [2, 4, 5, 4, 5];
@@ -346,11 +346,11 @@ public sealed class CorrelationValidationTests : IDisposable
}
[Fact]
public void Correlation_KnownValues_NoCorrelation()
public void Correlation_KnownValues_NoCorrel()
{
// X = [1, 2, 3, 4, 5], Y = [3, 3, 3, 3, 3] (constant)
// Should be NaN (or 0 with special handling)
var indicator = new Correlation(5);
var indicator = new Correl(5);
double[] x = [1, 2, 3, 4, 5];
double[] y = [3, 3, 3, 3, 3];
@@ -383,10 +383,10 @@ public sealed class CorrelationValidationTests : IDisposable
}
// Batch calculation
var batchResult = Correlation.Batch(seriesX, seriesY, 20);
var batchResult = Correl.Batch(seriesX, seriesY, 20);
// Streaming calculation
var streamingIndicator = new Correlation(20);
var streamingIndicator = new Correl(20);
for (int i = 0; i < seriesX.Count; i++)
{
streamingIndicator.Update(seriesX[i].Value, seriesY[i].Value);
@@ -420,10 +420,10 @@ public sealed class CorrelationValidationTests : IDisposable
}
// Span calculation
Correlation.Batch(seriesX, seriesY, output, 20);
Correl.Batch(seriesX, seriesY, output, 20);
// Streaming calculation
var streamingIndicator = new Correlation(20);
var streamingIndicator = new Correl(20);
for (int i = 0; i < length; i++)
{
streamingIndicator.Update(seriesX[i], seriesY[i]);
@@ -443,7 +443,7 @@ public sealed class CorrelationValidationTests : IDisposable
[Fact]
public void Correlation_ResetProducesSameResults()
{
var indicator = new Correlation(20);
var indicator = new Correl(20);
var gbmX = new GBM(startPrice: 100, mu: 0.02, sigma: 0.2, seed: 12345);
var gbmY = new GBM(startPrice: 50, mu: 0.01, sigma: 0.15, seed: 54321);
@@ -475,7 +475,7 @@ public sealed class CorrelationValidationTests : IDisposable
[Fact]
public void Correlation_SlidingWindow_MovesCorrectly()
{
var indicator = new Correlation(5);
var indicator = new Correl(5);
// Build up with known values for period 5
// After 5 values, window should be full
@@ -501,7 +501,7 @@ public sealed class CorrelationValidationTests : IDisposable
[Fact]
public void Correlation_SlidingWindow_DropsOldValues()
{
var indicator = new Correlation(3);
var indicator = new Correl(3);
// First window: perfectly correlated
indicator.Update(1, 2);
@@ -522,7 +522,7 @@ public sealed class CorrelationValidationTests : IDisposable
[Fact]
public void Correlation_LargeValues_MaintainsStability()
{
var indicator = new Correlation(20);
var indicator = new Correl(20);
for (int i = 0; i < 50; i++)
{
@@ -538,7 +538,7 @@ public sealed class CorrelationValidationTests : IDisposable
[Fact]
public void Correlation_SmallValues_MaintainsStability()
{
var indicator = new Correlation(20);
var indicator = new Correl(20);
// Use values that are small but not so small they cause numerical issues
for (int i = 0; i < 50; i++)
@@ -555,7 +555,7 @@ public sealed class CorrelationValidationTests : IDisposable
[Fact]
public void Correlation_MixedMagnitudes_HandlesCorrectly()
{
var indicator = new Correlation(20);
var indicator = new Correl(20);
for (int i = 0; i < 50; i++)
{
@@ -576,7 +576,7 @@ public sealed class CorrelationValidationTests : IDisposable
public void Correlation_HighPositiveCorrelation_DetectedCorrectly()
{
// Create two series with high positive correlation (r ≈ 0.95+)
var indicator = new Correlation(20);
var indicator = new Correl(20);
// Use deterministic data that creates high correlation
for (int i = 0; i < 100; i++)
@@ -593,7 +593,7 @@ public sealed class CorrelationValidationTests : IDisposable
public void Correlation_NegativeCorrelation_DetectedCorrectly()
{
// Create two series with negative correlation
var indicator = new Correlation(20);
var indicator = new Correl(20);
var random = new GBM(startPrice: 100.0, sigma: 1.0, seed: 42);
for (int i = 0; i < 100; i++)
@@ -612,7 +612,7 @@ public sealed class CorrelationValidationTests : IDisposable
// Create two series with weak correlation: pure independent noise, no shared trend.
// Use two independent GBMs (different seeds) and feed their incremental log-returns directly.
// With period=20 and fully independent noise sequences, correlation should be near zero.
var indicator = new Correlation(20);
var indicator = new Correl(20);
var gbmX = new GBM(startPrice: 100.0, sigma: 0.2, seed: 43);
var gbmY = new GBM(startPrice: 100.0, sigma: 0.2, seed: 9871);
var barsX = gbmX.Fetch(101, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
@@ -637,9 +637,9 @@ public sealed class CorrelationValidationTests : IDisposable
[Fact]
public void Correlation_DifferentPeriods_ProduceDifferentResults()
{
var indicator5 = new Correlation(5);
var indicator20 = new Correlation(20);
var indicator50 = new Correlation(50);
var indicator5 = new Correl(5);
var indicator20 = new Correl(20);
var indicator50 = new Correl(50);
var gbmX = new GBM(startPrice: 100, mu: 0.02, sigma: 0.2, seed: 12345);
var gbmY = new GBM(startPrice: 50, mu: 0.01, sigma: 0.15, seed: 54321);
@@ -660,8 +660,8 @@ public sealed class CorrelationValidationTests : IDisposable
[Fact]
public void Correlation_SmallPeriod_MoreVolatile()
{
var indicator3 = new Correlation(3);
var indicator30 = new Correlation(30);
var indicator3 = new Correl(3);
var indicator30 = new Correl(30);
var gbmX = new GBM(startPrice: 100, mu: 0.02, sigma: 0.2, seed: 12345);
var gbmY = new GBM(startPrice: 50, mu: 0.01, sigma: 0.15, seed: 54321);
@@ -732,7 +732,7 @@ public sealed class CorrelationValidationTests : IDisposable
Assert.True(length > 100, $"TALib Correl produced only {length} values");
// QuanTAlib streaming
var corr = new Correlation(period);
var corr = new Correl(period);
var qlValues = new double[_data.Count];
for (int i = 0; i < _data.Count; i++)
{
@@ -770,7 +770,7 @@ public sealed class CorrelationValidationTests : IDisposable
(int offset, int length) = outRange.GetOffsetAndLength(taOut.Length);
var corr = new Correlation(period);
var corr = new Correl(period);
var qlValues = new double[_data.Count];
for (int i = 0; i < _data.Count; i++)
{
+1 -1
View File
@@ -13,7 +13,7 @@
| **PineScript** | [covariance.pine](covariance.pine) |
- Covariance measures the joint variability of two random variables.
- **Similar:** [Correlation](../correlation/Correlation.md), [Beta](../beta/Beta.md) | **Trading note:** Rolling covariance; measures how two assets move together. Foundation of portfolio theory.
- **Similar:** [Correl](../correl/Correl.md), [Beta](../beta/Beta.md) | **Trading note:** Rolling covariance; measures how two assets move together. Foundation of portfolio theory.
- Validated against TA-Lib, Skender, and Tulip reference implementations where available.
Covariance measures the joint variability of two random variables. It indicates the direction of the linear relationship between variables.
+1 -1
View File
@@ -13,7 +13,7 @@
| **PineScript** | [granger.pine](granger.pine) |
- The Granger Causality test asks a precise, falsifiable question: does knowing the history of series X improve your ability to predict series Y, bey...
- **Similar:** [Cointegration](../cointegration/Cointegration.md), [Correlation](../correlation/Correlation.md) | **Trading note:** Granger causality test; determines if one time series can forecast another. Lead-lag detection.
- **Similar:** [Cointegration](../cointegration/Cointegration.md), [Correl](../correl/Correl.md) | **Trading note:** Granger causality test; determines if one time series can forecast another. Lead-lag detection.
- Validated against TA-Lib, Skender, and Tulip reference implementations where available.
## Introduction
+1 -1
View File
@@ -13,7 +13,7 @@
| **PineScript** | [spearman.pine](spearman.pine) |
- Spearman's ρ (rho) measures the strength and direction of monotonic association between two variables.
- **Similar:** [Correlation](../correlation/Correlation.md), [Kendall](../kendall/Kendall.md) | **Trading note:** Spearman rank correlation; non-parametric, detects monotonic (not just linear) relationships.
- **Similar:** [Correl](../correl/Correl.md), [Kendall](../kendall/Kendall.md) | **Trading note:** Spearman rank correlation; non-parametric, detects monotonic (not just linear) relationships.
- Validated against TA-Lib, Skender, and Tulip reference implementations where available.
Spearman's ρ (rho) measures the strength and direction of monotonic association between two variables. Unlike Pearson's correlation, which measures linear relationship, Spearman captures any monotonic relationship. A portfolio of stocks whose returns move monotonically together has different risk than one whose components merely share a linear trend. Spearman detects both.
+1 -1
View File
@@ -13,7 +13,7 @@
| **PineScript** | [theil.pine](theil.pine) |
- The Theil T Index is an information-theoretic measure of inequality (or concentration) within a distribution of positive values.
- **Similar:** [Correlation](../correlation/Correlation.md), [LinReg](../linreg/LinReg.md) | **Trading note:** TheilSen estimator; robust slope calculation using medians of pairwise slopes. Resistant to outliers.
- **Similar:** [Correl](../correl/Correl.md), [LinReg](../linreg/LinReg.md) | **Trading note:** TheilSen estimator; robust slope calculation using medians of pairwise slopes. Resistant to outliers.
- Validated against TA-Lib, Skender, and Tulip reference implementations where available.
## Introduction
+1 -1
View File
@@ -15,7 +15,7 @@ Trend indicators based on Infinite Impulse Response (IIR) filters. Recursive arc
| [GDEMA](gdema/Gdema.md) | Generalized Double Exponential MA | Generalized DEMA with configurable volume factor for tunable lag/smoothness trade-off. |
| [HEMA](hema/Hema.md) | Hull Exponential MA | EMA-domain Hull analog using half-life timing and de-lagged EMA cascade. |
| [HOLT](holt/Holt.md) | Holt Exponential Smoothing | Double exponential smoothing with separate level and trend components for adaptive trend-following. |
| [HTIT](htit/Htit.md) | Ehlers Hilbert Transform Instantaneous Trend (also known as HT_TRENDLINE) | Utilizes Hilbert Transform to isolate instantaneous trend component, providing zero-lag trendline with hybrid FIR-in-IIR design. |
| [HT_TRENDLINE](ht_trendline/HtTrendline.md) | Ehlers Hilbert Transform Instantaneous Trend | Utilizes Hilbert Transform to isolate instantaneous trend component, providing zero-lag trendline with hybrid FIR-in-IIR design. |
| [HWMA](hwma/Hwma.md) | Holt-Winters MA | Triple exponential smoothing. Tracks level, velocity, acceleration. Recursive IIR structure. |
| [JMA](jma/Jma.md) | Jurik MA | Adaptive filter achieving high noise reduction and low phase delay through multi-stage volatility normalization and dynamic parameter optimization. |
| [KAMA](kama/Kama.md) | Kaufman Adaptive MA | Automatically adjusts sensitivity based on market volatility using Efficiency Ratio, balancing responsiveness and stability. |
+1 -1
View File
@@ -14,7 +14,7 @@
| **Signature** | [decycler_signature](decycler_signature.md) |
- The Ehlers Decycler extracts the trend component from a price series by subtracting a 2-pole Butterworth high-pass filter from the source signal.
- **Similar:** [EMA](../ema/ema.md), [HTIT](../htit/htit.md) | **Complementary:** Cycle indicators | **Trading note:** Ehlers Decycler; high-pass complement removes cycle components to isolate trend.
- **Similar:** [EMA](../ema/ema.md), [HT_TRENDLINE](../ht_trendline/HtTrendline.md) | **Complementary:** Cycle indicators | **Trading note:** Ehlers Decycler; high-pass complement removes cycle components to isolate trend.
- Validated against TA-Lib, Skender, and Tulip reference implementations where available.
The Ehlers Decycler extracts the trend component from a price series by subtracting a 2-pole Butterworth high-pass filter from the source signal. Where most moving averages blur the boundary between trend and cycle, the Decycler defines it with a frequency-domain cutoff: cycles shorter than the specified period are removed, everything longer stays. The result is an overlay that hugs price with near-zero lag during trends and rejects short-term oscillations without the smoothing artifacts of convolution-based averages.
@@ -5,7 +5,7 @@ using TradingPlatform.BusinessLayer;
namespace QuanTAlib;
[SkipLocalsInit]
public sealed class HtitIndicator : Indicator, IWatchlistIndicator
public sealed class HtTrendlineIndicator : Indicator, IWatchlistIndicator
{
[InputParameter("Period", sortIndex: 1, 1, 2000, 1, 0)]
public int Period { get; set; } = 50; // Not used in calculation but kept for consistency
@@ -16,7 +16,7 @@ public sealed class HtitIndicator : Indicator, IWatchlistIndicator
[InputParameter("Show cold values", sortIndex: 21)]
public bool ShowColdValues { get; set; } = true;
private Htit _htit = null!;
private HtTrendline _htit = null!;
private readonly LineSeries _series;
private string _sourceName = null!;
private Func<IHistoryItem, double> _priceSelector = null!;
@@ -24,15 +24,15 @@ public sealed class HtitIndicator : Indicator, IWatchlistIndicator
public static int MinHistoryDepths => 0;
int IWatchlistIndicator.MinHistoryDepths => MinHistoryDepths;
public override string ShortName => $"HTIT:{_sourceName}";
public override string ShortName => $"HT_TRENDLINE:{_sourceName}";
public HtitIndicator()
public HtTrendlineIndicator()
{
OnBackGround = true;
SeparateWindow = false;
Name = "HTIT - Ehlers Hilbert Transform Instantaneous Trend";
Name = "HT_TRENDLINE - Ehlers Hilbert Transform Instantaneous Trend";
Description = "Ehlers Hilbert Transform Instantaneous Trend";
_series = new LineSeries(name: "HTIT", color: Color.Orange, width: 2, style: LineStyle.Solid);
_series = new LineSeries(name: "HT_TRENDLINE", color: Color.Orange, width: 2, style: LineStyle.Solid);
AddLineSeries(_series);
}
@@ -40,7 +40,7 @@ public sealed class HtitIndicator : Indicator, IWatchlistIndicator
{
_priceSelector = Source.GetPriceSelector();
_sourceName = Source.ToString();
_htit = new Htit();
_htit = new HtTrendline();
base.OnInit();
}
@@ -4,7 +4,7 @@ using System.Runtime.InteropServices;
namespace QuanTAlib;
/// <summary>
/// HTIT: Hilbert Transform Instantaneous Trendline (also known as HT_TRENDLINE)
/// HT_TRENDLINE: Hilbert Transform Instantaneous Trendline (also known as HT_TRENDLINE)
/// </summary>
/// <remarks>
/// Ehlers' adaptive trendline using Hilbert Transform cycle measurement.
@@ -12,10 +12,10 @@ namespace QuanTAlib;
///
/// Key features: homodyne discriminator, period-adaptive averaging window.
/// </remarks>
/// <seealso href="Htit.md">Detailed documentation</seealso>
/// <seealso href="htit.pine">Reference Pine Script implementation</seealso>
/// <seealso href="HtTrendline.md">Detailed documentation</seealso>
/// <seealso href="ht_trendline.pine">Reference Pine Script implementation</seealso>
[SkipLocalsInit]
public sealed class Htit : AbstractBase
public sealed class HtTrendline : AbstractBase
{
public override bool IsHot => _state.Index >= WarmupPeriod;
@@ -48,9 +48,9 @@ public sealed class Htit : AbstractBase
private const double TwoPi = 2.0 * Math.PI;
private const double MinDeltaRadians = Math.PI / 180.0; // 1 degree in radians
public Htit()
public HtTrendline()
{
Name = "Htit";
Name = "HtTrendline";
WarmupPeriod = 12;
_handler = Handle;
@@ -66,7 +66,7 @@ public sealed class Htit : AbstractBase
Init();
}
public Htit(ITValuePublisher source) : this()
public HtTrendline(ITValuePublisher source) : this()
{
source.Pub += _handler;
}
@@ -266,7 +266,7 @@ public sealed class Htit : AbstractBase
/// For high-performance batch-only processing, use the static Calculate method instead.
/// </summary>
/// <param name="source">Input time series</param>
/// <returns>Output time series with HTIT values</returns>
/// <returns>Output time series with HT_TRENDLINE values</returns>
public override TSeries Update(TSeries source)
{
if (source.Count == 0)
@@ -303,8 +303,8 @@ public sealed class Htit : AbstractBase
public static TSeries Batch(TSeries source)
{
var htit = new Htit();
return htit.Update(source);
var httrendline = new HtTrendline();
return httrendline.Update(source);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
@@ -513,9 +513,9 @@ public sealed class Htit : AbstractBase
}
}
public static (TSeries Results, Htit Indicator) Calculate(TSeries source)
public static (TSeries Results, HtTrendline Indicator) Calculate(TSeries source)
{
var indicator = new Htit();
var indicator = new HtTrendline();
TSeries results = indicator.Update(source);
return (results, indicator);
}
@@ -1,29 +1,29 @@
# HTIT: Ehlers Hilbert Transform Instantaneous Trend (also known as HT_TRENDLINE)
# HT_TRENDLINE: Ehlers Hilbert Transform Instantaneous Trend (also known as HT_TRENDLINE)
> *John Ehlers brought rocket science to trading. Literally. HTIT uses signal processing to find the trend by removing the cycle. It's not smoothing; it's extraction.*
> *John Ehlers brought rocket science to trading. Literally. HT_TRENDLINE uses signal processing to find the trend by removing the cycle. It's not smoothing; it's extraction.*
| Property | Value |
| ---------------- | -------------------------------- |
| **Category** | Trend (IIR MA) |
| **Inputs** | Source (close) |
| **Parameters** | None |
| **Outputs** | Single series (HTIT) |
| **Outputs** | Single series (HT_TRENDLINE) |
| **Output range** | Tracks input |
| **Warmup** | `12` bars |
| **PineScript** | [htit.pine](htit.pine) |
| **Signature** | [htit_signature](htit_signature.md) |
| **PineScript** | [ht_trendline.pine](ht_trendline.pine) |
| **Signature** | [ht_trendline_signature](ht_trendline_signature.md) |
- HTIT (Hilbert Transform Instantaneous Trend) is a trend-following indicator that doesn't rely on simple averaging.
- HT_TRENDLINE (Hilbert Transform Instantaneous Trend) is a trend-following indicator that doesn't rely on simple averaging.
- **Similar:** [MAMA](../mama/mama.md), [DEMA](../dema/dema.md) | **Complementary:** HT_DCPeriod | **Trading note:** Hilbert Transform trendline; cycle-adaptive smoothing.
- Validated against TA-Lib, Skender, and Tulip reference implementations where available.
HTIT (Hilbert Transform Instantaneous Trend) is a trend-following indicator that doesn't rely on simple averaging. Instead, it uses the Hilbert Transform to measure the dominant cycle period of the market and then computes a trendline that filters out that specific cycle. It adapts to the market's rhythm rather than imposing a fixed period.
HT_TRENDLINE (Hilbert Transform Instantaneous Trend) is a trend-following indicator that doesn't rely on simple averaging. Instead, it uses the Hilbert Transform to measure the dominant cycle period of the market and then computes a trendline that filters out that specific cycle. It adapts to the market's rhythm rather than imposing a fixed period.
## Historical Context
John Ehlers, a pioneer in applying DSP to trading, introduced this in his book *Rocket Science for Traders*. He recognized that markets have cyclic components (noise) and trend components. By identifying the cycle, you can mathematically subtract it to reveal the pure trend.
Most trend indicators (SMA, EMA) are low-pass filters: they let low frequencies (trend) pass and block high frequencies (noise). The problem is that "noise" in markets isn't random white noise; it's often cyclic. A fixed-period SMA might filter out a 10-day cycle perfectly but amplify a 20-day cycle. HTIT solves this by measuring the cycle first, then tuning the filter to kill exactly that frequency.
Most trend indicators (SMA, EMA) are low-pass filters: they let low frequencies (trend) pass and block high frequencies (noise). The problem is that "noise" in markets isn't random white noise; it's often cyclic. A fixed-period SMA might filter out a 10-day cycle perfectly but amplify a 20-day cycle. HT_TRENDLINE solves this by measuring the cycle first, then tuning the filter to kill exactly that frequency.
## Architecture & Physics
@@ -84,11 +84,11 @@ Where $\text{DC}$ is the integer part of the smoothed dominant cycle period.
The Instantaneous Trend is smoothed again using the same 4-bar WMA to remove any residual stepping artifacts from the integer period changes.
$$ \text{HTIT}_t = \frac{4 \text{IT}_t + 3 \text{IT}_{t-1} + 2 \text{IT}_{t-2} + \text{IT}_{t-3}}{10} $$
$$ \text{HT_TRENDLINE}_t = \frac{4 \text{IT}_t + 3 \text{IT}_{t-1} + 2 \text{IT}_{t-2} + \text{IT}_{t-3}}{10} $$
## Mathematical Precision & Implementation Philosophy
Like our MAMA implementation, QuanTAlib's HTIT prioritizes mathematical correctness over blind porting.
Like our MAMA implementation, QuanTAlib's HT_TRENDLINE prioritizes mathematical correctness over blind porting.
| Aspect | Other Libraries | QuanTAlib | Rationale |
| :----------------------- | :----------------- | :---------------------- | :-------------------------------------------- |
@@ -102,7 +102,7 @@ We use `atan2` for robust phase calculation and maintain full double precision t
## Performance Profile
HTIT is computationally heavier than a simple MA but lighter than MAMA. The main cost is the loop for the Instantaneous Trend calculation, which sums up to 50 past prices.
HT_TRENDLINE is computationally heavier than a simple MA but lighter than MAMA. The main cost is the loop for the Instantaneous Trend calculation, which sums up to 50 past prices.
### Operation Count (Streaming Mode, Scalar)
@@ -144,7 +144,7 @@ HTIT is computationally heavier than a simple MA but lighter than MAMA. The main
### Batch Mode (SIMD Analysis)
HTIT is **not SIMD-parallelizable** across bars due to:
HT_TRENDLINE is **not SIMD-parallelizable** across bars due to:
1. Recursive feedback in Hilbert transforms (I2, Q2 depend on previous values)
2. Period-dependent IT summation loop (variable iteration count)
3. Homodyne discriminator state dependencies
@@ -1,13 +1,13 @@
// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Ehlers Hilbert Transform Instantaneous Trend (HTIT)", "HTIT", overlay=true)
indicator("Ehlers Hilbert Transform Instantaneous Trend (HT_TRENDLINE)", "HT_TRENDLINE", overlay=true)
//@function Calculates the Hilbert Transform Instantaneous Trendline (HTIT)
//@param source Series to calculate HTIT from
//@returns HTIT value using Hilbert Transform with adaptive period estimation
//@function Calculates the Hilbert Transform Instantaneous Trendline (HT_TRENDLINE)
//@param source Series to calculate HT_TRENDLINE from
//@returns HT_TRENDLINE value using Hilbert Transform with adaptive period estimation
//@optimized Uses Hilbert Transform quadrature components for O(1) complexity per bar
htit(series float source) =>
httrendline(series float source) =>
var float price = na
var float smooth = na
var float detrender = 0.0
@@ -56,7 +56,7 @@ htit(series float source) =>
i_source = input.source(close, "Source")
// Calculation
htit_value = htit(i_source)
htit_value = httrendline(i_source)
// Plot
plot(htit_value, "HTIT", color=color.yellow, linewidth=2)
plot(htit_value, "HT_TRENDLINE", color=color.yellow, linewidth=2)
@@ -0,0 +1,3 @@
# HOLT Signature
![HT_TRENDLINE Signature](ht_trendline_signature.svg)
@@ -19508,7 +19508,7 @@ L 1441.19952 978.875435
</g>
</g>
<g id="text_245">
<!-- HTIT: Ehlers Hilbert Transform Instantaneous Trend (also known as HT_TRENDLINE) -->
<!-- HT_TRENDLINE: Ehlers Hilbert Transform Instantaneous Trend (also known as HT_TRENDLINE) -->
<g transform="translate(348.72851 19.3575) scale(0.16 -0.16)">
<defs>
<path id="DejaVuSans-Bold-3a" d="M 716 3500

Before

Width:  |  Height:  |  Size: 552 KiB

After

Width:  |  Height:  |  Size: 552 KiB

@@ -2,24 +2,24 @@ using TradingPlatform.BusinessLayer;
namespace QuanTAlib.Quantower.Tests;
public class HtitIndicatorTests
public class HtTrendlineIndicatorTests
{
[Fact]
public void Indicator_Initializes_Correctly()
{
var indicator = new HtitIndicator();
var indicator = new HtTrendlineIndicator();
indicator.Initialize();
Assert.Equal("HTIT - Ehlers Hilbert Transform Instantaneous Trend", indicator.Name);
Assert.StartsWith("HTIT", indicator.ShortName, StringComparison.Ordinal);
Assert.Equal("HT_TRENDLINE - Ehlers Hilbert Transform Instantaneous Trend", indicator.Name);
Assert.StartsWith("HT_TRENDLINE", indicator.ShortName, StringComparison.Ordinal);
Assert.Contains("Close", indicator.ShortName, StringComparison.Ordinal);
Assert.Equal(0, HtitIndicator.MinHistoryDepths);
Assert.Equal(0, HtTrendlineIndicator.MinHistoryDepths);
Assert.Single(indicator.LinesSeries);
}
[Fact]
public void Indicator_Updates_Correctly()
{
var indicator = new HtitIndicator();
var indicator = new HtTrendlineIndicator();
indicator.Initialize();
// Warmup
@@ -1,11 +1,11 @@
namespace QuanTAlib.Tests;
public class HtitTests
public class HtTrendlineTests
{
private readonly GBM _gbm;
public HtitTests()
public HtTrendlineTests()
{
_gbm = new GBM();
}
@@ -13,30 +13,30 @@ public class HtitTests
[Fact]
public void IsHot_BecomesTrue_AfterWarmup()
{
var htit = new Htit();
var httrendline = new HtTrendline();
for (int i = 0; i < 12; i++)
{
Assert.False(htit.IsHot);
htit.Update(new TValue(DateTime.UtcNow.Ticks, 100.0));
Assert.False(httrendline.IsHot);
httrendline.Update(new TValue(DateTime.UtcNow.Ticks, 100.0));
}
Assert.True(htit.IsHot);
Assert.True(httrendline.IsHot);
}
[Fact]
public void Update_Matches_Calculate()
{
var htit = new Htit();
var httrendline = new HtTrendline();
var data = _gbm.Fetch(100, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1)).Close;
var series = data;
var resultSeries = htit.Update(series);
var resultSeries = httrendline.Update(series);
// Reset and calculate streaming
htit.Reset();
httrendline.Reset();
var streamingResults = new List<double>();
foreach (var item in data)
{
streamingResults.Add(htit.Update(item).Value);
streamingResults.Add(httrendline.Update(item).Value);
}
for (int i = 0; i < resultSeries.Count; i++)
@@ -48,16 +48,16 @@ public class HtitTests
[Fact]
public void Calculate_Span_Matches_Update()
{
var htit = new Htit();
var httrendline = new HtTrendline();
var data = _gbm.Fetch(100, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1)).Close;
var series = data;
var resultSeries = htit.Update(series);
var resultSeries = httrendline.Update(series);
var spanInput = data.Values.ToArray();
var spanOutput = new double[spanInput.Length];
Htit.Batch(spanInput, spanOutput);
HtTrendline.Batch(spanInput, spanOutput);
for (int i = 0; i < resultSeries.Count; i++)
{
@@ -68,38 +68,38 @@ public class HtitTests
[Fact]
public void Handles_NaN()
{
var htit = new Htit();
htit.Update(new TValue(DateTime.UtcNow.Ticks, 100.0));
htit.Update(new TValue(DateTime.UtcNow.Ticks, double.NaN));
var httrendline = new HtTrendline();
httrendline.Update(new TValue(DateTime.UtcNow.Ticks, 100.0));
httrendline.Update(new TValue(DateTime.UtcNow.Ticks, double.NaN));
Assert.Equal(100.0, htit.Last.Value);
Assert.Equal(100.0, httrendline.Last.Value);
}
[Fact]
public void Htit_Calc_IsNew_AcceptsParameter()
{
var htit = new Htit();
htit.Update(new TValue(DateTime.UtcNow, 100), isNew: true);
Assert.Equal(100, htit.Last.Value);
var httrendline = new HtTrendline();
httrendline.Update(new TValue(DateTime.UtcNow, 100), isNew: true);
Assert.Equal(100, httrendline.Last.Value);
}
[Fact]
public void Htit_Reset_ClearsState()
{
var htit = new Htit();
htit.Update(new TValue(DateTime.UtcNow, 100));
htit.Update(new TValue(DateTime.UtcNow, 110));
var httrendline = new HtTrendline();
httrendline.Update(new TValue(DateTime.UtcNow, 100));
httrendline.Update(new TValue(DateTime.UtcNow, 110));
htit.Reset();
httrendline.Reset();
Assert.True(double.IsNaN(htit.Last.Value));
Assert.False(htit.IsHot);
Assert.True(double.IsNaN(httrendline.Last.Value));
Assert.False(httrendline.IsHot);
}
[Fact]
public void Htit_IterativeCorrections_RestoreToOriginalState()
{
var htit = new Htit();
var httrendline = new HtTrendline();
var gbm = new GBM(startPrice: 100.0, mu: 0.02, sigma: 0.1);
// Feed 20 new values (needs > 12 for warmup)
@@ -108,21 +108,21 @@ public class HtitTests
{
var bar = gbm.Next(isNew: true);
lastInput = new TValue(bar.Time, bar.Close);
htit.Update(lastInput, isNew: true);
httrendline.Update(lastInput, isNew: true);
}
// Remember state after 20 values
double valueAfterTwenty = htit.Last.Value;
double valueAfterTwenty = httrendline.Last.Value;
// Generate 9 corrections with isNew=false (different values)
for (int i = 0; i < 9; i++)
{
var bar = gbm.Next(isNew: false);
htit.Update(new TValue(bar.Time, bar.Close), isNew: false);
httrendline.Update(new TValue(bar.Time, bar.Close), isNew: false);
}
// Feed the remembered 20th input again with isNew=false
TValue finalValue = htit.Update(lastInput, isNew: false);
TValue finalValue = httrendline.Update(lastInput, isNew: false);
// Should match the original state after 20 values
Assert.Equal(valueAfterTwenty, finalValue.Value, 1e-9);
@@ -134,7 +134,7 @@ public class HtitTests
double[] source = [1, 2, 3, 4, 5];
double[] wrongSizeOutput = new double[3];
Assert.Throws<ArgumentException>(() => Htit.Batch(source.AsSpan(), wrongSizeOutput.AsSpan()));
Assert.Throws<ArgumentException>(() => HtTrendline.Batch(source.AsSpan(), wrongSizeOutput.AsSpan()));
}
[Fact]
@@ -143,7 +143,7 @@ public class HtitTests
double[] source = [100, 110, double.NaN, 120, 130];
double[] output = new double[5];
Htit.Batch(source.AsSpan(), output.AsSpan());
HtTrendline.Batch(source.AsSpan(), output.AsSpan());
foreach (var val in output)
{
@@ -160,18 +160,18 @@ public class HtitTests
var series = bars.Close;
// 1. Batch Mode
var batchSeries = Htit.Batch(series);
var batchSeries = HtTrendline.Batch(series);
double expected = batchSeries.Last.Value;
// 2. Span Mode
var tValues = series.Values.ToArray();
var spanInput = new ReadOnlySpan<double>(tValues);
var spanOutput = new double[tValues.Length];
Htit.Batch(spanInput, spanOutput);
HtTrendline.Batch(spanInput, spanOutput);
double spanResult = spanOutput[^1];
// 3. Streaming Mode
var streamingInd = new Htit();
var streamingInd = new HtTrendline();
for (int i = 0; i < series.Count; i++)
{
streamingInd.Update(series[i]);
@@ -180,7 +180,7 @@ public class HtitTests
// 4. Eventing Mode
var pubSource = new TSeries();
var eventingInd = new Htit(pubSource);
var eventingInd = new HtTrendline(pubSource);
for (int i = 0; i < series.Count; i++)
{
pubSource.Add(series[i]);
@@ -5,12 +5,12 @@ using TALib;
namespace QuanTAlib.Tests;
public sealed class HtitValidationTests : IDisposable
public sealed class HtTrendlineValidationTests : IDisposable
{
private readonly ValidationTestData _data;
private bool _disposed;
public HtitValidationTests()
public HtTrendlineValidationTests()
{
_data = new ValidationTestData(10000);
}
@@ -38,16 +38,16 @@ public sealed class HtitValidationTests : IDisposable
[Fact]
public void Validate_TaLib()
{
// Calculate TA-Lib HTIT
// Calculate TA-Lib HT_TRENDLINE
var input = _data.RawData.Span;
var output = new double[input.Length];
var retCode = TALib.Functions.HtTrendline(input, 0..^0, output, out var outRange);
Assert.Equal(TALib.Core.RetCode.Success, retCode);
// Calculate QuanTAlib HTIT
var htit = new Htit();
var quantalibResults = htit.Update(_data.Data);
// Calculate QuanTAlib HT_TRENDLINE
var httrendline = new HtTrendline();
var quantalibResults = httrendline.Update(_data.Data);
// Compare results
// TA-Lib HT_TRENDLINE has a lookback of 63
@@ -67,13 +67,13 @@ public sealed class HtitValidationTests : IDisposable
[Fact]
public void Validate_Skender_Batch()
{
// Calculate Skender HTIT
// Calculate Skender HT_TRENDLINE
var skenderResults = _data.SkenderQuotes.GetHtTrendline().ToList();
// Calculate QuanTAlib HTIT
var htit = new Htit();
// Calculate QuanTAlib HT_TRENDLINE
var httrendline = new HtTrendline();
var series = _data.Data;
var quantalibResults = htit.Update(series);
var quantalibResults = httrendline.Update(series);
// Compare results
// Skip warmup period (Skender needs 100 periods for convergence, but we can check after 50)
@@ -97,16 +97,16 @@ public sealed class HtitValidationTests : IDisposable
[Fact]
public void Validate_Skender_Streaming()
{
// Calculate Skender HTIT
// Calculate Skender HT_TRENDLINE
var skenderResults = _data.SkenderQuotes.GetHtTrendline().ToList();
// Calculate QuanTAlib HTIT Streaming
var htit = new Htit();
// Calculate QuanTAlib HT_TRENDLINE Streaming
var httrendline = new HtTrendline();
var streamingResults = new List<double>();
foreach (var item in _data.Data)
{
streamingResults.Add(htit.Update(item).Value);
streamingResults.Add(httrendline.Update(item).Value);
}
// Compare results
@@ -139,14 +139,14 @@ public sealed class HtitValidationTests : IDisposable
Volume = (double)q.Volume
}).ToList();
// Calculate Ooples HTIT
// Calculate Ooples HT_TRENDLINE
var stockData = new StockData(ooplesData);
var oResult = stockData.CalculateEhlersInstantaneousTrendlineV1();
var oValues = oResult.OutputValues["Eit"];
// Calculate QuanTAlib HTIT
var htit = new Htit();
var quantalibResults = htit.Update(_data.Data);
// Calculate QuanTAlib HT_TRENDLINE
var httrendline = new HtTrendline();
var quantalibResults = httrendline.Update(_data.Data);
// Compare results
// Ooples might have different warmup or calculation details
-3
View File
@@ -1,3 +0,0 @@
# HOLT Signature
![HTIT Signature](htit_signature.svg)
+2 -2
View File
@@ -4,7 +4,7 @@ Volume is market fuel. Price tells what happened; volume tells how hard the mark
| Indicator | Full Name | Description |
| :--- | :--- | :--- |
| [ADL](adl/Adl.md) | Accumulation/Distribution Line | Correlates price location within range to volume. Grandfather of volume flow analysis. |
| [AD](ad/Ad.md) | Accumulation/Distribution Line | Correlates price location within range to volume. Grandfather of volume flow analysis. |
| [ADOSC](adosc/Adosc.md) | Chaikin A/D Oscillator | Momentum indicator for AD Line. Predicts reversals by measuring acceleration of money flow. |
| [AOBV](aobv/Aobv.md) | Archer On-Balance Volume | Dual EMA smoothing of OBV for cleaner crossover signals. |
| [CMF](cmf/Cmf.md) | Chaikin Money Flow | Measures money flow volume over set period (typically 20-21 days). |
@@ -27,7 +27,7 @@ Volume is market fuel. Price tells what happened; volume tells how hard the mark
| [VF](vf/Vf.md) | Volume Force | Measures force of volume behind price movements using EMA smoothing. |
| [VO](vo/Vo.md) | Volume Oscillator | Difference between short and long volume moving averages. Shows volume momentum. |
| [VROC](vroc/Vroc.md) | Volume Rate of Change | Measures speed at which volume is changing over time. |
| [VWAD](vwad/Vwad.md) | Volume Weighted A/D | Similar to ADL but weights accumulation/distribution by volume. |
| [VWAD](vwad/Vwad.md) | Volume Weighted A/D | Similar to AD but weights accumulation/distribution by volume. |
| [VWAP](vwap/Vwap.md) | Volume Weighted Average Price | Average price weighted by volume. Common execution benchmark and intraday reference. |
| [VWMA](vwma/Vwma.md) | Volume Weighted Moving Average | Moving average where each price point is weighted by its volume over a sliding window. |
| [WAD](wad/Wad.md) | Williams Accumulation/Distribution | Measures cumulative buying/selling pressure using True Range and volume. |
@@ -5,35 +5,35 @@ using TradingPlatform.BusinessLayer;
namespace QuanTAlib;
[SkipLocalsInit]
public sealed class AdlIndicator : Indicator, IWatchlistIndicator
public sealed class AdIndicator : Indicator, IWatchlistIndicator
{
[InputParameter("Show cold values", sortIndex: 21)]
public bool ShowColdValues { get; set; } = true;
private Adl _adl = null!;
private Ad _ad = null!;
private readonly LineSeries _series;
public static int MinHistoryDepths => 0;
int IWatchlistIndicator.MinHistoryDepths => MinHistoryDepths;
public override string ShortName => "ADL";
public override string SourceCodeLink => "https://github.com/mihakralj/QuanTAlib/blob/main/lib/volume/adl/Adl.Quantower.cs";
public override string ShortName => "AD";
public override string SourceCodeLink => "https://github.com/mihakralj/QuanTAlib/blob/main/lib/volume/ad/Ad.Quantower.cs";
public AdlIndicator()
public AdIndicator()
{
OnBackGround = true;
SeparateWindow = true;
Name = "ADL - Accumulation/Distribution Line";
Name = "AD - Accumulation/Distribution Line";
Description = "Accumulation/Distribution Line";
_series = new LineSeries(name: "ADL", color: Color.Blue, width: 2, style: LineStyle.Solid);
_series = new LineSeries(name: "AD", color: Color.Blue, width: 2, style: LineStyle.Solid);
AddLineSeries(_series);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
protected override void OnInit()
{
_adl = new Adl();
_ad = new Ad();
base.OnInit();
}
@@ -41,8 +41,8 @@ public sealed class AdlIndicator : Indicator, IWatchlistIndicator
protected override void OnUpdate(UpdateArgs args)
{
TBar bar = this.GetInputBar(args);
TValue result = _adl.Update(bar, args.IsNewBar());
TValue result = _ad.Update(bar, args.IsNewBar());
_series.SetValue(result.Value, _adl.IsHot, ShowColdValues);
_series.SetValue(result.Value, _ad.IsHot, ShowColdValues);
}
}
+23 -23
View File
@@ -4,33 +4,33 @@ using System.Numerics;
namespace QuanTAlib;
/// <summary>
/// ADL: Accumulation/Distribution Line
/// AD: Accumulation/Distribution Line
/// </summary>
/// <remarks>
/// Cumulative indicator using volume and price to assess accumulation or distribution.
/// Rising ADL confirms accumulation; falling confirms distribution.
/// Rising AD confirms accumulation; falling confirms distribution.
///
/// Calculation: <c>MFM = [(Close - Low) - (High - Close)] / (High - Low)</c>,
/// <c>MFV = MFM × Volume</c>, <c>ADL = prev_ADL + MFV</c>. If High equals Low, MFM is 0.
/// <c>MFV = MFM × Volume</c>, <c>AD = prev_AD + MFV</c>. If High equals Low, MFM is 0.
/// </remarks>
/// <seealso href="Adl.md">Detailed documentation</seealso>
/// <seealso href="adl.pine">Reference Pine Script implementation</seealso>
/// <seealso href="Ad.md">Detailed documentation</seealso>
/// <seealso href="ad.pine">Reference Pine Script implementation</seealso>
[SkipLocalsInit]
public sealed class Adl : ITValuePublisher
public sealed class Ad : ITValuePublisher
{
private double _adl;
private double _p_adl;
private double _ad;
private double _p_ad;
private bool _isInitialized;
/// <summary>
/// Display name for the indicator.
/// </summary>
public static string Name => "ADL";
public static string Name => "AD";
public event TValuePublishedHandler? Pub;
/// <summary>
/// Current ADL value.
/// Current AD value.
/// </summary>
public TValue Last { get; private set; }
@@ -45,9 +45,9 @@ public sealed class Adl : ITValuePublisher
public bool IsHot => _isInitialized;
/// <summary>
/// Creates a new ADL indicator.
/// Creates a new AD indicator.
/// </summary>
public Adl()
public Ad()
{
_isInitialized = false;
}
@@ -58,8 +58,8 @@ public sealed class Adl : ITValuePublisher
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Reset()
{
_adl = 0;
_p_adl = 0;
_ad = 0;
_p_ad = 0;
_isInitialized = false;
Last = default;
}
@@ -69,11 +69,11 @@ public sealed class Adl : ITValuePublisher
{
if (isNew)
{
_p_adl = _adl;
_p_ad = _ad;
}
else
{
_adl = _p_adl;
_ad = _p_ad;
}
double highLowRange = input.High - input.Low;
@@ -85,19 +85,19 @@ public sealed class Adl : ITValuePublisher
}
double mfv = mfm * input.Volume;
_adl += mfv;
_ad += mfv;
_isInitialized = true;
Last = new TValue(input.Time, _adl);
Last = new TValue(input.Time, _ad);
Pub?.Invoke(this, new TValueEventArgs { Value = Last, IsNew = isNew });
return Last;
}
/// <summary>
/// Updates ADL with a TValue input.
/// Updates AD with a TValue input.
/// </summary>
/// <exception cref="NotSupportedException">
/// ADL requires OHLCV bar data to calculate the Money Flow Multiplier and Volume.
/// AD requires OHLCV bar data to calculate the Money Flow Multiplier and Volume.
/// Use Update(TBar) instead.
/// </exception>
#pragma warning disable S2325 // Method signature must match ITValuePublisher contract
@@ -105,7 +105,7 @@ public sealed class Adl : ITValuePublisher
#pragma warning restore S2325
{
throw new NotSupportedException(
"ADL requires OHLCV bar data to calculate the Money Flow Multiplier and Volume. " +
"AD requires OHLCV bar data to calculate the Money Flow Multiplier and Volume. " +
"Use Update(TBar) instead.");
}
@@ -219,9 +219,9 @@ public sealed class Adl : ITValuePublisher
}
}
public static (TSeries Results, Adl Indicator) Calculate(TBarSeries source)
public static (TSeries Results, Ad Indicator) Calculate(TBarSeries source)
{
var indicator = new Adl();
var indicator = new Ad();
TSeries results = indicator.Update(source);
return (results, indicator);
}
+16 -16
View File
@@ -1,4 +1,4 @@
# ADL: Accumulation/Distribution Line
# AD: Accumulation/Distribution Line
> *Volume precedes price.*
@@ -7,26 +7,26 @@
| **Category** | Volume |
| **Inputs** | OHLCV bar (TBar) |
| **Parameters** | None |
| **Outputs** | Single series (ADL) |
| **Outputs** | Single series (AD) |
| **Output range** | Unbounded |
| **Warmup** | 1 bar |
| **PineScript** | [adl.pine](adl.pine) |
| **PineScript** | [ad.pine](ad.pine) |
- The Accumulation/Distribution Line (ADL) is the bedrock of volume analysis.
- The Accumulation/Distribution Line (AD) is the bedrock of volume analysis.
- No configurable parameters; computation is stateless per bar.
- Validated against TA-Lib, Skender, and Tulip reference implementations where available.
The Accumulation/Distribution Line (ADL) is the bedrock of volume analysis. It attempts to answer a single, vital question: "Are the big players buying or selling?"
The Accumulation/Distribution Line (AD) is the bedrock of volume analysis. It attempts to answer a single, vital question: "Are the big players buying or selling?"
Unlike On-Balance Volume (OBV), which treats every up-day as 100% buying, ADL is nuanced. It looks at *where* the price closed within the day's range. A close near the high on massive volume screams "Accumulation." A close near the low on massive volume screams "Distribution."
Unlike On-Balance Volume (OBV), which treats every up-day as 100% buying, AD is nuanced. It looks at *where* the price closed within the day's range. A close near the high on massive volume screams "Accumulation." A close near the low on massive volume screams "Distribution."
## Historical Context
Developed by Marc Chaikin, the ADL was originally designed to spot divergences. Chaikin noticed that if a stock made a new high but the ADL failed to make a new high, a crash was imminent. He essentially quantified the "smart money" flow.
Developed by Marc Chaikin, the AD was originally designed to spot divergences. Chaikin noticed that if a stock made a new high but the AD failed to make a new high, a crash was imminent. He essentially quantified the "smart money" flow.
## Architecture & Physics
ADL is a cumulative indicator, meaning it has infinite memory. Today's value depends on the sum of all yesterdays.
AD is a cumulative indicator, meaning it has infinite memory. Today's value depends on the sum of all yesterdays.
The core mechanic is the **Money Flow Multiplier (MFM)**, also known as the Close Location Value (CLV). This value ranges from -1 to +1:
@@ -50,23 +50,23 @@ $$
MFV = MFM \times Volume
$$
### 3. Accumulation/Distribution Line (ADL)
### 3. Accumulation/Distribution Line (AD)
$$
ADL_t = ADL_{t-1} + MFV_t
AD_t = AD_{t-1} + MFV_t
$$
## Performance Profile
### Operation Count (Streaming Mode)
ADL computes Money Flow Multiplier (MFM) from bar data, multiplies by volume, and accumulates cumulatively — O(1).
AD computes Money Flow Multiplier (MFM) from bar data, multiplies by volume, and accumulates cumulatively — O(1).
| Operation | Count | Cost (cycles) | Subtotal |
| :--- | :---: | :---: | :---: |
| MFM = ((C-L)-(H-C)) / (H-L) | 1 | 5 cy | ~5 cy |
| MFV = MFM * Volume | 1 | 3 cy | ~3 cy |
| ADL += MFV (cumulative sum) | 1 | 1 cy | ~1 cy |
| AD += MFV (cumulative sum) | 1 | 1 cy | ~1 cy |
| Zero guard on H-L | 1 | 2 cy | ~2 cy |
| NaN guard + state update | 1 | 2 cy | ~2 cy |
| **Total** | **O(1)** | — | **~13 cy** |
@@ -89,12 +89,12 @@ O(1) cumulative indicator — no window, no buffer. Throughput ~4 ns/bar. Divisi
| :--- | :--- | :--- |
| **QuanTAlib** | ✅ | Validated. |
| **TA-Lib** | ✅ | Matches `TA_AD` exactly. |
| **Skender** | ✅ | Matches `GetAdl` exactly. |
| **Skender** | ✅ | Matches `GetAd` exactly. |
| **Tulip** | ✅ | Matches `ad` exactly. |
| **Ooples** | ✅ | Matches `CalculateAccumulationDistributionLine`. |
### Common Pitfalls
* **Gaps**: ADL ignores gaps. If a stock gaps up but closes near its low, ADL will register distribution, even if the price is higher than yesterday.
* **Scale**: The absolute value of ADL is meaningless; it depends on the start date of the data. Only the *trend* and *divergence* matter.
* **Volume Spikes**: A single bad data point with erroneous volume can permanently skew the ADL. Sanitize your data.
* **Gaps**: AD ignores gaps. If a stock gaps up but closes near its low, AD will register distribution, even if the price is higher than yesterday.
* **Scale**: The absolute value of AD is meaningless; it depends on the start date of the data. Only the *trend* and *divergence* matter.
* **Volume Spikes**: A single bad data point with erroneous volume can permanently skew the AD. Sanitize your data.
@@ -1,15 +1,15 @@
// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Accumulation/Distribution Line (ADL)", "ADL", overlay=false)
indicator("Accumulation/Distribution Line (AD)", "AD", overlay=false)
//@function Calculates the Accumulation/Distribution Line (ADL), a volume-based indicator that measures money flow into and out of a security
//@function Calculates the Accumulation/Distribution Line (AD), a volume-based indicator that measures money flow into and out of a security
//@param src_high The high price (default: built-in high)
//@param src_low The low price (default: built-in low)
//@param src_close The close price (default: built-in close)
//@param src_vol The volume (default: built-in volume)
//@returns The cumulative ADL value representing buying/selling pressure
adl(src_high = high, src_low = low, src_close = close, src_vol = volume) =>
//@returns The cumulative AD value representing buying/selling pressure
ad(src_high = high, src_low = low, src_close = close, src_vol = volume) =>
float mfm = 0.0
if not na(src_high) and not na(src_low) and not na(src_close)
mfm := (src_close - src_low) - (src_high - src_close)
@@ -22,7 +22,7 @@ adl(src_high = high, src_low = low, src_close = close, src_vol = volume) =>
// ---------- Inputs ----------
// ---------- Calculations ----------
adl_val = adl(high, low, close, volume)
ad_val = ad(high, low, close, volume)
// ---------- Plotting ----------
plot(adl_val, "ADL", color=color.yellow, linewidth=2)
plot(ad_val, "AD", color=color.yellow, linewidth=2)
@@ -3,39 +3,39 @@ using QuanTAlib;
namespace QuanTAlib.Tests;
public class AdlIndicatorTests
public class AdIndicatorTests
{
[Fact]
public void AdlIndicator_Constructor_SetsDefaults()
public void AdIndicator_Constructor_SetsDefaults()
{
var indicator = new AdlIndicator();
var indicator = new AdIndicator();
Assert.Equal("ADL - Accumulation/Distribution Line", indicator.Name);
Assert.Equal("AD - Accumulation/Distribution Line", indicator.Name);
Assert.True(indicator.SeparateWindow);
Assert.True(indicator.OnBackGround);
Assert.Equal(0, AdlIndicator.MinHistoryDepths);
Assert.Equal(0, AdIndicator.MinHistoryDepths);
}
[Fact]
public void AdlIndicator_ShortName_IsCorrect()
public void AdIndicator_ShortName_IsCorrect()
{
var indicator = new AdlIndicator();
Assert.Equal("ADL", indicator.ShortName);
var indicator = new AdIndicator();
Assert.Equal("AD", indicator.ShortName);
}
[Fact]
public void AdlIndicator_MinHistoryDepths_EqualsZero()
public void AdIndicator_MinHistoryDepths_EqualsZero()
{
var indicator = new AdlIndicator();
var indicator = new AdIndicator();
Assert.Equal(0, AdlIndicator.MinHistoryDepths);
Assert.Equal(0, AdIndicator.MinHistoryDepths);
Assert.Equal(0, ((IWatchlistIndicator)indicator).MinHistoryDepths);
}
[Fact]
public void AdlIndicator_Initialize_CreatesInternalAdl()
public void AdIndicator_Initialize_CreatesInternalAd()
{
var indicator = new AdlIndicator();
var indicator = new AdIndicator();
// Initialize should not throw
indicator.Initialize();
@@ -45,9 +45,9 @@ public class AdlIndicatorTests
}
[Fact]
public void AdlIndicator_ProcessUpdate_HistoricalBar_ComputesValue()
public void AdIndicator_ProcessUpdate_HistoricalBar_ComputesValue()
{
var indicator = new AdlIndicator();
var indicator = new AdIndicator();
indicator.Initialize();
// Add historical data
@@ -67,9 +67,9 @@ public class AdlIndicatorTests
}
[Fact]
public void AdlIndicator_ProcessUpdate_NewBar_ComputesValue()
public void AdIndicator_ProcessUpdate_NewBar_ComputesValue()
{
var indicator = new AdlIndicator();
var indicator = new AdIndicator();
indicator.Initialize();
var now = DateTime.UtcNow;
@@ -1,122 +1,122 @@
namespace QuanTAlib.Tests;
public class AdlTests
public class AdTests
{
[Fact]
public void Adl_BasicCalculation_ReturnsExpectedValues()
public void Ad_BasicCalculation_ReturnsExpectedValues()
{
// Arrange
var adl = new Adl();
var ad = new Ad();
var time = DateTime.UtcNow;
// Bar 1: Close=10, High=12, Low=8. Range=4.
// MFM = ((10-8) - (12-10)) / 4 = (2 - 2) / 4 = 0.
// Vol = 100. MFV = 0. ADL = 0.
// Vol = 100. MFV = 0. AD = 0.
var bar1 = new TBar(time, 10, 12, 8, 10, 100);
var val1 = adl.Update(bar1);
var val1 = ad.Update(bar1);
Assert.Equal(0, val1.Value);
// Bar 2: Close=12, High=12, Low=8. Range=4.
// MFM = ((12-8) - (12-12)) / 4 = (4 - 0) / 4 = 1.
// Vol = 200. MFV = 200. ADL = 0 + 200 = 200.
// Vol = 200. MFV = 200. AD = 0 + 200 = 200.
var bar2 = new TBar(time.AddMinutes(1), 10, 12, 8, 12, 200);
var val2 = adl.Update(bar2);
var val2 = ad.Update(bar2);
Assert.Equal(200, val2.Value);
// Bar 3: Close=8, High=12, Low=8. Range=4.
// MFM = ((8-8) - (12-8)) / 4 = (0 - 4) / 4 = -1.
// Vol = 100. MFV = -100. ADL = 200 - 100 = 100.
// Vol = 100. MFV = -100. AD = 200 - 100 = 100.
var bar3 = new TBar(time.AddMinutes(2), 12, 12, 8, 8, 100);
var val3 = adl.Update(bar3);
var val3 = ad.Update(bar3);
Assert.Equal(100, val3.Value);
}
[Fact]
public void Adl_IsNew_False_UpdatesSameBar()
public void Ad_IsNew_False_UpdatesSameBar()
{
var adl = new Adl();
var ad = new Ad();
var time = DateTime.UtcNow;
// Initial update
// MFM = 1, Vol = 100 -> ADL = 100
// MFM = 1, Vol = 100 -> AD = 100
var bar1 = new TBar(time, 10, 12, 8, 12, 100);
adl.Update(bar1, isNew: true);
Assert.Equal(100, adl.Last.Value);
ad.Update(bar1, isNew: true);
Assert.Equal(100, ad.Last.Value);
// Update same bar with different volume
// MFM = 1, Vol = 200 -> ADL = 200 (replaces previous 100)
// MFM = 1, Vol = 200 -> AD = 200 (replaces previous 100)
var bar1Update = new TBar(time, 10, 12, 8, 12, 200);
adl.Update(bar1Update, isNew: false);
Assert.Equal(200, adl.Last.Value);
ad.Update(bar1Update, isNew: false);
Assert.Equal(200, ad.Last.Value);
}
[Fact]
public void Adl_Reset_ClearsState()
public void Ad_Reset_ClearsState()
{
var adl = new Adl();
var ad = new Ad();
var bar = new TBar(DateTime.UtcNow, 10, 12, 8, 12, 100);
adl.Update(bar);
ad.Update(bar);
Assert.True(adl.IsHot);
Assert.NotEqual(0, adl.Last.Value);
Assert.True(ad.IsHot);
Assert.NotEqual(0, ad.Last.Value);
adl.Reset();
Assert.False(adl.IsHot);
Assert.Equal(0, adl.Last.Value);
ad.Reset();
Assert.False(ad.IsHot);
Assert.Equal(0, ad.Last.Value);
}
[Fact]
public void Adl_HighEqualsLow_HandlesDivisionByZero()
public void Ad_HighEqualsLow_HandlesDivisionByZero()
{
var adl = new Adl();
var ad = new Ad();
// High = Low = 10. Range = 0. MFM should be 0.
var bar = new TBar(DateTime.UtcNow, 10, 10, 10, 10, 100);
var val = adl.Update(bar);
var val = ad.Update(bar);
Assert.Equal(0, val.Value);
}
[Fact]
public void Adl_TValueUpdate_ThrowsNotSupportedException()
public void Ad_TValueUpdate_ThrowsNotSupportedException()
{
var adl = new Adl();
var ad = new Ad();
var bar = new TBar(DateTime.UtcNow, 10, 12, 8, 12, 100);
adl.Update(bar); // ADL = 100
ad.Update(bar); // AD = 100
// Update with TValue should throw since ADL requires OHLCV bar data
Assert.Throws<NotSupportedException>(() => adl.Update(new TValue(DateTime.UtcNow, 15)));
// Update with TValue should throw since AD requires OHLCV bar data
Assert.Throws<NotSupportedException>(() => ad.Update(new TValue(DateTime.UtcNow, 15)));
}
[Fact]
public void Adl_Name_IsCorrect()
public void Ad_Name_IsCorrect()
{
Assert.Equal("ADL", Adl.Name);
Assert.Equal("AD", Ad.Name);
}
[Fact]
public void Adl_PubEvent_FiresOnUpdate()
public void Ad_PubEvent_FiresOnUpdate()
{
var adl = new Adl();
var ad = new Ad();
bool eventFired = false;
adl.Pub += (object? sender, in TValueEventArgs args) => eventFired = true;
ad.Pub += (object? sender, in TValueEventArgs args) => eventFired = true;
adl.Update(new TBar(DateTime.UtcNow, 10, 12, 8, 10, 100));
ad.Update(new TBar(DateTime.UtcNow, 10, 12, 8, 10, 100));
Assert.True(eventFired);
}
[Fact]
public void Adl_UpdateTBarSeries_ReturnsCorrectSeries()
public void Ad_UpdateTBarSeries_ReturnsCorrectSeries()
{
var adl = new Adl();
var ad = new Ad();
var bars = new TBarSeries();
var time = DateTime.UtcNow;
// Add same bars as in BasicCalculation
bars.Add(new TBar(time, 10, 12, 8, 10, 100)); // ADL=0
bars.Add(new TBar(time.AddMinutes(1), 10, 12, 8, 12, 200)); // ADL=200
bars.Add(new TBar(time.AddMinutes(2), 12, 12, 8, 8, 100)); // ADL=100
bars.Add(new TBar(time, 10, 12, 8, 10, 100)); // AD=0
bars.Add(new TBar(time.AddMinutes(1), 10, 12, 8, 12, 200)); // AD=200
bars.Add(new TBar(time.AddMinutes(2), 12, 12, 8, 8, 100)); // AD=100
var result = adl.Update(bars);
var result = ad.Update(bars);
Assert.Equal(3, result.Count);
Assert.Equal(0, result[0].Value);
@@ -125,7 +125,7 @@ public class AdlTests
}
[Fact]
public void Adl_CalculateTBarSeries_ReturnsCorrectSeries()
public void Ad_CalculateTBarSeries_ReturnsCorrectSeries()
{
var bars = new TBarSeries();
var time = DateTime.UtcNow;
@@ -134,7 +134,7 @@ public class AdlTests
bars.Add(new TBar(time.AddMinutes(1), 10, 12, 8, 12, 200));
bars.Add(new TBar(time.AddMinutes(2), 12, 12, 8, 8, 100));
var result = Adl.Batch(bars);
var result = Ad.Batch(bars);
Assert.Equal(3, result.Count);
Assert.Equal(0, result[0].Value);
@@ -143,7 +143,7 @@ public class AdlTests
}
[Fact]
public void Adl_CalculateSpan_ReturnsCorrectValues()
public void Ad_CalculateSpan_ReturnsCorrectValues()
{
double[] high = { 12, 12, 12 };
double[] low = { 8, 8, 8 };
@@ -151,7 +151,7 @@ public class AdlTests
double[] volume = { 100, 200, 100 };
double[] output = new double[3];
Adl.Batch(high, low, close, volume, output);
Ad.Batch(high, low, close, volume, output);
Assert.Equal(0, output[0]);
Assert.Equal(200, output[1]);
@@ -159,7 +159,7 @@ public class AdlTests
}
[Fact]
public void Adl_CalculateSpan_ThrowsOnMismatchedLengths()
public void Ad_CalculateSpan_ThrowsOnMismatchedLengths()
{
double[] high = { 10, 11 };
double[] low = { 9, 10 };
@@ -168,19 +168,19 @@ public class AdlTests
double[] output = new double[2];
Assert.Throws<ArgumentException>(() =>
Adl.Batch(high, low, close, volume, output));
Ad.Batch(high, low, close, volume, output));
}
[Fact]
public void Adl_Calculate_EmptySeries_ReturnsEmpty()
public void Ad_Calculate_EmptySeries_ReturnsEmpty()
{
var bars = new TBarSeries();
var result = Adl.Batch(bars);
var result = Ad.Batch(bars);
Assert.Empty(result);
}
[Fact]
public void Adl_CalculateSpan_SimdPath_ReturnsCorrectValues()
public void Ad_CalculateSpan_SimdPath_ReturnsCorrectValues()
{
const int count = 100; // Enough to trigger SIMD
double[] high = new double[count];
@@ -190,7 +190,7 @@ public class AdlTests
double[] output = new double[count];
// Setup: High=12, Low=8, Close=12 (MFM=1), Vol=10
// Expected ADL increments by 10 each step.
// Expected AD increments by 10 each step.
for (int i = 0; i < count; i++)
{
high[i] = 12;
@@ -199,7 +199,7 @@ public class AdlTests
volume[i] = 10;
}
Adl.Batch(high, low, close, volume, output);
Ad.Batch(high, low, close, volume, output);
for (int i = 0; i < count; i++)
{
@@ -4,35 +4,35 @@ using OoplesFinance.StockIndicators.Models;
namespace QuanTAlib.Tests;
public class AdlValidationTests
public class AdValidationTests
{
private readonly ValidationTestData _data;
public AdlValidationTests()
public AdValidationTests()
{
_data = new ValidationTestData();
}
[Fact]
public void Adl_Matches_Skender()
public void Ad_Matches_Skender()
{
// Skender
var skenderResults = _data.SkenderQuotes.GetAdl();
var skenderValues = skenderResults.Select(x => x.Adl).ToArray();
// QuanTAlib
var adl = new Adl();
var ad = new Ad();
var quantalibValues = new List<double>();
foreach (var bar in _data.Bars)
{
quantalibValues.Add(adl.Update(bar).Value);
quantalibValues.Add(ad.Update(bar).Value);
}
ValidationHelper.VerifyData(quantalibValues.ToArray(), skenderValues, 0, 100, ValidationHelper.SkenderTolerance);
}
[Fact]
public void Adl_Matches_Talib()
public void Ad_Matches_Talib()
{
// TA-Lib
var high = _data.Bars.High.Values.ToArray();
@@ -45,18 +45,18 @@ public class AdlValidationTests
Assert.Equal(TALib.Core.RetCode.Success, retCode);
// QuanTAlib
var adl = new Adl();
var ad = new Ad();
var quantalibValues = new List<double>();
foreach (var bar in _data.Bars)
{
quantalibValues.Add(adl.Update(bar).Value);
quantalibValues.Add(ad.Update(bar).Value);
}
ValidationHelper.VerifyData(quantalibValues.ToArray(), talibValues, outRange, 0, 100, ValidationHelper.TalibTolerance);
}
[Fact]
public void Adl_Matches_Tulip()
public void Ad_Matches_Tulip()
{
// Tulip
var high = _data.Bars.High.Values.ToArray();
@@ -73,18 +73,18 @@ public class AdlValidationTests
var tulipValues = outputs[0];
// QuanTAlib
var adl = new Adl();
var ad = new Ad();
var quantalibValues = new List<double>();
foreach (var bar in _data.Bars)
{
quantalibValues.Add(adl.Update(bar).Value);
quantalibValues.Add(ad.Update(bar).Value);
}
ValidationHelper.VerifyData(quantalibValues.ToArray(), tulipValues, 0, 100, ValidationHelper.TulipTolerance);
}
[Fact]
public void Adl_Matches_Ooples()
public void Ad_Matches_Ooples()
{
// Ooples
var ooplesData = _data.SkenderQuotes.Select(q => new TickerData
@@ -102,11 +102,11 @@ public class AdlValidationTests
var oValues = oResult.OutputValues["Adl"];
// QuanTAlib
var adl = new Adl();
var ad = new Ad();
var quantalibValues = new List<double>();
foreach (var bar in _data.Bars)
{
quantalibValues.Add(adl.Update(bar).Value);
quantalibValues.Add(ad.Update(bar).Value);
}
ValidationHelper.VerifyData(quantalibValues.ToArray(), oValues.ToArray(), 0, 100, ValidationHelper.OoplesTolerance);

Some files were not shown because too many files have changed in this diff Show More