mirror of
https://github.com/mihakralj/QuanTAlib.git
synced 2026-08-18 10:38:05 +00:00
Merge branch 'dev' into main
This commit is contained in:
@@ -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. |
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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 10–30 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 10–30 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 (1–10 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: 30–50% 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: 30–50% 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)
|
||||
|
||||
+23
-23
@@ -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);
|
||||
}
|
||||
}
|
||||
+33
-33
@@ -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();
|
||||
}
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user